generated from VLADIMIR/template
up
This commit is contained in:
@@ -1544,6 +1544,7 @@ message UploadPlaylistTrackReq {
|
||||
string name = 2;
|
||||
string filename = 3;
|
||||
bytes data = 4;
|
||||
int32 groupID = 5;
|
||||
}
|
||||
|
||||
message UploadPlaylistTrackRsp {
|
||||
|
||||
@@ -4866,6 +4866,10 @@
|
||||
"data": {
|
||||
"type": "string",
|
||||
"format": "byte"
|
||||
},
|
||||
"groupID": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
proto "evening_detective_server/proto"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
func TestPlaylistOrderErrorCodes(t *testing.T) {
|
||||
@@ -48,3 +49,28 @@ func TestPlaylistOrderRequiresAuthentication(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadPlaylistTrackGroupCompatibility(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
body string
|
||||
want int32
|
||||
}{
|
||||
{"old request", `{"playlistID":1,"name":"Track","filename":"track.mp3","data":"AQ=="}`, 0},
|
||||
{"group request", `{"playlistID":1,"groupID":7,"name":"Track","filename":"track.mp3","data":"AQ=="}`, 7},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var req proto.UploadPlaylistTrackReq
|
||||
if err := protojson.Unmarshal([]byte(tc.body), &req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if req.GetGroupID() != tc.want {
|
||||
t.Fatalf("groupID: got %d, want %d", req.GetGroupID(), tc.want)
|
||||
}
|
||||
_, err := (&server{}).UploadPlaylistTrack(context.Background(), &req)
|
||||
if status.Code(err) != codes.PermissionDenied {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ func (s *server) UploadPlaylistTrack(ctx context.Context, req *proto.UploadPlayl
|
||||
if err := s.playlistService.UploadTrack(
|
||||
ctx,
|
||||
int(req.GetPlaylistID()),
|
||||
int(req.GetGroupID()),
|
||||
req.GetName(),
|
||||
req.GetFilename(),
|
||||
req.GetData(),
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestExportSnapshotAndEditableGame(t *testing.T) {
|
||||
requireNoError(t, err)
|
||||
selected, err := music.Create(ctx, "Selected by screen")
|
||||
requireNoError(t, err)
|
||||
requireNoError(t, music.AddTrack(ctx, selected.ID, "Grouped", "grouped.mp3"))
|
||||
requireNoError(t, music.AddTrack(ctx, selected.ID, 0, "Grouped", "grouped.mp3"))
|
||||
selected, err = music.SaveGroup(ctx, selected.ID, 0, "Final", "stop")
|
||||
requireNoError(t, err)
|
||||
selected, err = music.SetTrackGroup(ctx, selected.ID, selected.Tracks[0].ID, selected.Groups[0].ID)
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestGroupEditingProtectsLivePlayback(t *testing.T) {
|
||||
p, err := f.repo.Create(ctx, "Music")
|
||||
requireNoError(t, err)
|
||||
for _, name := range []string{"one", "two", "three"} {
|
||||
requireNoError(t, f.repo.AddTrack(ctx, p.ID, name, name+".mp3"))
|
||||
requireNoError(t, f.repo.AddTrack(ctx, p.ID, 0, name, name+".mp3"))
|
||||
}
|
||||
p, err = f.repo.SaveGroup(ctx, p.ID, 0, "First", "repeat")
|
||||
requireNoError(t, err)
|
||||
@@ -101,7 +101,7 @@ func TestGroupAssignmentsAndPlaybackRejectForeignIDs(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
p, err := f.repo.Create(ctx, "One")
|
||||
requireNoError(t, err)
|
||||
requireNoError(t, f.repo.AddTrack(ctx, p.ID, "Track", "track.mp3"))
|
||||
requireNoError(t, f.repo.AddTrack(ctx, p.ID, 0, "Track", "track.mp3"))
|
||||
p, err = f.repo.SaveGroup(ctx, p.ID, 0, "Empty", "repeat")
|
||||
requireNoError(t, err)
|
||||
other, err := f.repo.Create(ctx, "Other")
|
||||
@@ -132,7 +132,7 @@ func TestConcurrentCompletionAdvancesOnce(t *testing.T) {
|
||||
p, err := f.repo.Create(ctx, "Concurrent completion")
|
||||
requireNoError(t, err)
|
||||
for _, name := range []string{"one", "two"} {
|
||||
requireNoError(t, f.repo.AddTrack(ctx, p.ID, name, name+".mp3"))
|
||||
requireNoError(t, f.repo.AddTrack(ctx, p.ID, 0, name, name+".mp3"))
|
||||
}
|
||||
before, err := f.repo.SetMusic(ctx, f.gameID, p.ID, 0, 0, 50, false, playback, 0)
|
||||
requireNoError(t, err)
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestReorderPreservesPlaybackAndRejectsChangedMembership(t *testing.T) {
|
||||
p, err := f.repo.Create(ctx, "Music")
|
||||
requireNoError(t, err)
|
||||
for _, name := range []string{"One", "Two", "Three"} {
|
||||
requireNoError(t, f.repo.AddTrack(ctx, p.ID, name, name+".mp3"))
|
||||
requireNoError(t, f.repo.AddTrack(ctx, p.ID, 0, name, name+".mp3"))
|
||||
}
|
||||
p, err = f.repo.Get(ctx, p.ID)
|
||||
requireNoError(t, err)
|
||||
@@ -90,7 +90,7 @@ func TestConcurrentAdditionsKeepDistinctPositions(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
failures := make(chan error, count)
|
||||
for i := 0; i < count; i++ {
|
||||
wg.Go(func() { failures <- f.repo.AddTrack(ctx, p.ID, "Track", fmt.Sprintf("track-%d.mp3", i)) })
|
||||
wg.Go(func() { failures <- f.repo.AddTrack(ctx, p.ID, 0, "Track", fmt.Sprintf("track-%d.mp3", i)) })
|
||||
}
|
||||
wg.Wait()
|
||||
close(failures)
|
||||
@@ -172,7 +172,7 @@ func TestReorderReadersSeeConsistentTrackAndIndex(t *testing.T) {
|
||||
p, err := f.repo.Create(ctx, "Snapshot")
|
||||
requireNoError(t, err)
|
||||
for _, name := range []string{"a", "b", "c"} {
|
||||
requireNoError(t, f.repo.AddTrack(ctx, p.ID, name, name+".mp3"))
|
||||
requireNoError(t, f.repo.AddTrack(ctx, p.ID, 0, name, name+".mp3"))
|
||||
}
|
||||
p, err = f.repo.Get(ctx, p.ID)
|
||||
requireNoError(t, err)
|
||||
|
||||
@@ -242,7 +242,7 @@ func (r *Repo) Delete(ctx context.Context, id int) error {
|
||||
}
|
||||
|
||||
// AddTrack adds a track to a playlist.
|
||||
func (r *Repo) AddTrack(ctx context.Context, playlistID int, name, filename string) error {
|
||||
func (r *Repo) AddTrack(ctx context.Context, playlistID, groupID int, name, filename string) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -251,6 +251,14 @@ func (r *Repo) AddTrack(ctx context.Context, playlistID int, name, filename stri
|
||||
if err := lockPlaylist(ctx, tx, playlistID); err != nil {
|
||||
return err
|
||||
}
|
||||
if groupID != 0 {
|
||||
if _, err := readGroup(ctx, tx, playlistID, groupID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := groupInUse(ctx, tx, playlistID, groupID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var position int
|
||||
if err := tx.QueryRow(
|
||||
ctx,
|
||||
@@ -266,13 +274,14 @@ func (r *Repo) AddTrack(ctx context.Context, playlistID int, name, filename stri
|
||||
_, err = tx.Exec(
|
||||
ctx,
|
||||
`
|
||||
INSERT INTO playlist_tracks (playlist_id, name, filename, position)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
INSERT INTO playlist_tracks (playlist_id, name, filename, position, group_id)
|
||||
VALUES ($1, $2, $3, $4, NULLIF($5, 0))
|
||||
`,
|
||||
playlistID,
|
||||
name,
|
||||
filename,
|
||||
position,
|
||||
groupID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package playlists_repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAddTrackSavesTargetGroupAtomically(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
target string
|
||||
playback int32
|
||||
activeGroup bool
|
||||
want error
|
||||
}{
|
||||
{name: "ungrouped", target: "none"},
|
||||
{name: "empty group", target: "second"},
|
||||
{name: "populated group", target: "first"},
|
||||
{name: "missing group", target: "missing", want: ErrGroupNotFound},
|
||||
{name: "foreign group", target: "foreign", want: ErrGroupNotFound},
|
||||
{name: "deleted group", target: "deleted", want: ErrGroupNotFound},
|
||||
{name: "playing group", target: "first", playback: 2, activeGroup: true, want: ErrPlaylistInUse},
|
||||
{name: "paused group", target: "first", playback: 3, activeGroup: true, want: ErrPlaylistInUse},
|
||||
{name: "another group playing", target: "second", playback: 2, activeGroup: true},
|
||||
{name: "whole playlist playing", target: "second", playback: 2, want: ErrPlaylistInUse},
|
||||
{name: "legacy ungrouped upload during playback", target: "none", playback: 2},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f := newPlaylistFixture(t)
|
||||
ctx := context.Background()
|
||||
p, err := f.repo.Create(ctx, "Upload")
|
||||
requireNoError(t, err)
|
||||
p, err = f.repo.SaveGroup(ctx, p.ID, 0, "First", "repeat")
|
||||
requireNoError(t, err)
|
||||
first := p.Groups[0].ID
|
||||
p, err = f.repo.SaveGroup(ctx, p.ID, 0, "Second", "stop")
|
||||
requireNoError(t, err)
|
||||
second := p.Groups[1].ID
|
||||
requireNoError(t, f.repo.AddTrack(ctx, p.ID, first, "Existing", "existing.mp3"))
|
||||
groupID := first
|
||||
switch tc.target {
|
||||
case "none":
|
||||
groupID = 0
|
||||
case "second", "deleted":
|
||||
groupID = second
|
||||
case "missing":
|
||||
groupID = second + 1000000
|
||||
case "foreign":
|
||||
other, err := f.repo.Create(ctx, "Other")
|
||||
requireNoError(t, err)
|
||||
other, err = f.repo.SaveGroup(ctx, other.ID, 0, "Foreign", "repeat")
|
||||
requireNoError(t, err)
|
||||
groupID = other.Groups[0].ID
|
||||
}
|
||||
if tc.target == "deleted" {
|
||||
_, err := f.repo.DeleteGroup(ctx, p.ID, groupID)
|
||||
requireNoError(t, err)
|
||||
}
|
||||
if tc.playback != 0 {
|
||||
activeGroup := 0
|
||||
if tc.activeGroup {
|
||||
activeGroup = first
|
||||
}
|
||||
_, err := f.repo.SetMusic(ctx, f.gameID, p.ID, 0, 0, 50, false, tc.playback, activeGroup)
|
||||
requireNoError(t, err)
|
||||
}
|
||||
before, err := f.repo.Get(ctx, p.ID)
|
||||
requireNoError(t, err)
|
||||
err = f.repo.AddTrack(ctx, p.ID, groupID, "New", "new.mp3")
|
||||
if !errors.Is(err, tc.want) {
|
||||
t.Fatalf("expected %v, got %v", tc.want, err)
|
||||
}
|
||||
after, err := f.repo.Get(ctx, p.ID)
|
||||
requireNoError(t, err)
|
||||
if tc.want != nil {
|
||||
if !reflect.DeepEqual(before.Tracks, after.Tracks) {
|
||||
t.Fatal("rejected upload changed playlist tracks")
|
||||
}
|
||||
return
|
||||
}
|
||||
if len(after.Tracks) != len(before.Tracks)+1 {
|
||||
t.Fatal("upload did not add exactly one track")
|
||||
}
|
||||
track := after.Tracks[len(after.Tracks)-1]
|
||||
if track.GroupID != groupID || track.Name != "New" || track.Position != before.Tracks[len(before.Tracks)-1].Position+1 {
|
||||
t.Fatalf("track was not appended to the target group: %+v", track)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -226,7 +226,7 @@ func TestImportPreservesGameAndIsIdempotent(t *testing.T) {
|
||||
for _, name := range []string{"Local first", "Local second"} {
|
||||
playlist, err := playlistRepo.Create(ctx, name)
|
||||
check(t, err)
|
||||
check(t, playlistRepo.AddTrack(ctx, playlist.ID, name, name+".mp3"))
|
||||
check(t, playlistRepo.AddTrack(ctx, playlist.ID, 0, name, name+".mp3"))
|
||||
}
|
||||
source := snapshot()
|
||||
source.Data.Game.Format = tc.format
|
||||
|
||||
@@ -54,6 +54,9 @@ func TestGroupValidationRejectsInvalidRequestsBeforeRepository(t *testing.T) {
|
||||
}, ErrInvalidTrack},
|
||||
{"invalid group", func(s *Service) error { _, err := s.DeleteGroup(context.Background(), 1, 0); return err }, ErrInvalidTrack},
|
||||
{"duplicate order", func(s *Service) error { _, err := s.ReorderGroups(context.Background(), 1, []int{1, 1}); return err }, ErrInvalidTrack},
|
||||
{"negative upload group", func(s *Service) error {
|
||||
return s.UploadTrack(context.Background(), 1, -1, "Track", "track.mp3", []byte("data"))
|
||||
}, ErrInvalidTrack},
|
||||
{"invalid assignment", func(s *Service) error { _, err := s.SetTrackGroup(context.Background(), 1, 2, -1); return err }, ErrInvalidTrack},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -88,8 +88,8 @@ func (s *Service) Delete(ctx context.Context, id int) error {
|
||||
}
|
||||
|
||||
// UploadTrack stores an MP3 file and adds it to a playlist.
|
||||
func (s *Service) UploadTrack(ctx context.Context, playlistID int, name, filename string, data []byte) error {
|
||||
if playlistID <= 0 || strings.TrimSpace(name) == "" || len(data) == 0 || !strings.HasSuffix(strings.ToLower(filename), ".mp3") {
|
||||
func (s *Service) UploadTrack(ctx context.Context, playlistID, groupID int, name, filename string, data []byte) error {
|
||||
if playlistID <= 0 || groupID < 0 || strings.TrimSpace(name) == "" || len(data) == 0 || !strings.HasSuffix(strings.ToLower(filename), ".mp3") {
|
||||
return ErrInvalidTrack
|
||||
}
|
||||
if _, err := s.repo.Get(ctx, playlistID); err != nil {
|
||||
@@ -99,7 +99,7 @@ func (s *Service) UploadTrack(ctx context.Context, playlistID int, name, filenam
|
||||
if err != nil || fileType != "audio" || !strings.HasSuffix(strings.ToLower(stored), ".mp3") {
|
||||
return ErrInvalidTrack
|
||||
}
|
||||
if err := s.repo.AddTrack(ctx, playlistID, strings.TrimSpace(name), stored); err != nil {
|
||||
if err := s.repo.AddTrack(ctx, playlistID, groupID, strings.TrimSpace(name), stored); err != nil {
|
||||
_ = s.files.DeleteFile(ctx, stored)
|
||||
return mapError(err)
|
||||
}
|
||||
|
||||
+11
-2
@@ -3046,6 +3046,7 @@ type UploadPlaylistTrackReq struct {
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Filename string `protobuf:"bytes,3,opt,name=filename,proto3" json:"filename,omitempty"`
|
||||
Data []byte `protobuf:"bytes,4,opt,name=data,proto3" json:"data,omitempty"`
|
||||
GroupID int32 `protobuf:"varint,5,opt,name=groupID,proto3" json:"groupID,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -3108,6 +3109,13 @@ func (x *UploadPlaylistTrackReq) GetData() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *UploadPlaylistTrackReq) GetGroupID() int32 {
|
||||
if x != nil {
|
||||
return x.GroupID
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type UploadPlaylistTrackRsp struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Playlist *Playlist `protobuf:"bytes,1,opt,name=playlist,proto3" json:"playlist,omitempty"`
|
||||
@@ -14077,14 +14085,15 @@ const file_main_proto_rawDesc = "" +
|
||||
"\bplaylist\x18\x01 \x01(\v2(.crabs.evening_detective_server.PlaylistR\bplaylist\"#\n" +
|
||||
"\x11DeletePlaylistReq\x12\x0e\n" +
|
||||
"\x02ID\x18\x01 \x01(\x05R\x02ID\"\x13\n" +
|
||||
"\x11DeletePlaylistRsp\"|\n" +
|
||||
"\x11DeletePlaylistRsp\"\x96\x01\n" +
|
||||
"\x16UploadPlaylistTrackReq\x12\x1e\n" +
|
||||
"\n" +
|
||||
"playlistID\x18\x01 \x01(\x05R\n" +
|
||||
"playlistID\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x1a\n" +
|
||||
"\bfilename\x18\x03 \x01(\tR\bfilename\x12\x12\n" +
|
||||
"\x04data\x18\x04 \x01(\fR\x04data\"^\n" +
|
||||
"\x04data\x18\x04 \x01(\fR\x04data\x12\x18\n" +
|
||||
"\agroupID\x18\x05 \x01(\x05R\agroupID\"^\n" +
|
||||
"\x16UploadPlaylistTrackRsp\x12D\n" +
|
||||
"\bplaylist\x18\x01 \x01(\v2(.crabs.evening_detective_server.PlaylistR\bplaylist\"V\n" +
|
||||
"\x18ReorderPlaylistTracksReq\x12\x1e\n" +
|
||||
|
||||
Reference in New Issue
Block a user