feat: add radarr support #9
|
@ -18,6 +18,8 @@ func TestErrorCode_String(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestError(t *testing.T) {
|
func TestError(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
errToWrap := errors.New("WithErr")
|
errToWrap := errors.New("WithErr")
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
38
internal/domain/radarr_webhook_payload.go
Normal file
38
internal/domain/radarr_webhook_payload.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
type RadarrEventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RadarrEventTypeImport RadarrEventType = "Import"
|
||||||
|
RadarrEventTypeTest RadarrEventType = "Test"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnsupportedRadarrEventType = NewError(WithCode(ErrorCodeValidation), WithMessage("unsupported event type"))
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRadarrEventType(s string) (RadarrEventType, error) {
|
||||||
|
conv := RadarrEventType(s)
|
||||||
|
switch conv {
|
||||||
|
case RadarrEventTypeImport,
|
||||||
|
RadarrEventTypeTest:
|
||||||
|
return conv, nil
|
||||||
|
default:
|
||||||
|
return "", ErrUnsupportedSonarrEventType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RadarrEventType) String() string {
|
||||||
|
return string(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RadarrMovie struct {
|
||||||
|
ID int64
|
||||||
|
Title string
|
||||||
|
ReleaseDate string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RadarrWebhookPayload struct {
|
||||||
|
EventType RadarrEventType
|
||||||
|
Movie RadarrMovie
|
||||||
|
}
|
36
internal/domain/radarr_webhook_payload_test.go
Normal file
36
internal/domain/radarr_webhook_payload_test.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package domain_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/domain"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewRadarrEventType(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
events := []domain.RadarrEventType{domain.RadarrEventTypeImport, domain.RadarrEventTypeTest}
|
||||||
|
|
||||||
|
for _, ev := range events {
|
||||||
|
res, err := domain.NewRadarrEventType(ev.String())
|
||||||
|
assert.Equal(t, ev, res)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ERR: invalid event type", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
events := []string{"test1", "test2", "aaaa", "bbb"}
|
||||||
|
|
||||||
|
for _, ev := range events {
|
||||||
|
res, err := domain.NewRadarrEventType(ev)
|
||||||
|
assert.Zero(t, res)
|
||||||
|
assert.ErrorIs(t, err, domain.ErrUnsupportedRadarrEventType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -12,7 +12,9 @@ func TestNewSonarrEventType(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("OK", func(t *testing.T) {
|
t.Run("OK", func(t *testing.T) {
|
||||||
events := []domain.SonarrEventType{domain.SonarrEventTypeDownload}
|
t.Parallel()
|
||||||
|
|
||||||
|
events := []domain.SonarrEventType{domain.SonarrEventTypeDownload, domain.SonarrEventTypeTest}
|
||||||
|
|
||||||
for _, ev := range events {
|
for _, ev := range events {
|
||||||
res, err := domain.NewSonarrEventType(ev.String())
|
res, err := domain.NewSonarrEventType(ev.String())
|
||||||
|
@ -22,6 +24,8 @@ func TestNewSonarrEventType(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ERR: invalid event type", func(t *testing.T) {
|
t.Run("ERR: invalid event type", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
events := []string{"test1", "test2", "aaaa", "bbb"}
|
events := []string{"test1", "test2", "aaaa", "bbb"}
|
||||||
|
|
||||||
for _, ev := range events {
|
for _, ev := range events {
|
||||||
|
|
115
internal/rest/internal/mock/radarr_service.gen.go
Normal file
115
internal/rest/internal/mock/radarr_service.gen.go
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
// Code generated by counterfeiter. DO NOT EDIT.
|
||||||
|
package mock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/domain"
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FakeRadarrService struct {
|
||||||
|
ProcessStub func(context.Context, domain.RadarrWebhookPayload) error
|
||||||
|
processMutex sync.RWMutex
|
||||||
|
processArgsForCall []struct {
|
||||||
|
arg1 context.Context
|
||||||
|
arg2 domain.RadarrWebhookPayload
|
||||||
|
}
|
||||||
|
processReturns struct {
|
||||||
|
result1 error
|
||||||
|
}
|
||||||
|
processReturnsOnCall map[int]struct {
|
||||||
|
result1 error
|
||||||
|
}
|
||||||
|
invocations map[string][][]interface{}
|
||||||
|
invocationsMutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeRadarrService) Process(arg1 context.Context, arg2 domain.RadarrWebhookPayload) error {
|
||||||
|
fake.processMutex.Lock()
|
||||||
|
ret, specificReturn := fake.processReturnsOnCall[len(fake.processArgsForCall)]
|
||||||
|
fake.processArgsForCall = append(fake.processArgsForCall, struct {
|
||||||
|
arg1 context.Context
|
||||||
|
arg2 domain.RadarrWebhookPayload
|
||||||
|
}{arg1, arg2})
|
||||||
|
stub := fake.ProcessStub
|
||||||
|
fakeReturns := fake.processReturns
|
||||||
|
fake.recordInvocation("Process", []interface{}{arg1, arg2})
|
||||||
|
fake.processMutex.Unlock()
|
||||||
|
if stub != nil {
|
||||||
|
return stub(arg1, arg2)
|
||||||
|
}
|
||||||
|
if specificReturn {
|
||||||
|
return ret.result1
|
||||||
|
}
|
||||||
|
return fakeReturns.result1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeRadarrService) ProcessCallCount() int {
|
||||||
|
fake.processMutex.RLock()
|
||||||
|
defer fake.processMutex.RUnlock()
|
||||||
|
return len(fake.processArgsForCall)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeRadarrService) ProcessCalls(stub func(context.Context, domain.RadarrWebhookPayload) error) {
|
||||||
|
fake.processMutex.Lock()
|
||||||
|
defer fake.processMutex.Unlock()
|
||||||
|
fake.ProcessStub = stub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeRadarrService) ProcessArgsForCall(i int) (context.Context, domain.RadarrWebhookPayload) {
|
||||||
|
fake.processMutex.RLock()
|
||||||
|
defer fake.processMutex.RUnlock()
|
||||||
|
argsForCall := fake.processArgsForCall[i]
|
||||||
|
return argsForCall.arg1, argsForCall.arg2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeRadarrService) ProcessReturns(result1 error) {
|
||||||
|
fake.processMutex.Lock()
|
||||||
|
defer fake.processMutex.Unlock()
|
||||||
|
fake.ProcessStub = nil
|
||||||
|
fake.processReturns = struct {
|
||||||
|
result1 error
|
||||||
|
}{result1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeRadarrService) ProcessReturnsOnCall(i int, result1 error) {
|
||||||
|
fake.processMutex.Lock()
|
||||||
|
defer fake.processMutex.Unlock()
|
||||||
|
fake.ProcessStub = nil
|
||||||
|
if fake.processReturnsOnCall == nil {
|
||||||
|
fake.processReturnsOnCall = make(map[int]struct {
|
||||||
|
result1 error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fake.processReturnsOnCall[i] = struct {
|
||||||
|
result1 error
|
||||||
|
}{result1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeRadarrService) Invocations() map[string][][]interface{} {
|
||||||
|
fake.invocationsMutex.RLock()
|
||||||
|
defer fake.invocationsMutex.RUnlock()
|
||||||
|
fake.processMutex.RLock()
|
||||||
|
defer fake.processMutex.RUnlock()
|
||||||
|
copiedInvocations := map[string][][]interface{}{}
|
||||||
|
for key, value := range fake.invocations {
|
||||||
|
copiedInvocations[key] = value
|
||||||
|
}
|
||||||
|
return copiedInvocations
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeRadarrService) recordInvocation(key string, args []interface{}) {
|
||||||
|
fake.invocationsMutex.Lock()
|
||||||
|
defer fake.invocationsMutex.Unlock()
|
||||||
|
if fake.invocations == nil {
|
||||||
|
fake.invocations = map[string][][]interface{}{}
|
||||||
|
}
|
||||||
|
if fake.invocations[key] == nil {
|
||||||
|
fake.invocations[key] = [][]interface{}{}
|
||||||
|
}
|
||||||
|
fake.invocations[key] = append(fake.invocations[key], args)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ rest.RadarrService = new(FakeRadarrService)
|
|
@ -16,18 +16,26 @@ type SonarrService interface {
|
||||||
Process(ctx context.Context, payload domain.SonarrWebhookPayload) error
|
Process(ctx context.Context, payload domain.SonarrWebhookPayload) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebhookHandler struct {
|
//counterfeiter:generate -o internal/mock/radarr_service.gen.go . RadarrService
|
||||||
sonarrSvc SonarrService
|
type RadarrService interface {
|
||||||
|
Process(ctx context.Context, payload domain.RadarrWebhookPayload) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWebhookHandler(sonarrSvc SonarrService) *WebhookHandler {
|
type WebhookHandler struct {
|
||||||
|
sonarrSvc SonarrService
|
||||||
|
radarrSvc RadarrService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebhookHandler(sonarrSvc SonarrService, radarrSvc RadarrService) *WebhookHandler {
|
||||||
return &WebhookHandler{
|
return &WebhookHandler{
|
||||||
sonarrSvc: sonarrSvc,
|
sonarrSvc: sonarrSvc,
|
||||||
|
radarrSvc: radarrSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *WebhookHandler) Register(r chi.Router) {
|
func (h *WebhookHandler) Register(r chi.Router) {
|
||||||
r.Post("/webhook/sonarr", h.handleSonarrWebhook)
|
r.Post("/webhook/sonarr", h.handleSonarrWebhook)
|
||||||
|
r.Post("/webhook/radarr", h.handleRadarrWebhook)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SonarrSeries struct {
|
type SonarrSeries struct {
|
||||||
|
@ -90,3 +98,48 @@ func (h *WebhookHandler) handleSonarrWebhook(w http.ResponseWriter, r *http.Requ
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RadarrMovie struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
ReleaseDate string `json:"releaseDate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RadarrWebhookRequest struct {
|
||||||
|
EventType string `json:"eventType"`
|
||||||
|
Movie RadarrMovie `json:"movie"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *WebhookHandler) handleRadarrWebhook(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req RadarrWebhookRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
renderErrorResponse(w, domain.NewError(
|
||||||
|
domain.WithErr(err),
|
||||||
|
domain.WithCode(domain.ErrorCodeValidation),
|
||||||
|
domain.WithMessage("invalid request body"),
|
||||||
|
))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eventType, err := domain.NewRadarrEventType(req.EventType)
|
||||||
|
if err != nil {
|
||||||
|
renderErrorResponse(w, fmt.Errorf("domain.NewRadarrEventType: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := domain.RadarrWebhookPayload{
|
||||||
|
EventType: eventType,
|
||||||
|
Movie: domain.RadarrMovie{
|
||||||
|
ID: req.Movie.ID,
|
||||||
|
Title: req.Movie.Title,
|
||||||
|
ReleaseDate: req.Movie.ReleaseDate,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.radarrSvc.Process(r.Context(), payload); err != nil {
|
||||||
|
renderErrorResponse(w, fmt.Errorf("RadarrService.Process: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
|
@ -117,7 +117,7 @@ func TestWebhookHandler_Sonarr(t *testing.T) {
|
||||||
tt.setup(&svc)
|
tt.setup(&svc)
|
||||||
}
|
}
|
||||||
router := chi.NewRouter()
|
router := chi.NewRouter()
|
||||||
rest.NewWebhookHandler(&svc).Register(router)
|
rest.NewWebhookHandler(&svc, nil).Register(router)
|
||||||
|
|
||||||
resp := doRequest(router, http.MethodPost, "/webhook/sonarr", tt.body)
|
resp := doRequest(router, http.MethodPost, "/webhook/sonarr", tt.body)
|
||||||
defer assert.NoError(t, resp.Body.Close())
|
defer assert.NoError(t, resp.Body.Close())
|
||||||
|
@ -129,3 +129,102 @@ func TestWebhookHandler_Sonarr(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWebhookHandler_Radarr(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setup func(svc *mock.FakeRadarrService)
|
||||||
|
body io.Reader
|
||||||
|
expectedStatus int
|
||||||
|
target any
|
||||||
|
expectedResponse any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OK: event type=Import",
|
||||||
|
setup: func(svc *mock.FakeRadarrService) {
|
||||||
|
svc.ProcessReturns(nil)
|
||||||
|
},
|
||||||
|
body: func() *bytes.Buffer {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = json.NewEncoder(&buf).Encode(rest.RadarrWebhookRequest{
|
||||||
|
EventType: "Import",
|
||||||
|
Movie: rest.RadarrMovie{
|
||||||
|
ID: 111,
|
||||||
|
Title: "Title 1",
|
||||||
|
ReleaseDate: "1970-01-01",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return &buf
|
||||||
|
}(),
|
||||||
|
expectedStatus: http.StatusNoContent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: only JSON is accepted",
|
||||||
|
setup: func(svc *mock.FakeRadarrService) {},
|
||||||
|
body: func() *bytes.Buffer {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = gob.NewEncoder(&buf).Encode(rest.RadarrWebhookRequest{
|
||||||
|
EventType: "Import",
|
||||||
|
Movie: rest.RadarrMovie{
|
||||||
|
ID: 111,
|
||||||
|
Title: "Title 1",
|
||||||
|
ReleaseDate: "1970-01-01",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return &buf
|
||||||
|
}(),
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
target: &rest.ErrorResponse{},
|
||||||
|
expectedResponse: &rest.ErrorResponse{
|
||||||
|
Error: rest.APIError{
|
||||||
|
Code: domain.ErrorCodeValidation.String(),
|
||||||
|
Message: "invalid request body",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: unsupported event type",
|
||||||
|
setup: func(svc *mock.FakeRadarrService) {},
|
||||||
|
body: func() *bytes.Buffer {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = json.NewEncoder(&buf).Encode(rest.RadarrWebhookRequest{
|
||||||
|
EventType: "xxxx",
|
||||||
|
})
|
||||||
|
return &buf
|
||||||
|
}(),
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
target: &rest.ErrorResponse{},
|
||||||
|
expectedResponse: &rest.ErrorResponse{
|
||||||
|
Error: rest.APIError{
|
||||||
|
Code: domain.ErrUnsupportedRadarrEventType.Code().String(),
|
||||||
|
Message: domain.ErrUnsupportedRadarrEventType.Message(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
svc := mock.FakeRadarrService{}
|
||||||
|
if tt.setup != nil {
|
||||||
|
tt.setup(&svc)
|
||||||
|
}
|
||||||
|
router := chi.NewRouter()
|
||||||
|
rest.NewWebhookHandler(nil, &svc).Register(router)
|
||||||
|
|
||||||
|
resp := doRequest(router, http.MethodPost, "/webhook/radarr", tt.body)
|
||||||
|
defer assert.NoError(t, resp.Body.Close())
|
||||||
|
if tt.target != nil && tt.expectedResponse != nil {
|
||||||
|
assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, tt.expectedStatus, resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
56
internal/service/radarr.go
Normal file
56
internal/service/radarr.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Radarr struct {
|
||||||
|
publisher Publisher
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRadarr(publisher Publisher) *Radarr {
|
||||||
|
return &Radarr{
|
||||||
|
publisher: publisher,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Radarr) Process(ctx context.Context, payload domain.RadarrWebhookPayload) error {
|
||||||
|
title, err := s.buildTitle(payload.EventType, payload.Movie)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("buildTitle: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := s.buildMessage(payload.EventType, payload.Movie)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("buildMessage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.publisher.Publish(ctx, title, msg); err != nil {
|
||||||
|
return fmt.Errorf("Publisher.Publish: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Radarr) buildTitle(evType domain.RadarrEventType, _ domain.RadarrMovie) (string, error) {
|
||||||
|
switch evType {
|
||||||
|
case domain.RadarrEventTypeImport,
|
||||||
|
domain.RadarrEventTypeTest:
|
||||||
|
return "New movie available (Radarr)", nil
|
||||||
|
default:
|
||||||
|
return "", domain.ErrUnsupportedRadarrEventType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Radarr) buildMessage(evType domain.RadarrEventType, movie domain.RadarrMovie) (string, error) {
|
||||||
|
switch evType {
|
||||||
|
case domain.RadarrEventTypeImport,
|
||||||
|
domain.RadarrEventTypeTest:
|
||||||
|
return movie.Title, nil
|
||||||
|
default:
|
||||||
|
return "", domain.ErrUnsupportedRadarrEventType
|
||||||
|
}
|
||||||
|
}
|
56
internal/service/radarr_test.go
Normal file
56
internal/service/radarr_test.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package service_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/domain"
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/service"
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/service/internal/mock"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRadarr_Process(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, evType := range [...]domain.RadarrEventType{
|
||||||
|
domain.RadarrEventTypeImport,
|
||||||
|
domain.RadarrEventTypeTest,
|
||||||
|
} {
|
||||||
|
t.Run("event type="+evType.String(), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
publisher := &mock.FakePublisher{}
|
||||||
|
publisher.PublishReturns(nil)
|
||||||
|
|
||||||
|
payload := generateRadarrWebhookPayload(evType)
|
||||||
|
|
||||||
|
err := service.NewRadarr(publisher).Process(context.Background(), payload)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, title, msg := publisher.PublishArgsForCall(0)
|
||||||
|
assert.Equal(t, "New movie available (Radarr)", title)
|
||||||
|
assert.Equal(t, payload.Movie.Title, msg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRadarrWebhookPayload(ev domain.RadarrEventType) domain.RadarrWebhookPayload {
|
||||||
|
s := rand.NewSource(time.Now().UnixNano())
|
||||||
|
r := rand.New(s)
|
||||||
|
|
||||||
|
return domain.RadarrWebhookPayload{
|
||||||
|
EventType: ev,
|
||||||
|
Movie: domain.RadarrMovie{
|
||||||
|
ID: r.Int63(),
|
||||||
|
Title: uuid.NewString(),
|
||||||
|
ReleaseDate: "1970-01-01",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,7 +41,7 @@ func (s *Sonarr) buildTitle(evType domain.SonarrEventType, series domain.SonarrS
|
||||||
switch evType {
|
switch evType {
|
||||||
case domain.SonarrEventTypeDownload,
|
case domain.SonarrEventTypeDownload,
|
||||||
domain.SonarrEventTypeTest:
|
domain.SonarrEventTypeTest:
|
||||||
return series.Title + " - New episode (Sonarr)", nil
|
return series.Title + " - New episode available (Sonarr)", nil
|
||||||
default:
|
default:
|
||||||
return "", domain.ErrUnsupportedSonarrEventType
|
return "", domain.ErrUnsupportedSonarrEventType
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ func TestSonarr_Process(t *testing.T) {
|
||||||
require.Equal(t, len(payload.Episodes), publisher.PublishCallCount())
|
require.Equal(t, len(payload.Episodes), publisher.PublishCallCount())
|
||||||
for i, ep := range payload.Episodes {
|
for i, ep := range payload.Episodes {
|
||||||
_, title, msg := publisher.PublishArgsForCall(i)
|
_, title, msg := publisher.PublishArgsForCall(i)
|
||||||
assert.Equal(t, payload.Series.Title+" - New episode (Sonarr)", title)
|
assert.Equal(t, payload.Series.Title+" - New episode available (Sonarr)", title)
|
||||||
assert.Equal(t, fmt.Sprintf("S%d.E%d %s", ep.SeasonNumber, ep.EpisodeNumber, ep.Title), msg)
|
assert.Equal(t, fmt.Sprintf("S%d.E%d %s", ep.SeasonNumber, ep.EpisodeNumber, ep.Title), msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
48
main.go
48
main.go
|
@ -2,7 +2,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -71,6 +70,11 @@ func newRouter() (*chi.Mux, error) {
|
||||||
return nil, fmt.Errorf("newSonarrService: %w", err)
|
return nil, fmt.Errorf("newSonarrService: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
radarrSvc, err := newRadarrService()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("newRadarrService: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(
|
r.Use(
|
||||||
middleware.RealIP,
|
middleware.RealIP,
|
||||||
|
@ -81,39 +85,53 @@ func newRouter() (*chi.Mux, error) {
|
||||||
middleware.Recoverer,
|
middleware.Recoverer,
|
||||||
middleware.Heartbeat("/health"),
|
middleware.Heartbeat("/health"),
|
||||||
)
|
)
|
||||||
rest.NewWebhookHandler(sonarrSvc).Register(r)
|
rest.NewWebhookHandler(sonarrSvc, radarrSvc).Register(r)
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newRadarrService() (*service.Radarr, error) {
|
||||||
|
ntfy, err := newNtfyService("RADARR")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("newNtfyService: %w", err)
|
||||||
|
}
|
||||||
|
return service.NewRadarr(ntfy), nil
|
||||||
|
}
|
||||||
|
|
||||||
func newSonarrService() (*service.Sonarr, error) {
|
func newSonarrService() (*service.Sonarr, error) {
|
||||||
url := os.Getenv("SONARR_NTFY_URL")
|
ntfy, err := newNtfyService("SONARR")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("newNtfyService: %w", err)
|
||||||
|
}
|
||||||
|
return service.NewSonarr(ntfy), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNtfyService(envPrefix string) (*service.Ntfy, error) {
|
||||||
|
url := os.Getenv(envPrefix + "_NTFY_URL")
|
||||||
if url == "" {
|
if url == "" {
|
||||||
url = ntfyDefaultUrl
|
url = ntfyDefaultUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
topic := os.Getenv("SONARR_NTFY_TOPIC")
|
topic := os.Getenv(envPrefix + "_NTFY_TOPIC")
|
||||||
if topic == "" {
|
if topic == "" {
|
||||||
return nil, errors.New(`env "SONARR_NTFY_TOPIC" is required`)
|
return nil, fmt.Errorf(`env "%s_NTFY_TOPIC" is required`, envPrefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
timeout := os.Getenv("SONARR_NTFY_REQ_TIMEOUT")
|
timeout := os.Getenv(envPrefix + "_NTFY_REQ_TIMEOUT")
|
||||||
parsedTimeout := publisherDefaultTimeout
|
parsedTimeout := publisherDefaultTimeout
|
||||||
var err error
|
var err error
|
||||||
if timeout != "" {
|
if timeout != "" {
|
||||||
parsedTimeout, err = time.ParseDuration(timeout)
|
parsedTimeout, err = time.ParseDuration(timeout)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(`os.Getenv("SONARR_NTFY_REQ_TIMEOUT"): %w`, err)
|
return nil, fmt.Errorf(`os.Getenv("%s_NTFY_REQ_TIMEOUT"): %w`, envPrefix, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return service.NewSonarr(
|
return service.NewNtfy(
|
||||||
service.NewNtfy(
|
&http.Client{Timeout: parsedTimeout},
|
||||||
&http.Client{Timeout: parsedTimeout},
|
url,
|
||||||
url,
|
topic,
|
||||||
topic,
|
os.Getenv(envPrefix+"_NTFY_USERNAME"),
|
||||||
os.Getenv("SONARR_NTFY_USERNAME"),
|
os.Getenv(envPrefix+"_NTFY_PASSWORD"),
|
||||||
os.Getenv("SONARR_NTFY_PASSWORD"),
|
|
||||||
),
|
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Reference in New Issue
Block a user