- notifier: show artist display names (not internal IDs) in digest; resolve names from artist_settings and fall back to ID when unavailable - notifier: skip sending an empty digest to avoid daily spam - config: require telegram token/chat_id when enabled - web: warn loudly when auth is disabled on a non-loopback bind; add HTTP server timeouts - web: treat SetReleaseIgnored "release not found" as benign redirect (0 rows) - musicbrainz: reject low-score/name-mismatched MBID resolutions instead of silently caching the wrong artist - database: remove dead duplicate err check; harden DSN param appending - musicbrainz: check rows.Err() after iterating existing releases
166 lines
4.9 KiB
Go
166 lines
4.9 KiB
Go
package notifier
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"naviwatcher/internal/scanner"
|
|
)
|
|
|
|
type stubSender struct {
|
|
sent []string
|
|
failErr error
|
|
}
|
|
|
|
func (s *stubSender) Send(ctx context.Context, message string) error {
|
|
if s.failErr != nil {
|
|
return s.failErr
|
|
}
|
|
s.sent = append(s.sent, message)
|
|
return nil
|
|
}
|
|
|
|
func TestFormatDigest_Empty(t *testing.T) {
|
|
got := FormatDigest(nil, "http://ui", nil)
|
|
if got != "NaviWatcher: no new missing releases found." {
|
|
t.Fatalf("unexpected empty digest: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestFormatDigest_GroupsByArtistAndCounts(t *testing.T) {
|
|
missing := []scanner.MissingRelease{
|
|
{ArtistID: "art-b", Title: "Zebra", RGID: "r3"},
|
|
{ArtistID: "art-a", Title: "Alpha", RGID: "r1"},
|
|
{ArtistID: "art-a", Title: "Beta", RGID: "r2"},
|
|
}
|
|
got := FormatDigest(missing, "http://localhost:8080/", nil)
|
|
if !strings.Contains(got, "art-a (2):") {
|
|
t.Errorf("expected art-a with count 2, got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "art-b (1):") {
|
|
t.Errorf("expected art-b with count 1, got:\n%s", got)
|
|
}
|
|
// art-a should be alphabetically before art-b.
|
|
if strings.Index(got, "art-a") > strings.Index(got, "art-b") {
|
|
t.Errorf("artists not sorted: got:\n%s", got)
|
|
}
|
|
// Releases within artist sorted: Alpha before Beta.
|
|
aIdx := strings.Index(got, "Alpha")
|
|
bIdx := strings.Index(got, "Beta")
|
|
if aIdx > bIdx {
|
|
t.Errorf("titles not sorted: got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "View details: http://localhost:8080") {
|
|
t.Errorf("expected UI link, got:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestFormatDigest_UsesArtistNameWhenProvided(t *testing.T) {
|
|
missing := []scanner.MissingRelease{
|
|
{ArtistID: "art-2", Title: "Zebra", RGID: "r3"},
|
|
{ArtistID: "art-1", Title: "Alpha", RGID: "r1"},
|
|
{ArtistID: "art-1", Title: "Beta", RGID: "r2"},
|
|
}
|
|
names := map[string]string{"art-1": "Alpha Artist", "art-2": "Zebra Artist"}
|
|
got := FormatDigest(missing, "", names)
|
|
// Display names are used as labels and sorted alphabetically by name.
|
|
if !strings.Contains(got, "Alpha Artist (2):") {
|
|
t.Errorf("expected name label with count 2, got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "Zebra Artist (1):") {
|
|
t.Errorf("expected name label with count 1, got:\n%s", got)
|
|
}
|
|
if strings.Index(got, "Alpha Artist") > strings.Index(got, "Zebra Artist") {
|
|
t.Errorf("artists not sorted by name: got:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestFormatDigest_FallsBackToIDWhenNameMissing(t *testing.T) {
|
|
missing := []scanner.MissingRelease{
|
|
{ArtistID: "art-1", Title: "Alpha", RGID: "r1"},
|
|
{ArtistID: "art-2", Title: "Beta", RGID: "r2"},
|
|
}
|
|
// Name map present but does not cover art-2 -> falls back to ID.
|
|
names := map[string]string{"art-1": "Named Artist"}
|
|
got := FormatDigest(missing, "", names)
|
|
if !strings.Contains(got, "Named Artist (1):") {
|
|
t.Errorf("expected named artist label, got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "art-2 (1):") {
|
|
t.Errorf("expected ID fallback for art-2, got:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestFormatDigest_EmptyUIBaseURLOmitsLink(t *testing.T) {
|
|
missing := []scanner.MissingRelease{{ArtistID: "a", Title: "x", RGID: "r1"}}
|
|
got := FormatDigest(missing, "", nil)
|
|
if strings.Contains(got, "View details:") {
|
|
t.Errorf("did not expect UI link when base URL empty: got:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestTelegramSender_Success(t *testing.T) {
|
|
var gotReq sendMessageRequest
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/botTOKEN/sendMessage" {
|
|
t.Errorf("unexpected path %q", r.URL.Path)
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&gotReq); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s := &telegramSender{
|
|
httpClient: srv.Client(),
|
|
token: "TOKEN",
|
|
chatID: "CHAT",
|
|
baseURL: srv.URL,
|
|
}
|
|
if err := s.Send(context.Background(), "hello"); err != nil {
|
|
t.Fatalf("Send returned error: %v", err)
|
|
}
|
|
if gotReq.ChatID != "CHAT" || gotReq.Text != "hello" || !gotReq.DisableWebPagePreview {
|
|
t.Errorf("unexpected payload: %+v", gotReq)
|
|
}
|
|
}
|
|
|
|
func TestTelegramSender_Non2xxError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
_, _ = w.Write([]byte(`{"description":"Unauthorized"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s := &telegramSender{
|
|
httpClient: srv.Client(),
|
|
token: "TOKEN",
|
|
chatID: "CHAT",
|
|
baseURL: srv.URL,
|
|
}
|
|
err := s.Send(context.Background(), "hi")
|
|
if err == nil {
|
|
t.Fatal("expected error on non-2xx")
|
|
}
|
|
if !strings.Contains(err.Error(), "401") {
|
|
t.Errorf("expected status in error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSenderInterfaceFailurePropagated(t *testing.T) {
|
|
// Verifies the Sender interface can be used by callers and failures surface.
|
|
want := errors.New("boom")
|
|
s := &stubSender{failErr: want}
|
|
err := s.Send(context.Background(), "msg")
|
|
if !errors.Is(err, want) {
|
|
t.Fatalf("expected wrapped error %v, got %v", want, err)
|
|
}
|
|
}
|