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