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.
107 lines
3.7 KiB
Python
107 lines
3.7 KiB
Python
"""First-run setup, login, and the basic frame-visibility permission
|
|
gate (require_frame_view/can_view_frame) -- the things every other
|
|
endpoint's own permission test implicitly depends on already working."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from app.models import Frame
|
|
|
|
from .conftest import get_csrf_token, link_user, login, make_user
|
|
|
|
|
|
def test_setup_creates_admin_and_claims_migrated_frame(client, db_session):
|
|
resp = client.post("/setup", data={
|
|
"username": "alice", "password": "hunter22", "display_name": "Alice",
|
|
})
|
|
assert resp.status_code == 303
|
|
assert resp.headers["location"] == "/"
|
|
|
|
frame = db_session.get(Frame, 1)
|
|
assert frame is not None
|
|
assert frame.owner_user_id is not None
|
|
assert frame.controlled_by_user_id is not None
|
|
|
|
|
|
def test_setup_only_works_once(client):
|
|
resp = client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
|
assert resp.status_code == 303
|
|
|
|
resp = client.post("/setup", data={"username": "mallory", "password": "hunter22"})
|
|
assert resp.status_code == 403
|
|
|
|
|
|
def test_login_requires_correct_password(client):
|
|
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
|
client.cookies.clear()
|
|
|
|
resp = client.post("/login", data={"username": "alice", "password": "wrong"})
|
|
assert resp.status_code == 401
|
|
|
|
resp = client.post("/login", data={"username": "alice", "password": "hunter22"})
|
|
assert resp.status_code == 303
|
|
|
|
|
|
def test_root_redirects_to_setup_before_any_user_exists(client):
|
|
resp = client.get("/")
|
|
assert resp.status_code == 303
|
|
assert resp.headers["location"] == "/setup"
|
|
|
|
|
|
def test_unauthenticated_request_redirects_to_login_once_a_user_exists(client):
|
|
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
|
client.cookies.clear()
|
|
|
|
resp = client.get("/")
|
|
assert resp.status_code == 303
|
|
assert resp.headers["location"] == "/login"
|
|
|
|
|
|
def test_user_not_linked_to_a_frame_cannot_view_it(client, db_session):
|
|
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
|
bob = make_user(db_session, "bob")
|
|
db_session.flush()
|
|
|
|
client.cookies.clear()
|
|
login(client, "bob")
|
|
|
|
resp = client.get("/frames/1")
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_linked_user_can_view_but_not_configure_by_default(client, db_session):
|
|
"""Being linked grants view access; whether they can also *control*
|
|
(change settings/take the wheel) is a separate, narrower gate --
|
|
require_frame_control, exercised via the permission-boundary tests
|
|
for individual features rather than here."""
|
|
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)
|
|
|
|
client.cookies.clear()
|
|
login(client, "bob")
|
|
|
|
resp = client.get("/frames/1")
|
|
assert resp.status_code == 200
|
|
|
|
|
|
def test_csrf_token_required_for_settings_save(client):
|
|
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
|
resp = client.post("/settings", data={
|
|
"display_name": "Alice", "email": "[email protected]", "csrf_token": "bogus",
|
|
})
|
|
assert resp.status_code == 403
|
|
|
|
|
|
def test_settings_save_round_trips_with_real_csrf_token(client, db_session):
|
|
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
|
csrf = get_csrf_token(client, "/settings")
|
|
resp = client.post("/settings", data={
|
|
"display_name": "Alice Smith", "email": "[email protected]", "csrf_token": csrf,
|
|
})
|
|
assert resp.status_code in (200, 303), resp.text
|
|
|
|
from app.models import User
|
|
user = db_session.query(User).filter_by(username="alice").one()
|
|
assert user.display_name == "Alice Smith"
|