File size: 3,814 Bytes
4b09d2d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3fde616
 
 
 
 
 
 
 
4b09d2d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
"""Google sign-in (Supabase PKCE): feature flag, redirect flow, callback
find-or-create, and password-login guard for Google-only accounts. The
Supabase exchange itself is mocked - no network in tests."""

import pytest
from fastapi.testclient import TestClient

from app import auth, auth_google
from app.main import app


@pytest.fixture()
def client():
    with TestClient(app) as c:
        yield c


@pytest.fixture()
def google_env(monkeypatch):
    monkeypatch.setenv("SUPABASE_URL", "https://fakeproj.supabase.co")
    monkeypatch.setenv("SUPABASE_ANON_KEY", "fake-anon-key")
    monkeypatch.setenv("CCR_APP_URL", "http://testserver")


def test_unconfigured_instance_hides_and_refuses_google(client):
    assert client.get("/api/auth/me").json().get("google_available") is False
    assert client.get("/api/auth/google/login", follow_redirects=False).status_code == 503


def test_login_redirects_to_supabase_with_pkce(client, google_env):
    me = client.get("/api/auth/me").json()
    assert me["google_available"] is True

    resp = client.get("/api/auth/google/login", follow_redirects=False)
    assert resp.status_code == 307
    loc = resp.headers["location"]
    assert loc.startswith("https://fakeproj.supabase.co/auth/v1/authorize?")
    assert "provider=google" in loc
    assert "code_challenge=" in loc and "code_challenge_method=s256" in loc
    assert "redirect_to=http%3A%2F%2Ftestserver%2Fapi%2Fauth%2Fgoogle%2Fcallback" in loc
    assert auth_google.VERIFIER_COOKIE in resp.cookies


def test_app_url_ignores_pasted_whitespace(monkeypatch):
    """A dashboard-pasted CCR_APP_URL can carry a trailing newline. Untrimmed
    it reaches Supabase as %0A inside redirect_to, so the redirect never
    matches the allow list (production, 2026-08-17)."""
    monkeypatch.setenv("CCR_APP_URL", "  https://example.org/\n")
    assert auth_google.app_url() == "https://example.org"


def test_callback_creates_user_and_signs_in(client, google_env, monkeypatch):
    monkeypatch.setattr(
        auth_google, "exchange",
        lambda code, verifier: {"email": "pi@lab.edu", "name": "The PI"},
    )
    client.cookies.set(auth_google.VERIFIER_COOKIE, auth.sign_payload({"v": "verifier123"}))

    resp = client.get("/api/auth/google/callback?code=abc", follow_redirects=False)
    assert resp.status_code == 307 and resp.headers["location"] == "/"

    me = client.get("/api/auth/me").json()
    assert me["signed_in"] is True and me["email"] == "pi@lab.edu" and me["name"] == "The PI"

    # second sign-in reuses the same account (no duplicate users)
    client.cookies.set(auth_google.VERIFIER_COOKIE, auth.sign_payload({"v": "verifier456"}))
    client.get("/api/auth/google/callback?code=def", follow_redirects=False)
    from app.db import SessionLocal
    from app.models import User

    db = SessionLocal()
    try:
        assert db.query(User).filter_by(email="pi@lab.edu").count() == 1
    finally:
        db.close()


def test_callback_without_verifier_fails_safely(client, google_env):
    resp = client.get("/api/auth/google/callback?code=abc", follow_redirects=False)
    assert resp.status_code == 307
    assert "auth_error=" in resp.headers["location"]


def test_google_only_account_cannot_password_login(client, google_env, monkeypatch):
    monkeypatch.setattr(
        auth_google, "exchange",
        lambda code, verifier: {"email": "gonly@lab.edu", "name": "G Only"},
    )
    client.cookies.set(auth_google.VERIFIER_COOKIE, auth.sign_payload({"v": "v1"}))
    client.get("/api/auth/google/callback?code=abc", follow_redirects=False)
    client.post("/api/auth/logout")

    resp = client.post(
        "/api/auth/login", json={"email": "gonly@lab.edu", "password": "password123"}
    )
    assert resp.status_code == 401
    assert "Google" in resp.json()["detail"]