package api import ( "encoding/json" "github.com/Fuwn/plutia/internal/config" "github.com/Fuwn/plutia/internal/storage" "github.com/Fuwn/plutia/internal/types" "io" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestResolveRateLimitPerIP(t *testing.T) { store, err := storage.OpenPebble(t.TempDir()) if err != nil { t.Fatalf("open pebble: %v", err) } defer store.Close() if err := store.PutState(types.StateV1{Version: 1, DID: "did:plc:alice", DIDDocument: []byte(`{"id":"did:plc:alice"}`), ChainTipHash: "tip", LatestOpSeq: 1, UpdatedAt: time.Now().UTC()}); err != nil { t.Fatalf("put state: %v", err) } cfg := config.Default() cfg.RequestTimeout = 10 * time.Second cfg.RateLimit.ResolveRPS = 1 cfg.RateLimit.ResolveBurst = 1 cfg.RateLimit.ProofRPS = 1 cfg.RateLimit.ProofBurst = 1 h := NewServer(cfg, store, nil, nil).Handler() req1 := httptest.NewRequest(http.MethodGet, "/did/did:plc:alice", nil) req1.RemoteAddr = "203.0.113.7:12345" rr1 := httptest.NewRecorder() h.ServeHTTP(rr1, req1) if rr1.Code != http.StatusOK { t.Fatalf("first request status: got %d want %d", rr1.Code, http.StatusOK) } req2 := httptest.NewRequest(http.MethodGet, "/did/did:plc:alice", nil) req2.RemoteAddr = "203.0.113.7:12345" rr2 := httptest.NewRecorder() h.ServeHTTP(rr2, req2) if rr2.Code != http.StatusTooManyRequests { t.Fatalf("second request status: got %d want %d", rr2.Code, http.StatusTooManyRequests) } } func TestStatusIncludesBuildInfo(t *testing.T) { store, err := storage.OpenPebble(t.TempDir()) if err != nil { t.Fatalf("open pebble: %v", err) } defer store.Close() h := NewServer(config.Default(), store, nil, nil, WithBuildInfo(BuildInfo{ Version: "v0.1.0", Commit: "abc123", BuildDate: "2026-02-26T00:00:00Z", GoVersion: "go1.test", })).Handler() req := httptest.NewRequest(http.MethodGet, "/status", nil) rr := httptest.NewRecorder() h.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Fatalf("status code: got %d want %d", rr.Code, http.StatusOK) } var payload map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &payload); err != nil { t.Fatalf("decode status: %v", err) } build, ok := payload["build"].(map[string]any) if !ok { t.Fatalf("missing build section: %v", payload) } if got := build["version"]; got != "v0.1.0" { t.Fatalf("unexpected build version: %v", got) } if got := build["commit"]; got != "abc123" { t.Fatalf("unexpected build commit: %v", got) } } func TestMetricsExposeRequiredSeries(t *testing.T) { store, err := storage.OpenPebble(t.TempDir()) if err != nil { t.Fatalf("open pebble: %v", err) } defer store.Close() h := NewServer(config.Default(), store, nil, nil).Handler() req := httptest.NewRequest(http.MethodGet, "/metrics", nil) rr := httptest.NewRecorder() h.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Fatalf("metrics status: got %d want %d", rr.Code, http.StatusOK) } body, _ := io.ReadAll(rr.Body) text := string(body) for _, metric := range []string{ "ingest_ops_total", "ingest_ops_per_second", "ingest_lag_ops", "verify_failures_total", "checkpoint_duration_seconds", "checkpoint_sequence", "disk_bytes_total", "did_count", "thin_cache_hits_total", "thin_cache_misses_total", "thin_cache_entries", "thin_cache_evictions_total", } { if !strings.Contains(text, metric) { t.Fatalf("metrics output missing %q", metric) } } }