agilentics / boiler
"""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)