64 tests covering: auth/setup and the CSRF gate, the "owner adds their own data, anyone linked can mute it" permission pattern shared across calendar-select/tasks-source/whiteboard-source, migration correctness (fresh install, idempotent re-run, expected columns), battery estimate outlier rejection, calendar_feed's fetch/merge/partial-failure handling, webdav_client's fetch/list-directory, the whiteboard force-refresh throttle bypass and browse endpoint, and render-size invariants across calendar views/orientations. No DB/HTTP fixtures need Docker, Node, or a real Immich/CalDAV/WebDAV server -- a fresh temp SQLite file plus a couple of small local HTTP servers as test doubles cover it all. Table data is wiped and reseeded between tests rather than relying on SQLAlchemy's transaction-rollback isolation pattern, which needs a pysqlite event-listener workaround app/db.py's engine doesn't have and has no reason to gain just for tests. Wired into .gitea/workflows/server-docker-build.yml as its own job that build-and-push now depends on, so a failing suite blocks the image push rather than just running alongside it for show.
179 lines
7.1 KiB
Python
179 lines
7.1 KiB
Python
"""The "owner controls adding their own data; anyone linked can mute it"
|
|
permission pattern, repeated across calendar-select, tasks-source, and
|
|
whiteboard-source -- exercised at the HTTP layer (not just unit-level)
|
|
since the whole point is verifying the *endpoint's* authorization check,
|
|
not just a helper function's logic."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from app.models import Frame, FrameCalendar, User
|
|
|
|
from .conftest import csrf_headers, link_user, login, make_user
|
|
|
|
|
|
def _setup_two_linked_users(client, db_session) -> Frame:
|
|
"""alice is the frame's admin/owner (via /setup); bob is a second
|
|
user linked to the same frame #1 but neither owns nor controls it."""
|
|
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
|
bob = make_user(db_session, "bob")
|
|
frame = db_session.get(Frame, 1)
|
|
link_user(db_session, bob, frame)
|
|
return frame
|
|
|
|
|
|
# --- whiteboard-source ---
|
|
|
|
def test_whiteboard_source_owner_can_set_it(client, db_session):
|
|
_setup_two_linked_users(client, db_session)
|
|
# alice is still logged in from /setup
|
|
resp = client.post("/api/frames/1/whiteboard-source", json={
|
|
"url": "https://cloud.example.com/dav/files/alice/board.whiteboard",
|
|
}, headers=csrf_headers(client))
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
frame = db_session.get(Frame, 1)
|
|
assert frame.whiteboard_user_id is not None
|
|
assert frame.whiteboard_url == "https://cloud.example.com/dav/files/alice/board.whiteboard"
|
|
|
|
|
|
def test_whiteboard_source_set_always_targets_the_caller(client, db_session):
|
|
"""bob has no way to point the frame at someone else's account --
|
|
there's no target-user field in the request at all, so a "set" call
|
|
from bob always attaches to bob, even if he pastes alice's URL."""
|
|
_setup_two_linked_users(client, db_session)
|
|
client.cookies.clear()
|
|
login(client, "bob")
|
|
|
|
resp = client.post("/api/frames/1/whiteboard-source", json={
|
|
"url": "https://cloud.example.com/dav/files/alice/board.whiteboard",
|
|
}, headers=csrf_headers(client))
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
frame = db_session.get(Frame, 1)
|
|
bob_row = db_session.query(User).filter_by(username="bob").one()
|
|
assert frame.whiteboard_user_id == bob_row.id
|
|
|
|
|
|
def test_whiteboard_source_anyone_linked_can_clear(client, db_session):
|
|
_setup_two_linked_users(client, db_session)
|
|
client.post("/api/frames/1/whiteboard-source", json={"url": "https://cloud.example.com/board.whiteboard"},
|
|
headers=csrf_headers(client))
|
|
|
|
client.cookies.clear()
|
|
login(client, "bob")
|
|
resp = client.post("/api/frames/1/whiteboard-source", json={"url": None}, headers=csrf_headers(client))
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
frame = db_session.get(Frame, 1)
|
|
assert frame.whiteboard_url == ""
|
|
assert frame.whiteboard_user_id is None
|
|
|
|
|
|
def test_whiteboard_source_rejects_non_http_url(client, db_session):
|
|
_setup_two_linked_users(client, db_session)
|
|
resp = client.post("/api/frames/1/whiteboard-source", json={"url": "javascript:alert(1)"},
|
|
headers=csrf_headers(client))
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_whiteboard_source_unlinked_user_cannot_touch_it(client, db_session):
|
|
_setup_two_linked_users(client, db_session)
|
|
make_user(db_session, "mallory") # exists, but never linked to frame 1
|
|
client.cookies.clear()
|
|
login(client, "mallory")
|
|
|
|
resp = client.post("/api/frames/1/whiteboard-source", json={"url": "https://x.example.com/b.whiteboard"},
|
|
headers=csrf_headers(client, "/settings"))
|
|
assert resp.status_code == 404
|
|
|
|
|
|
# --- tasks-source ---
|
|
|
|
def test_tasks_source_set_always_targets_the_caller(client, db_session):
|
|
_setup_two_linked_users(client, db_session)
|
|
client.cookies.clear()
|
|
login(client, "bob")
|
|
|
|
resp = client.post("/api/frames/1/tasks-source", json={"calendar_key": "caldav:/some/tasks/"},
|
|
headers=csrf_headers(client))
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
bob_row = db_session.query(User).filter_by(username="bob").one()
|
|
frame = db_session.get(Frame, 1)
|
|
assert frame.calendar_tasks_user_id == bob_row.id
|
|
assert frame.calendar_tasks_calendar_key == "caldav:/some/tasks/"
|
|
|
|
|
|
def test_tasks_source_anyone_linked_can_clear(client, db_session):
|
|
_setup_two_linked_users(client, db_session)
|
|
client.post("/api/frames/1/tasks-source", json={"calendar_key": "caldav:/alice/tasks/"},
|
|
headers=csrf_headers(client))
|
|
|
|
client.cookies.clear()
|
|
login(client, "bob")
|
|
resp = client.post("/api/frames/1/tasks-source", json={"calendar_key": None}, headers=csrf_headers(client))
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
frame = db_session.get(Frame, 1)
|
|
assert frame.calendar_tasks_user_id is None
|
|
assert frame.calendar_tasks_calendar_key is None
|
|
|
|
|
|
# --- calendar-select ---
|
|
|
|
def test_calendar_select_bob_cannot_add_alices_calendar(client, db_session):
|
|
_setup_two_linked_users(client, db_session)
|
|
alice_id = db_session.query(User).filter_by(username="alice").one().id
|
|
|
|
client.cookies.clear()
|
|
login(client, "bob")
|
|
resp = client.post("/api/frames/1/calendar-select", json={
|
|
"user_id": alice_id, "calendar_key": "ics", "included": True,
|
|
}, headers=csrf_headers(client))
|
|
assert resp.status_code == 403
|
|
|
|
|
|
def test_calendar_select_owner_can_add_their_own(client, db_session):
|
|
_setup_two_linked_users(client, db_session)
|
|
alice_id = db_session.query(User).filter_by(username="alice").one().id
|
|
|
|
resp = client.post("/api/frames/1/calendar-select", json={
|
|
"user_id": alice_id, "calendar_key": "ics", "included": True,
|
|
}, headers=csrf_headers(client))
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
row = db_session.query(FrameCalendar).filter_by(frame_id=1, user_id=alice_id, calendar_key="ics").one()
|
|
assert row.included is True
|
|
|
|
|
|
def test_calendar_select_bob_can_mute_alices_calendar(client, db_session):
|
|
"""Muting is a display-preference veto anyone linked gets, unlike
|
|
adding -- the one-sided half of this endpoint's permission split."""
|
|
_setup_two_linked_users(client, db_session)
|
|
alice_id = db_session.query(User).filter_by(username="alice").one().id
|
|
client.post("/api/frames/1/calendar-select", json={
|
|
"user_id": alice_id, "calendar_key": "ics", "included": True,
|
|
}, headers=csrf_headers(client))
|
|
|
|
client.cookies.clear()
|
|
login(client, "bob")
|
|
resp = client.post("/api/frames/1/calendar-select", json={
|
|
"user_id": alice_id, "calendar_key": "ics", "included": False,
|
|
}, headers=csrf_headers(client))
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
row = db_session.query(FrameCalendar).filter_by(frame_id=1, user_id=alice_id, calendar_key="ics").one()
|
|
assert row.included is False
|
|
|
|
|
|
def test_calendar_select_cannot_mute_a_calendar_that_was_never_added(client, db_session):
|
|
_setup_two_linked_users(client, db_session)
|
|
alice_id = db_session.query(User).filter_by(username="alice").one().id
|
|
|
|
client.cookies.clear()
|
|
login(client, "bob")
|
|
resp = client.post("/api/frames/1/calendar-select", json={
|
|
"user_id": alice_id, "calendar_key": "ics", "included": False,
|
|
}, headers=csrf_headers(client))
|
|
assert resp.status_code == 404
|