"""Route-level tests for the bookmarks feature: CRUD, upsert-on-duplicate, ownership, auth, bulk import (dedupe/counts), and the per-user cap. Uses a throwaway accounts DB (conftest points THERMOGRAPH_ACCOUNTS_DB at a temp file), same as test_api_accounts.py.""" import pytest from fastapi.testclient import TestClient from web import app as appmod from accounts 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_bookmarks_require_auth(): c = _client() assert c.get(f"{V2}/bookmarks").status_code == 401 assert c.post(f"{V2}/bookmarks", json={"lat": 1, "lon": 1, "label": "x"}).status_code == 401 assert c.post(f"{V2}/bookmarks/import", json={"items": []}).status_code == 401 def test_bookmark_crud(): c = _client() _login(c, "bm-crud@example.com") r = c.post(f"{V2}/bookmarks", json={"lat": 47.6, "lon": -122.3, "label": "Seattle"}) assert r.status_code == 201 bm = r.json() assert bm["cell_id"] and bm["label"] == "Seattle" assert bm["lat"] == 47.6 and bm["lon"] == -122.3 listed = c.get(f"{V2}/bookmarks").json()["bookmarks"] assert len(listed) == 1 and listed[0]["id"] == bm["id"] patched = c.patch(f"{V2}/bookmarks/{bm['id']}", json={"label": "Home"}) assert patched.status_code == 200 assert patched.json()["label"] == "Home" assert c.delete(f"{V2}/bookmarks/{bm['id']}").status_code == 204 assert c.get(f"{V2}/bookmarks").json()["bookmarks"] == [] def test_bookmark_upsert_on_duplicate(): c = _client() _login(c, "bm-upsert@example.com") body = {"lat": 33.4, "lon": -112.0, "label": "Phoenix"} first = c.post(f"{V2}/bookmarks", json=body) assert first.status_code == 201 bm_id = first.json()["id"] # Re-bookmarking the same location (same cell) upserts the label — 200, not 409. second = c.post(f"{V2}/bookmarks", json={**body, "label": "Phoenix Home"}) assert second.status_code == 200 assert second.json()["id"] == bm_id assert second.json()["label"] == "Phoenix Home" assert len(c.get(f"{V2}/bookmarks").json()["bookmarks"]) == 1 def test_bookmark_validation(): c = _client() _login(c, "bm-valid@example.com") assert c.post(f"{V2}/bookmarks", json={"lat": 1, "lon": 1, "label": " "}).status_code == 422 assert c.post(f"{V2}/bookmarks", json={"lat": 1, "lon": 1, "label": "x" * 81}).status_code == 422 assert c.post(f"{V2}/bookmarks", json={"lat": 91, "lon": 1, "label": "x"}).status_code == 422 assert c.post(f"{V2}/bookmarks", json={"lat": 1, "lon": 181, "label": "x"}).status_code == 422 # a label that's only over-length due to surrounding whitespace is fine once trimmed ok = c.post(f"{V2}/bookmarks", json={"lat": 2, "lon": 2, "label": " Trimmed "}) assert ok.status_code == 201 and ok.json()["label"] == "Trimmed" def test_ownership_is_404(): a, b = _client(), _client() _login(a, "bm-owner-a@example.com") _login(b, "bm-owner-b@example.com") bm_id = a.post(f"{V2}/bookmarks", json={"lat": 40.7, "lon": -74.0, "label": "NYC"}).json()["id"] # B cannot see, patch, or delete A's bookmark — 404, not 403 (no existence leak) assert b.patch(f"{V2}/bookmarks/{bm_id}", json={"label": "Nope"}).status_code == 404 assert b.delete(f"{V2}/bookmarks/{bm_id}").status_code == 404 assert b.get(f"{V2}/bookmarks").json()["bookmarks"] == [] def test_bookmark_import_dedupe_and_counts(): c = _client() _login(c, "bm-import@example.com") # Pre-existing bookmark at the Seattle cell, with a label import must NOT overwrite. existing = c.post( f"{V2}/bookmarks", json={"lat": 47.6, "lon": -122.3, "label": "Original"} ).json() assert existing["label"] == "Original" r = c.post(f"{V2}/bookmarks/import", json={"items": [ {"lat": 47.6, "lon": -122.3, "label": "Should Not Overwrite"}, # dup of existing cell {"lat": 34.0, "lon": -118.2, "label": "LA"}, # new {"lat": 34.0, "lon": -118.2, "label": "LA Dup"}, # dup within payload {"lat": 51.5, "lon": -0.1, "label": "London"}, # new ]}) assert r.status_code == 200 body = r.json() assert body["imported"] == 2 assert body["skipped"] == 2 assert len(body["bookmarks"]) == 3 by_label = {b["label"] for b in body["bookmarks"]} assert by_label == {"Original", "LA", "London"} # existing label untouched def test_bookmark_cap(): c = _client() _login(c, "bm-cap@example.com") for i in range(200): r = c.post(f"{V2}/bookmarks", json={"lat": i * 0.1, "lon": 0.0, "label": f"spot-{i}"}) assert r.status_code == 201, r.text over = c.post(f"{V2}/bookmarks", json={"lat": 89.0, "lon": 179.0, "label": "one too many"}) assert over.status_code == 409 assert len(c.get(f"{V2}/bookmarks").json()["bookmarks"]) == 200 def test_bookmark_import_stops_at_cap(): c = _client() _login(c, "bm-import-cap@example.com") for i in range(198): r = c.post(f"{V2}/bookmarks", json={"lat": i * 0.1, "lon": 10.0, "label": f"spot-{i}"}) assert r.status_code == 201, r.text r = c.post(f"{V2}/bookmarks/import", json={"items": [ {"lat": 60.0, "lon": 60.0, "label": "a"}, {"lat": 61.0, "lon": 61.0, "label": "b"}, {"lat": 62.0, "lon": 62.0, "label": "c"}, {"lat": 63.0, "lon": 63.0, "label": "d"}, {"lat": 64.0, "lon": 64.0, "label": "e"}, ]}) assert r.status_code == 200 body = r.json() # Only 2 slots remained before the 200 cap; the rest count as skipped. assert body["imported"] == 2 assert body["skipped"] == 3 assert len(body["bookmarks"]) == 200