aboutsummaryrefslogtreecommitdiff
path: root/internal/api/server_hardening_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/server_hardening_test.go')
-rw-r--r--internal/api/server_hardening_test.go119
1 files changed, 119 insertions, 0 deletions
diff --git a/internal/api/server_hardening_test.go b/internal/api/server_hardening_test.go
new file mode 100644
index 0000000..bb9f24c
--- /dev/null
+++ b/internal/api/server_hardening_test.go
@@ -0,0 +1,119 @@
+package api
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/Fuwn/plutia/internal/config"
+ "github.com/Fuwn/plutia/internal/storage"
+ "github.com/Fuwn/plutia/internal/types"
+)
+
+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",
+ } {
+ if !strings.Contains(text, metric) {
+ t.Fatalf("metrics output missing %q", metric)
+ }
+ }
+}