mcp connection
Some checks failed
CI / fixture (push) Has been cancelled
CI / test (push) Has been cancelled

This commit is contained in:
2026-09-22 10:58:02 +05:30
parent 4e1f746b22
commit f2aa3b3ad8
53 changed files with 12515 additions and 37 deletions

View File

@@ -0,0 +1,275 @@
package httpserver_test
import (
"context"
"io"
"log/slog"
"strings"
"sync"
"testing"
"time"
"github.com/krow/krow-backend/go-api/internal/httpserver"
)
// Scheduler lifecycle.
//
// What is under test is the GOROUTINE, not the deletes — those are covered in
// internal/oauth and internal/ratelimit against real data. Here the questions
// are: does it start, does it do a pass, does it stop when told, does a failure
// take the process with it, and is running it twice safe.
/* ── Lifecycle ──────────────────────────────────────────────────────────── */
// It runs one pass IMMEDIATELY, before the first tick. A process that has been
// down should not carry a backlog for a further hour.
func TestMaintenanceRunsOnceImmediately(t *testing.T) {
a := newOAuthAPI(t)
m := a.srv.Maintenance()
if m == nil {
t.Fatal("a configured deployment returned no Maintenance")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
done := make(chan struct{})
go func() {
httpserver.SweepMaintenance(ctx, m, slog.New(slog.NewTextHandler(io.Discard, nil)))
close(done)
}()
// The immediate pass is the only one that will happen inside the test's
// lifetime — the ticker is an hour. Give it a moment, then stop.
time.Sleep(200 * time.Millisecond)
cancel()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("the sweeper did not stop within 5s of cancellation")
}
}
// Cancellation must return promptly, or a shutdown hangs on a goroutine nobody
// is waiting for.
func TestMaintenanceStopsOnCancellation(t *testing.T) {
a := newOAuthAPI(t)
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
httpserver.SweepMaintenance(ctx, a.srv.Maintenance(),
slog.New(slog.NewTextHandler(io.Discard, nil)))
close(done)
}()
time.Sleep(100 * time.Millisecond)
start := time.Now()
cancel()
select {
case <-done:
if elapsed := time.Since(start); elapsed > 2*time.Second {
t.Errorf("stopping took %v; shutdown would block on it", elapsed)
}
case <-time.After(5 * time.Second):
t.Fatal("the sweeper ignored cancellation")
}
}
// An already-cancelled context must not run a pass and must return at once.
func TestMaintenanceWithAnAlreadyCancelledContextReturns(t *testing.T) {
a := newOAuthAPI(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()
done := make(chan struct{})
go func() {
httpserver.SweepMaintenance(ctx, a.srv.Maintenance(),
slog.New(slog.NewTextHandler(io.Discard, nil)))
close(done)
}()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("the sweeper did not return on an already-cancelled context")
}
}
/* ── It does the work ───────────────────────────────────────────────────── */
// One pass removes dead rows and leaves live ones. The detailed retention rules
// are tested in internal/oauth; this asserts the scheduler is wired to them.
func TestMaintenanceSweepRemovesDeadRows(t *testing.T) {
a := newOAuthAPI(t)
ctx := context.Background()
// A grant that is already past its expiry and its grace.
if _, err := a.h.Pool.Exec(ctx,
`INSERT INTO oauth_clients (client_id, client_name, redirect_uris)
VALUES ('sweep-client', 'Sweep', ARRAY['https://a.test/cb'])`); err != nil {
t.Fatalf("client: %v", err)
}
userID, _ := seededUser(t, a.h.Pool)
var orgID string
if err := a.h.Pool.QueryRow(ctx,
`SELECT org_id::text FROM users WHERE id = $1::uuid`, userID).Scan(&orgID); err != nil {
t.Fatalf("org: %v", err)
}
if _, err := a.h.Pool.Exec(ctx,
`INSERT INTO oauth_grants
(code_hash, client_id, user_id, org_id, redirect_uri, scopes, resource,
code_challenge, code_challenge_method, created_date, expires_at)
VALUES (repeat('a', 64), 'sweep-client', $1::uuid, $2::uuid, 'https://a.test/cb',
ARRAY['krow.read'], $3, repeat('B', 43), 'S256',
now() - interval '3 hours', now() - interval '3 hours' + interval '1 minute')`,
userID, orgID, testMCPResource); err != nil {
t.Fatalf("grant: %v", err)
}
// An expired rate-limit bucket.
if _, err := a.h.Pool.Exec(ctx,
`INSERT INTO rate_limits (bucket, window_start, count, expires_at)
VALUES ('test:old', now() - interval '3 hours', 5, now() - interval '2 hours')`); err != nil {
t.Fatalf("bucket: %v", err)
}
// And a live one, which must survive.
if _, err := a.h.Pool.Exec(ctx,
`INSERT INTO rate_limits (bucket, window_start, count, expires_at)
VALUES ('test:live', now(), 1, now() + interval '1 hour')`); err != nil {
t.Fatalf("bucket: %v", err)
}
result, err := a.srv.Maintenance().Sweep(ctx)
if err != nil {
t.Fatalf("Sweep: %v", err)
}
if result.Grants != 1 {
t.Errorf("removed %d grants, want 1", result.Grants)
}
if result.RateLimits != 1 {
t.Errorf("removed %d rate-limit rows, want 1", result.RateLimits)
}
if result.Total() != 2 {
t.Errorf("Total() = %d, want 2", result.Total())
}
var live int
if err := a.h.Pool.QueryRow(ctx,
`SELECT count(*) FROM rate_limits WHERE bucket = 'test:live'`).Scan(&live); err != nil {
t.Fatalf("count: %v", err)
}
if live != 1 {
t.Error("the live rate-limit window was swept")
}
}
// Running it repeatedly must be safe and must converge to removing nothing.
func TestRepeatedMaintenanceIsSafe(t *testing.T) {
a := newOAuthAPI(t)
ctx := context.Background()
m := a.srv.Maintenance()
for i := 0; i < 3; i++ {
result, err := m.Sweep(ctx)
if err != nil {
t.Fatalf("pass %d: %v", i+1, err)
}
if i > 0 && result.Total() != 0 {
t.Errorf("pass %d removed %d rows; a repeat pass should find nothing", i+1, result.Total())
}
}
}
// Two instances sweep concurrently with no coordination. Neither may error.
// Run with -race.
func TestConcurrentMaintenanceIsSafe(t *testing.T) {
a := newOAuthAPI(t)
ctx := context.Background()
const instances = 4
var wg sync.WaitGroup
errs := make(chan error, instances)
for i := 0; i < instances; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if _, err := a.srv.Maintenance().Sweep(ctx); err != nil {
errs <- err
}
}()
}
wg.Wait()
close(errs)
for err := range errs {
t.Errorf("concurrent sweep errored: %v", err)
}
}
/* ── Failure isolation ──────────────────────────────────────────────────── */
// A failing sweep must be logged and survived, not fatal. The database is
// closed underneath the sweeper, which is the closest thing to a real outage a
// test can arrange.
func TestMaintenanceSurvivesADatabaseFailure(t *testing.T) {
a := newOAuthAPI(t)
m := a.srv.Maintenance()
var logged strings.Builder
log := slog.New(slog.NewTextHandler(&logged, &slog.HandlerOptions{Level: slog.LevelDebug}))
// A cancelled context makes every statement fail immediately.
dead, cancel := context.WithCancel(context.Background())
cancel()
if _, err := m.Sweep(dead); err == nil {
t.Log("note: the sweep reported no error on a cancelled context")
}
// The goroutine wrapper must not panic or exit the process on that.
ctx, stop := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
httpserver.SweepMaintenance(ctx, m, log)
close(done)
}()
time.Sleep(150 * time.Millisecond)
stop()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("the sweeper did not stop")
}
}
/* ── It is absent when the surface is ───────────────────────────────────── */
// A deployment without OAuth has nothing to sweep, and must not start a ticker
// that runs for the life of the process doing nothing.
func TestMaintenanceIsNilWhenTheSurfaceIsDisabled(t *testing.T) {
a := newAPI(t) // the standard fixture: no OAuth configuration
if m := a.srv.Maintenance(); m != nil {
t.Error("an unconfigured deployment returned a Maintenance sweeper")
}
// And the runner must return immediately rather than tick forever.
done := make(chan struct{})
go func() {
httpserver.SweepMaintenance(context.Background(), a.srv.Maintenance(),
slog.New(slog.NewTextHandler(io.Discard, nil)))
close(done)
}()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("the sweeper ticked despite having nothing to sweep")
}
}