"""Who gets in, and who does not.""" from __future__ import annotations import pytest from apps.board import gitea PROTECTED = [ "/comunidad/", "/comunidad/nuevo", "/comunidad/miembros", "/comunidad/miembros/nuevo", "/comunidad/mis-datos", ] @pytest.mark.parametrize("path", PROTECTED) def test_anonymous_is_sent_to_login(client, path): response = client.get(path) assert response.status_code == 302 assert "/comunidad/login" in response.headers["Location"] def _stub_gitea(monkeypatch, login): monkeypatch.setattr(gitea, "exchange_code", lambda code, uri: "token") monkeypatch.setattr(gitea, "fetch_user", lambda token: { "login": login, "full_name": login.title(), "email": f"{login}@example.com", }) def _callback(client, monkeypatch, login): _stub_gitea(monkeypatch, login) with client.session_transaction() as session: session["oauth_state"] = "state123" return client.get("/comunidad/auth/callback?code=abc&state=state123") def test_a_gitea_account_is_not_a_membership(client, monkeypatch): """The single most important rule in the app: Gitea says who you are, the members table says whether you belong. The translations bot has a perfectly valid Gitea account and must not get in.""" response = _callback(client, monkeypatch, "vienalatina-translations") assert response.status_code == 302 with client.session_transaction() as session: assert "member_id" not in session def test_member_signs_in(client, monkeypatch, make_member): make_member("maria") response = _callback(client, monkeypatch, "maria") assert response.status_code == 302 with client.session_transaction() as session: assert "member_id" in session def test_suspended_member_cannot_sign_in(client, monkeypatch, make_member): make_member("expulsada", active=0) _callback(client, monkeypatch, "expulsada") with client.session_transaction() as session: assert "member_id" not in session def test_suspension_takes_effect_on_the_next_request(client, db, make_member, sign_in): """The role is read per request, not cached in the cookie, so revoking access does not wait for a session to expire.""" member_id = make_member("temporal") sign_in(member_id) assert client.get("/comunidad/").status_code == 200 db.execute("UPDATE members SET active = 0 WHERE id = ?", (member_id,)) assert client.get("/comunidad/").status_code == 302 def test_callback_rejects_a_mismatched_state(client, monkeypatch, make_member): make_member("maria") _stub_gitea(monkeypatch, "maria") with client.session_transaction() as session: session["oauth_state"] = "the-real-state" client.get("/comunidad/auth/callback?code=abc&state=attacker-state") with client.session_transaction() as session: assert "member_id" not in session def test_post_without_csrf_is_refused(client, make_member, sign_in): sign_in(make_member("maria")) response = client.post("/comunidad/nuevo", data={"title": "Hola", "body": "Texto"}) assert response.status_code == 400 def test_login_redirect_cannot_be_pointed_offsite(client, monkeypatch, make_member): make_member("maria") _stub_gitea(monkeypatch, "maria") with client.session_transaction() as session: session["oauth_state"] = "state123" response = client.get( "/comunidad/auth/callback?code=abc&state=state123&next=https://evil.example.com/" ) assert "evil.example.com" not in response.headers["Location"] def test_responses_say_do_not_index(client): response = client.get("/comunidad/login") assert response.headers["X-Robots-Tag"] == "noindex, nofollow"