package admin import ( "context" "fmt" "net/http/httptest" "path/filepath" "sync/atomic" "testing" "time" "atcr.io/pkg/atproto" "atcr.io/pkg/hold/pds" ) // The dashboard's top-users panel used to resolve a handle for every user with // a quota record before sorting and truncating to ten. On a hold with ~500 crew // that is ~500 serial identity lookups to render ten rows, each allowed 10s by // the shared directory's HTTP client — one stall consumed the whole reverse // proxy budget and the client hung up mid-render. // // The property worth pinning is the LOOKUP COUNT, not the wall clock. Timing // would pass or fail on how fast the machine is; the count fails precisely when // someone moves resolution back above the truncation. // seededPDS returns a hold PDS carrying one layer record for each of n distinct // user DIDs, which is what GetAllUserQuotas derives its rows from. // // A file-backed path, not ":memory:" — libsql scopes an in-memory database to a // single connection, and the records index that QuotasByDID reads is disabled // without it. func seededPDS(t *testing.T, n int) *pds.HoldPDS { t.Helper() dir := t.TempDir() p, err := pds.NewHoldPDS( context.Background(), "did:web:top-users-test", "http://127.0.0.1:0", "http://127.0.0.1:0", filepath.Join(dir, "hold.db"), filepath.Join(dir, "signing.key"), false, ) if err != nil { t.Fatalf("NewHoldPDS: %v", err) } // Bootstrap writes the repo root; without it the first record write fails // with "cannot serialize undefined cid". Bootstrap prints a banner to // stdout, which is noise here but harmless. if err := p.Bootstrap(context.Background(), nil, pds.BootstrapConfig{ OwnerDID: "did:plc:topuserstestowner", Public: true, }); err != nil { t.Fatalf("Bootstrap: %v", err) } records := make([]*atproto.LayerRecord, 0, n) for i := range n { did := fmt.Sprintf("did:plc:topuserstest%08d", i) records = append(records, atproto.NewLayerRecord( fmt.Sprintf("sha256:%064x", i), int64((i+1)*1024), "application/vnd.oci.image.layer.v1.tar+gzip", did, "at://"+did+"/io.atcr.manifest/m"+fmt.Sprint(i), )) } if _, err := p.BatchCreateLayerRecords(context.Background(), records); err != nil { t.Fatalf("seed layer records: %v", err) } // BatchCreateLayerRecords writes to the CAR store only. The records index // that QuotasByDID reads is fed from the repo event stream, which is not // running here, so index it explicitly. if err := p.BackfillRecordsIndex(context.Background()); err != nil { t.Fatalf("backfill records index: %v", err) } return p } // TestTopUsers_ResolvesOnlyTheRowsItRenders is the regression guard: with 50 // users on the hold and a limit of 10, exactly 10 identity lookups may happen. func TestTopUsers_ResolvesOnlyTheRowsItRenders(t *testing.T) { const users, limit = 50, 10 var lookups atomic.Int32 prev := resolveHandle resolveHandle = func(ctx context.Context, did string) string { lookups.Add(1) return "handle-for-" + did } t.Cleanup(func() { resolveHandle = prev }) ui := &AdminUI{pds: seededPDS(t, users)} req := httptest.NewRequest("GET", fmt.Sprintf("/admin/api/top-users?limit=%d", limit), nil) rr := httptest.NewRecorder() ui.handleTopUsersAPI(rr, req) if rr.Code != 200 { t.Fatalf("status = %d, want 200 (body %q)", rr.Code, rr.Body.String()) } if got := lookups.Load(); got != limit { t.Errorf("performed %d identity lookups to render %d rows (hold has %d users); "+ "resolution is scaling with hold size again, not with the limit", got, limit, users) } } // TestTopUsers_SlowLookupDegradesToBareDID: resolution runs under its own // deadline, so one stalled lookup renders a bare DID instead of taking the // request down with it. Without the bound this blocks until the directory's own // 10s timeout, which is the entire reverse proxy budget. func TestTopUsers_SlowLookupDegradesToBareDID(t *testing.T) { prev := resolveHandle resolveHandle = func(ctx context.Context, did string) string { <-ctx.Done() // never answers; only the deadline releases it return "" } t.Cleanup(func() { resolveHandle = prev }) ui := &AdminUI{pds: seededPDS(t, 3)} req := httptest.NewRequest("GET", "/admin/api/top-users?limit=3", nil) rr := httptest.NewRecorder() start := time.Now() ui.handleTopUsersAPI(rr, req) elapsed := time.Since(start) if rr.Code != 200 { t.Fatalf("status = %d, want 200", rr.Code) } // Generous bound: asserting the panel returns at all rather than timing it. if elapsed > topUsersResolveTimeout+3*time.Second { t.Errorf("panel took %v with stalled lookups; the resolve deadline did not apply", elapsed) } }