# retoor from datetime import datetime, timedelta, timezone JSON = {"Accept": "application/json"} INVALID_MESSAGE = "This invite link is invalid or has expired." def _join_uid(admin, name, contact): response = admin.post("/join", json={"name": name, "contact": contact}, headers=JSON) assert response.status_code == 200 rows = admin.get("/admin/joins", headers=JSON).json()["requests"] return next(row for row in rows if row["name"] == name)["uid"] def _issue(admin, uid): response = admin.post(f"/admin/joins/{uid}/invite", headers=JSON) assert response.status_code == 200, response.text[:300] return response.json()["data"] def _claim(anon, token, payload): return anon.post(f"/invite/{token}", json=payload, headers=JSON) def test_invite_full_claim_flow(anon, admin): uid = _join_uid(admin, "Invitee One", "invitee.one@example.invalid") issued = _issue(admin, uid) token = issued["claim_url"].rsplit("/", 1)[-1] page = anon.get(f"/invite/{token}") assert page.status_code == 200 assert "Accept your invite" in page.text claimed = _claim( anon, token, { "username": "invitee_one", "email": "invitee.one@example.invalid", "password": "secret-one-1", "password_confirm": "secret-one-1", "terms": True, }, ) assert claimed.status_code == 200, claimed.text[:300] assert claimed.json()["data"]["username"] == "invitee_one" assert "session" in anon.cookies rows = admin.get("/admin/joins", headers=JSON).json()["requests"] row = next(row for row in rows if row["uid"] == uid) assert row["status"] == "accepted" assert row["invite_open"] is False again = _claim( anon, token, { "username": "invitee_other", "email": "invitee.one@example.invalid", "password": "secret-one-2", "password_confirm": "secret-one-2", "terms": True, }, ) assert again.status_code == 404 assert again.json()["error"]["message"] == INVALID_MESSAGE def test_invite_issue_requires_admin(anon, member, admin): uid = _join_uid(admin, "Invitee Two", "invitee.two@example.invalid") assert anon.post(f"/admin/joins/{uid}/invite", headers=JSON).status_code == 401 assert member.post(f"/admin/joins/{uid}/invite", headers=JSON).status_code == 403 assert anon.post(f"/admin/joins/{uid}/invite/revoke", headers=JSON).status_code == 401 def test_invite_declined_request_is_refused(admin): uid = _join_uid(admin, "Invitee Three", "invitee.three@example.invalid") declined = admin.post(f"/admin/joins/{uid}/status", json={"status": "declined"}, headers=JSON) assert declined.status_code == 200 refused = admin.post(f"/admin/joins/{uid}/invite", headers=JSON) assert refused.status_code == 409 def test_invite_wrong_email_is_rejected(anon, admin): uid = _join_uid(admin, "Invitee Four", "invitee.four@example.invalid") token = _issue(admin, uid)["claim_url"].rsplit("/", 1)[-1] response = _claim( anon, token, { "username": "invitee_four", "email": "someone.else@example.invalid", "password": "secret-four-1", "password_confirm": "secret-four-1", "terms": True, }, ) assert response.status_code == 400 assert "different email address" in response.json()["error"]["message"] def test_invite_expired_token_reveals_nothing(anon, admin): from molodetz.database import db uid = _join_uid(admin, "Invitee Five", "invitee.five@example.invalid") issued = _issue(admin, uid) token = issued["claim_url"].rsplit("/", 1)[-1] past = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat() with db: db.query("UPDATE member_invites SET expires_at = :past WHERE join_request_uid = :uid", past=past, uid=uid) page = anon.get(f"/invite/{token}") assert page.status_code == 404 assert INVALID_MESSAGE in page.text response = _claim( anon, token, { "username": "invitee_five", "email": "invitee.five@example.invalid", "password": "secret-five-1", "password_confirm": "secret-five-1", "terms": True, }, ) assert response.status_code == 404 assert response.json()["error"]["message"] == INVALID_MESSAGE def test_invite_unknown_token_reveals_nothing(anon): page = anon.get("/invite/does-not-exist") assert page.status_code == 404 assert INVALID_MESSAGE in page.text response = _claim( anon, "does-not-exist", {"username": "ghost_user", "email": "ghost@example.invalid", "password": "secret-1", "password_confirm": "secret-1", "terms": True}, ) assert response.status_code == 404 assert response.json()["error"]["message"] == INVALID_MESSAGE def test_invite_revoke_kills_token(anon, admin): uid = _join_uid(admin, "Invitee Six", "invitee.six@example.invalid") token = _issue(admin, uid)["claim_url"].rsplit("/", 1)[-1] revoked = admin.post(f"/admin/joins/{uid}/invite/revoke", headers=JSON) assert revoked.status_code == 200 assert revoked.json()["data"]["revoked"] == 1 response = _claim( anon, token, { "username": "invitee_six", "email": "invitee.six@example.invalid", "password": "secret-six-1", "password_confirm": "secret-six-1", "terms": True, }, ) assert response.status_code == 404 def test_invite_reissue_revokes_previous(anon, admin): uid = _join_uid(admin, "Invitee Seven", "invitee.seven@example.invalid") first = _issue(admin, uid)["claim_url"].rsplit("/", 1)[-1] second = _issue(admin, uid)["claim_url"].rsplit("/", 1)[-1] assert first != second stale = _claim( anon, first, { "username": "invitee_seven", "email": "invitee.seven@example.invalid", "password": "secret-seven-1", "password_confirm": "secret-seven-1", "terms": True, }, ) assert stale.status_code == 404 fresh = _claim( anon, second, { "username": "invitee_seven", "email": "invitee.seven@example.invalid", "password": "secret-seven-1", "password_confirm": "secret-seven-1", "terms": True, }, ) assert fresh.status_code == 200 def test_invite_claim_validation(anon, admin): uid = _join_uid(admin, "Invitee Eight", "invitee.eight@example.invalid") token = _issue(admin, uid)["claim_url"].rsplit("/", 1)[-1] short = _claim( anon, token, { "username": "invitee_eight", "email": "invitee.eight@example.invalid", "password": "short", "password_confirm": "short", "terms": True, }, ) assert short.status_code == 422 mismatch = _claim( anon, token, { "username": "invitee_eight", "email": "invitee.eight@example.invalid", "password": "secret-eight-1", "password_confirm": "secret-eight-2", "terms": True, }, ) assert mismatch.status_code == 400 no_terms = _claim( anon, token, { "username": "invitee_eight", "email": "invitee.eight@example.invalid", "password": "secret-eight-1", "password_confirm": "secret-eight-1", "terms": False, }, ) assert no_terms.status_code == 400 assert "house rules" in no_terms.json()["error"]["message"] def test_invite_html_claim_flow(anon, admin): uid = _join_uid(admin, "Invitee Nine", "invitee.nine@example.invalid") token = _issue(admin, uid)["claim_url"].rsplit("/", 1)[-1] bad_form = anon.post( f"/invite/{token}", data={ "username": "x", "email": "invitee.nine@example.invalid", "password": "secret-nine-1", "password_confirm": "secret-nine-1", "terms": "true", }, ) assert bad_form.status_code == 400 assert "Accept your invite" in bad_form.text response = anon.post( f"/invite/{token}", data={ "username": "invitee_nine", "email": "invitee.nine@example.invalid", "password": "secret-nine-1", "password_confirm": "secret-nine-1", "terms": "true", }, ) assert response.status_code in (302, 303) assert "session" in anon.cookies used = anon.get(f"/invite/{token}") assert used.status_code == 404 assert INVALID_MESSAGE in used.text