🚧 WIP
This commit is contained in:
+124
-32
@@ -200,40 +200,47 @@ class TestLoadImeiMap:
|
||||
load_imei_map(str(path))
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def app_module(tmp_path):
|
||||
"""Import api.app with a prepared environment (create_app runs at import
|
||||
time). Yields the reloaded module and restores the environment after."""
|
||||
import importlib
|
||||
import os
|
||||
|
||||
# connect_timeout keeps the DB-connecting tests fast even when
|
||||
# localhost:5432 is silently dropped by a firewall (psycopg otherwise
|
||||
# waits out the OS-level TCP timeout).
|
||||
tokens = tmp_path / "app_tokens.json"
|
||||
tokens.write_text(
|
||||
json.dumps({"tokens": {"test-app": "secret-abc"}}), encoding="utf-8"
|
||||
)
|
||||
imei_map = tmp_path / "imei_map.json"
|
||||
imei_map.write_text(
|
||||
json.dumps({"350123456789012": "Greenhouse sensor A"}), encoding="utf-8"
|
||||
)
|
||||
env = {
|
||||
"APP_TOKENS_FILE": str(tokens),
|
||||
"IMEI_MAP_FILE": str(imei_map),
|
||||
"DB_DSN": "postgresql://user:pass@localhost:5432/db?connect_timeout=2",
|
||||
}
|
||||
old = {key: os.environ.get(key) for key in env}
|
||||
os.environ.update(env)
|
||||
try:
|
||||
import api.app as app_module
|
||||
|
||||
importlib.reload(app_module)
|
||||
yield app_module
|
||||
finally:
|
||||
for key, value in old.items():
|
||||
if value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
class TestApiAuth:
|
||||
"""Light tests of the API auth dependency using the ASGI app directly."""
|
||||
|
||||
@pytest.fixture()
|
||||
def app_module(self, tmp_path):
|
||||
import importlib
|
||||
import os
|
||||
|
||||
# api.app runs create_app() at import time, so the environment must be
|
||||
# prepared before importing. connect_timeout keeps the DB-connecting
|
||||
# tests fast even when localhost:5432 is silently dropped by a firewall
|
||||
# (psycopg otherwise waits out the OS-level TCP timeout).
|
||||
tokens = tmp_path / "app_tokens.json"
|
||||
tokens.write_text(
|
||||
json.dumps({"tokens": {"test-app": "secret-abc"}}), encoding="utf-8"
|
||||
)
|
||||
env = {
|
||||
"APP_TOKENS_FILE": str(tokens),
|
||||
"DB_DSN": "postgresql://user:pass@localhost:5432/db?connect_timeout=2",
|
||||
}
|
||||
old = {key: os.environ.get(key) for key in env}
|
||||
os.environ.update(env)
|
||||
try:
|
||||
import api.app as app_module
|
||||
|
||||
importlib.reload(app_module)
|
||||
yield app_module
|
||||
finally:
|
||||
for key, value in old.items():
|
||||
if value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = value
|
||||
|
||||
def test_health_is_public(self, app_module) -> None:
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@@ -289,4 +296,89 @@ class TestApiAuth:
|
||||
params={"from": value},
|
||||
headers={"X-App-Token": "secret-abc"},
|
||||
)
|
||||
assert response.status_code != 422
|
||||
assert response.status_code != 422
|
||||
|
||||
|
||||
class TestConfigEndpoints:
|
||||
"""GET/PUT/POST /configs/{name} — reads, validated JSON writes."""
|
||||
|
||||
@pytest.fixture()
|
||||
def client(self, app_module):
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
return TestClient(app_module.app)
|
||||
|
||||
HDRS = {"X-App-Token": "secret-abc"}
|
||||
|
||||
def test_get_imei_map(self, client, app_module) -> None:
|
||||
r = client.get("/configs/imei_map", headers=self.HDRS)
|
||||
assert r.status_code == 200
|
||||
assert r.json() == {"350123456789012": "Greenhouse sensor A"}
|
||||
|
||||
def test_get_app_tokens(self, client) -> None:
|
||||
r = client.get("/configs/app_tokens", headers=self.HDRS)
|
||||
assert r.status_code == 200
|
||||
assert r.json() == {"tokens": {"test-app": "secret-abc"}}
|
||||
|
||||
def test_get_unknown_config_404(self, client) -> None:
|
||||
r = client.get("/configs/does-not-exist", headers=self.HDRS)
|
||||
assert r.status_code == 404
|
||||
|
||||
def test_config_endpoints_require_token(self, client) -> None:
|
||||
assert client.get("/configs/imei_map").status_code == 401
|
||||
assert client.put("/configs/imei_map", json={}).status_code == 401
|
||||
|
||||
def test_put_valid_imei_map(self, client, app_module) -> None:
|
||||
import os
|
||||
|
||||
body = {"350123456789012": "Greenhouse sensor A", "490154203237518": "Water tank sensor B"}
|
||||
r = client.put("/configs/imei_map", json=body, headers=self.HDRS)
|
||||
assert r.status_code == 200
|
||||
assert r.json() == {"status": "updated", "config": "imei_map"}
|
||||
# file on disk holds the new content
|
||||
with open(os.environ["IMEI_MAP_FILE"], encoding="utf-8") as f:
|
||||
assert json.load(f) == body
|
||||
|
||||
def test_post_is_alias_for_put(self, client) -> None:
|
||||
body = {"350123456789012": "Greenhouse sensor A"}
|
||||
r = client.post("/configs/imei_map", json=body, headers=self.HDRS)
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_put_rejects_non_imei_keys(self, client) -> None:
|
||||
r = client.put("/configs/imei_map", json={"0011223344556677": "X"}, headers=self.HDRS)
|
||||
assert r.status_code == 422
|
||||
assert "15-digit IMEI" in r.json()["detail"]
|
||||
|
||||
def test_put_rejects_non_string_values(self, client) -> None:
|
||||
r = client.put("/configs/imei_map", json={"350123456789012": 42}, headers=self.HDRS)
|
||||
assert r.status_code == 422
|
||||
|
||||
def test_put_rejects_bad_token_file_shape(self, client) -> None:
|
||||
# JSON object, but not the {"tokens": {...}} shape
|
||||
r = client.put("/configs/app_tokens", json={"foo": "bar"}, headers=self.HDRS)
|
||||
assert r.status_code == 422
|
||||
|
||||
def test_put_rejects_empty_token_secret(self, client) -> None:
|
||||
r = client.put(
|
||||
"/configs/app_tokens", json={"tokens": {"app": ""}}, headers=self.HDRS
|
||||
)
|
||||
assert r.status_code == 422
|
||||
|
||||
def test_non_object_body_rejected_by_fastapi(self, client) -> None:
|
||||
# JSON but not an object: FastAPI's dict[str, Any] annotation rejects
|
||||
# arrays and scalars before the handler runs
|
||||
r = client.put("/configs/imei_map", json=[1, 2, 3], headers=self.HDRS)
|
||||
assert r.status_code == 422
|
||||
r = client.put("/configs/imei_map", json="just a string", headers=self.HDRS)
|
||||
assert r.status_code == 422
|
||||
|
||||
def test_app_tokens_update_reloads_auth(self, client, app_module) -> None:
|
||||
# rotate the token, then authenticate with the NEW one
|
||||
new_tokens = {"tokens": {"test-app": "secret-abc", "rotated": "fresh-secret"}}
|
||||
r = client.put("/configs/app_tokens", json=new_tokens, headers=self.HDRS)
|
||||
assert r.status_code == 200
|
||||
r = client.get("/configs/app_tokens", headers={"X-App-Token": "fresh-secret"})
|
||||
assert r.status_code == 200
|
||||
# the old token still works too (it was kept in the file)
|
||||
r = client.get("/configs/app_tokens", headers=self.HDRS)
|
||||
assert r.status_code == 200
|
||||
Reference in New Issue
Block a user