coda/internal/ntfy/client_test.go

290 lines
7.1 KiB
Go

package ntfy
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"coda/internal/config"
"coda/internal/models"
)
type mockStore struct {
data map[string]string
}
func (m *mockStore) GetAll(_ context.Context) (map[string]string, error) {
result := make(map[string]string, len(m.data))
for k, v := range m.data {
result[k] = v
}
return result, nil
}
func (m *mockStore) Set(_ context.Context, key, value string) error {
if m.data == nil {
m.data = map[string]string{}
}
m.data[key] = value
return nil
}
func (m *mockStore) Delete(_ context.Context, key string) error {
delete(m.data, key)
return nil
}
func newClientWithOverrides(t *testing.T, overrides map[string]string) *Client {
t.Helper()
svc, err := config.NewService(context.Background(), &mockStore{data: overrides})
if err != nil {
t.Fatalf("NewService: %v", err)
}
return New(svc)
}
func readPayload(t *testing.T, r *http.Request) payload {
t.Helper()
b, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("read body: %v", err)
}
var p payload
if err := json.Unmarshal(b, &p); err != nil {
t.Fatalf("unmarshal payload: %v (body: %s)", err, string(b))
}
return p
}
func TestNotifySendsDoneMessage(t *testing.T) {
var (
gotMethod string
gotPath string
gotContentType string
gotPayload payload
wasCalled atomic.Bool
)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
wasCalled.Store(true)
gotMethod = r.Method
gotPath = r.URL.Path
gotContentType = r.Header.Get("Content-Type")
gotPayload = readPayload(t, r)
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
c := newClientWithOverrides(t, map[string]string{
"DRY_RUN": "false",
"NTFY_ENABLED": "true",
"NTFY_URL": srv.URL,
"NTFY_TOPIC": "coda",
})
err := c.Notify(context.Background(), &models.Job{
ID: "job-1",
Artist: "Artist",
Album: "Album",
Status: models.StatusDone,
})
if err != nil {
t.Fatalf("Notify: %v", err)
}
if !wasCalled.Load() {
t.Fatal("expected HTTP request to be made")
}
if gotMethod != http.MethodPost {
t.Errorf("method = %q, want POST", gotMethod)
}
if gotPath != "/" {
t.Errorf("path = %q, want /", gotPath)
}
if gotContentType != "application/json" {
t.Errorf("Content-Type = %q, want application/json", gotContentType)
}
if gotPayload.Topic != "coda" {
t.Errorf("topic = %q, want coda", gotPayload.Topic)
}
if gotPayload.Title != "Coda: Artist - Album completed" {
t.Errorf("title = %q, want completed title", gotPayload.Title)
}
if gotPayload.Message != "Conversion completed successfully" {
t.Errorf("message = %q, want success message", gotPayload.Message)
}
if gotPayload.Priority != 3 {
t.Errorf("priority = %d, want 3", gotPayload.Priority)
}
if len(gotPayload.Tags) != 1 || gotPayload.Tags[0] != "white_check_mark" {
t.Errorf("tags = %v, want [white_check_mark]", gotPayload.Tags)
}
}
func TestNotifySendsErrorMessage(t *testing.T) {
var gotPayload payload
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPayload = readPayload(t, r)
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
c := newClientWithOverrides(t, map[string]string{
"DRY_RUN": "false",
"NTFY_ENABLED": "true",
"NTFY_URL": srv.URL,
"NTFY_TOPIC": "coda",
})
err := c.Notify(context.Background(), &models.Job{
ID: "job-1",
Artist: "Artist",
Album: "Album",
Status: models.StatusError,
ErrorMsg: "validation failed",
})
if err != nil {
t.Fatalf("Notify: %v", err)
}
if gotPayload.Title != "Coda: Artist - Album failed" {
t.Errorf("title = %q, want failed title", gotPayload.Title)
}
if gotPayload.Message != "validation failed" {
t.Errorf("message = %q, want error message", gotPayload.Message)
}
if gotPayload.Priority != 4 {
t.Errorf("priority = %d, want 4", gotPayload.Priority)
}
if len(gotPayload.Tags) != 1 || gotPayload.Tags[0] != "x" {
t.Errorf("tags = %v, want [x]", gotPayload.Tags)
}
}
func TestNotifyErrorMsgWithJSONContent(t *testing.T) {
var gotPayload payload
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPayload = readPayload(t, r)
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
c := newClientWithOverrides(t, map[string]string{
"DRY_RUN": "false",
"NTFY_ENABLED": "true",
"NTFY_URL": srv.URL,
"NTFY_TOPIC": "coda",
})
jsonErr := `{"error": "ffmpeg exit code 1", "details": "no such file"}`
err := c.Notify(context.Background(), &models.Job{
ID: "job-1",
Artist: "Artist",
Album: "Album",
Status: models.StatusError,
ErrorMsg: jsonErr,
})
if err != nil {
t.Fatalf("Notify: %v", err)
}
if gotPayload.Message != jsonErr {
t.Errorf("message = %q, want raw JSON string as message", gotPayload.Message)
}
}
func TestNotifyDryRunSkipsRequest(t *testing.T) {
var wasCalled atomic.Bool
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
wasCalled.Store(true)
}))
defer srv.Close()
c := newClientWithOverrides(t, map[string]string{
"DRY_RUN": "true",
"NTFY_ENABLED": "true",
"NTFY_URL": srv.URL,
"NTFY_TOPIC": "coda",
})
if err := c.Notify(context.Background(), &models.Job{ID: "job-1"}); err != nil {
t.Fatalf("Notify: %v", err)
}
if wasCalled.Load() {
t.Error("no HTTP request should be made in dry run")
}
}
func TestNotifyDisabledSkipsRequest(t *testing.T) {
var wasCalled atomic.Bool
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
wasCalled.Store(true)
}))
defer srv.Close()
c := newClientWithOverrides(t, map[string]string{
"NTFY_ENABLED": "false",
"NTFY_URL": srv.URL,
"NTFY_TOPIC": "coda",
})
if err := c.Notify(context.Background(), &models.Job{ID: "job-1"}); err != nil {
t.Fatalf("Notify: %v", err)
}
if wasCalled.Load() {
t.Error("no HTTP request should be made when ntfy is disabled")
}
}
func TestNotifySendsBearerToken(t *testing.T) {
var gotAuth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get("Authorization")
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
c := newClientWithOverrides(t, map[string]string{
"DRY_RUN": "false",
"NTFY_ENABLED": "true",
"NTFY_URL": srv.URL,
"NTFY_TOPIC": "coda",
"NTFY_TOKEN": "secret",
})
if err := c.Notify(context.Background(), &models.Job{ID: "job-1", Status: models.StatusDone}); err != nil {
t.Fatalf("Notify: %v", err)
}
if gotAuth != "Bearer secret" {
t.Errorf("authorization = %q, want Bearer secret", gotAuth)
}
}
func TestNotifyNetworkErrorReturnsError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
url := srv.URL
srv.Close()
c := newClientWithOverrides(t, map[string]string{
"DRY_RUN": "false",
"NTFY_ENABLED": "true",
"NTFY_URL": url,
"NTFY_TOPIC": "coda",
})
if err := c.Notify(context.Background(), &models.Job{ID: "job-1"}); err == nil {
t.Fatal("expected network error")
}
}