Refactor arrtrix webhook to use fixed path and remove legacy config
Some checks failed
Test action / kaas (push) Failing after 1s
Some checks failed
Test action / kaas (push) Failing after 1s
- 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
This commit is contained in:
parent
fe627f3aab
commit
bbfe6867c8
11 changed files with 211 additions and 285 deletions
6
.editorconfig
Normal file
6
.editorconfig
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
charset = utf-8
|
||||||
5
.gitattributes
vendored
5
.gitattributes
vendored
|
|
@ -1,4 +1 @@
|
||||||
* text=auto
|
* text=auto eol=lf
|
||||||
core.autocrlf=false
|
|
||||||
core.eol=lf
|
|
||||||
core.filemode=false
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
keyFile = "/var/lib/element-call/key";
|
keyFile = "/var/lib/element-call/key";
|
||||||
|
|
||||||
mkMautrix = bridge: i: conf: {
|
mkMautrix = bridge: i: conf: {
|
||||||
${bridge} =
|
${bridge} = mkMerge [
|
||||||
{
|
{
|
||||||
enable = true;
|
enable = true;
|
||||||
registerToSynapse = true;
|
registerToSynapse = true;
|
||||||
|
|
@ -43,7 +43,8 @@
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// conf;
|
conf
|
||||||
|
];
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
options.${namespace}.services.communication.matrix = {
|
options.${namespace}.services.communication.matrix = {
|
||||||
|
|
@ -110,7 +111,13 @@ in {
|
||||||
(mkMautrix "mautrix-signal" 1 {})
|
(mkMautrix "mautrix-signal" 1 {})
|
||||||
(mkMautrix "mautrix-telegram" 2 {})
|
(mkMautrix "mautrix-telegram" 2 {})
|
||||||
(mkMautrix "mautrix-whatsapp" 3 {})
|
(mkMautrix "mautrix-whatsapp" 3 {})
|
||||||
(mkMautrix "arrtrix" 4 {})
|
(mkMautrix "arrtrix" 4 {
|
||||||
|
settings.network.webhooks.radarr = {
|
||||||
|
enabled = true;
|
||||||
|
path = "/_arrtrix/webhooks/radarr";
|
||||||
|
secret = "";
|
||||||
|
};
|
||||||
|
})
|
||||||
{
|
{
|
||||||
matrix-synapse = {
|
matrix-synapse = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,6 @@
|
||||||
settingsFormat = pkgs.formats.json {};
|
settingsFormat = pkgs.formats.json {};
|
||||||
|
|
||||||
defaultConfig = {
|
defaultConfig = {
|
||||||
network.webhooks.radarr = {
|
|
||||||
enabled = false;
|
|
||||||
path = "/_arrtrix/webhooks/radarr";
|
|
||||||
secret = "";
|
|
||||||
};
|
|
||||||
bridge = {
|
bridge = {
|
||||||
command_prefix = "!arr";
|
command_prefix = "!arr";
|
||||||
relay.enabled = true;
|
relay.enabled = true;
|
||||||
|
|
|
||||||
|
|
@ -120,3 +120,40 @@ encryption:
|
||||||
t.Fatalf("expected hidden double puppet secrets to stay internal-only")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,40 +14,23 @@ import (
|
||||||
//go:embed example-config.yaml
|
//go:embed example-config.yaml
|
||||||
var ExampleConfig string
|
var ExampleConfig string
|
||||||
|
|
||||||
type Config struct {
|
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
func upgradeConfig(helper up.Helper) {}
|
func upgradeConfig(helper up.Helper) {}
|
||||||
|
|
||||||
func (s *ArrtrixConnector) GetConfig() (string, any, up.Upgrader) {
|
func (s *ArrtrixConnector) GetConfig() (string, any, up.Upgrader) {
|
||||||
s.Config.applyDefaults()
|
|
||||||
return ExampleConfig, &s.Config, up.SimpleUpgrader(upgradeConfig)
|
return ExampleConfig, &s.Config, up.SimpleUpgrader(upgradeConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ArrtrixConnector) ValidateConfig() error {
|
func (s *ArrtrixConnector) ValidateConfig() error {
|
||||||
s.Config.applyDefaults()
|
return nil
|
||||||
return s.Config.Validate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ArrtrixConnector) MountRoutes(router *http.ServeMux) error {
|
func (s *ArrtrixConnector) MountRoutes(router *http.ServeMux) error {
|
||||||
s.Config.applyDefaults()
|
|
||||||
if s.Bridge == nil {
|
if s.Bridge == nil {
|
||||||
return fmt.Errorf("bridge is not initialized")
|
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)
|
var _ bridgev2.ConfigValidatingNetwork = (*ArrtrixConnector)(nil)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
# Arrtrix-specific runtime options.
|
# No network-specific config is required yet.
|
||||||
#
|
#
|
||||||
webhooks:
|
# Arr-stack webhooks are exposed automatically on the fixed built-in path:
|
||||||
radarr:
|
# POST /_arrtrix/webhook
|
||||||
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.
|
|
||||||
|
|
|
||||||
|
|
@ -14,30 +14,21 @@ import (
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const ArrWebhookPath = "/_arrtrix/webhook"
|
||||||
defaultRadarrWebhookPath = "/_arrtrix/webhooks/radarr"
|
|
||||||
radarrSecretHeader = "X-Arrtrix-Webhook-Secret"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNoManagementRoom = errors.New("no management room configured")
|
ErrNoManagementRoom = errors.New("no management room configured")
|
||||||
ErrAmbiguousManagementRoom = errors.New("multiple management rooms configured")
|
ErrAmbiguousManagementRoom = errors.New("multiple management rooms configured")
|
||||||
)
|
)
|
||||||
|
|
||||||
type RadarrConfig struct {
|
type payload struct {
|
||||||
Enabled bool `yaml:"enabled"`
|
EventType string `json:"eventType"`
|
||||||
Path string `yaml:"path"`
|
Movie *movie `json:"movie"`
|
||||||
Secret string `yaml:"secret"`
|
MovieFile *movieFile `json:"movieFile"`
|
||||||
|
IsUpgrade bool `json:"isUpgrade"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type radarrPayload struct {
|
type movie struct {
|
||||||
EventType string `json:"eventType"`
|
|
||||||
Movie *radarrMovie `json:"movie"`
|
|
||||||
MovieFile *radarrMovieFile `json:"movieFile"`
|
|
||||||
IsUpgrade bool `json:"isUpgrade"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type radarrMovie struct {
|
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Year int `json:"year"`
|
Year int `json:"year"`
|
||||||
ImdbID string `json:"imdbId"`
|
ImdbID string `json:"imdbId"`
|
||||||
|
|
@ -45,7 +36,7 @@ type radarrMovie struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type radarrMovieFile struct {
|
type movieFile struct {
|
||||||
Quality string `json:"quality"`
|
Quality string `json:"quality"`
|
||||||
RelativePath string `json:"relativePath"`
|
RelativePath string `json:"relativePath"`
|
||||||
SceneName string `json:"sceneName"`
|
SceneName string `json:"sceneName"`
|
||||||
|
|
@ -60,62 +51,30 @@ type noticeSender interface {
|
||||||
SendNotice(context.Context, id.RoomID, string) error
|
SendNotice(context.Context, id.RoomID, string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type RadarrHandler struct {
|
type ArrHandler struct {
|
||||||
config RadarrConfig
|
|
||||||
resolver roomResolver
|
resolver roomResolver
|
||||||
sender noticeSender
|
sender noticeSender
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RadarrConfig) ApplyDefaults() {
|
func MountArr(router *http.ServeMux, bridge *bridgev2.Bridge) error {
|
||||||
if c.Path == "" {
|
if bridge == nil {
|
||||||
c.Path = defaultRadarrWebhookPath
|
return fmt.Errorf("bridge is not initialized")
|
||||||
}
|
}
|
||||||
}
|
handler := &ArrHandler{
|
||||||
|
|
||||||
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,
|
|
||||||
resolver: bridgeRoomResolver{bridge: bridge},
|
resolver: bridgeRoomResolver{bridge: bridge},
|
||||||
sender: bridgeNoticeSender{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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RadarrHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *ArrHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if !authorized(r, h.config.Secret) {
|
var body payload
|
||||||
http.Error(w, "invalid webhook secret", http.StatusUnauthorized)
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload radarrPayload
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
||||||
http.Error(w, "invalid webhook payload", http.StatusBadRequest)
|
http.Error(w, "invalid webhook payload", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(payload.EventType) == "" {
|
if strings.TrimSpace(body.EventType) == "" {
|
||||||
http.Error(w, "missing eventType", http.StatusBadRequest)
|
http.Error(w, "missing eventType", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +89,7 @@ func (h *RadarrHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
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)
|
http.Error(w, "failed to deliver webhook", http.StatusBadGateway)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -164,6 +123,7 @@ func (r bridgeRoomResolver) ResolveManagementRoom(ctx context.Context) (id.RoomI
|
||||||
if err = rows.Err(); err != nil {
|
if err = rows.Err(); err != nil {
|
||||||
return "", fmt.Errorf("failed to iterate management rooms: %w", err)
|
return "", fmt.Errorf("failed to iterate management rooms: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch len(owners) {
|
switch len(owners) {
|
||||||
case 0:
|
case 0:
|
||||||
return "", ErrNoManagementRoom
|
return "", ErrNoManagementRoom
|
||||||
|
|
@ -187,43 +147,30 @@ func (s bridgeNoticeSender) SendNotice(ctx context.Context, roomID id.RoomID, ma
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func authorized(r *http.Request, secret string) bool {
|
func renderNotice(body payload) string {
|
||||||
if secret == "" {
|
title := "Arr"
|
||||||
return true
|
if body.Movie != nil {
|
||||||
}
|
title = body.Movie.Title
|
||||||
if r.Header.Get(radarrSecretHeader) == secret {
|
if body.Movie.Year != 0 {
|
||||||
return true
|
title = fmt.Sprintf("%s (%d)", title, body.Movie.Year)
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := []string{fmt.Sprintf("**Radarr %s**", payload.EventType)}
|
lines := []string{fmt.Sprintf("**Arr %s**", body.EventType)}
|
||||||
if title != "Radarr" {
|
if title != "Arr" {
|
||||||
lines = append(lines, fmt.Sprintf("Movie: %s", title))
|
lines = append(lines, fmt.Sprintf("Movie: %s", title))
|
||||||
}
|
}
|
||||||
if payload.MovieFile != nil && payload.MovieFile.Quality != "" {
|
if body.MovieFile != nil && body.MovieFile.Quality != "" {
|
||||||
lines = append(lines, fmt.Sprintf("Quality: %s", payload.MovieFile.Quality))
|
lines = append(lines, fmt.Sprintf("Quality: %s", body.MovieFile.Quality))
|
||||||
}
|
}
|
||||||
if payload.MovieFile != nil && payload.MovieFile.RelativePath != "" {
|
if body.MovieFile != nil && body.MovieFile.RelativePath != "" {
|
||||||
lines = append(lines, fmt.Sprintf("File: `%s`", payload.MovieFile.RelativePath))
|
lines = append(lines, fmt.Sprintf("File: `%s`", body.MovieFile.RelativePath))
|
||||||
}
|
}
|
||||||
if payload.EventType == "Download" {
|
if body.EventType == "Download" {
|
||||||
lines = append(lines, fmt.Sprintf("Upgrade: %t", payload.IsUpgrade))
|
lines = append(lines, fmt.Sprintf("Upgrade: %t", body.IsUpgrade))
|
||||||
}
|
}
|
||||||
if payload.Movie != nil && payload.Movie.ImdbID != "" {
|
if body.Movie != nil && body.Movie.ImdbID != "" {
|
||||||
lines = append(lines, fmt.Sprintf("IMDb: `%s`", payload.Movie.ImdbID))
|
lines = append(lines, fmt.Sprintf("IMDb: `%s`", body.Movie.ImdbID))
|
||||||
}
|
}
|
||||||
return strings.Join(lines, "\n")
|
return strings.Join(lines, "\n")
|
||||||
}
|
}
|
||||||
|
|
@ -238,4 +185,4 @@ func convertUserIDs(users []id.UserID) []string {
|
||||||
|
|
||||||
var _ roomResolver = bridgeRoomResolver{}
|
var _ roomResolver = bridgeRoomResolver{}
|
||||||
var _ noticeSender = bridgeNoticeSender{}
|
var _ noticeSender = bridgeNoticeSender{}
|
||||||
var _ http.Handler = (*RadarrHandler)(nil)
|
var _ http.Handler = (*ArrHandler)(nil)
|
||||||
114
packages/arrtrix/pkg/webhook/arr_test.go
Normal file
114
packages/arrtrix/pkg/webhook/arr_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue