From bbfe6867c8bf9a2452f611e1992d918b705bd2e3 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Thu, 16 Apr 2026 09:47:00 +0200 Subject: [PATCH] Refactor arrtrix webhook to use fixed path and remove legacy config - Switch arrtrix webhook to a fixed path: /_arrtrix/webhook - Remove Radarr-specific and secret-based config from arrtrix - Simplify connector and webhook handler logic - Update NixOS module to drop legacy webhook config - Add new tests for generic arrtrix webhook handler --- .editorconfig | 6 + .gitattributes | 5 +- .../services/communication/matrix/default.nix | 13 +- .../nixos/temp/services/arrtrix/default.nix | 5 - packages/arrtrix/pkg/config/config_test.go | 37 +++++ packages/arrtrix/pkg/connector/config.go | 23 +-- packages/arrtrix/pkg/connector/config_test.go | 23 --- .../arrtrix/pkg/connector/example-config.yaml | 12 +- .../arrtrix/pkg/webhook/{radarr.go => arr.go} | 127 +++++------------ packages/arrtrix/pkg/webhook/arr_test.go | 114 +++++++++++++++ packages/arrtrix/pkg/webhook/radarr_test.go | 131 ------------------ 11 files changed, 211 insertions(+), 285 deletions(-) create mode 100644 .editorconfig delete mode 100644 packages/arrtrix/pkg/connector/config_test.go rename packages/arrtrix/pkg/webhook/{radarr.go => arr.go} (53%) create mode 100644 packages/arrtrix/pkg/webhook/arr_test.go delete mode 100644 packages/arrtrix/pkg/webhook/radarr_test.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e62b828 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 diff --git a/.gitattributes b/.gitattributes index 780e15a..6313b56 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1 @@ -* text=auto -core.autocrlf=false -core.eol=lf -core.filemode=false +* text=auto eol=lf diff --git a/modules/nixos/services/communication/matrix/default.nix b/modules/nixos/services/communication/matrix/default.nix index c9c11f1..607fa72 100644 --- a/modules/nixos/services/communication/matrix/default.nix +++ b/modules/nixos/services/communication/matrix/default.nix @@ -18,7 +18,7 @@ keyFile = "/var/lib/element-call/key"; mkMautrix = bridge: i: conf: { - ${bridge} = + ${bridge} = mkMerge [ { enable = true; registerToSynapse = true; @@ -43,7 +43,8 @@ }; }; } - // conf; + conf + ]; }; in { options.${namespace}.services.communication.matrix = { @@ -110,7 +111,13 @@ in { (mkMautrix "mautrix-signal" 1 {}) (mkMautrix "mautrix-telegram" 2 {}) (mkMautrix "mautrix-whatsapp" 3 {}) - (mkMautrix "arrtrix" 4 {}) + (mkMautrix "arrtrix" 4 { + settings.network.webhooks.radarr = { + enabled = true; + path = "/_arrtrix/webhooks/radarr"; + secret = ""; + }; + }) { matrix-synapse = { enable = true; diff --git a/modules/nixos/temp/services/arrtrix/default.nix b/modules/nixos/temp/services/arrtrix/default.nix index 67ff0b9..618de39 100644 --- a/modules/nixos/temp/services/arrtrix/default.nix +++ b/modules/nixos/temp/services/arrtrix/default.nix @@ -15,11 +15,6 @@ settingsFormat = pkgs.formats.json {}; defaultConfig = { - network.webhooks.radarr = { - enabled = false; - path = "/_arrtrix/webhooks/radarr"; - secret = ""; - }; bridge = { command_prefix = "!arr"; relay.enabled = true; diff --git a/packages/arrtrix/pkg/config/config_test.go b/packages/arrtrix/pkg/config/config_test.go index dc08292..84b09df 100644 --- a/packages/arrtrix/pkg/config/config_test.go +++ b/packages/arrtrix/pkg/config/config_test.go @@ -120,3 +120,40 @@ encryption: t.Fatalf("expected hidden double puppet secrets to stay internal-only") } } + +func TestLoadIgnoresLegacyWebhookSettings(t *testing.T) { + cfg, err := Load([]byte(` +network: + webhooks: + radarr: + enabled: true + path: /_arrtrix/webhooks/radarr + secret: legacy-secret +bridge: + command_prefix: "!arr" +homeserver: + address: http://127.0.0.1:8008 + domain: test.local +appservice: + id: arrtrix + bot: + username: arrtrixbot + displayname: Arrtrix Bot + username_template: arrtrix_{{.}} +database: + type: sqlite3-fk-wal + uri: file:arrtrix.db?_txlock=immediate +logging: + min_level: info + writers: + - type: stdout + format: pretty-colored +`)) + if err != nil { + t.Fatalf("Load returned error: %v", err) + } + + if cfg == nil { + t.Fatal("expected config to load") + } +} diff --git a/packages/arrtrix/pkg/connector/config.go b/packages/arrtrix/pkg/connector/config.go index 3702cee..2cdec34 100644 --- a/packages/arrtrix/pkg/connector/config.go +++ b/packages/arrtrix/pkg/connector/config.go @@ -14,40 +14,23 @@ import ( //go:embed example-config.yaml var ExampleConfig string -type Config struct { - Webhooks WebhooksConfig `yaml:"webhooks"` -} - -type WebhooksConfig struct { - Radarr webhook.RadarrConfig `yaml:"radarr"` -} - -func (c *Config) applyDefaults() { - c.Webhooks.Radarr.ApplyDefaults() -} - -func (c *Config) Validate() error { - return c.Webhooks.Radarr.Validate() -} +type Config struct{} func upgradeConfig(helper up.Helper) {} func (s *ArrtrixConnector) GetConfig() (string, any, up.Upgrader) { - s.Config.applyDefaults() return ExampleConfig, &s.Config, up.SimpleUpgrader(upgradeConfig) } func (s *ArrtrixConnector) ValidateConfig() error { - s.Config.applyDefaults() - return s.Config.Validate() + return nil } func (s *ArrtrixConnector) MountRoutes(router *http.ServeMux) error { - s.Config.applyDefaults() if s.Bridge == nil { return fmt.Errorf("bridge is not initialized") } - return webhook.MountRadarr(router, s.Bridge, s.Config.Webhooks.Radarr) + return webhook.MountArr(router, s.Bridge) } var _ bridgev2.ConfigValidatingNetwork = (*ArrtrixConnector)(nil) diff --git a/packages/arrtrix/pkg/connector/config_test.go b/packages/arrtrix/pkg/connector/config_test.go deleted file mode 100644 index 5199308..0000000 --- a/packages/arrtrix/pkg/connector/config_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package connector - -import "testing" - -func TestConfigDefaultsApplyRadarrWebhookPath(t *testing.T) { - var cfg Config - - cfg.applyDefaults() - - if cfg.Webhooks.Radarr.Path == "" { - t.Fatal("expected radarr webhook path default to be set") - } -} - -func TestConfigValidateRejectsEnabledWebhookWithoutSecret(t *testing.T) { - cfg := Config{} - cfg.Webhooks.Radarr.Enabled = true - cfg.applyDefaults() - - if err := cfg.Validate(); err == nil { - t.Fatal("expected missing secret to fail validation") - } -} diff --git a/packages/arrtrix/pkg/connector/example-config.yaml b/packages/arrtrix/pkg/connector/example-config.yaml index 5fa52c6..9c11ddf 100644 --- a/packages/arrtrix/pkg/connector/example-config.yaml +++ b/packages/arrtrix/pkg/connector/example-config.yaml @@ -1,10 +1,4 @@ -# Arrtrix-specific runtime options. +# No network-specific config is required yet. # -webhooks: - radarr: - enabled: false - path: /_arrtrix/webhooks/radarr - secret: "" - # The first implementation delivers notifications to the only configured - # management room. If more than one management room exists, the webhook is - # rejected until routing is configured more explicitly. +# Arr-stack webhooks are exposed automatically on the fixed built-in path: +# POST /_arrtrix/webhook diff --git a/packages/arrtrix/pkg/webhook/radarr.go b/packages/arrtrix/pkg/webhook/arr.go similarity index 53% rename from packages/arrtrix/pkg/webhook/radarr.go rename to packages/arrtrix/pkg/webhook/arr.go index 6f74342..42e350c 100644 --- a/packages/arrtrix/pkg/webhook/radarr.go +++ b/packages/arrtrix/pkg/webhook/arr.go @@ -14,30 +14,21 @@ import ( "maunium.net/go/mautrix/id" ) -const ( - defaultRadarrWebhookPath = "/_arrtrix/webhooks/radarr" - radarrSecretHeader = "X-Arrtrix-Webhook-Secret" -) +const ArrWebhookPath = "/_arrtrix/webhook" var ( ErrNoManagementRoom = errors.New("no management room configured") ErrAmbiguousManagementRoom = errors.New("multiple management rooms configured") ) -type RadarrConfig struct { - Enabled bool `yaml:"enabled"` - Path string `yaml:"path"` - Secret string `yaml:"secret"` +type payload struct { + EventType string `json:"eventType"` + Movie *movie `json:"movie"` + MovieFile *movieFile `json:"movieFile"` + IsUpgrade bool `json:"isUpgrade"` } -type radarrPayload struct { - EventType string `json:"eventType"` - Movie *radarrMovie `json:"movie"` - MovieFile *radarrMovieFile `json:"movieFile"` - IsUpgrade bool `json:"isUpgrade"` -} - -type radarrMovie struct { +type movie struct { Title string `json:"title"` Year int `json:"year"` ImdbID string `json:"imdbId"` @@ -45,7 +36,7 @@ type radarrMovie struct { Path string `json:"path"` } -type radarrMovieFile struct { +type movieFile struct { Quality string `json:"quality"` RelativePath string `json:"relativePath"` SceneName string `json:"sceneName"` @@ -60,62 +51,30 @@ type noticeSender interface { SendNotice(context.Context, id.RoomID, string) error } -type RadarrHandler struct { - config RadarrConfig +type ArrHandler struct { resolver roomResolver sender noticeSender } -func (c *RadarrConfig) ApplyDefaults() { - if c.Path == "" { - c.Path = defaultRadarrWebhookPath +func MountArr(router *http.ServeMux, bridge *bridgev2.Bridge) error { + if bridge == nil { + return fmt.Errorf("bridge is not initialized") } -} - -func (c *RadarrConfig) Validate() error { - c.ApplyDefaults() - if !c.Enabled { - return nil - } - if !strings.HasPrefix(c.Path, "/") { - return fmt.Errorf("network.webhooks.radarr.path must start with /") - } - if strings.TrimSpace(c.Secret) == "" { - return fmt.Errorf("network.webhooks.radarr.secret must be set when the webhook is enabled") - } - return nil -} - -func MountRadarr(router *http.ServeMux, bridge *bridgev2.Bridge, cfg RadarrConfig) error { - cfg.ApplyDefaults() - if !cfg.Enabled { - return nil - } - if err := cfg.Validate(); err != nil { - return err - } - - handler := &RadarrHandler{ - config: cfg, + handler := &ArrHandler{ resolver: bridgeRoomResolver{bridge: bridge}, sender: bridgeNoticeSender{bridge: bridge}, } - router.Handle(fmt.Sprintf("POST %s", cfg.Path), handler) + router.Handle(fmt.Sprintf("POST %s", ArrWebhookPath), handler) return nil } -func (h *RadarrHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if !authorized(r, h.config.Secret) { - http.Error(w, "invalid webhook secret", http.StatusUnauthorized) - return - } - - var payload radarrPayload - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { +func (h *ArrHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var body payload + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "invalid webhook payload", http.StatusBadRequest) return } - if strings.TrimSpace(payload.EventType) == "" { + if strings.TrimSpace(body.EventType) == "" { http.Error(w, "missing eventType", http.StatusBadRequest) return } @@ -130,7 +89,7 @@ func (h *RadarrHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - if err = h.sender.SendNotice(r.Context(), roomID, renderRadarrNotice(payload)); err != nil { + if err = h.sender.SendNotice(r.Context(), roomID, renderNotice(body)); err != nil { http.Error(w, "failed to deliver webhook", http.StatusBadGateway) return } @@ -164,6 +123,7 @@ func (r bridgeRoomResolver) ResolveManagementRoom(ctx context.Context) (id.RoomI if err = rows.Err(); err != nil { return "", fmt.Errorf("failed to iterate management rooms: %w", err) } + switch len(owners) { case 0: return "", ErrNoManagementRoom @@ -187,43 +147,30 @@ func (s bridgeNoticeSender) SendNotice(ctx context.Context, roomID id.RoomID, ma return err } -func authorized(r *http.Request, secret string) bool { - if secret == "" { - return true - } - if r.Header.Get(radarrSecretHeader) == secret { - return true - } - if bearer := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "); bearer == secret && bearer != r.Header.Get("Authorization") { - return true - } - return r.URL.Query().Get("secret") == secret -} - -func renderRadarrNotice(payload radarrPayload) string { - title := "Radarr" - if payload.Movie != nil { - title = payload.Movie.Title - if payload.Movie.Year != 0 { - title = fmt.Sprintf("%s (%d)", title, payload.Movie.Year) +func renderNotice(body payload) string { + title := "Arr" + if body.Movie != nil { + title = body.Movie.Title + if body.Movie.Year != 0 { + title = fmt.Sprintf("%s (%d)", title, body.Movie.Year) } } - lines := []string{fmt.Sprintf("**Radarr %s**", payload.EventType)} - if title != "Radarr" { + lines := []string{fmt.Sprintf("**Arr %s**", body.EventType)} + if title != "Arr" { lines = append(lines, fmt.Sprintf("Movie: %s", title)) } - if payload.MovieFile != nil && payload.MovieFile.Quality != "" { - lines = append(lines, fmt.Sprintf("Quality: %s", payload.MovieFile.Quality)) + if body.MovieFile != nil && body.MovieFile.Quality != "" { + lines = append(lines, fmt.Sprintf("Quality: %s", body.MovieFile.Quality)) } - if payload.MovieFile != nil && payload.MovieFile.RelativePath != "" { - lines = append(lines, fmt.Sprintf("File: `%s`", payload.MovieFile.RelativePath)) + if body.MovieFile != nil && body.MovieFile.RelativePath != "" { + lines = append(lines, fmt.Sprintf("File: `%s`", body.MovieFile.RelativePath)) } - if payload.EventType == "Download" { - lines = append(lines, fmt.Sprintf("Upgrade: %t", payload.IsUpgrade)) + if body.EventType == "Download" { + lines = append(lines, fmt.Sprintf("Upgrade: %t", body.IsUpgrade)) } - if payload.Movie != nil && payload.Movie.ImdbID != "" { - lines = append(lines, fmt.Sprintf("IMDb: `%s`", payload.Movie.ImdbID)) + if body.Movie != nil && body.Movie.ImdbID != "" { + lines = append(lines, fmt.Sprintf("IMDb: `%s`", body.Movie.ImdbID)) } return strings.Join(lines, "\n") } @@ -238,4 +185,4 @@ func convertUserIDs(users []id.UserID) []string { var _ roomResolver = bridgeRoomResolver{} var _ noticeSender = bridgeNoticeSender{} -var _ http.Handler = (*RadarrHandler)(nil) +var _ http.Handler = (*ArrHandler)(nil) diff --git a/packages/arrtrix/pkg/webhook/arr_test.go b/packages/arrtrix/pkg/webhook/arr_test.go new file mode 100644 index 0000000..b7ac511 --- /dev/null +++ b/packages/arrtrix/pkg/webhook/arr_test.go @@ -0,0 +1,114 @@ +package webhook + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "maunium.net/go/mautrix/id" +) + +type stubRoomResolver struct { + roomID id.RoomID + err error +} + +func (s stubRoomResolver) ResolveManagementRoom(context.Context) (id.RoomID, error) { + return s.roomID, s.err +} + +type stubNoticeSender struct { + roomID id.RoomID + message string + err error +} + +func (s *stubNoticeSender) SendNotice(_ context.Context, roomID id.RoomID, message string) error { + s.roomID = roomID + s.message = message + return s.err +} + +func TestMountArrRequiresBridge(t *testing.T) { + router := http.NewServeMux() + if err := MountArr(router, nil); err == nil { + t.Fatal("expected nil bridge to fail") + } +} + +func TestArrHandlerDeliversNotice(t *testing.T) { + sender := &stubNoticeSender{} + handler := &ArrHandler{ + resolver: stubRoomResolver{roomID: "!room:test"}, + sender: sender, + } + + req := httptest.NewRequest(http.MethodPost, ArrWebhookPath, strings.NewReader(`{"eventType":"Download","movie":{"title":"Dune","year":2021,"imdbId":"tt1160419"},"movieFile":{"quality":"1080p","relativePath":"Dune (2021)/Dune.mkv"},"isUpgrade":false}`)) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusAccepted { + t.Fatalf("expected accepted status, got %d", rec.Code) + } + if sender.roomID != "!room:test" { + t.Fatalf("expected notice sent to management room, got %q", sender.roomID) + } + if !strings.Contains(sender.message, "**Arr Download**") || !strings.Contains(sender.message, "Dune (2021)") { + t.Fatalf("unexpected message: %s", sender.message) + } +} + +func TestArrHandlerReportsAmbiguousManagementRoom(t *testing.T) { + handler := &ArrHandler{ + resolver: stubRoomResolver{err: ErrAmbiguousManagementRoom}, + sender: &stubNoticeSender{}, + } + + req := httptest.NewRequest(http.MethodPost, ArrWebhookPath, strings.NewReader(`{"eventType":"Test"}`)) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusConflict { + t.Fatalf("expected conflict status, got %d", rec.Code) + } +} + +func TestRenderNoticeForTestEvent(t *testing.T) { + msg := renderNotice(payload{EventType: "Test"}) + if strings.TrimSpace(msg) != "**Arr Test**" { + t.Fatalf("unexpected test-event message: %q", msg) + } +} + +func TestArrHandlerReturnsBadGatewayOnSendFailure(t *testing.T) { + handler := &ArrHandler{ + resolver: stubRoomResolver{roomID: "!room:test"}, + sender: &stubNoticeSender{err: errors.New("send failed")}, + } + + req := httptest.NewRequest(http.MethodPost, ArrWebhookPath, strings.NewReader(`{"eventType":"Test"}`)) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusBadGateway { + t.Fatalf("expected bad gateway status, got %d", rec.Code) + } +} + +func TestArrHandlerRejectsMissingEventType(t *testing.T) { + handler := &ArrHandler{ + resolver: stubRoomResolver{roomID: "!room:test"}, + sender: &stubNoticeSender{}, + } + + req := httptest.NewRequest(http.MethodPost, ArrWebhookPath, strings.NewReader(`{"movie":{"title":"Dune"}}`)) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Fatalf("expected bad request status, got %d", rec.Code) + } +} diff --git a/packages/arrtrix/pkg/webhook/radarr_test.go b/packages/arrtrix/pkg/webhook/radarr_test.go deleted file mode 100644 index d4fc962..0000000 --- a/packages/arrtrix/pkg/webhook/radarr_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package webhook - -import ( - "context" - "errors" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "maunium.net/go/mautrix/id" -) - -type stubRoomResolver struct { - roomID id.RoomID - err error -} - -func (s stubRoomResolver) ResolveManagementRoom(context.Context) (id.RoomID, error) { - return s.roomID, s.err -} - -type stubNoticeSender struct { - roomID id.RoomID - message string - err error -} - -func (s *stubNoticeSender) SendNotice(_ context.Context, roomID id.RoomID, message string) error { - s.roomID = roomID - s.message = message - return s.err -} - -func TestRadarrConfigDefaultsAndValidation(t *testing.T) { - cfg := RadarrConfig{Enabled: true, Secret: "secret"} - cfg.ApplyDefaults() - if cfg.Path != defaultRadarrWebhookPath { - t.Fatalf("expected default path %q, got %q", defaultRadarrWebhookPath, cfg.Path) - } - if err := cfg.Validate(); err != nil { - t.Fatalf("expected config to validate, got %v", err) - } -} - -func TestRadarrConfigRequiresSecretWhenEnabled(t *testing.T) { - cfg := RadarrConfig{Enabled: true} - if err := cfg.Validate(); err == nil { - t.Fatal("expected missing secret to fail validation") - } -} - -func TestRadarrHandlerRejectsUnauthorizedRequests(t *testing.T) { - handler := &RadarrHandler{ - config: RadarrConfig{Enabled: true, Secret: "secret"}, - resolver: stubRoomResolver{roomID: "!room:test"}, - sender: &stubNoticeSender{}, - } - - req := httptest.NewRequest(http.MethodPost, defaultRadarrWebhookPath, strings.NewReader(`{"eventType":"Test"}`)) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusUnauthorized { - t.Fatalf("expected unauthorized status, got %d", rec.Code) - } -} - -func TestRadarrHandlerDeliversNotice(t *testing.T) { - sender := &stubNoticeSender{} - handler := &RadarrHandler{ - config: RadarrConfig{Enabled: true, Secret: "secret"}, - resolver: stubRoomResolver{roomID: "!room:test"}, - sender: sender, - } - - req := httptest.NewRequest(http.MethodPost, defaultRadarrWebhookPath+"?secret=secret", strings.NewReader(`{"eventType":"Download","movie":{"title":"Dune","year":2021,"imdbId":"tt1160419"},"movieFile":{"quality":"1080p","relativePath":"Dune (2021)/Dune.mkv"},"isUpgrade":false}`)) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusAccepted { - t.Fatalf("expected accepted status, got %d", rec.Code) - } - if sender.roomID != "!room:test" { - t.Fatalf("expected notice sent to management room, got %q", sender.roomID) - } - if !strings.Contains(sender.message, "**Radarr Download**") || !strings.Contains(sender.message, "Dune (2021)") { - t.Fatalf("unexpected message: %s", sender.message) - } -} - -func TestRadarrHandlerReportsAmbiguousManagementRoom(t *testing.T) { - handler := &RadarrHandler{ - config: RadarrConfig{Enabled: true, Secret: "secret"}, - resolver: stubRoomResolver{err: ErrAmbiguousManagementRoom}, - sender: &stubNoticeSender{}, - } - - req := httptest.NewRequest(http.MethodPost, defaultRadarrWebhookPath, strings.NewReader(`{"eventType":"Test"}`)) - req.Header.Set(radarrSecretHeader, "secret") - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusConflict { - t.Fatalf("expected conflict status, got %d", rec.Code) - } -} - -func TestRenderRadarrNoticeForTestEvent(t *testing.T) { - msg := renderRadarrNotice(radarrPayload{EventType: "Test"}) - if strings.TrimSpace(msg) != "**Radarr Test**" { - t.Fatalf("unexpected test-event message: %q", msg) - } -} - -func TestRadarrHandlerReturnsBadGatewayOnSendFailure(t *testing.T) { - handler := &RadarrHandler{ - config: RadarrConfig{Enabled: true, Secret: "secret"}, - resolver: stubRoomResolver{roomID: "!room:test"}, - sender: &stubNoticeSender{err: errors.New("send failed")}, - } - - req := httptest.NewRequest(http.MethodPost, defaultRadarrWebhookPath, strings.NewReader(`{"eventType":"Test"}`)) - req.Header.Set(radarrSecretHeader, "secret") - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusBadGateway { - t.Fatalf("expected bad gateway status, got %d", rec.Code) - } -}