Add a scan->notify->web end-to-end smoke test against a seeded :memory: DB, fix .gitignore so the cmd/naviwatcher source dir is not excluded by the root binary rule, and document the new sync.interval field in config.yaml.example.
160 lines
4.6 KiB
Go
160 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"naviwatcher/internal/config"
|
|
"naviwatcher/internal/database"
|
|
"naviwatcher/internal/notifier"
|
|
"naviwatcher/internal/scanner"
|
|
"naviwatcher/internal/web"
|
|
)
|
|
|
|
// TestDataFlowSmoke seeds an in-memory DB with a monitored artist, one local
|
|
// album, and a missing external release, then exercises the full
|
|
// scan -> notify -> web pipeline end to end:
|
|
// - ScanAll reports the missing release
|
|
// - NotifyOnce (with a stub sender) sends exactly the missing release and
|
|
// marks it as sent, so a second run reports nothing
|
|
// - The Web UI dashboard requires auth and renders the artist + missing count,
|
|
// and the artist detail page renders the missing release
|
|
//
|
|
// This is the acceptance smoke check for Task 9.
|
|
func TestDataFlowSmoke(t *testing.T) {
|
|
db, err := database.New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("open :memory: db: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Seed: one monitored artist with one local album ...
|
|
if err := database.SaveArtistSettings(db, &database.ArtistSettings{
|
|
ID: "ar1",
|
|
Name: "Pink Floyd",
|
|
MBID: "abcdef",
|
|
Monitored: true,
|
|
}); err != nil {
|
|
t.Fatalf("seed artist: %v", err)
|
|
}
|
|
if err := database.SaveLocalAlbum(db, &database.LocalAlbum{
|
|
ID: "al1",
|
|
ArtistID: "ar1",
|
|
Title: "The Wall",
|
|
}); err != nil {
|
|
t.Fatalf("seed local album: %v", err)
|
|
}
|
|
// ... and a missing external release (not present locally).
|
|
if err := database.SaveExternalRelease(db, &database.ExternalRelease{
|
|
RGID: "rg-missing",
|
|
ArtistID: "ar1",
|
|
Title: "Animals",
|
|
Type: "Album",
|
|
ReleaseDate: "1977-01-01",
|
|
}); err != nil {
|
|
t.Fatalf("seed external release: %v", err)
|
|
}
|
|
|
|
// 1) ScanAll should surface exactly the missing release.
|
|
missing, err := scanner.ScanAll(ctx, db, 0.85)
|
|
if err != nil {
|
|
t.Fatalf("ScanAll: %v", err)
|
|
}
|
|
if len(missing) != 1 {
|
|
t.Fatalf("expected 1 missing release, got %d", len(missing))
|
|
}
|
|
if missing[0].RGID != "rg-missing" || missing[0].Title != "Animals" {
|
|
t.Fatalf("unexpected missing release: %+v", missing[0])
|
|
}
|
|
|
|
// 2) Notifier: stub sender, first run notifies 1, second run notifies 0.
|
|
var sent []string
|
|
stub := stubSender{onSend: func(msg string) error {
|
|
sent = append(sent, msg)
|
|
return nil
|
|
}}
|
|
tgCfg := config.TelegramConfig{Enabled: true}
|
|
|
|
n1, err := notifier.NotifyOnce(ctx, db, stub, tgCfg, "http://localhost:8080")
|
|
if err != nil {
|
|
t.Fatalf("NotifyOnce #1: %v", err)
|
|
}
|
|
if n1 != 1 {
|
|
t.Fatalf("expected NotifyOnce to send 1, got %d", n1)
|
|
}
|
|
if len(sent) != 1 || !strings.Contains(sent[0], "Animals") {
|
|
t.Fatalf("digest missing expected content: %v", sent)
|
|
}
|
|
|
|
n2, err := notifier.NotifyOnce(ctx, db, stub, tgCfg, "http://localhost:8080")
|
|
if err != nil {
|
|
t.Fatalf("NotifyOnce #2: %v", err)
|
|
}
|
|
if n2 != 0 {
|
|
t.Fatalf("expected second NotifyOnce to send 0 (already sent), got %d", n2)
|
|
}
|
|
|
|
// 3) Web UI: dashboard requires basic auth and renders the artist.
|
|
srvCfg := &config.ServerConfig{
|
|
Host: "localhost",
|
|
Port: 0,
|
|
Username: "admin",
|
|
Password: "secret",
|
|
}
|
|
srv := web.NewServer(srvCfg, db, "http://localhost:8080")
|
|
|
|
// Unauthenticated -> 401.
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
srv.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401 for unauthenticated dashboard, got %d", rec.Code)
|
|
}
|
|
|
|
// Authenticated -> 200 with the artist name and missing count.
|
|
rec = httptest.NewRecorder()
|
|
req = httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.SetBasicAuth("admin", "secret")
|
|
srv.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 for authenticated dashboard, got %d", rec.Code)
|
|
}
|
|
body := rec.Body.String()
|
|
if !strings.Contains(body, "Pink Floyd") {
|
|
t.Fatalf("dashboard did not render artist name; body head:\n%s", body[:min(400, len(body))])
|
|
}
|
|
|
|
// Artist detail page renders the missing release.
|
|
rec = httptest.NewRecorder()
|
|
req = httptest.NewRequest(http.MethodGet, "/artist/ar1", nil)
|
|
req.SetBasicAuth("admin", "secret")
|
|
srv.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 for artist page, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "Animals") {
|
|
t.Fatalf("artist page did not render missing release 'Animals'")
|
|
}
|
|
}
|
|
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// stubSender is a test double for notifier.Sender.
|
|
type stubSender struct {
|
|
onSend func(message string) error
|
|
}
|
|
|
|
func (s stubSender) Send(_ context.Context, message string) error {
|
|
return s.onSend(message)
|
|
}
|