ci / module boundaries / boundaries (push) Successful in 6s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 2m1s
ci / ts / ts (packages + web) (push) Failing after 5m13s
ci / api / api (push) Successful in 41s
ci / db / db (push) Failing after 1m33s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 2m25s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 2m19s
78 lines
2.9 KiB
Go
78 lines
2.9 KiB
Go
//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")
|
|
})
|
|
}
|
|
}
|