Skip to content

Commit 82bcbd8

Browse files
authored
fix(tests): collect tests/, and correct the two assertions that never ran (#314)
The issue calls these order-dependent — passing under `pytest`, failing under `pytest <file>`. They are not. `testpaths` listed `tests/integration`, `tests/e2e`, `tests/benchmarks` and `tests/perf` but never `tests/` itself, so the four modules directly under it were not collected by a bare `pytest` at all. The tests "passed" by not running. 26 tests were in that gap, including a lifespan hydration test and fifteen audit-log tests. Replacing the four entries with `"tests"` collects them; the marker filter in `addopts` still holds e2e and perf out, so nothing new runs in CI that should not. That leaves exactly the two failures the issue describes, and both are the test being wrong rather than the code. `test_host_settings_ignores_env` asserted "HostSettings must NOT read env". That is not this codebase's contract: precedence is env → DB → default and env has to keep winning, or an upgrade silently changes a deployment's behaviour. `HostSettings` declares `env_prefix="SM_"` for that reason. Rewritten as three tests covering what the prefix actually guarantees — prefixed env wins, an unprefixed name is ignored, the default applies when neither is set. `test_session_wins_over_bad_bearer` asserted a valid session rescues a bad `Authorization: Bearer`. `resolve_user` does the opposite. Keeping the code is the deliberate call: falling through would make an invalid token indistinguishable from no token, so a client whose credential expired keeps working on whatever other identity it carries and its 401s depend on what else is in the request — while gaining nothing, since the fall-through can only resolve the session's own identity, which the caller already had. The precedence is now stated where it is implemented, with a sibling test proving the session alone still succeeds so the 401 is the header's doing. Closes #295
1 parent c6b0195 commit 82bcbd8

4 files changed

Lines changed: 70 additions & 11 deletions

File tree

‎modules/users/users/provider.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ class UsersAuthProvider:
7474
_is_auth_provider = True
7575

7676
async def resolve_user(self, request: Request) -> UserContext | None:
77+
# An ``Authorization`` header is an explicit claim, and it decides the
78+
# request: a bad token returns None rather than falling through to the
79+
# session cookie. Falling through would make an invalid token
80+
# indistinguishable from no token, so a client whose credential expired
81+
# silently keeps working on whatever other identity it carries and its
82+
# 401s depend on what else is in the request. It gains nothing either —
83+
# it can only resolve the session's own identity, which the caller
84+
# already had. Pinned by ``test_a_bad_bearer_is_not_rescued_by_a_valid
85+
# _session``, which asserted the opposite for as long as it went
86+
# uncollected.
7787
auth_header = request.headers.get("authorization", "")
7888
if auth_header.startswith("Bearer "):
7989
return await self._resolve_bearer(request.scope, auth_header[7:])

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ invalid-assignment = "ignore"
117117

118118
[tool.pytest.ini_options]
119119
asyncio_mode = "auto"
120-
testpaths = ["framework/cli/tests", "framework/core/tests", "framework/db/tests", "framework/hosting/tests", "framework/testing/tests", "host/tests", "modules/auth/tests", "modules/dashboard/tests", "modules/users/tests", "modules/permissions/tests", "modules/background_tasks/tests", "modules/file_storage/tests", "modules/settings/tests", "modules/feature_flags/tests", "modules/keycloak/tests", "modules/audit_log/tests", "modules/branding/tests", "modules/site_lock/tests", "scripts/tests", "tests/integration", "tests/e2e", "tests/benchmarks", "tests/perf"]
120+
testpaths = ["framework/cli/tests", "framework/core/tests", "framework/db/tests", "framework/hosting/tests", "framework/testing/tests", "host/tests", "modules/auth/tests", "modules/dashboard/tests", "modules/users/tests", "modules/permissions/tests", "modules/background_tasks/tests", "modules/file_storage/tests", "modules/settings/tests", "modules/feature_flags/tests", "modules/keycloak/tests", "modules/audit_log/tests", "modules/branding/tests", "modules/site_lock/tests", "scripts/tests", "tests"]
121121
markers = [
122122
"e2e: end-to-end tests requiring a live browser",
123123
"perf: performance benchmarks (opt-in; run via `make bench`)",

‎tests/test_bootstrap_settings.py‎

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,36 @@ def test_bootstrap_placeholder_secret_blocks_production(monkeypatch):
4545
BootstrapSettings()
4646

4747

48-
def test_host_settings_ignores_env(monkeypatch):
49-
# HostSettings must NOT read env — env-sprawl is what we're removing.
48+
def test_host_settings_reads_its_own_prefixed_env(monkeypatch):
49+
"""Env beats the default. This test asserted the opposite and never ran.
50+
51+
``testpaths`` did not list ``tests/``, so nothing here was collected by a
52+
bare ``pytest`` — the stale assertion sat green for as long as it took to
53+
notice. The contract it claimed ("HostSettings must NOT read env") is not
54+
the one the codebase has: precedence is env → DB → default, and env has to
55+
keep winning or an upgrade silently changes a deployment's behaviour
56+
(CLAUDE.md § Conventions). ``HostSettings`` declares ``env_prefix="SM_"``
57+
for exactly that reason.
58+
"""
5059
monkeypatch.setenv("SM_MULTI_TENANT", "true")
51-
hs = HostSettings()
52-
assert hs.multi_tenant is False # default wins; env ignored
60+
assert HostSettings().multi_tenant is True
61+
62+
63+
def test_host_settings_ignores_unprefixed_env(monkeypatch):
64+
"""What the ``env_prefix`` is actually defending against.
65+
66+
Without it a bare ``HostSettings()`` would read unprefixed names, and
67+
``LOG_LEVEL`` in particular is a common variable that has nothing to do
68+
with this app.
69+
"""
70+
monkeypatch.delenv("SM_MULTI_TENANT", raising=False)
71+
monkeypatch.setenv("MULTI_TENANT", "true")
72+
assert HostSettings().multi_tenant is False
73+
74+
75+
def test_host_settings_default_wins_when_env_is_unset(monkeypatch):
76+
monkeypatch.delenv("SM_MULTI_TENANT", raising=False)
77+
assert HostSettings().multi_tenant is False
5378

5479

5580
def test_host_settings_default_locale_must_be_supported():

‎tests/test_principal_resolver_integration.py‎

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,38 @@ async def test_no_auth_header_on_api_returns_401(pat_client):
7878

7979

8080
@pytest.mark.anyio
81-
async def test_session_wins_over_bad_bearer(authenticated_client):
82-
"""A valid session cookie + Bearer bad -> 200 via session; resolver not consulted.
83-
84-
The ``authenticated_client`` fixture already carries an admin session cookie;
85-
here we additionally send a bad bearer to prove the session path wins.
86-
Endpoint is any admin-readable, non-user-enumerating route."""
81+
async def test_a_bad_bearer_is_not_rescued_by_a_valid_session(authenticated_client):
82+
"""An explicitly presented credential that is invalid fails the request.
83+
84+
This test previously asserted the opposite — that the session cookie wins
85+
and "the resolver is not consulted" — and had never run: ``testpaths`` did
86+
not list ``tests/``, so a bare ``pytest`` collected nothing here. The
87+
behaviour it described is not what ``UsersAuthProvider.resolve_user`` does;
88+
the ``Authorization`` header is checked first and a bad token returns
89+
``None`` without falling through.
90+
91+
Keeping the code and correcting the test is the deliberate call. Falling
92+
through would make an invalid token indistinguishable from no token at all,
93+
so a client whose credential has expired or been revoked silently keeps
94+
working on whatever other identity it happens to carry, and its 401s become
95+
dependent on what else is in the request. Nothing is gained by the
96+
fall-through either: it can only ever resolve the session's own identity,
97+
which the caller already had.
98+
99+
The narrow cost is a browser that attaches a stale ``Authorization`` header
100+
to a page request. Nothing in this app does that — pages authenticate with
101+
the session cookie.
102+
"""
87103
resp = await authenticated_client.get(
88104
"/api/permissions/",
89105
headers={"Authorization": "Bearer bad"},
90106
)
107+
assert resp.status_code == 401
108+
109+
110+
@pytest.mark.anyio
111+
async def test_the_same_session_succeeds_without_the_bad_header(authenticated_client):
112+
"""The other half: the session itself is fine, so the 401 above is the
113+
header's doing and not a broken fixture."""
114+
resp = await authenticated_client.get("/api/permissions/")
91115
assert resp.status_code == 200

0 commit comments

Comments
 (0)