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) } }