"""Tests for the web layer: login, the brew-process page and the API.""" from __future__ import annotations import pytest from conftest import seed_readings @pytest.fixture() def logged_in(client, repo): """A client with an authenticated session and a user in the database.""" repo.users.insert_one({"username": "beer", "password": "beer_pass"}) response = client.post("/login", data={"username": "beer", "password": "beer_pass"}, follow_redirects=True) assert response.status_code == 200 return client # ------------------------------------------------------------------ public # def test_healthz_is_public(client): response = client.get("/healthz") assert response.status_code == 200 assert response.json == {"status": "ok"} def test_root_redirects_to_login(client): response = client.get("/") assert response.status_code == 302 assert "/login" in response.headers["Location"] def test_login_page_renders(client): response = client.get("/login") assert response.status_code == 200 assert b"Brew Log" in response.data def test_login_with_bad_credentials_is_rejected(client, repo): repo.users.insert_one({"username": "beer", "password": "beer_pass"}) response = client.post("/login", data={"username": "beer", "password": "wrong"}, follow_redirects=True) assert response.status_code == 200 assert b"not recognised" in response.data def test_protected_pages_redirect_when_logged_out(client): for path in ("/brews", "/runs/1"): response = client.get(path) assert response.status_code == 302, path assert "/login" in response.headers["Location"], path # ------------------------------------------------------------- brew pages # def test_brews_page_lists_runs(logged_in, repo): seed_readings(repo, [ ("1", "Pale Ale", "1050", "2026-01-01", False, "start"), ("1", "Pale Ale", "1010", "2026-01-20", True, "Keghing"), ("2", "Ginger Beer", "1045", "2026-02-01", False, ""), ]) response = logged_in.get("/brews") assert response.status_code == 200 body = response.data.decode() assert "Pale Ale" in body assert "Ginger Beer" in body # The comment on the reading should be surfaced. assert "Keghing" in body def test_run_detail_page_renders(logged_in, repo): seed_readings(repo, [ ("1", "Pale Ale", "1050", "2026-01-01", False, "start"), ("1", "Pale Ale", "1010", "2026-01-20", True, "Keghing"), ]) response = logged_in.get("/runs/1") assert response.status_code == 200 body = response.data.decode() assert "Pale Ale" in body assert "Keghing" in body assert "5.25%" in body # ------------------------------------------------------------- write paths # def test_recording_a_reading_stores_it(logged_in, repo): response = logged_in.post("/brews", data={ "beer_run_id": "5", "beer_run_type": "Test Porter", "Date": "2026-03-04", "sg": "1045", "comment": "Hops in", }, follow_redirects=True) assert response.status_code == 200 doc = repo.readings.find_one({"beer_run_id": "5"}) assert doc is not None assert doc["sg"] == "1045" assert doc["beer_type"] == "Test Porter" assert doc["comment"] == "Hops in" assert doc["final_reading"] == "False" def test_recording_a_reading_twice_updates_in_place(logged_in, repo): """The unique key is (date, beer_run_id) — no duplicate rows.""" for sg in ("1045", "1035"): logged_in.post("/brews", data={ "beer_run_id": "6", "beer_run_type": "Test", "Date": "2026-03-04", "sg": sg, }) count = repo.readings.count_documents({"beer_run_id": "6"}) doc = repo.readings.find_one({"beer_run_id": "6"}) assert count == 1 assert doc["sg"] == "1035" def test_setting_a_final_reading_closes_the_run(logged_in, repo): seed_readings(repo, [("7", "Ale", "1045", "2026-03-01", False, "")]) response = logged_in.post("/runs/7/final", data={"date": "2026-03-01"}, follow_redirects=True) assert response.status_code == 200 doc = repo.readings.find_one({"beer_run_id": "7"}) assert doc["final_reading"] == "True" def test_unsetting_a_final_reading_reactivates_the_run(logged_in, repo): seed_readings(repo, [ ("7", "Ale", "1045", "2026-03-01", False, ""), ("7", "Ale", "1010", "2026-03-20", True, ""), ]) logged_in.post("/runs/7/unfinal", data={"date": "2026-03-20"}, follow_redirects=True) doc = repo.readings.find_one({"beer_run_id": "7", "date": "2026-03-20"}) assert doc["final_reading"] == "False" # -------------------------------------------------------------------- API # # ----------------------------------------------------------------- misc # def test_static_assets_are_served(client): for path in ("/static/js/app.js", "/static/css/app.css", "/static/img/logo.svg"): response = client.get(path) assert response.status_code == 200, path assert len(response.data) > 100, path def test_logout_clears_the_session(logged_in): response = logged_in.post("/logout", follow_redirects=True) assert response.status_code == 200 # After logging out the protected page bounces back to the login page. response = logged_in.get("/brews") assert response.status_code == 302 assert "/login" in response.headers["Location"] def test_out_of_range_reading_is_saved_but_warned(logged_in, repo): response = logged_in.post("/brews", data={ "beer_run_id": "12", "beer_run_type": "Odd", "Date": "2026-05-01", "sg": "950", }, follow_redirects=True) assert response.status_code == 200 assert b"outside the usual hydrometer range" in response.data # It is still stored — the warning is advisory, not a rejection. assert repo.readings.find_one({"beer_run_id": "12"}) is not None