//go:build integration package dbtest import ( "context" "log/slog" "os" "path/filepath" "testing" "github.com/google/uuid" "github.com/stretchr/testify/require" "pipu/api/internal/library" "pipu/api/internal/library/meta" "pipu/api/internal/platform/db" ) type portraitProber struct{ artist string } func (p portraitProber) Probe(context.Context, string) (library.Probe, error) { return library.Probe{Format: "flac", Codec: "flac", DurationMs: 180000, Tags: meta.Fields{Artist: p.artist + ", Guest " + p.artist, AlbumArtist: p.artist, Album: "Portrait album", Title: "Song", Track: "1"}}, nil } func TestArtistImagesPreserveLocalExternalAndConcurrentPortraits(t *testing.T) { pool := connect(t) ctx := context.Background() require.NoError(t, db.Migrate(ctx, pool)) store := library.NewStore(pool) source, err := store.SaveSource(ctx, library.Source{ID: uuid.New(), Name: "portraits", Type: library.SourceLocal, Status: "mounted", TagsFirst: true, NFSOptions: "ro"}) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.DeleteSource(ctx, source.ID)) require.NoError(t, store.CleanupOrphans(ctx)) }) root := t.TempDir() for _, scenario := range []string{"empty", "local", "external", "concurrent"} { t.Run(scenario, func(t *testing.T) { artist := "Portrait " + uuid.NewString() scanner := library.NewScanner(store, portraitProber{artist}, library.NewProgressBoard(), slog.New(slog.DiscardHandler)) rel := scenario + ".flac" require.NoError(t, os.WriteFile(filepath.Join(root, rel), []byte(scenario), 0o644)) file, err := scanner.ScanFile(ctx, source, root, rel) require.NoError(t, err) id, err := store.ArtistWithoutImageForFile(ctx, file) require.NoError(t, err) if scenario == "local" { require.NoError(t, store.SetArtistImageIfMissing(ctx, id, "existing-local")) } if scenario == "external" || scenario == "concurrent" { _, err = pool.Exec(ctx, `UPDATE artists SET genius_image_url='https://example.org/portrait.jpg' WHERE id=$1`, id) require.NoError(t, err) } if scenario != "empty" { _, err = store.ArtistWithoutImageForFile(ctx, file) require.ErrorIs(t, err, library.ErrNotFound) } // Even a caller that looked up the artist before another writer cannot overwrite it. require.NoError(t, store.SetArtistImageIfMissing(ctx, id, "uploaded-portrait")) a, err := store.Artist(ctx, id) require.NoError(t, err) switch scenario { case "empty": require.Equal(t, "uploaded-portrait", *a.ImagePath) case "local": require.Equal(t, "existing-local", *a.ImagePath) default: require.Nil(t, a.ImagePath) require.Equal(t, "https://example.org/portrait.jpg", *a.ImageURL) } var guestImage *string require.NoError(t, pool.QueryRow(ctx, `SELECT image_path FROM artists WHERE name=$1`, "Guest "+artist).Scan(&guestImage)) require.Nil(t, guestImage, "a parent folder portrait belongs to the album artist, not guests") }) } }