📦 EqualifyEverything / equalify-reflow

📄 test_basic_login_roundtrip.py · 155 lines
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155"""Integration: basic-mode login → me → logout round-trip."""

from __future__ import annotations

import pytest

COMMON_HEADERS = {"Origin": "http://testserver", "Sec-Fetch-Site": "same-origin"}


@pytest.mark.integration
def test_config_reports_basic_mode(auth_basic_client) -> None:  # type: ignore[no-untyped-def]
    resp = auth_basic_client.get("/api/v1/auth/config")
    assert resp.status_code == 200
    body = resp.json()
    assert body["mode"] == "basic"
    assert len(body["providers"]) == 1
    assert body["providers"][0]["id"] == "basic"


@pytest.mark.integration
def test_login_succeeds_and_sets_cookies(
    auth_basic_client,  # type: ignore[no-untyped-def]
    basic_password: str,
) -> None:
    resp = auth_basic_client.post(
        "/api/v1/auth/login",
        json={"username": "alice", "password": basic_password},
        headers=COMMON_HEADERS,
    )
    assert resp.status_code == 200, resp.text
    body = resp.json()
    assert body["sub"] == "alice"
    assert body["provider_id"] == "basic"
    # Both cookies present on the response.
    cookies = {c.name: c for c in auth_basic_client.cookies.jar}
    assert "reflow_session" in cookies
    assert "reflow_session_csrf" in cookies


@pytest.mark.integration
def test_login_with_wrong_password_returns_401(
    auth_basic_client,  # type: ignore[no-untyped-def]
) -> None:
    resp = auth_basic_client.post(
        "/api/v1/auth/login",
        json={"username": "alice", "password": "wrong"},
        headers=COMMON_HEADERS,
    )
    assert resp.status_code == 401


@pytest.mark.integration
def test_login_with_unknown_user_returns_401(
    auth_basic_client,  # type: ignore[no-untyped-def]
) -> None:
    resp = auth_basic_client.post(
        "/api/v1/auth/login",
        json={"username": "bob", "password": "anything"},
        headers=COMMON_HEADERS,
    )
    assert resp.status_code == 401


@pytest.mark.integration
def test_me_anonymous_returns_401(auth_basic_client) -> None:  # type: ignore[no-untyped-def]
    resp = auth_basic_client.get("/api/v1/auth/me")
    assert resp.status_code == 401


@pytest.mark.integration
def test_me_after_login_returns_identity(
    auth_basic_client,  # type: ignore[no-untyped-def]
    basic_password: str,
) -> None:
    auth_basic_client.post(
        "/api/v1/auth/login",
        json={"username": "alice", "password": basic_password},
        headers=COMMON_HEADERS,
    )
    resp = auth_basic_client.get("/api/v1/auth/me")
    assert resp.status_code == 200
    assert resp.json()["sub"] == "alice"


@pytest.mark.integration
def test_logout_clears_cookies(
    auth_basic_client,  # type: ignore[no-untyped-def]
    basic_password: str,
) -> None:
    auth_basic_client.post(
        "/api/v1/auth/login",
        json={"username": "alice", "password": basic_password},
        headers=COMMON_HEADERS,
    )
    csrf_token = auth_basic_client.cookies.get("reflow_session_csrf")
    assert csrf_token

    resp = auth_basic_client.post(
        "/api/v1/auth/logout",
        headers={**COMMON_HEADERS, "X-CSRF-Token": csrf_token},
    )
    assert resp.status_code == 200, resp.text

    # /me should now 401.
    me = auth_basic_client.get("/api/v1/auth/me")
    assert me.status_code == 401


@pytest.mark.integration
def test_logout_without_csrf_rejected(
    auth_basic_client,  # type: ignore[no-untyped-def]
    basic_password: str,
) -> None:
    auth_basic_client.post(
        "/api/v1/auth/login",
        json={"username": "alice", "password": basic_password},
        headers=COMMON_HEADERS,
    )
    resp = auth_basic_client.post("/api/v1/auth/logout", headers=COMMON_HEADERS)
    assert resp.status_code == 403


@pytest.mark.integration
def test_login_cross_origin_rejected(
    auth_basic_client,  # type: ignore[no-untyped-def]
    basic_password: str,
) -> None:
    resp = auth_basic_client.post(
        "/api/v1/auth/login",
        json={"username": "alice", "password": basic_password},
        headers={"Origin": "https://evil.example", "Sec-Fetch-Site": "cross-site"},
    )
    assert resp.status_code == 403


@pytest.mark.integration
def test_none_mode_config_endpoint_is_minimal(
    auth_none_client,  # type: ignore[no-untyped-def]
) -> None:
    resp = auth_none_client.get("/api/v1/auth/config")
    assert resp.status_code == 200
    body = resp.json()
    assert body["mode"] == "none"
    assert body["providers"] == []


@pytest.mark.integration
def test_none_mode_login_disabled(auth_none_client) -> None:  # type: ignore[no-untyped-def]
    resp = auth_none_client.post(
        "/api/v1/auth/login",
        json={"username": "alice", "password": "anything"},
        headers=COMMON_HEADERS,
    )
    assert resp.status_code == 404