b08a0b1646
- Add POST /api/artists/{id}/refresh to re-fetch metadata from gRPC service
- Add DELETE /api/artists/{id} with cascade delete via PostgreSQL
- Add e2e tests for both flows covering happy path, not found, idempotency
- Extend testutil with GetArtistUpdatedAt, CountAlbumsByArtist, DELETE helper
241 lines
5.9 KiB
Go
241 lines
5.9 KiB
Go
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fujin/music-agregator/testing/e2e/testutil"
|
|
)
|
|
|
|
// TestDeleteArtist_Flow covers section 1.3 of FLOWS.md:
|
|
// 1. User deletes artist
|
|
// 2. Cascading delete: artists → albums → album_releases → tracks, wanted_albums, download_queue entries
|
|
// 3. track_files records removed (no physical file deletion)
|
|
func TestDeleteArtist_Flow(t *testing.T) {
|
|
env := testutil.NewTestEnv(t)
|
|
defer env.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
artistName := "Air"
|
|
|
|
if err := env.CleanupArtistByName(ctx, artistName); err != nil {
|
|
t.Fatalf("cleanup failed: %v", err)
|
|
}
|
|
|
|
syncResp, err := env.POST("/api/sync", map[string]any{
|
|
"artist": artistName,
|
|
"store": true,
|
|
"download": false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
syncResp.AssertStatus(t, 200)
|
|
|
|
var syncResult struct {
|
|
ArtistID string `json:"artist_id"`
|
|
ArtistName string `json:"artist_name"`
|
|
AlbumsStored int `json:"albums_stored"`
|
|
}
|
|
if err := syncResp.DecodeJSON(&syncResult); err != nil {
|
|
t.Fatalf("failed to decode sync response: %v", err)
|
|
}
|
|
|
|
artistID := syncResult.ArtistID
|
|
|
|
albumCount, err := env.CountAlbumsByArtist(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("failed to count albums: %v", err)
|
|
}
|
|
if albumCount == 0 {
|
|
t.Fatal("expected albums to exist before delete")
|
|
}
|
|
|
|
t.Run("DeleteArtist", func(t *testing.T) {
|
|
deleteResp, err := env.DELETE("/api/artists/" + artistID)
|
|
if err != nil {
|
|
t.Fatalf("delete request failed: %v", err)
|
|
}
|
|
deleteResp.AssertStatus(t, 200)
|
|
|
|
var deleteResult struct {
|
|
Deleted bool `json:"deleted"`
|
|
Message string `json:"message"`
|
|
}
|
|
if err := deleteResp.DecodeJSON(&deleteResult); err != nil {
|
|
t.Fatalf("failed to decode delete response: %v", err)
|
|
}
|
|
|
|
if !deleteResult.Deleted {
|
|
t.Error("expected deleted=true")
|
|
}
|
|
})
|
|
|
|
t.Run("ArtistNoLongerExists", func(t *testing.T) {
|
|
artist, err := env.GetArtistByForeignID(ctx, artistID)
|
|
if err == nil && artist != nil {
|
|
t.Error("expected artist to be deleted from database")
|
|
}
|
|
})
|
|
|
|
t.Run("AlbumsCascadeDeleted", func(t *testing.T) {
|
|
albumCount, err := env.CountAlbumsByArtist(ctx, artistID)
|
|
if err == nil && albumCount > 0 {
|
|
t.Errorf("expected albums to be cascade deleted, found %d", albumCount)
|
|
}
|
|
})
|
|
|
|
t.Run("ArtistNotInLibraryList", func(t *testing.T) {
|
|
artistsResp, err := env.GET("/api/library/artists")
|
|
if err != nil {
|
|
t.Fatalf("list artists failed: %v", err)
|
|
}
|
|
artistsResp.AssertStatus(t, 200)
|
|
|
|
var artists []struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := artistsResp.DecodeJSON(&artists); err != nil {
|
|
t.Fatalf("failed to decode artists: %v", err)
|
|
}
|
|
|
|
for _, a := range artists {
|
|
if a.Name == syncResult.ArtistName {
|
|
t.Errorf("deleted artist %q still appears in library list", syncResult.ArtistName)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDeleteArtist_NotFound(t *testing.T) {
|
|
env := testutil.NewTestEnv(t)
|
|
defer env.Close()
|
|
|
|
deleteResp, err := env.DELETE("/api/artists/nonexistent-artist-id-99999")
|
|
if err != nil {
|
|
t.Fatalf("delete request failed: %v", err)
|
|
}
|
|
|
|
deleteResp.AssertStatus(t, 404)
|
|
|
|
var errorResp struct {
|
|
Error string `json:"error"`
|
|
}
|
|
if err := deleteResp.DecodeJSON(&errorResp); err != nil {
|
|
t.Fatalf("failed to decode error response: %v", err)
|
|
}
|
|
|
|
if errorResp.Error == "" {
|
|
t.Error("expected error message in response")
|
|
}
|
|
}
|
|
|
|
func TestDeleteArtist_VerifyStatsDecreased(t *testing.T) {
|
|
env := testutil.NewTestEnv(t)
|
|
defer env.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
artistName := "Lamb"
|
|
|
|
if err := env.CleanupArtistByName(ctx, artistName); err != nil {
|
|
t.Fatalf("cleanup failed: %v", err)
|
|
}
|
|
|
|
syncResp, err := env.POST("/api/sync", map[string]any{
|
|
"artist": artistName,
|
|
"store": true,
|
|
"download": false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
syncResp.AssertStatus(t, 200)
|
|
|
|
var syncResult struct {
|
|
ArtistID string `json:"artist_id"`
|
|
}
|
|
syncResp.DecodeJSON(&syncResult)
|
|
|
|
statsBeforeResp, err := env.GET("/api/library/stats")
|
|
if err != nil {
|
|
t.Fatalf("stats request failed: %v", err)
|
|
}
|
|
var statsBefore struct {
|
|
Artists int64 `json:"artists"`
|
|
Albums int64 `json:"albums"`
|
|
}
|
|
statsBeforeResp.DecodeJSON(&statsBefore)
|
|
|
|
deleteResp, err := env.DELETE("/api/artists/" + syncResult.ArtistID)
|
|
if err != nil {
|
|
t.Fatalf("delete request failed: %v", err)
|
|
}
|
|
deleteResp.AssertStatus(t, 200)
|
|
|
|
statsAfterResp, err := env.GET("/api/library/stats")
|
|
if err != nil {
|
|
t.Fatalf("stats request failed: %v", err)
|
|
}
|
|
var statsAfter struct {
|
|
Artists int64 `json:"artists"`
|
|
Albums int64 `json:"albums"`
|
|
}
|
|
statsAfterResp.DecodeJSON(&statsAfter)
|
|
|
|
if statsAfter.Artists >= statsBefore.Artists {
|
|
t.Errorf("expected artist count to decrease: before=%d, after=%d",
|
|
statsBefore.Artists, statsAfter.Artists)
|
|
}
|
|
|
|
if statsAfter.Albums >= statsBefore.Albums {
|
|
t.Errorf("expected album count to decrease: before=%d, after=%d",
|
|
statsBefore.Albums, statsAfter.Albums)
|
|
}
|
|
}
|
|
|
|
func TestDeleteArtist_Idempotent(t *testing.T) {
|
|
env := testutil.NewTestEnv(t)
|
|
defer env.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
artistName := "Tricky"
|
|
|
|
if err := env.CleanupArtistByName(ctx, artistName); err != nil {
|
|
t.Fatalf("cleanup failed: %v", err)
|
|
}
|
|
|
|
syncResp, err := env.POST("/api/sync", map[string]any{
|
|
"artist": artistName,
|
|
"store": true,
|
|
"download": false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
syncResp.AssertStatus(t, 200)
|
|
|
|
var syncResult struct {
|
|
ArtistID string `json:"artist_id"`
|
|
}
|
|
syncResp.DecodeJSON(&syncResult)
|
|
|
|
firstDelete, err := env.DELETE("/api/artists/" + syncResult.ArtistID)
|
|
if err != nil {
|
|
t.Fatalf("first delete failed: %v", err)
|
|
}
|
|
firstDelete.AssertStatus(t, 200)
|
|
|
|
secondDelete, err := env.DELETE("/api/artists/" + syncResult.ArtistID)
|
|
if err != nil {
|
|
t.Fatalf("second delete failed: %v", err)
|
|
}
|
|
secondDelete.AssertStatus(t, 404)
|
|
}
|