StampLogin appends one login_events row per user per day via an atomic INSERT ... SELECT ... WHERE NOT EXISTS, run through withUser so the throttle probe is itself RLS-scoped to the caller. DeleteUser now deletes login_events explicitly (no FK = no cascade — the summary_actions footgun, repeated). Extends the two-user RLS isolation proof and the delete-account proof to cover login_events, and adds throttle / new-day / user-scoping tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
84 lines
3.1 KiB
Go
84 lines
3.1 KiB
Go
package store_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// countLoginEvents counts a user's login_events via the superuser pool, which
|
|
// bypasses RLS — so the assertion sees the true row count regardless of scope.
|
|
func countLoginEvents(t *testing.T, p *pgxpool.Pool, userID string) 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
|
|
}
|
|
|
|
// TestStampLoginThrottlesToOnePerDay: repeated stamps within the same day insert
|
|
// exactly one row — the throttle that keeps login_events one-row-per-active-day.
|
|
func TestStampLoginThrottlesToOnePerDay(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newStore(t)
|
|
super := rawPool(t)
|
|
resetDB(t, super)
|
|
_, err := super.Exec(ctx, `INSERT INTO users (id) VALUES ($1)`, userA)
|
|
require.NoError(t, err)
|
|
|
|
for i := 0; i < 3; i++ {
|
|
require.NoError(t, s.StampLogin(ctx, userA))
|
|
}
|
|
require.Equal(t, 1, countLoginEvents(t, super, userA),
|
|
"three same-day stamps must collapse to one row")
|
|
}
|
|
|
|
// TestStampLoginRecordsOncePerNewDay: with yesterday's row already present, a
|
|
// stamp today is NOT throttled — it appends the day's row, so distinct active days
|
|
// accumulate (the substrate the gate's distinct-week count reads).
|
|
func TestStampLoginRecordsOncePerNewDay(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newStore(t)
|
|
super := rawPool(t)
|
|
resetDB(t, super)
|
|
_, err := super.Exec(ctx, `INSERT INTO users (id) VALUES ($1)`, userA)
|
|
require.NoError(t, err)
|
|
|
|
// Seed an event dated yesterday (before today's start), so the throttle's
|
|
// "row exists with seen_at >= start-of-today" probe finds nothing for today.
|
|
_, err = super.Exec(ctx,
|
|
`INSERT INTO login_events (user_id, seen_at) VALUES ($1, NOW() - INTERVAL '1 day')`, userA)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, s.StampLogin(ctx, userA))
|
|
require.Equal(t, 2, countLoginEvents(t, super, userA),
|
|
"a stamp on a new day must append a second row")
|
|
|
|
// A second stamp the same day is throttled again.
|
|
require.NoError(t, s.StampLogin(ctx, userA))
|
|
require.Equal(t, 2, countLoginEvents(t, super, userA),
|
|
"the same-day repeat must not add a third row")
|
|
}
|
|
|
|
// TestStampLoginIsUserScoped: one user's stamp lands only on that user's rows —
|
|
// the throttle probe is RLS-scoped, so user B's existing same-day row neither
|
|
// suppresses nor is touched by user A's stamp.
|
|
func TestStampLoginIsUserScoped(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newStore(t)
|
|
super := rawPool(t)
|
|
resetDB(t, super)
|
|
_, err := super.Exec(ctx, `INSERT INTO users (id) VALUES ($1), ($2)`, userA, userB)
|
|
require.NoError(t, err)
|
|
|
|
// B already has a same-day row; it must not throttle A's first stamp.
|
|
_, err = super.Exec(ctx, `INSERT INTO login_events (user_id) VALUES ($1)`, userB)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, s.StampLogin(ctx, userA))
|
|
require.Equal(t, 1, countLoginEvents(t, super, userA), "A's stamp must record despite B's same-day row")
|
|
require.Equal(t, 1, countLoginEvents(t, super, userB), "A's stamp must not touch B's rows")
|
|
}
|