"""Sign-up, email confirmation, and the two gates in front of a session.
The gates are the point of this file. Everything else about sign-up is ordinary
form handling; what is worth pinning is that neither gate can be got past by the
paths that look like they might - a correct password, a confirmed address, or a
Google identity.
"""
import pytest
from app.core import auth as auth_core
from app.core import registration as registration_core
from app.core.context import Invalid
from db.db_declarations import EmailVerificationToken, User
from utils.db_wrapper import db_session
PASSWORD = "correcthorsebatterystaple"
# --- core -------------------------------------------------------------------
def test_registration_shuts_both_gates(session):
user, org = registration_core.register_org_and_owner(
session, "new@example.com", PASSWORD, "New Co")
assert auth_core.awaiting_verification(user)
assert auth_core.awaiting_approval(user)
assert org.slug
def test_provisioning_opens_both_gates(session):
"""The default, and the reason it is the default: every provisioning path
creates an account somebody already vouched for."""
user = auth_core.create_user(session, "cli@example.com", PASSWORD)
assert not auth_core.awaiting_verification(user)
assert not auth_core.awaiting_approval(user)
def test_a_failed_signup_leaves_nothing_behind(session):
auth_core.create_user(session, "taken@example.com", PASSWORD)
with pytest.raises(Invalid):
registration_core.register_org_and_owner(
session, "taken@example.com", PASSWORD, "Second Co")
# One user, and no organisation was made for the attempt.
assert session.query(User).filter(User.email == "taken@example.com").count() == 1
def test_verification_opens_only_the_first_gate(session):
user, _ = registration_core.register_org_and_owner(
session, "new@example.com", PASSWORD, "New Co")
token = auth_core.request_email_verification(session, user)
auth_core.verify_email(session, token)
session.refresh(user)
assert not auth_core.awaiting_verification(user)
# Confirming an address is not an operator deciding the account belongs here.
assert auth_core.awaiting_approval(user)
def test_a_verification_token_works_once(session):
user, _ = registration_core.register_org_and_owner(
session, "new@example.com", PASSWORD, "New Co")
token = auth_core.request_email_verification(session, user)
auth_core.verify_email(session, token)
with pytest.raises(Exception):
auth_core.verify_email(session, token)
def test_requesting_a_new_link_burns_the_old_one(session):
user, _ = registration_core.register_org_and_owner(
session, "new@example.com", PASSWORD, "New Co")
first = auth_core.request_email_verification(session, user)
second = auth_core.request_email_verification(session, user)
with pytest.raises(Exception):
auth_core.verify_email(session, first)
auth_core.verify_email(session, second)
def test_only_the_digest_is_stored(session):
"""The plaintext lives in the emailed link and nowhere else, so a database
disclosure hands over nothing that can be replayed."""
user, _ = registration_core.register_org_and_owner(
session, "new@example.com", PASSWORD, "New Co")
token = auth_core.request_email_verification(session, user)
rows = session.query(EmailVerificationToken).all()
assert rows
for row in rows:
assert token not in row.token_hash
assert len(row.token_hash) == 64
def test_resend_says_nothing_about_the_address(session):
"""None for every case the caller must not be able to tell apart."""
assert auth_core.resend_email_verification(session, "nobody@example.com") is None
verified = auth_core.create_user(session, "done@example.com", PASSWORD)
assert not auth_core.awaiting_verification(verified)
assert auth_core.resend_email_verification(session, "done@example.com") is None
user, _ = registration_core.register_org_and_owner(
session, "waiting@example.com", PASSWORD, "Co")
assert auth_core.resend_email_verification(session, "waiting@example.com") is not None
def test_approve_needs_admin(session):
from app.core.context import Forbidden, Principal, Scope
user, _ = registration_core.register_org_and_owner(
session, "new@example.com", PASSWORD, "New Co")
member = Principal(subject="m@example.com", surface="ui",
scopes=frozenset({Scope.READ, Scope.WRITE}), org_id=1, user_id=1)
with pytest.raises(Forbidden):
auth_core.approve_user(session, member, user.id)
# --- the façade -------------------------------------------------------------
def _signup(client, email="new@example.com"):
return client.post("/api/signup", json={
"email": email, "password": PASSWORD, "org_name": "New Co"})
def test_signup_does_not_sign_anybody_in(app_client):
r = _signup(app_client)
assert r.status_code == 200, r.get_json()
assert r.get_json()["pending"] is True
# No session: "/" still bounces to the sign-in page.
assert app_client.get("/").headers["Location"].endswith("/login")
def test_an_unverified_account_cannot_sign_in(app_client):
_signup(app_client)
r = app_client.post("/api/login",
json={"email": "new@example.com", "password": PASSWORD})
assert r.status_code == 403
# The flag, not the wording - it is what the page keys its resend control off.
assert r.get_json()["unverified"] is True
def test_a_verified_but_unapproved_account_still_cannot(app_client):
_signup(app_client)
user = db_session.query(User).filter(User.email == "new@example.com").one()
auth_core.verify_email(db_session, auth_core.request_email_verification(db_session, user))
r = app_client.post("/api/login",
json={"email": "new@example.com", "password": PASSWORD})
assert r.status_code == 403
assert r.get_json()["pendingApproval"] is True
assert r.get_json()["error"] == auth_core.APPROVAL_PENDING_MESSAGE
def test_both_gates_open_lets_them_in(app_client):
_signup(app_client)
user = db_session.query(User).filter(User.email == "new@example.com").one()
auth_core.verify_email(db_session, auth_core.request_email_verification(db_session, user))
user.approved_at = user.created_at
db_session.commit()
r = app_client.post("/api/login",
json={"email": "new@example.com", "password": PASSWORD})
assert r.status_code == 200, r.get_json()
assert app_client.get("/").status_code == 200
def test_resend_answers_the_same_for_any_address(app_client):
"""A stranger typing somebody else's address learns nothing."""
for email in ("nobody@example.com", "new@example.com"):
r = app_client.post("/api/signup/resend", json={"email": email})
assert r.status_code == 200
assert r.get_json() == {"ok": True}
def test_verify_page_reports_a_bad_token(app_client):
r = app_client.get("/verify?token=not-a-real-token")
assert r.status_code == 400
assert "did not work" in r.get_data(as_text=True)