The notification dropdown is anchored to the bell button, which on mobile sits left of the account button rather than at the screen edge, so the wide panel overflowed off the left of the viewport (title/text cut off). On narrow screens, drop .notif's positioning context so the dropdown anchors to the .acct cluster at the header's right edge, keeping it fully on-screen. Also add route-level tests for the accounts feature (auth flow, subscription CRUD, duplicate/validation, ownership 404s, notifications), plus a THERMOGRAPH_ACCOUNTS_DB override so the suite writes to a throwaway DB and stays hermetic.
101 lines
3.9 KiB
Python
101 lines
3.9 KiB
Python
"""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
|