admin_test.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. // Tests for the M13a W5 JWT gate in deliverd-fcm. We test
  2. // the gate wiring (which routes are protected, the role
  3. // policy, and the cross-channel safety net). The actual
  4. // DLQ handlers need a real Postgres pool; those paths are
  5. // covered by the M8 smoke and the m13a E2E.
  6. package main
  7. import (
  8. "crypto/hmac"
  9. "crypto/sha256"
  10. "encoding/base64"
  11. "encoding/json"
  12. "io"
  13. "log/slog"
  14. "net/http"
  15. "net/http/httptest"
  16. "os"
  17. "testing"
  18. "time"
  19. )
  20. const testSecret = "test-secret-with-32-bytes-min-len-abc"
  21. func mintTestJWT(t *testing.T, secret, role string) string {
  22. t.Helper()
  23. hb, _ := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT"})
  24. body, _ := json.Marshal(map[string]any{
  25. "sub": "u-test",
  26. "tid": "t-test",
  27. "role": role,
  28. "typ": "access",
  29. "iss": "broad-announce",
  30. "exp": time.Now().Add(15 * time.Minute).Unix(),
  31. "iat": time.Now().Unix(),
  32. })
  33. enc := base64.RawURLEncoding.EncodeToString(hb) + "." + base64.RawURLEncoding.EncodeToString(body)
  34. mac := hmac.New(sha256.New, []byte(secret))
  35. mac.Write([]byte(enc))
  36. return enc + "." + base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
  37. }
  38. func discardLogger() *slog.Logger {
  39. return slog.New(slog.NewTextHandler(io.Discard, nil))
  40. }
  41. func TestWireAdminRoutes_NoSecret_Disabled(t *testing.T) {
  42. os.Unsetenv("BA_AUTHD_JWT_SECRET")
  43. mux := http.NewServeMux()
  44. wireAdminRoutes(mux, nil, discardLogger())
  45. for _, tc := range []struct{ method, path string }{
  46. {"GET", "/v1/admin/dlq"},
  47. {"GET", "/v1/admin/dlq/1"},
  48. } {
  49. rr := httptest.NewRecorder()
  50. mux.ServeHTTP(rr, httptest.NewRequest(tc.method, tc.path, nil))
  51. if rr.Code != http.StatusNotFound {
  52. t.Errorf("%s %s with no secret: got %d, want 404", tc.method, tc.path, rr.Code)
  53. }
  54. }
  55. }
  56. func TestWireAdminRoutes_WithSecret_Gated(t *testing.T) {
  57. t.Setenv("BA_AUTHD_JWT_SECRET", testSecret)
  58. t.Setenv("BA_AUTHD_ISSUER", "broad-announce")
  59. mux := http.NewServeMux()
  60. wireAdminRoutes(mux, nil, discardLogger())
  61. for _, tc := range []struct{ method, path string }{
  62. {"GET", "/v1/admin/dlq"},
  63. {"GET", "/v1/admin/dlq/1"},
  64. } {
  65. rr := httptest.NewRecorder()
  66. mux.ServeHTTP(rr, httptest.NewRequest(tc.method, tc.path, nil))
  67. // The handler panics because pool is nil. We don't
  68. // care about the panic body — the test only checks
  69. // that the gate fired (401) before reaching the
  70. // handler.
  71. if rr.Code != http.StatusUnauthorized {
  72. t.Errorf("%s %s with secret+no-token: status = %d, want 401", tc.method, tc.path, rr.Code)
  73. }
  74. }
  75. }
  76. func TestAdminDLQ_AnyAuthenticatedUser_CanList(t *testing.T) {
  77. // Both GETs are read-only. viewer is fine.
  78. t.Setenv("BA_AUTHD_JWT_SECRET", testSecret)
  79. mux := http.NewServeMux()
  80. // Use the real RequireAuth middleware so we test the actual
  81. // route wiring path. We don't need a real pool — the
  82. // handler will panic on the first DB call, but the test
  83. // is satisfied as long as the request was authorized.
  84. wireAdminRoutes(mux, nil, discardLogger())
  85. for _, role := range []string{"viewer", "tenant_admin", "super_admin"} {
  86. t.Run(role, func(t *testing.T) {
  87. tok := mintTestJWT(t, testSecret, role)
  88. req := httptest.NewRequest("GET", "/v1/admin/dlq", nil)
  89. req.Header.Set("Authorization", "Bearer "+tok)
  90. rr := httptest.NewRecorder()
  91. func() {
  92. defer func() {
  93. // expected: pool is nil
  94. _ = recover()
  95. }()
  96. mux.ServeHTTP(rr, req)
  97. }()
  98. // We expect 500 (panic) NOT 401/403 — the gate
  99. // authorized the request, the handler then died
  100. // on nil pool.
  101. if rr.Code == http.StatusUnauthorized || rr.Code == http.StatusForbidden {
  102. t.Errorf("%s: status = %d, want gate passed (500/panic expected)", role, rr.Code)
  103. }
  104. })
  105. }
  106. }