Files
tfaour 31adc34a19
Build and push server image / test (push) Successful in 1m18s
Build and push server image / build-and-push (push) Successful in 2m12s
Add a real pytest suite, gating the CI build
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.
2026-07-23 18:51:54 -04:00

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"