| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- // 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())
- }
- })
- }
- }
|