fix: a credential, an erased account and a stale level stay out of the log
Round 2 of the E22 review, on what round 1 changed. `redact_query` decodes a parameter before the text rules see it, which is what lets `%40` be recognised as an address — but `_SECRET_PARAM_RE` was written for the raw, encoded form, where a value holds no space, `&` or quote. On decoded text its value class stops at the first one, so `?state=a%20b` kept its tail and `?code=%20abc` was not matched at all. The parameter-NAME rule now decides, on the key, before any text rule runs. `forget_user` dropped the account's level elevation without recomputing the root level, so a process whose only elevated account had just deleted itself kept building TRACE records to throw away, until some later request happened to recompute it. Every other path that drops an elevation recomputes. The sink's writer took the erasure lock and THEN waited for a pooled connection, while every request path already holds a connection when it reaches that lock — so under pool pressure the writer held a process-global lock against the very requests that could hand a connection back. Same order on both sides now; the lock still spans the check, the insert and the commit. The storage-budget notice said the rest of the hour was not kept, and then smaller batches kept landing underneath it: the window slides and a refused batch is not charged to it. Says what it does. Tests: the route's half of the erasure rule had no test — only the sink's did — and a source-level contract test now covers the third insert nobody has written yet. `logs._erased` is process-global with an hour's memory while the DB fixture hands serial ids back out from 1, so it is swept before each test like the rate limiters are.
This commit is contained in:
@@ -180,18 +180,31 @@ def test_an_erased_accounts_late_lines_are_stored_without_it(stored, db, make_us
|
||||
or the erasing request's own access line, logged after the handler committed."""
|
||||
_sink, log, rows = stored
|
||||
user = make_user()
|
||||
try:
|
||||
log.warning("queued before the erasure", extra={"user_id": user.id})
|
||||
purge_user(db, user, BackgroundTasks())
|
||||
log.info(
|
||||
"DELETE /api/me/account 200", extra={**logs.event("http.request"), "user_id": user.id}
|
||||
)
|
||||
assert [(r.msg, r.user_id) for r in rows()] == [
|
||||
("queued before the erasure", None),
|
||||
("DELETE /api/me/account 200", None),
|
||||
]
|
||||
finally:
|
||||
logs._erased.clear()
|
||||
# `logs._erased` outlives the test; conftest's `_reset_log_erasures` clears it before each one.
|
||||
log.warning("queued before the erasure", extra={"user_id": user.id})
|
||||
purge_user(db, user, BackgroundTasks())
|
||||
log.info("DELETE /api/me/account 200", extra={**logs.event("http.request"), "user_id": user.id})
|
||||
assert [(r.msg, r.user_id) for r in rows()] == [
|
||||
("queued before the erasure", None),
|
||||
("DELETE /api/me/account 200", None),
|
||||
]
|
||||
|
||||
|
||||
def test_every_path_into_the_table_drops_an_erased_account():
|
||||
"""The two behavioural tests above cover the two writers there are today (this sink and
|
||||
`POST /api/diagnostics/events`). This one covers the THIRD one nobody has written yet: a new
|
||||
`insert(LogEvent)` that forgets the filter would leave an erased account's rows named, and no
|
||||
test of an existing path can notice. Source-level on purpose — it is a rule about call sites."""
|
||||
import pathlib
|
||||
|
||||
root = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
unfiltered = [
|
||||
f"{path.relative_to(root)}:{n}"
|
||||
for path in root.rglob("*.py")
|
||||
for n, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1)
|
||||
if "insert(LogEvent)" in line and "without_erased" not in line
|
||||
]
|
||||
assert unfiltered == [], f"insert(LogEvent) without logsink.without_erased at {unfiltered}"
|
||||
|
||||
|
||||
def test_a_nul_character_costs_nothing_but_itself(stored):
|
||||
|
||||
Reference in New Issue
Block a user