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
266 lines
7.1 KiB
Go
266 lines
7.1 KiB
Go
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fujin/music-agregator/testing/e2e/testutil"
|
|
)
|
|
|
|
// TestRefreshArtist_Flow covers section 1.2 of FLOWS.md:
|
|
// 1. Manual or scheduled trigger
|
|
// 2. Re-fetches artist + albums from MetadataService
|
|
// 3. Upserts new/changed albums, keeps existing
|
|
// 4. Updates artist_metadata.updated_at
|
|
func TestRefreshArtist_Flow(t *testing.T) {
|
|
env := testutil.NewTestEnv(t)
|
|
defer env.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
artistName := "Depeche Mode"
|
|
|
|
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("initial sync failed: %v", err)
|
|
}
|
|
syncResp.AssertStatus(t, 200)
|
|
|
|
var syncResult struct {
|
|
ArtistID string `json:"artist_id"`
|
|
ArtistName string `json:"artist_name"`
|
|
}
|
|
if err := syncResp.DecodeJSON(&syncResult); err != nil {
|
|
t.Fatalf("failed to decode sync response: %v", err)
|
|
}
|
|
|
|
artistID := syncResult.ArtistID
|
|
actualArtistName := syncResult.ArtistName
|
|
|
|
initialUpdatedAt, err := env.GetArtistUpdatedAt(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get initial updated_at: %v", err)
|
|
}
|
|
|
|
initialAlbumCount, err := env.CountAlbumsByArtist(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("failed to count initial albums: %v", err)
|
|
}
|
|
|
|
time.Sleep(1 * time.Second)
|
|
|
|
t.Run("ManualRefreshTrigger", func(t *testing.T) {
|
|
refreshResp, err := env.POST("/api/artists/"+artistID+"/refresh", nil)
|
|
if err != nil {
|
|
t.Fatalf("refresh request failed: %v", err)
|
|
}
|
|
refreshResp.AssertStatus(t, 200)
|
|
|
|
var refreshResult struct {
|
|
ArtistID string `json:"artist_id"`
|
|
ArtistName string `json:"artist_name"`
|
|
AlbumsUpdated int `json:"albums_updated"`
|
|
AlbumsAdded int `json:"albums_added"`
|
|
}
|
|
if err := refreshResp.DecodeJSON(&refreshResult); err != nil {
|
|
t.Fatalf("failed to decode refresh response: %v", err)
|
|
}
|
|
|
|
if refreshResult.ArtistID != artistID {
|
|
t.Errorf("expected artist_id=%q, got %q", artistID, refreshResult.ArtistID)
|
|
}
|
|
if refreshResult.ArtistName != actualArtistName {
|
|
t.Errorf("expected artist_name=%q, got %q", actualArtistName, refreshResult.ArtistName)
|
|
}
|
|
})
|
|
|
|
t.Run("UpdatedAtIncreased", func(t *testing.T) {
|
|
newUpdatedAt, err := env.GetArtistUpdatedAt(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get new updated_at: %v", err)
|
|
}
|
|
|
|
if !newUpdatedAt.After(initialUpdatedAt) {
|
|
t.Errorf("expected updated_at to increase: was %v, now %v", initialUpdatedAt, newUpdatedAt)
|
|
}
|
|
})
|
|
|
|
t.Run("AlbumsPreserved", func(t *testing.T) {
|
|
newAlbumCount, err := env.CountAlbumsByArtist(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("failed to count albums after refresh: %v", err)
|
|
}
|
|
|
|
if newAlbumCount < initialAlbumCount {
|
|
t.Errorf("expected albums to be preserved: was %d, now %d", initialAlbumCount, newAlbumCount)
|
|
}
|
|
})
|
|
|
|
t.Cleanup(func() {
|
|
env.CleanupArtistByName(context.Background(), actualArtistName)
|
|
})
|
|
}
|
|
|
|
func TestRefreshArtist_NotFound(t *testing.T) {
|
|
env := testutil.NewTestEnv(t)
|
|
defer env.Close()
|
|
|
|
refreshResp, err := env.POST("/api/artists/nonexistent-artist-id-12345/refresh", nil)
|
|
if err != nil {
|
|
t.Fatalf("refresh request failed: %v", err)
|
|
}
|
|
|
|
refreshResp.AssertStatus(t, 404)
|
|
|
|
var errorResp struct {
|
|
Error string `json:"error"`
|
|
}
|
|
if err := refreshResp.DecodeJSON(&errorResp); err != nil {
|
|
t.Fatalf("failed to decode error response: %v", err)
|
|
}
|
|
|
|
if errorResp.Error == "" {
|
|
t.Error("expected error message in response")
|
|
}
|
|
}
|
|
|
|
func TestRefreshArtist_UpsertsNewAlbums(t *testing.T) {
|
|
env := testutil.NewTestEnv(t)
|
|
defer env.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
artistName := "Massive Attack"
|
|
|
|
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,
|
|
"album": "Mezzanine",
|
|
"store": true,
|
|
"download": false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("initial sync failed: %v", err)
|
|
}
|
|
syncResp.AssertStatus(t, 200)
|
|
|
|
var syncResult struct {
|
|
ArtistID string `json:"artist_id"`
|
|
ArtistName string `json:"artist_name"`
|
|
TotalAlbums int `json:"total_albums"`
|
|
}
|
|
if err := syncResp.DecodeJSON(&syncResult); err != nil {
|
|
t.Fatalf("failed to decode sync response: %v", err)
|
|
}
|
|
|
|
artistID := syncResult.ArtistID
|
|
actualArtistName := syncResult.ArtistName
|
|
|
|
initialAlbumCount, err := env.CountAlbumsByArtist(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("failed to count initial albums: %v", err)
|
|
}
|
|
|
|
refreshResp, err := env.POST("/api/artists/"+artistID+"/refresh", nil)
|
|
if err != nil {
|
|
t.Fatalf("refresh request failed: %v", err)
|
|
}
|
|
refreshResp.AssertStatus(t, 200)
|
|
|
|
var refreshResult struct {
|
|
AlbumsAdded int `json:"albums_added"`
|
|
}
|
|
if err := refreshResp.DecodeJSON(&refreshResult); err != nil {
|
|
t.Fatalf("failed to decode refresh response: %v", err)
|
|
}
|
|
|
|
finalAlbumCount, err := env.CountAlbumsByArtist(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("failed to count final albums: %v", err)
|
|
}
|
|
|
|
if finalAlbumCount <= initialAlbumCount {
|
|
t.Logf("no new albums added (initial sync had filtered subset)")
|
|
}
|
|
|
|
if refreshResult.AlbumsAdded > 0 && finalAlbumCount != initialAlbumCount+int64(refreshResult.AlbumsAdded) {
|
|
t.Errorf("album count mismatch: initial=%d, added=%d, final=%d",
|
|
initialAlbumCount, refreshResult.AlbumsAdded, finalAlbumCount)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
env.CleanupArtistByName(context.Background(), actualArtistName)
|
|
})
|
|
}
|
|
|
|
func TestRefreshArtist_IdempotentRefresh(t *testing.T) {
|
|
env := testutil.NewTestEnv(t)
|
|
defer env.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
artistName := "Portishead"
|
|
|
|
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("initial sync failed: %v", err)
|
|
}
|
|
syncResp.AssertStatus(t, 200)
|
|
|
|
var syncResult struct {
|
|
ArtistID string `json:"artist_id"`
|
|
ArtistName string `json:"artist_name"`
|
|
}
|
|
syncResp.DecodeJSON(&syncResult)
|
|
artistID := syncResult.ArtistID
|
|
actualArtistName := syncResult.ArtistName
|
|
|
|
firstRefresh, err := env.POST("/api/artists/"+artistID+"/refresh", nil)
|
|
if err != nil {
|
|
t.Fatalf("first refresh failed: %v", err)
|
|
}
|
|
firstRefresh.AssertStatus(t, 200)
|
|
|
|
countAfterFirst, _ := env.CountAlbumsByArtist(ctx, artistID)
|
|
|
|
secondRefresh, err := env.POST("/api/artists/"+artistID+"/refresh", nil)
|
|
if err != nil {
|
|
t.Fatalf("second refresh failed: %v", err)
|
|
}
|
|
secondRefresh.AssertStatus(t, 200)
|
|
|
|
countAfterSecond, _ := env.CountAlbumsByArtist(ctx, artistID)
|
|
|
|
if countAfterSecond != countAfterFirst {
|
|
t.Errorf("expected idempotent refresh: album count was %d after first, %d after second",
|
|
countAfterFirst, countAfterSecond)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
env.CleanupArtistByName(context.Background(), actualArtistName)
|
|
})
|
|
}
|