feat(web): stamp login_events on every gated request

The registration gate, once it resolves the authenticated subject to a tapir
user_id, calls StampLogin (store-throttled to one row per user per day). Best-
effort: a stamp failure is logged and swallowed so it never breaks the request.
This is what makes the read-side Stage-0 usage signal actually accrue.

Tests cover the happy-path stamp, the same-day throttle, and that an
unregistered subject (redirected to /register) is never stamped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 23:46:03 +02:00
co-authored by Claude Opus 4.8
parent de54cd33b2
commit 2fac735837
3 changed files with 55 additions and 0 deletions
+43
View File
@@ -7,6 +7,7 @@ import (
"strings"
"testing"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/stretchr/testify/require"
)
@@ -74,6 +75,48 @@ func TestRegisterCreatesExactlyOneUserAndIdentity(t *testing.T) {
require.Equal(t, 1, totalIdents)
}
// loginEventCount counts login_events for the stub user via the raw pool.
func loginEventCount(t *testing.T, p *pgxpool.Pool) int {
t.Helper()
var n int
require.NoError(t, p.QueryRow(context.Background(),
`SELECT count(*) FROM login_events WHERE user_id = $1`, userID).Scan(&n))
return n
}
// TestGateStampsLoginEventThrottled: a gated request for a registered user stamps
// exactly one login event, and a same-day repeat is throttled to no new row — the
// read-side Stage-0 signal flowing from the registration gate.
func TestGateStampsLoginEventThrottled(t *testing.T) {
app := newApp(t) // stubSubject → userID
p := rawPool(t)
resetDB(t, p)
require.Equal(t, 0, loginEventCount(t, p))
rec := do(t, app, httptest.NewRequest(http.MethodGet, "/", nil))
require.Equal(t, http.StatusOK, rec.Code)
require.Equal(t, 1, loginEventCount(t, p), "a gated request must stamp one login event")
do(t, app, httptest.NewRequest(http.MethodGet, "/", nil))
require.Equal(t, 1, loginEventCount(t, p), "a same-day repeat must not stamp again")
}
// TestUnregisteredSubjectIsNotStamped: a subject with no tapir user is redirected
// to /register and never reaches the stamp (no user_id to attribute it to).
func TestUnregisteredSubjectIsNotStamped(t *testing.T) {
app := newAppAs(t, "unregistered-sub")
p := rawPool(t)
truncateAll(t, p)
rec := do(t, app, httptest.NewRequest(http.MethodGet, "/", nil))
require.Equal(t, http.StatusFound, rec.Code)
var n int
require.NoError(t, p.QueryRow(context.Background(),
`SELECT count(*) FROM login_events`).Scan(&n))
require.Equal(t, 0, n, "an unregistered subject must not stamp a login event")
}
func TestRegisterRejectsMissingFields(t *testing.T) {
app := newAppAs(t, "incomplete-subject")
truncateAll(t, rawPool(t))