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
185 lines
8.1 KiB
Go
185 lines
8.1 KiB
Go
//go:build integration
|
|
|
|
package uploads
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/stretchr/testify/require"
|
|
"pipu/api/internal/auth"
|
|
"pipu/api/internal/library"
|
|
"pipu/api/internal/musicbrainz"
|
|
"pipu/api/internal/platform/db"
|
|
)
|
|
|
|
type testReleaseCatalogue struct {
|
|
release musicbrainz.Release
|
|
searches int
|
|
err error
|
|
}
|
|
|
|
func (c *testReleaseCatalogue) Search(context.Context, musicbrainz.Query) ([]musicbrainz.Release, error) {
|
|
c.searches++
|
|
return []musicbrainz.Release{c.release}, c.err
|
|
}
|
|
func (c *testReleaseCatalogue) Cover(context.Context, musicbrainz.Release) (musicbrainz.Cover, error) {
|
|
return musicbrainz.Cover{Data: []byte{0xff, 0xd8, 0xff, 1}, MIME: "image/jpeg", Exact: true}, nil
|
|
}
|
|
|
|
func TestReleaseJobsAndAtomicApplication(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, "release-"+author.String())
|
|
require.NoError(t, err)
|
|
source, err := library.NewStore(pool).SaveSource(ctx, library.Source{ID: uuid.New(), Name: "release-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)
|
|
oldCover := "covers/original.jpg"
|
|
sum1, sum2 := sha256.Sum256([]byte(uuid.NewString())), sha256.Sum256([]byte(uuid.NewString()))
|
|
batch, reserved, err := store.CreateBatch(ctx, author, nil, []NewFile{{ClientID: "1", Name: "one.flac", Size: 10, SHA256: sum1[:]}, {ClientID: "2", Name: "two.flac", Size: 10, SHA256: sum2[:]}}, DefaultSettings, &oldCover)
|
|
require.NoError(t, err)
|
|
require.Len(t, reserved, 2)
|
|
// Isolate from batches produced by other integration tests.
|
|
_, err = pool.Exec(ctx, `UPDATE upload_release_jobs SET state='not_found' WHERE batch_id<>$1`, batch)
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
_, _ = pool.Exec(ctx, `DELETE FROM upload_batches WHERE id=$1`, batch)
|
|
_ = store.ReconcileQuota(ctx)
|
|
}()
|
|
provider := &testReleaseCatalogue{release: musicbrainz.Release{ID: uuid.NewString(), Artist: "Artist", Title: "Album", Type: "Single", Date: "2024-01-01", Tracks: []musicbrainz.Track{
|
|
{Title: "Song 1", Artist: "Artist", Disc: 1, Position: 1, DurationMs: 180000},
|
|
{Title: "Song 2", Artist: "Artist", Disc: 1, Position: 2, DurationMs: 190000},
|
|
}}}
|
|
svc := NewService(store, &previewCatalog{source: source}, previewRoot(t.TempDir()), nil, nil, nil, library.FPCalc{}, Config{StagingDir: t.TempDir()}, slog.New(slog.DiscardHandler))
|
|
svc.SetReleaseCatalogue(provider)
|
|
review := auth.WithPrincipal(ctx, auth.Principal{UserID: author, Role: auth.RoleAdmin, ReviewsUploads: true})
|
|
for i, r := range reserved {
|
|
require.NoError(t, store.ChangeStatus(ctx, r.FileID, []Status{StatusUploading}, StatusProcessing))
|
|
m := Meta{Artist: "Artist", AlbumArtist: "Artist", Album: "Album", Title: "SONG " + string(rune('1'+i))}
|
|
n := i + 1
|
|
m.Track = &n
|
|
raw, _ := json.Marshal(m)
|
|
duration := 180000 + i*10000
|
|
require.NoError(t, store.Transition(ctx, r.FileID, []Status{StatusProcessing}, StatusPendingReview, func(tx pgx.Tx) error {
|
|
_, e := tx.Exec(ctx, `UPDATE upload_files SET metadata=$2,duration_ms=$3 WHERE id=$1`, r.FileID, raw, duration)
|
|
return e
|
|
}))
|
|
if i == 0 {
|
|
_, err = store.ClaimRelease(ctx)
|
|
require.ErrorIs(t, err, ErrNotFound, "lookup waits until the whole batch is processed")
|
|
}
|
|
}
|
|
job, err := store.ClaimRelease(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, batch, job.ID)
|
|
files, err := store.AllInBatch(ctx, batch)
|
|
require.NoError(t, err)
|
|
edited := *files[0].Meta
|
|
year := 2025
|
|
edited.Year = &year
|
|
require.NoError(t, store.UpdateMetadata(ctx, files[0].ID, edited, true, placement{}))
|
|
require.NoError(t, store.SaveRelease(ctx, job, ReleaseReport{State: "ready", InputHash: "stale", Candidates: []ReleaseCandidate{}}))
|
|
report, err := store.ReleaseReport(ctx, batch)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "queued", report.State, "an old worker lease cannot overwrite an edit")
|
|
require.True(t, svc.lookupNextRelease(ctx))
|
|
report, err = svc.GetReleaseReport(review, batch)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "ready", report.State)
|
|
require.True(t, report.Candidates[0].Complete)
|
|
_, err = svc.SearchReleases(review, batch, musicbrainz.Query{Audio: true})
|
|
require.ErrorContains(t, err, "audio_disabled")
|
|
identifier := &testAudioIdentifier{}
|
|
svc.SetAudioIdentifier(identifier)
|
|
provider.release.Tracks[0].RecordingID, provider.release.Tracks[1].RecordingID = uuid.NewString(), uuid.NewString()
|
|
svc.encodedFingerprint = func(_ context.Context, filename string) (string, error) {
|
|
if strings.Contains(filename, reserved[0].FileID.String()) {
|
|
identifier.id = provider.release.Tracks[0].RecordingID
|
|
} else {
|
|
identifier.id = provider.release.Tracks[1].RecordingID
|
|
}
|
|
return "encoded", nil
|
|
}
|
|
_, err = svc.SearchReleases(review, batch, musicbrainz.Query{Audio: true})
|
|
require.NoError(t, err)
|
|
require.True(t, svc.lookupNextRelease(ctx))
|
|
report, err = svc.GetReleaseReport(review, batch)
|
|
require.NoError(t, err)
|
|
require.True(t, report.AudioAvailable)
|
|
require.Len(t, report.AudioResults, 2)
|
|
require.Equal(t, "acoustid", report.Candidates[0].Matches[0].Method)
|
|
require.True(t, report.Candidates[0].Complete)
|
|
before, err := store.AllInBatch(ctx, batch)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "SONG 1", before[0].Meta.Title)
|
|
// Looking up the release never changes tags or cover.
|
|
require.Equal(t, oldCover, *before[0].CoverPath)
|
|
used, _, err := store.Usage(ctx, author)
|
|
require.NoError(t, err)
|
|
applied, err := svc.ApplyRelease(review, batch, provider.release.ID, true, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, provider.release.ID, *applied.SelectedRelease)
|
|
persisted, err := store.ReleaseReport(ctx, batch)
|
|
require.NoError(t, err)
|
|
require.Len(t, persisted.AudioResults, 2, "metadata update trigger must not erase confirmed recognition")
|
|
after, err := store.AllInBatch(ctx, batch)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "Song 1", after[0].Meta.Title)
|
|
require.Equal(t, oldCover, *after[0].CoverPath)
|
|
require.Equal(t, StatusPendingReview, after[0].Status)
|
|
require.Equal(t, provider.release.ID, after[0].Tags["musicbrainz_albumid"])
|
|
require.NotNil(t, after[0].TargetRelPath)
|
|
require.Contains(t, *after[0].TargetRelPath, "01 - Song 1.flac")
|
|
newUsed, _, err := store.Usage(ctx, author)
|
|
require.NoError(t, err)
|
|
require.Equal(t, used, newUsed)
|
|
_, err = svc.ApplyRelease(review, batch, provider.release.ID, false, true)
|
|
require.NoError(t, err)
|
|
after, err = store.AllInBatch(ctx, batch)
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, oldCover, *after[0].CoverPath)
|
|
// A queued manual lookup makes the old candidate inapplicable immediately.
|
|
_, err = svc.SearchReleases(review, batch, musicbrainz.Query{})
|
|
require.NoError(t, err)
|
|
_, err = svc.ApplyRelease(review, batch, provider.release.ID, true, false)
|
|
require.ErrorContains(t, err, "release_stale")
|
|
provider.err = errors.New("provider offline")
|
|
require.True(t, svc.lookupNextRelease(ctx))
|
|
report, err = store.ReleaseReport(ctx, batch)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "error", report.State)
|
|
_, err = store.ClaimRelease(ctx)
|
|
require.ErrorIs(t, err, ErrNotFound, "failures back off instead of blocking upload processing")
|
|
_, err = svc.GetReleaseReport(ctx, batch)
|
|
require.Error(t, err, "review permission is mandatory")
|
|
key := "integration:" + uuid.NewString()
|
|
require.NoError(t, store.PutMetadataCache(ctx, key, []byte("cached"), time.Hour))
|
|
data, ok, err := store.GetMetadataCache(ctx, key)
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
require.Equal(t, "cached", string(data))
|
|
require.NoError(t, store.PutMetadataCache(ctx, key, data, -time.Second))
|
|
require.NoError(t, store.ExpireMetadataCache(ctx))
|
|
_, ok, err = store.GetMetadataCache(ctx, key)
|
|
require.NoError(t, err)
|
|
require.False(t, ok)
|
|
}
|