promote: dev → main (location bookmarks) #119
1 changed files with 160 additions and 0 deletions
160
backend/tests/accounts/test_bookmarks.py
Normal file
160
backend/tests/accounts/test_bookmarks.py
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
"""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
|
||||
Loading…
Reference in a new issue