feat: add notifier Sender interface and digest formatter

This commit is contained in:
2026-07-19 22:28:41 +03:00
parent 0635ca8a87
commit 3af33bd728
4 changed files with 264 additions and 4 deletions

View File

@@ -0,0 +1,129 @@
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")
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/")
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_EmptyUIBaseURLOmitsLink(t *testing.T) {
missing := []scanner.MissingRelease{{ArtistID: "a", Title: "x", RGID: "r1"}}
got := FormatDigest(missing, "")
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)
}
}