ci / module boundaries / boundaries (push) Successful in 10s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m30s
ci / api / api (push) Successful in 58s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m1s
ci / db / db (push) Failing after 25s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m36s
ci / ts / ts (packages + web) (push) Failing after 6m3s
154 lines
6.8 KiB
Go
154 lines
6.8 KiB
Go
//go:build integration
|
|
|
|
package uploads
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
"pipu/api/internal/auth"
|
|
"pipu/api/internal/library"
|
|
"pipu/api/internal/musicbrainz"
|
|
"pipu/api/internal/platform/db"
|
|
)
|
|
|
|
type groupedCatalogue map[string]musicbrainz.Release
|
|
|
|
func (c groupedCatalogue) Search(_ context.Context, q musicbrainz.Query) ([]musicbrainz.Release, error) {
|
|
return []musicbrainz.Release{c[q.Album]}, nil
|
|
}
|
|
func (c groupedCatalogue) Cover(context.Context, musicbrainz.Release) (musicbrainz.Cover, error) {
|
|
return musicbrainz.Cover{Data: []byte{0xff, 0xd8, 0xff, 1}, MIME: "image/jpeg", Exact: true}, nil
|
|
}
|
|
|
|
func TestReleaseGroupsIsolateJobsMetadataAndArtwork(t *testing.T) {
|
|
address := os.Getenv("TEST_DATABASE_URL")
|
|
if address == "" {
|
|
t.Skip("TEST_DATABASE_URL is not set")
|
|
}
|
|
ctx := context.Background()
|
|
pool, err := db.Connect(ctx, address)
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
require.NoError(t, db.Migrate(ctx, pool))
|
|
author := uuid.New()
|
|
_, err = pool.Exec(ctx, `INSERT INTO users(id,username,password_hash,role) VALUES($1,$2,'x','admin')`, author, "groups-"+author.String())
|
|
require.NoError(t, err)
|
|
source, err := library.NewStore(pool).SaveSource(ctx, library.Source{ID: uuid.New(), Name: "groups-test", Type: library.SourceLocal, Status: "mounted", TagsFirst: true, AcceptsUploads: true, WriteTemplate: nullable("{albumartist}/{album}/{track:02} - {title}.{ext}")})
|
|
require.NoError(t, err)
|
|
store := NewStore(pool)
|
|
sumA, sumB := sha256.Sum256([]byte(uuid.NewString())), sha256.Sum256([]byte(uuid.NewString()))
|
|
cover := "covers/original.jpg"
|
|
batch, reserved, err := store.CreateBatch(ctx, author, nil, []NewFile{{ClientID: "a", Name: "a.flac", Size: 10, SHA256: sumA[:]}, {ClientID: "b", Name: "b.flac", Size: 10, SHA256: sumB[:]}}, DefaultSettings, &cover)
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
_, _ = pool.Exec(ctx, `DELETE FROM upload_batches WHERE id=$1`, batch)
|
|
_ = store.ReconcileQuota(ctx)
|
|
}()
|
|
_, err = pool.Exec(ctx, `UPDATE upload_release_jobs SET state='not_found' WHERE batch_id<>$1`, batch)
|
|
require.NoError(t, err)
|
|
for i, f := range reserved {
|
|
m := Meta{Artist: "Artist", AlbumArtist: "Artist", Album: []string{"Album A", "Album B"}[i], Title: "Song"}
|
|
raw, _ := json.Marshal(m)
|
|
_, err = pool.Exec(ctx, `UPDATE upload_files SET status='pending_review',metadata=$2,duration_ms=180000 WHERE id=$1`, f.FileID, raw)
|
|
require.NoError(t, err)
|
|
}
|
|
catalogue := groupedCatalogue{}
|
|
for _, album := range []string{"Album A", "Album B"} {
|
|
catalogue[album] = musicbrainz.Release{ID: uuid.NewString(), Title: album, Artist: "Artist", Type: "Album", Tracks: []musicbrainz.Track{{Title: "Song", Artist: "Artist", Disc: 1, Position: 1, DurationMs: 180000}}}
|
|
}
|
|
svc := NewService(store, &previewCatalog{source: source}, previewRoot(t.TempDir()), nil, nil, nil, library.FPCalc{}, Config{StagingDir: t.TempDir()}, slog.New(slog.DiscardHandler))
|
|
svc.SetReleaseCatalogue(catalogue)
|
|
review := auth.WithPrincipal(ctx, auth.Principal{UserID: author, Role: auth.RoleAdmin, ReviewsUploads: true})
|
|
root, err := svc.GetReleaseReport(review, batch)
|
|
require.NoError(t, err)
|
|
require.Len(t, root.Groups, 2)
|
|
require.Equal(t, "needs_input", root.State)
|
|
group := map[string]string{}
|
|
for _, g := range root.Groups {
|
|
group[g.Album] = g.ID
|
|
require.False(t, g.HasCover, "ambiguous original batch artwork must not be copied to every album")
|
|
}
|
|
a, b := group["Album A"], group["Album B"]
|
|
_, err = svc.SearchReleases(review, batch, musicbrainz.Query{})
|
|
require.ErrorContains(t, err, "release_group_required")
|
|
_, err = svc.GetReleaseReport(review, batch, uuid.NewString())
|
|
require.ErrorContains(t, err, "release_group_not_found")
|
|
require.True(t, svc.lookupNextRelease(ctx))
|
|
require.True(t, svc.lookupNextRelease(ctx))
|
|
reportA, err := svc.GetReleaseReport(review, batch, a)
|
|
require.NoError(t, err)
|
|
require.True(t, reportA.Candidates[0].Complete)
|
|
reportB, err := svc.GetReleaseReport(review, batch, b)
|
|
require.NoError(t, err)
|
|
require.True(t, reportB.Candidates[0].Complete)
|
|
_, err = svc.ApplyRelease(review, batch, catalogue["Album B"].ID, true, false, a)
|
|
require.ErrorContains(t, err, "release_not_found")
|
|
used, _, err := store.Usage(ctx, author)
|
|
require.NoError(t, err)
|
|
_, err = svc.ApplyRelease(review, batch, catalogue["Album B"].ID, true, true, b)
|
|
require.NoError(t, err)
|
|
aFiles, err := store.AllInReleaseGroup(ctx, batch, a)
|
|
require.NoError(t, err)
|
|
require.Nil(t, aFiles[0].CoverPath)
|
|
require.Equal(t, "Album A", aFiles[0].Meta.Album)
|
|
require.Empty(t, aFiles[0].Tags["musicbrainz_albumid"])
|
|
bFiles, err := store.AllInReleaseGroup(ctx, batch, b)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, bFiles[0].CoverPath)
|
|
require.Equal(t, catalogue["Album B"].ID, bFiles[0].Tags["musicbrainz_albumid"])
|
|
kept, err := store.StagingInUse(ctx)
|
|
require.NoError(t, err)
|
|
require.True(t, kept[*bFiles[0].CoverPath])
|
|
afterA, err := svc.GetReleaseReport(review, batch, a)
|
|
require.NoError(t, err)
|
|
require.Equal(t, reportA.InputHash, afterA.InputHash)
|
|
require.Nil(t, afterA.SelectedRelease)
|
|
newUsed, _, err := store.Usage(ctx, author)
|
|
require.NoError(t, err)
|
|
require.Equal(t, used, newUsed)
|
|
// A job lease for A survives a metadata edit in B.
|
|
_, err = svc.SearchReleases(review, batch, musicbrainz.Query{GroupID: a})
|
|
require.NoError(t, err)
|
|
job, err := store.ClaimRelease(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, a, job.GroupKey)
|
|
edited := *bFiles[0].Meta
|
|
edited.Title = "Edited B"
|
|
require.NoError(t, store.UpdateMetadata(ctx, bFiles[0].ID, edited, true, placement{}))
|
|
require.NoError(t, store.SaveRelease(ctx, job, reportA))
|
|
afterA, err = store.ReleaseReport(ctx, batch, a)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "ready", afterA.State)
|
|
afterB, err := store.ReleaseReport(ctx, batch, b)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "queued", afterB.State)
|
|
require.NoError(t, store.EnsureReleaseGroups(ctx, batch))
|
|
groups, err := store.ReleaseGroups(ctx, batch)
|
|
require.NoError(t, err)
|
|
require.Len(t, groups, 2)
|
|
// Explicit whole-batch cover editing still affects every subgroup.
|
|
shared := "covers/shared.jpg"
|
|
require.NoError(t, store.SetCover(ctx, batch, &shared))
|
|
aFiles, err = store.AllInReleaseGroup(ctx, batch, a)
|
|
require.NoError(t, err)
|
|
require.Equal(t, shared, *aFiles[0].CoverPath)
|
|
bFiles, err = store.AllInReleaseGroup(ctx, batch, b)
|
|
require.NoError(t, err)
|
|
require.Equal(t, shared, *bFiles[0].CoverPath)
|
|
require.NoError(t, store.ChangeStatus(ctx, bFiles[0].ID, []Status{StatusPendingReview}, StatusWithdrawn))
|
|
unused, err := store.UnusedReleaseCovers(ctx, batch)
|
|
require.NoError(t, err)
|
|
require.Empty(t, unused, "shared artwork is still needed by group A")
|
|
require.NoError(t, store.ChangeStatus(ctx, aFiles[0].ID, []Status{StatusPendingReview}, StatusWithdrawn))
|
|
unused, err = store.UnusedReleaseCovers(ctx, batch)
|
|
require.NoError(t, err)
|
|
require.Contains(t, unused, shared)
|
|
}
|