admin_test.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. // Tests for the M13a W5 JWT gate in archiverd. We test the
  2. // gate wiring + the trigger-channel semantics. We don't spin
  3. // up the runLoop (no Postgres / ClickHouse in unit tests); the
  4. // admin handler is decoupled from the loop via the channel.
  5. package main
  6. import (
  7. "crypto/hmac"
  8. "crypto/sha256"
  9. "encoding/base64"
  10. "encoding/json"
  11. "io"
  12. "log/slog"
  13. "net/http"
  14. "net/http/httptest"
  15. "os"
  16. "testing"
  17. "time"
  18. )
  19. const testSecret = "test-secret-with-32-bytes-min-len-abc"
  20. func mintTestJWT(t *testing.T, secret, role string) string {
  21. t.Helper()
  22. hb, _ := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT"})
  23. body, _ := json.Marshal(map[string]any{
  24. "sub": "u-test",
  25. "tid": "t-test",
  26. "role": role,
  27. "typ": "access",
  28. "iss": "broad-announce",
  29. "exp": time.Now().Add(15 * time.Minute).Unix(),
  30. "iat": time.Now().Unix(),
  31. })
  32. enc := base64.RawURLEncoding.EncodeToString(hb) + "." + base64.RawURLEncoding.EncodeToString(body)
  33. mac := hmac.New(sha256.New, []byte(secret))
  34. mac.Write([]byte(enc))
  35. return enc + "." + base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
  36. }
  37. func discardLogger() *slog.Logger {
  38. return slog.New(slog.NewTextHandler(io.Discard, nil))
  39. }
  40. // TestWireAdminRoutes_NoSecret_Disabled verifies that with
  41. // BA_AUTHD_JWT_SECRET unset, the admin route is NOT registered.
  42. func TestWireAdminRoutes_NoSecret_Disabled(t *testing.T) {
  43. os.Unsetenv("BA_AUTHD_JWT_SECRET")
  44. mux := http.NewServeMux()
  45. triggerCh := make(chan struct{}, 1)
  46. wireAdminRoutes(mux, triggerCh, discardLogger())
  47. rr := httptest.NewRecorder()
  48. mux.ServeHTTP(rr, httptest.NewRequest("POST", "/v1/admin/archiver/run", nil))
  49. if rr.Code != http.StatusNotFound {
  50. t.Errorf("with no secret: got %d, want 404 (route unregistered)", rr.Code)
  51. }
  52. }
  53. // TestWireAdminRoutes_WithSecret_Gated verifies that with the
  54. // secret set, the admin route is 401 without a token.
  55. func TestWireAdminRoutes_WithSecret_Gated(t *testing.T) {
  56. t.Setenv("BA_AUTHD_JWT_SECRET", testSecret)
  57. t.Setenv("BA_AUTHD_ISSUER", "broad-announce")
  58. mux := http.NewServeMux()
  59. triggerCh := make(chan struct{}, 1)
  60. wireAdminRoutes(mux, triggerCh, discardLogger())
  61. rr := httptest.NewRecorder()
  62. mux.ServeHTTP(rr, httptest.NewRequest("POST", "/v1/admin/archiver/run", nil))
  63. if rr.Code != http.StatusUnauthorized {
  64. t.Errorf("with secret+no-token: status = %d, want 401", rr.Code)
  65. }
  66. }
  67. // TestAdminRunNow_Auth_Fires verifies that an authenticated user
  68. // firing /v1/admin/archiver/run actually delivers a value on the
  69. // trigger channel. The handler returns 200 with triggered=true.
  70. func TestAdminRunNow_Auth_Fires(t *testing.T) {
  71. t.Setenv("BA_AUTHD_JWT_SECRET", testSecret)
  72. tok := mintTestJWT(t, testSecret, "viewer")
  73. mux := http.NewServeMux()
  74. triggerCh := make(chan struct{}, 1)
  75. wireAdminRoutes(mux, triggerCh, discardLogger())
  76. req := httptest.NewRequest("POST", "/v1/admin/archiver/run", nil)
  77. req.Header.Set("Authorization", "Bearer "+tok)
  78. rr := httptest.NewRecorder()
  79. mux.ServeHTTP(rr, req)
  80. if rr.Code != http.StatusOK {
  81. t.Fatalf("status = %d, want 200; body = %s", rr.Code, rr.Body.String())
  82. }
  83. var body map[string]any
  84. if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
  85. t.Fatalf("decode: %v", err)
  86. }
  87. if trig, _ := body["triggered"].(bool); !trig {
  88. t.Errorf("triggered = %v, want true", body["triggered"])
  89. }
  90. // Channel should have exactly one pending signal.
  91. select {
  92. case <-triggerCh:
  93. // good
  94. default:
  95. t.Error("triggerCh is empty after a successful fire")
  96. }
  97. }
  98. // TestAdminRunNow_Coalesces verifies the buffer-size-1 semantics:
  99. // a second concurrent request (before the loop drains the first)
  100. // gets 202 Accepted with triggered=false / reason=already_pending.
  101. func TestAdminRunNow_Coalesces(t *testing.T) {
  102. t.Setenv("BA_AUTHD_JWT_SECRET", testSecret)
  103. tok := mintTestJWT(t, testSecret, "viewer")
  104. mux := http.NewServeMux()
  105. triggerCh := make(chan struct{}, 1)
  106. // Pre-fill the channel as if a previous run is in flight.
  107. triggerCh <- struct{}{}
  108. wireAdminRoutes(mux, triggerCh, discardLogger())
  109. req := httptest.NewRequest("POST", "/v1/admin/archiver/run", nil)
  110. req.Header.Set("Authorization", "Bearer "+tok)
  111. rr := httptest.NewRecorder()
  112. mux.ServeHTTP(rr, req)
  113. if rr.Code != http.StatusAccepted {
  114. t.Errorf("with already-pending trigger: status = %d, want 202", rr.Code)
  115. }
  116. var body map[string]any
  117. if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
  118. t.Fatalf("decode: %v", err)
  119. }
  120. if trig, _ := body["triggered"].(bool); trig {
  121. t.Errorf("triggered = true, want false (coalesced)")
  122. }
  123. if reason, _ := body["reason"].(string); reason != "already_pending" {
  124. t.Errorf("reason = %q, want %q", reason, "already_pending")
  125. }
  126. }
  127. // TestAdminRolePolicy_ViewerCanTrigger verifies that the
  128. // run-now endpoint is open to any authenticated user (viewer
  129. // included). The archiver is idempotent (Postgres advisory
  130. // lock) so a viewer firing it is safe.
  131. func TestAdminRolePolicy_ViewerCanTrigger(t *testing.T) {
  132. t.Setenv("BA_AUTHD_JWT_SECRET", testSecret)
  133. mux := http.NewServeMux()
  134. triggerCh := make(chan struct{}, 1)
  135. wireAdminRoutes(mux, triggerCh, discardLogger())
  136. for _, role := range []string{"viewer", "tenant_admin", "super_admin"} {
  137. t.Run(role, func(t *testing.T) {
  138. tok := mintTestJWT(t, testSecret, role)
  139. // drain the channel so each subtest starts clean
  140. select {
  141. case <-triggerCh:
  142. default:
  143. }
  144. req := httptest.NewRequest("POST", "/v1/admin/archiver/run", nil)
  145. req.Header.Set("Authorization", "Bearer "+tok)
  146. rr := httptest.NewRecorder()
  147. mux.ServeHTTP(rr, req)
  148. if rr.Code != http.StatusOK {
  149. t.Errorf("%s: status = %d, want 200; body = %s", role, rr.Code, rr.Body.String())
  150. }
  151. })
  152. }
  153. }