Files
pipu-music/services/api/internal/playback/playlists_integration_test.go
T
mrleo1nidandCursor fa79394869
release / release (версия и заметки) (push) Successful in 6s
release / android (APK в релиз) (push) Successful in 4m8s
feat: add personal playlists and shareable track links
EOF

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 23:37:53 +03:00

101 lines
4.3 KiB
Go

//go:build integration
package playback
import (
"context"
"os"
"sync"
"testing"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/stretchr/testify/require"
"pipu/api/internal/api/gen"
"pipu/api/internal/auth"
"pipu/api/internal/library"
"pipu/api/internal/platform/apperr"
"pipu/api/internal/platform/db"
)
func TestPlaylistPersistenceAndIsolation(t *testing.T) {
url := os.Getenv("TEST_DATABASE_URL")
if url == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := db.Connect(ctx, url)
require.NoError(t, err)
defer pool.Close()
require.NoError(t, db.Migrate(ctx, pool))
owner, other, artist, album, first, second, id := uuid.New(), uuid.New(), uuid.New(), uuid.New(), uuid.New(), uuid.New(), uuid.New()
_, err = pool.Exec(ctx, `INSERT INTO users(id,username,password_hash,role) VALUES($1,$2,'x','user'),($3,$4,'x','user')`, owner, owner.String(), other, other.String())
require.NoError(t, err)
defer pool.Exec(ctx, `DELETE FROM users WHERE id=ANY($1)`, []uuid.UUID{owner, other})
_, err = pool.Exec(ctx, `INSERT INTO artists(id,name,sort_name,name_norm) VALUES($1,'Artist','artist',$2)`, artist, artist.String())
require.NoError(t, err)
defer pool.Exec(ctx, `DELETE FROM artists WHERE id=$1`, artist)
_, err = pool.Exec(ctx, `INSERT INTO albums(id,title,title_norm,album_artist_id) VALUES($1,'Album','album',$2)`, album, artist)
require.NoError(t, err)
defer pool.Exec(ctx, `DELETE FROM albums WHERE id=$1`, album)
_, err = pool.Exec(ctx, `INSERT INTO tracks(id,title,title_norm,album_id,duration_ms) VALUES($1,'First','first',$3,1000),($2,'Second','second',$3,2000)`, first, second, album)
require.NoError(t, err)
defer pool.Exec(ctx, `DELETE FROM tracks WHERE id=ANY($1)`, []uuid.UUID{first, second})
st := NewStore(pool)
h := &Handler{store: st, catalog: library.NewStore(pool), dto: func() library.DTO { return library.DTO{} }}
require.NoError(t, st.savePlaylist(ctx, owner, id, " Mix ", "Description", []uuid.UUID{first, second}, 0, true))
got, err := h.playlist(ctx, owner, id)
require.NoError(t, err)
require.Equal(t, "Mix", got.Name)
require.Equal(t, int64(3000), got.DurationMs)
require.Equal(t, first, got.Tracks[0].Id)
_, err = h.playlist(ctx, other, id)
require.ErrorIs(t, err, apperr.NotFound("playlist.not_found", ""))
require.ErrorIs(t, st.savePlaylist(ctx, other, id, "stolen", "", nil, 1, false), apperr.NotFound("playlist.not_found", ""))
_, err = h.DeletePlaylist(auth.WithPrincipal(ctx, auth.Principal{UserID: other}), gen.DeletePlaylistRequestObject{Id: id})
require.ErrorIs(t, err, apperr.NotFound("playlist.not_found", ""))
require.NoError(t, st.savePlaylist(ctx, owner, id, "Mix", "", []uuid.UUID{second, first}, 1, false))
require.ErrorIs(t, st.savePlaylist(ctx, owner, id, "stale", "", nil, 1, false), apperr.Conflict("playlist.conflict", ""))
// A missing track rolls back both metadata and membership.
require.Error(t, st.savePlaylist(ctx, owner, id, "broken", "", []uuid.UUID{uuid.New()}, 2, false))
got, err = h.playlist(ctx, owner, id)
require.NoError(t, err)
require.Equal(t, 2, got.Revision)
require.Equal(t, "Mix", got.Name)
require.Equal(t, second, got.Tracks[0].Id)
// Exactly one concurrent write may consume a revision.
results := make(chan error, 2)
var wg sync.WaitGroup
for range 2 {
wg.Add(1)
go func() {
defer wg.Done()
results <- st.savePlaylist(ctx, owner, id, "Updated", "", []uuid.UUID{first}, 2, false)
}()
}
wg.Wait()
close(results)
successes := 0
for e := range results {
if e == nil {
successes++
} else {
require.ErrorIs(t, e, apperr.Conflict("playlist.conflict", ""))
}
}
require.Equal(t, 1, successes)
_, err = h.DeletePlaylist(auth.WithPrincipal(ctx, auth.Principal{UserID: owner}), gen.DeletePlaylistRequestObject{Id: id})
require.NoError(t, err)
var remaining int
require.NoError(t, pool.QueryRow(ctx, `SELECT count(*) FROM playlist_tracks WHERE playlist_id=$1`, id).Scan(&remaining))
require.Zero(t, remaining)
// Deleting a playlist never deletes catalog tracks.
require.NoError(t, pool.QueryRow(ctx, `SELECT count(*) FROM tracks WHERE id=ANY($1)`, []uuid.UUID{first, second}).Scan(&remaining))
require.Equal(t, 2, remaining)
rows, err := pool.Query(ctx, `SELECT id FROM playlists WHERE id=$1`, id)
require.NoError(t, err)
ids, err := pgx.CollectRows(rows, pgx.RowTo[uuid.UUID])
require.NoError(t, err)
require.Empty(t, ids)
}