Embeds a DB-IP Lite country MMDB (~5MB) in the binary via go:embed, keeping the single-binary deployment story clean. Country codes are stored alongside login attempts and sessions, shown in the dashboard (Top IPs, Top Countries card, Recent/Active Sessions, session detail). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
70 lines
1.7 KiB
Go
70 lines
1.7 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestRunRetentionDeletesOldRecords(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
logger := slog.Default()
|
|
|
|
// Insert an old login attempt (200 days ago).
|
|
oldTime := time.Now().AddDate(0, 0, -200).UTC().Format(time.RFC3339)
|
|
_, err := store.db.Exec(`
|
|
INSERT INTO login_attempts (username, password, ip, count, first_seen, last_seen)
|
|
VALUES ('old', 'old', '1.1.1.1', 1, ?, ?)`, oldTime, oldTime)
|
|
if err != nil {
|
|
t.Fatalf("insert old attempt: %v", err)
|
|
}
|
|
|
|
// Insert a recent login attempt.
|
|
if err := store.RecordLoginAttempt(ctx, "new", "new", "2.2.2.2", ""); err != nil {
|
|
t.Fatalf("insert recent attempt: %v", err)
|
|
}
|
|
|
|
// Run retention with a short interval. Cancel immediately after first run.
|
|
retentionCtx, cancel := context.WithCancel(ctx)
|
|
done := make(chan struct{})
|
|
go func() {
|
|
RunRetention(retentionCtx, store, 90, 24*time.Hour, logger)
|
|
close(done)
|
|
}()
|
|
|
|
// Give it a moment to run the initial prune.
|
|
time.Sleep(100 * time.Millisecond)
|
|
cancel()
|
|
<-done
|
|
|
|
// Verify old record was deleted.
|
|
var count int
|
|
store.db.QueryRow(`SELECT COUNT(*) FROM login_attempts`).Scan(&count)
|
|
if count != 1 {
|
|
t.Errorf("remaining attempts = %d, want 1", count)
|
|
}
|
|
}
|
|
|
|
func TestRunRetentionCancellation(t *testing.T) {
|
|
store := newTestStore(t)
|
|
logger := slog.Default()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
go func() {
|
|
RunRetention(ctx, store, 90, time.Millisecond, logger)
|
|
close(done)
|
|
}()
|
|
|
|
// Cancel and verify it exits.
|
|
cancel()
|
|
select {
|
|
case <-done:
|
|
// OK
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("RunRetention did not exit after cancel")
|
|
}
|
|
}
|