thermograph/tests/test_api_accounts.py

102 lines
3.9 KiB
Python
Raw Normal View History

"""Route-level tests for the accounts feature: auth (fastapi-users), subscription
CRUD, and the notifications endpoints. Uses a throwaway accounts DB (conftest points
THERMOGRAPH_ACCOUNTS_DB at a temp file) so nothing is written into the repo."""
import pytest
from fastapi.testclient import TestClient
import app as appmod
import db
V2 = "/thermograph/api/v2"
PW = "supersecret123"
@pytest.fixture(scope="module", autouse=True)
def _tables():
# TestClient(app) (no context manager) skips the lifespan, so create the
# account tables directly against the temp DB.
db.Base.metadata.create_all(db.sync_engine)
def _client():
return TestClient(appmod.app)
def _login(client, email):
r = client.post(f"{V2}/auth/register", json={"email": email, "password": PW})
assert r.status_code in (201, 400) # 400 only if the email already exists
r = client.post(f"{V2}/auth/login", data={"username": email, "password": PW})
assert r.status_code == 204 # sets the session cookie on the client
def test_auth_flow():
c = _client()
_login(c, "auth-flow@example.com")
me = c.get(f"{V2}/users/me")
assert me.status_code == 200 and me.json()["email"] == "auth-flow@example.com"
assert c.post(f"{V2}/auth/logout").status_code == 204
assert c.get(f"{V2}/users/me").status_code == 401
def test_requires_auth():
c = _client()
assert c.get(f"{V2}/subscriptions").status_code == 401
assert c.get(f"{V2}/notifications").status_code == 401
def test_subscription_crud():
c = _client()
_login(c, "crud@example.com")
r = c.post(f"{V2}/subscriptions", json={
"lat": 47.6, "lon": -122.3, "label": "Seattle", "threshold": 96,
"metrics": ["tmax", "feels"], "kind": "observed", "two_sided": True,
})
assert r.status_code == 201
sub = r.json()
assert sub["cell_id"] and sub["threshold"] == 96 and sub["metrics"] == ["tmax", "feels"]
assert len(c.get(f"{V2}/subscriptions").json()) == 1
patched = c.patch(f"{V2}/subscriptions/{sub['id']}", json={"threshold": 99, "active": False})
assert patched.status_code == 200
assert patched.json()["threshold"] == 99 and patched.json()["active"] is False
assert c.delete(f"{V2}/subscriptions/{sub['id']}").status_code == 204
assert c.get(f"{V2}/subscriptions").json() == []
def test_duplicate_and_validation():
c = _client()
_login(c, "dup@example.com")
body = {"lat": 33.4, "lon": -112.0, "threshold": 97, "metrics": ["tmax"], "kind": "observed"}
assert c.post(f"{V2}/subscriptions", json=body).status_code == 201
assert c.post(f"{V2}/subscriptions", json=body).status_code == 409 # same cell + kind
# a forecast subscription on the same cell is allowed (different kind)
assert c.post(f"{V2}/subscriptions", json={**body, "kind": "forecast"}).status_code == 201
# threshold must be 95..99
assert c.post(f"{V2}/subscriptions", json={**body, "threshold": 90}).status_code == 422
# at least one metric required
assert c.post(f"{V2}/subscriptions", json={**body, "metrics": []}).status_code == 422
def test_ownership_is_404():
a, b = _client(), _client()
_login(a, "owner-a@example.com")
_login(b, "owner-b@example.com")
sub_id = a.post(f"{V2}/subscriptions", json={
"lat": 40.7, "lon": -74.0, "threshold": 95, "metrics": ["tmax"], "kind": "observed",
}).json()["id"]
# B cannot see, patch, or delete A's subscription — 404, not 403 (no existence leak)
assert b.patch(f"{V2}/subscriptions/{sub_id}", json={"threshold": 98}).status_code == 404
assert b.delete(f"{V2}/subscriptions/{sub_id}").status_code == 404
assert b.get(f"{V2}/subscriptions").json() == []
def test_notifications_empty_and_read_all():
c = _client()
_login(c, "notif@example.com")
r = c.get(f"{V2}/notifications")
assert r.status_code == 200
assert r.json() == {"notifications": [], "unread_count": 0}
assert c.post(f"{V2}/notifications/read-all").status_code == 204