// Tests for the M13a W5 JWT gate in archiverd. We test the // gate wiring + the trigger-channel semantics. We don't spin // up the runLoop (no Postgres / ClickHouse in unit tests); the // admin handler is decoupled from the loop via the channel. package main import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "io" "log/slog" "net/http" "net/http/httptest" "os" "testing" "time" ) const testSecret = "test-secret-with-32-bytes-min-len-abc" func mintTestJWT(t *testing.T, secret, role string) string { t.Helper() hb, _ := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT"}) body, _ := json.Marshal(map[string]any{ "sub": "u-test", "tid": "t-test", "role": role, "typ": "access", "iss": "broad-announce", "exp": time.Now().Add(15 * time.Minute).Unix(), "iat": time.Now().Unix(), }) enc := base64.RawURLEncoding.EncodeToString(hb) + "." + base64.RawURLEncoding.EncodeToString(body) mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(enc)) return enc + "." + base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) } func discardLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } // TestWireAdminRoutes_NoSecret_Disabled verifies that with // BA_AUTHD_JWT_SECRET unset, the admin route is NOT registered. func TestWireAdminRoutes_NoSecret_Disabled(t *testing.T) { os.Unsetenv("BA_AUTHD_JWT_SECRET") mux := http.NewServeMux() triggerCh := make(chan struct{}, 1) wireAdminRoutes(mux, triggerCh, discardLogger()) rr := httptest.NewRecorder() mux.ServeHTTP(rr, httptest.NewRequest("POST", "/v1/admin/archiver/run", nil)) if rr.Code != http.StatusNotFound { t.Errorf("with no secret: got %d, want 404 (route unregistered)", rr.Code) } } // TestWireAdminRoutes_WithSecret_Gated verifies that with the // secret set, the admin route is 401 without a token. func TestWireAdminRoutes_WithSecret_Gated(t *testing.T) { t.Setenv("BA_AUTHD_JWT_SECRET", testSecret) t.Setenv("BA_AUTHD_ISSUER", "broad-announce") mux := http.NewServeMux() triggerCh := make(chan struct{}, 1) wireAdminRoutes(mux, triggerCh, discardLogger()) rr := httptest.NewRecorder() mux.ServeHTTP(rr, httptest.NewRequest("POST", "/v1/admin/archiver/run", nil)) if rr.Code != http.StatusUnauthorized { t.Errorf("with secret+no-token: status = %d, want 401", rr.Code) } } // TestAdminRunNow_Auth_Fires verifies that an authenticated user // firing /v1/admin/archiver/run actually delivers a value on the // trigger channel. The handler returns 200 with triggered=true. func TestAdminRunNow_Auth_Fires(t *testing.T) { t.Setenv("BA_AUTHD_JWT_SECRET", testSecret) tok := mintTestJWT(t, testSecret, "viewer") mux := http.NewServeMux() triggerCh := make(chan struct{}, 1) wireAdminRoutes(mux, triggerCh, discardLogger()) req := httptest.NewRequest("POST", "/v1/admin/archiver/run", nil) req.Header.Set("Authorization", "Bearer "+tok) rr := httptest.NewRecorder() mux.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body = %s", rr.Code, rr.Body.String()) } var body map[string]any if err := json.NewDecoder(rr.Body).Decode(&body); err != nil { t.Fatalf("decode: %v", err) } if trig, _ := body["triggered"].(bool); !trig { t.Errorf("triggered = %v, want true", body["triggered"]) } // Channel should have exactly one pending signal. select { case <-triggerCh: // good default: t.Error("triggerCh is empty after a successful fire") } } // TestAdminRunNow_Coalesces verifies the buffer-size-1 semantics: // a second concurrent request (before the loop drains the first) // gets 202 Accepted with triggered=false / reason=already_pending. func TestAdminRunNow_Coalesces(t *testing.T) { t.Setenv("BA_AUTHD_JWT_SECRET", testSecret) tok := mintTestJWT(t, testSecret, "viewer") mux := http.NewServeMux() triggerCh := make(chan struct{}, 1) // Pre-fill the channel as if a previous run is in flight. triggerCh <- struct{}{} wireAdminRoutes(mux, triggerCh, discardLogger()) req := httptest.NewRequest("POST", "/v1/admin/archiver/run", nil) req.Header.Set("Authorization", "Bearer "+tok) rr := httptest.NewRecorder() mux.ServeHTTP(rr, req) if rr.Code != http.StatusAccepted { t.Errorf("with already-pending trigger: status = %d, want 202", rr.Code) } var body map[string]any if err := json.NewDecoder(rr.Body).Decode(&body); err != nil { t.Fatalf("decode: %v", err) } if trig, _ := body["triggered"].(bool); trig { t.Errorf("triggered = true, want false (coalesced)") } if reason, _ := body["reason"].(string); reason != "already_pending" { t.Errorf("reason = %q, want %q", reason, "already_pending") } } // TestAdminRolePolicy_ViewerCanTrigger verifies that the // run-now endpoint is open to any authenticated user (viewer // included). The archiver is idempotent (Postgres advisory // lock) so a viewer firing it is safe. func TestAdminRolePolicy_ViewerCanTrigger(t *testing.T) { t.Setenv("BA_AUTHD_JWT_SECRET", testSecret) mux := http.NewServeMux() triggerCh := make(chan struct{}, 1) wireAdminRoutes(mux, triggerCh, discardLogger()) for _, role := range []string{"viewer", "tenant_admin", "super_admin"} { t.Run(role, func(t *testing.T) { tok := mintTestJWT(t, testSecret, role) // drain the channel so each subtest starts clean select { case <-triggerCh: default: } req := httptest.NewRequest("POST", "/v1/admin/archiver/run", nil) req.Header.Set("Authorization", "Bearer "+tok) rr := httptest.NewRecorder() mux.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("%s: status = %d, want 200; body = %s", role, rr.Code, rr.Body.String()) } }) } }