Task 8's ignore/restore/ignore-singles actions live on the artist detail
page, but the dashboard rendered artist names as plain text with no link,
making those actions unreachable through normal UI navigation. Wrap the
name in an anchor to /artist/{id} and assert the link in the dashboard test.
472 lines
14 KiB
Go
472 lines
14 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"naviwatcher/internal/config"
|
|
"naviwatcher/internal/database"
|
|
)
|
|
|
|
// seedArtist inserts an artist_settings row and returns its ID.
|
|
func seedArtist(t *testing.T, db *database.DB, id, name, mbid string, monitored bool) {
|
|
t.Helper()
|
|
if err := database.SaveArtistSettings(db, &database.ArtistSettings{
|
|
ID: id,
|
|
Name: name,
|
|
MBID: mbid,
|
|
Monitored: monitored,
|
|
}); err != nil {
|
|
t.Fatalf("seed artist %s: %v", id, err)
|
|
}
|
|
}
|
|
|
|
// seedLocalAlbum inserts a local_albums row.
|
|
func seedLocalAlbum(t *testing.T, db *database.DB, id, artistID, title string) {
|
|
t.Helper()
|
|
if err := database.SaveLocalAlbum(db, &database.LocalAlbum{ID: id, ArtistID: artistID, Title: title}); err != nil {
|
|
t.Fatalf("seed local album %s: %v", id, err)
|
|
}
|
|
}
|
|
|
|
// seedExternalRelease inserts an external_releases row (not ignored).
|
|
func seedExternalRelease(t *testing.T, db *database.DB, rgid, artistID, title string) {
|
|
t.Helper()
|
|
if err := database.SaveExternalRelease(db, &database.ExternalRelease{
|
|
RGID: rgid,
|
|
ArtistID: artistID,
|
|
Title: title,
|
|
}); err != nil {
|
|
t.Fatalf("seed external release %s: %v", rgid, err)
|
|
}
|
|
}
|
|
|
|
func newServer(t *testing.T, user, pass string) (*Server, *database.DB) {
|
|
t.Helper()
|
|
db, err := database.New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
cfg := &config.ServerConfig{Host: "0.0.0.0", Port: 8080, Username: user, Password: pass}
|
|
s := NewServer(cfg, db, "http://ui.example", 0)
|
|
return s, db
|
|
}
|
|
|
|
func TestDashboard_Authenticated200(t *testing.T) {
|
|
s, db := newServer(t, "admin", "secret")
|
|
seedArtist(t, db, "a1", "Radiohead", "mbid-1", true)
|
|
seedLocalAlbum(t, db, "l1", "a1", "OK Computer")
|
|
// One missing release (no local album matches "Kid A").
|
|
seedExternalRelease(t, db, "r1", "a1", "Kid A")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.SetBasicAuth("admin", "secret")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
body := rec.Body.String()
|
|
if !strings.Contains(body, "Radiohead") {
|
|
t.Errorf("expected artist name in body, got:\n%s", body)
|
|
}
|
|
if !strings.Contains(body, "1") {
|
|
t.Errorf("expected missing count rendered, got:\n%s", body)
|
|
}
|
|
if !strings.Contains(body, "mbid-1") {
|
|
t.Errorf("expected MBID rendered, got:\n%s", body)
|
|
}
|
|
// The dashboard must link each artist to its detail page, otherwise the
|
|
// ignore/restore/singles actions on that page are unreachable via normal UI
|
|
// navigation.
|
|
if !strings.Contains(body, `href="/artist/a1"`) {
|
|
t.Errorf("expected link to artist detail page, got:\n%s", body)
|
|
}
|
|
}
|
|
|
|
func TestDashboard_Unauthenticated401(t *testing.T) {
|
|
s, _ := newServer(t, "admin", "secret")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Header().Get("WWW-Authenticate"), "Basic") {
|
|
t.Errorf("expected Basic auth challenge, got headers: %v", rec.Header())
|
|
}
|
|
}
|
|
|
|
func TestDashboard_WrongPassword401(t *testing.T) {
|
|
s, _ := newServer(t, "admin", "secret")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.SetBasicAuth("admin", "wrong")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401 for wrong password, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestDashboard_NoAuthWhenDisabled(t *testing.T) {
|
|
// When username or password is empty, auth is bypassed.
|
|
s, db := newServer(t, "", "")
|
|
seedArtist(t, db, "a1", "Boards of Canada", "", true)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 when auth disabled, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "Boards of Canada") {
|
|
t.Errorf("expected artist rendered, got:\n%s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestDashboard_SkipsUnmonitored(t *testing.T) {
|
|
s, db := newServer(t, "admin", "secret")
|
|
seedArtist(t, db, "mon", "Monitored Artist", "", true)
|
|
seedArtist(t, db, "unmon", "Unmonitored Artist", "", false)
|
|
seedExternalRelease(t, db, "r1", "unmon", "Should not appear")
|
|
seedExternalRelease(t, db, "r2", "mon", "Missing here")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.SetBasicAuth("admin", "secret")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
body := rec.Body.String()
|
|
if strings.Contains(body, "Unmonitored Artist") {
|
|
t.Errorf("unmonitored artist should not appear, got:\n%s", body)
|
|
}
|
|
if !strings.Contains(body, "Monitored Artist") {
|
|
t.Errorf("monitored artist should appear, got:\n%s", body)
|
|
}
|
|
}
|
|
|
|
func TestDashboard_EmptyState(t *testing.T) {
|
|
s, _ := newServer(t, "admin", "secret")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.SetBasicAuth("admin", "secret")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "No monitored artists") {
|
|
t.Errorf("expected empty-state message, got:\n%s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestDashboard_NotFoundForOtherPaths(t *testing.T) {
|
|
s, _ := newServer(t, "admin", "secret")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/favicon.ico", nil)
|
|
req.SetBasicAuth("admin", "secret")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404 for non-root path, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestArtistDetail_RendersLocalAndMissing(t *testing.T) {
|
|
s, db := newServer(t, "admin", "secret")
|
|
seedArtist(t, db, "a1", "Radiohead", "mbid-1", true)
|
|
seedLocalAlbum(t, db, "l1", "a1", "OK Computer")
|
|
seedLocalAlbum(t, db, "l2", "a1", "The Bends")
|
|
// One missing release (no local album matches "Kid A").
|
|
seedExternalRelease(t, db, "r1", "a1", "Kid A")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/artist/a1", nil)
|
|
req.SetBasicAuth("admin", "secret")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
body := rec.Body.String()
|
|
for _, want := range []string{"Radiohead", "OK Computer", "The Bends", "Kid A", "mbid-1"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("expected %q in artist page, got:\n%s", want, body)
|
|
}
|
|
}
|
|
if !strings.Contains(body, `name="rgid" value="r1"`) {
|
|
t.Errorf("expected ignore form for r1, got:\n%s", body)
|
|
}
|
|
}
|
|
|
|
func TestArtistDetail_UnknownArtist404(t *testing.T) {
|
|
s, _ := newServer(t, "admin", "secret")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/artist/does-not-exist", nil)
|
|
req.SetBasicAuth("admin", "secret")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404 for unknown artist, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestArtistDetail_BarePath404(t *testing.T) {
|
|
s, _ := newServer(t, "admin", "secret")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/artist/", nil)
|
|
req.SetBasicAuth("admin", "secret")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404 for bare /artist/, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestArtistDetail_RequiresGet(t *testing.T) {
|
|
s, _ := newServer(t, "admin", "secret")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/artist/a1", nil)
|
|
req.SetBasicAuth("admin", "secret")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected 405 for POST on detail, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestArchive_RendersIgnoredReleases(t *testing.T) {
|
|
s, db := newServer(t, "admin", "secret")
|
|
seedArtist(t, db, "a1", "Radiohead", "", true)
|
|
// An ignored external release.
|
|
if err := database.SaveExternalRelease(db, &database.ExternalRelease{
|
|
RGID: "r-ignored",
|
|
ArtistID: "a1",
|
|
Title: "Ignored Album",
|
|
IsIgnored: true,
|
|
}); err != nil {
|
|
t.Fatalf("seed ignored release: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/archive", nil)
|
|
req.SetBasicAuth("admin", "secret")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
body := rec.Body.String()
|
|
if !strings.Contains(body, "Ignored Album") {
|
|
t.Errorf("expected ignored release in archive, got:\n%s", body)
|
|
}
|
|
if !strings.Contains(body, `name="rgid" value="r-ignored"`) {
|
|
t.Errorf("expected restore form for r-ignored, got:\n%s", body)
|
|
}
|
|
}
|
|
|
|
func TestArchive_EmptyState(t *testing.T) {
|
|
s, _ := newServer(t, "admin", "secret")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/archive", nil)
|
|
req.SetBasicAuth("admin", "secret")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "No ignored releases") {
|
|
t.Errorf("expected empty archive message, got:\n%s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestArchive_RequiresGet(t *testing.T) {
|
|
s, _ := newServer(t, "admin", "secret")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/archive", nil)
|
|
req.SetBasicAuth("admin", "secret")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected 405 for POST on archive, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestIgnoreAction_SetsFlagAndRemovesFromDashboard(t *testing.T) {
|
|
s, db := newServer(t, "admin", "secret")
|
|
seedArtist(t, db, "a1", "Radiohead", "", true)
|
|
seedExternalRelease(t, db, "r1", "a1", "Kid A")
|
|
|
|
// Before ignore: dashboard shows 1 missing.
|
|
before := dashboardMissingCount(t, s, "Radiohead")
|
|
if before != 1 {
|
|
t.Fatalf("expected 1 missing before ignore, got %d", before)
|
|
}
|
|
|
|
// POST ignore.
|
|
form := strings.NewReader("rgid=r1")
|
|
req := httptest.NewRequest(http.MethodPost, "/artist/a1/ignore", form)
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.SetBasicAuth("admin", "secret")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected 303 redirect, got %d", rec.Code)
|
|
}
|
|
if loc := rec.Header().Get("Location"); loc != "/artist/a1" {
|
|
t.Errorf("expected redirect to /artist/a1, got %q", loc)
|
|
}
|
|
|
|
// Flag persisted.
|
|
rel, err := database.GetExternalRelease(db, "r1")
|
|
if err != nil {
|
|
t.Fatalf("get release: %v", err)
|
|
}
|
|
if !rel.IsIgnored {
|
|
t.Errorf("expected r1 to be ignored")
|
|
}
|
|
|
|
// Dashboard missing count drops to 0.
|
|
after := dashboardMissingCount(t, s, "Radiohead")
|
|
if after != 0 {
|
|
t.Fatalf("expected 0 missing after ignore, got %d", after)
|
|
}
|
|
}
|
|
|
|
func TestIgnoreAction_RequiresAuth(t *testing.T) {
|
|
s, db := newServer(t, "admin", "secret")
|
|
seedArtist(t, db, "a1", "Radiohead", "", true)
|
|
seedExternalRelease(t, db, "r1", "a1", "Kid A")
|
|
|
|
form := strings.NewReader("rgid=r1")
|
|
req := httptest.NewRequest(http.MethodPost, "/artist/a1/ignore", form)
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401 without auth, got %d", rec.Code)
|
|
}
|
|
// Flag must remain unset.
|
|
rel, err := database.GetExternalRelease(db, "r1")
|
|
if err != nil {
|
|
t.Fatalf("get release: %v", err)
|
|
}
|
|
if rel.IsIgnored {
|
|
t.Errorf("release must not be ignored without auth")
|
|
}
|
|
}
|
|
|
|
func TestRestoreAction_ClearsFlag(t *testing.T) {
|
|
s, db := newServer(t, "admin", "secret")
|
|
seedArtist(t, db, "a1", "Radiohead", "", true)
|
|
if err := database.SaveExternalRelease(db, &database.ExternalRelease{
|
|
RGID: "r1",
|
|
ArtistID: "a1",
|
|
Title: "Kid A",
|
|
IsIgnored: true,
|
|
}); err != nil {
|
|
t.Fatalf("seed ignored release: %v", err)
|
|
}
|
|
|
|
form := strings.NewReader("rgid=r1")
|
|
req := httptest.NewRequest(http.MethodPost, "/artist/a1/restore", form)
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.SetBasicAuth("admin", "secret")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected 303 redirect, got %d", rec.Code)
|
|
}
|
|
rel, err := database.GetExternalRelease(db, "r1")
|
|
if err != nil {
|
|
t.Fatalf("get release: %v", err)
|
|
}
|
|
if rel.IsIgnored {
|
|
t.Errorf("expected r1 to be restored (not ignored)")
|
|
}
|
|
}
|
|
|
|
func TestIgnoreSingles_TogglesFlag(t *testing.T) {
|
|
s, db := newServer(t, "admin", "secret")
|
|
seedArtist(t, db, "a1", "Radiohead", "", true)
|
|
|
|
// Toggle on.
|
|
req := httptest.NewRequest(http.MethodPost, "/artist/a1/ignore-singles", nil)
|
|
req.SetBasicAuth("admin", "secret")
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected 303 on toggle, got %d", rec.Code)
|
|
}
|
|
settings, err := database.GetArtistSettings(db, "a1")
|
|
if err != nil {
|
|
t.Fatalf("get settings: %v", err)
|
|
}
|
|
if !settings.IgnoreSingles {
|
|
t.Errorf("expected ignore_singles = true after first toggle")
|
|
}
|
|
|
|
// Toggle off.
|
|
req2 := httptest.NewRequest(http.MethodPost, "/artist/a1/ignore-singles", nil)
|
|
req2.SetBasicAuth("admin", "secret")
|
|
rec2 := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec2, req2)
|
|
settings, err = database.GetArtistSettings(db, "a1")
|
|
if err != nil {
|
|
t.Fatalf("get settings: %v", err)
|
|
}
|
|
if settings.IgnoreSingles {
|
|
t.Errorf("expected ignore_singles = false after second toggle")
|
|
}
|
|
}
|
|
|
|
func TestIgnoreSingles_RequiresAuth(t *testing.T) {
|
|
s, _ := newServer(t, "admin", "secret")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/artist/a1/ignore-singles", nil)
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401 without auth, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
// dashboardMissingCount returns the missing-release count for the artist with
|
|
// the given name from the dashboard view model (0 if the artist is absent).
|
|
func dashboardMissingCount(t *testing.T, s *Server, artistName string) int {
|
|
t.Helper()
|
|
data, err := s.buildDashboardData(context.Background(), 0)
|
|
if err != nil {
|
|
t.Fatalf("build dashboard data: %v", err)
|
|
}
|
|
for _, a := range data.Artists {
|
|
if a.Name == artistName {
|
|
return a.MissingCount
|
|
}
|
|
}
|
|
return 0
|
|
}
|