@@ -70,27 +70,49 @@ func TestAddPlaylistItemsAcceptsExact100URIRequestBody(t *testing.T) {
7070 }
7171}
7272
73- func TestAddPlaylistItemsOmitsPositionAndDoesNotRetry (t * testing.T ) {
74- for _ , status := range []int {http .StatusTooManyRequests , http .StatusServiceUnavailable } {
75- t .Run (http .StatusText (status ), func (t * testing.T ) {
76- calls := 0
77- httpClient := & http.Client {Transport : roundTripFunc (func (request * http.Request ) (* http.Response , error ) {
78- calls ++
79- body , err := io .ReadAll (request .Body )
80- if err != nil {
81- t .Fatal (err )
73+ func TestPlaylistMutationHTTPStatusesAreTypedAndNeverRetried (t * testing.T ) {
74+ for _ , method := range []string {http .MethodPost , http .MethodDelete } {
75+ for _ , test := range []struct {
76+ status int
77+ uncertain bool
78+ }{
79+ {status : http .StatusBadRequest },
80+ {status : http .StatusTooManyRequests },
81+ {status : http .StatusInternalServerError , uncertain : true },
82+ {status : http .StatusBadGateway , uncertain : true },
83+ {status : http .StatusServiceUnavailable , uncertain : true },
84+ {status : http .StatusGatewayTimeout , uncertain : true },
85+ } {
86+ t .Run (method + " " + http .StatusText (test .status ), func (t * testing.T ) {
87+ calls := 0
88+ httpClient := & http.Client {Transport : roundTripFunc (func (request * http.Request ) (* http.Response , error ) {
89+ calls ++
90+ body , err := io .ReadAll (request .Body )
91+ if err != nil {
92+ t .Fatal (err )
93+ }
94+ if method == http .MethodPost && (strings .Contains (string (body ), "position" ) || strings .Contains (string (body ), "tracks" )) {
95+ t .Fatalf ("body=%s" , body )
96+ }
97+ return response (test .status , "" ), nil
98+ })}
99+ spotify := Client {HTTPClient : httpClient }
100+ var err error
101+ if method == http .MethodPost {
102+ _ , err = spotify .AddPlaylistItems (context .Background (), mutationPlaylistID , []string {"spotify:track:" + mutationTrackID }, nil )
103+ } else {
104+ _ , err = spotify .RemovePlaylistItemAtPosition (context .Background (), mutationPlaylistID , "spotify:track:" + mutationTrackID , 2 , "before" )
82105 }
83- if strings .Contains (string (body ), "position" ) || strings .Contains (string (body ), "tracks" ) {
84- t .Fatalf ("body=%s" , body )
106+ if test .uncertain {
107+ assertMutationUncertain (t , err , method , ErrUpstream )
108+ } else {
109+ assertMutationRejected (t , err , method , test .status , ErrUpstream )
85110 }
86- return response (status , "" ), nil
87- })}
88- _ , err := (Client {HTTPClient : httpClient }).AddPlaylistItems (context .Background (), mutationPlaylistID , []string {"spotify:track:" + mutationTrackID }, nil )
89- var uncertain * MutationOutcomeUncertainError
90- if ! errors .Is (err , ErrUpstream ) || errors .As (err , & uncertain ) || calls != 1 {
91- t .Fatalf ("calls=%d error=%v" , calls , err )
92- }
93- })
111+ if calls != 1 {
112+ t .Fatalf ("calls=%d error=%v" , calls , err )
113+ }
114+ })
115+ }
94116 }
95117}
96118
@@ -120,18 +142,29 @@ func TestRemovePlaylistItemsByURIUsesCurrentItemsShapeAndSnapshot(t *testing.T)
120142 }
121143}
122144
123- func TestRemovePlaylistItemsByURIDoesNotRetry (t * testing.T ) {
124- calls := 0
125- httpClient := & http.Client {Transport : roundTripFunc (func (* http.Request ) (* http.Response , error ) {
145+ func TestRemovePlaylistItemAtPositionUsesSpecificOccurrenceAndSnapshot (t * testing.T ) {
146+ var calls int
147+ httpClient := & http.Client {Transport : roundTripFunc (func (request * http.Request ) (* http.Response , error ) {
126148 calls ++
127- return response (http .StatusServiceUnavailable , "" ), nil
149+ if request .Method != http .MethodDelete || request .URL .Path != "/v1/playlists/" + mutationPlaylistID + "/items" ||
150+ request .Header .Get ("Content-Type" ) != "application/json" {
151+ t .Fatalf ("request=%s %s" , request .Method , request .URL .RequestURI ())
152+ }
153+ var body map [string ]json.RawMessage
154+ if err := json .NewDecoder (request .Body ).Decode (& body ); err != nil {
155+ t .Fatal (err )
156+ }
157+ if len (body ) != 2 || string (body ["snapshot_id" ]) != `"after-add"` ||
158+ string (body ["items" ]) != `[{"uri":"spotify:track:` + mutationTrackID + `","positions":[2]}]` {
159+ t .Fatalf ("body=%v" , body )
160+ }
161+ return response (http .StatusOK , `{"snapshot_id":"after-remove"}` ), nil
128162 })}
129- _ , err := (Client {HTTPClient : httpClient }).RemovePlaylistItemsByURI (
130- context .Background (), mutationPlaylistID , "spotify:track:" + mutationTrackID , "before " ,
163+ snapshot , err := (Client {HTTPClient : httpClient , BaseURL : "https://api.spotify.invalid/v1" }).RemovePlaylistItemAtPosition (
164+ context .Background (), mutationPlaylistID , "spotify:track:" + mutationTrackID , 2 , "after-add " ,
131165 )
132- var uncertain * MutationOutcomeUncertainError
133- if ! errors .Is (err , ErrUpstream ) || errors .As (err , & uncertain ) || calls != 1 {
134- t .Fatalf ("calls=%d error=%v" , calls , err )
166+ if err != nil || snapshot != "after-remove" || calls != 1 {
167+ t .Fatalf ("snapshot=%q calls=%d error=%v" , snapshot , calls , err )
135168 }
136169}
137170
@@ -150,6 +183,10 @@ func TestPlaylistMutationsDoNotRetryTransportErrors(t *testing.T) {
150183 _ , err := spotify .RemovePlaylistItemsByURI (context .Background (), mutationPlaylistID , "spotify:track:" + mutationTrackID , "before" )
151184 return err
152185 }},
186+ {name : "remove position" , method : http .MethodDelete , call : func (spotify Client ) error {
187+ _ , err := spotify .RemovePlaylistItemAtPosition (context .Background (), mutationPlaylistID , "spotify:track:" + mutationTrackID , 2 , "before" )
188+ return err
189+ }},
153190 } {
154191 t .Run (test .name , func (t * testing.T ) {
155192 calls := 0
@@ -259,6 +296,10 @@ func TestPlaylistMutationsRejectInvalidInputsAndMarkMissingSnapshotsUncertain(t
259296 _ , err := spotify .RemovePlaylistItemsByURI (context .Background (), mutationPlaylistID , validURI , " " )
260297 return err
261298 },
299+ func () error {
300+ _ , err := spotify .RemovePlaylistItemAtPosition (context .Background (), mutationPlaylistID , validURI , - 1 , "snapshot" )
301+ return err
302+ },
262303 } {
263304 err := call ()
264305 var uncertain * MutationOutcomeUncertainError
@@ -267,10 +308,7 @@ func TestPlaylistMutationsRejectInvalidInputsAndMarkMissingSnapshotsUncertain(t
267308 }
268309 }
269310 _ , err := (Client {BaseURL : "://" }).AddPlaylistItems (context .Background (), mutationPlaylistID , []string {validURI }, nil )
270- var uncertain * MutationOutcomeUncertainError
271- if ! errors .Is (err , ErrUpstream ) || errors .As (err , & uncertain ) {
272- t .Fatalf ("request construction error=%v" , err )
273- }
311+ assertMutationRejected (t , err , http .MethodPost , 0 , ErrUpstream )
274312 _ , err = spotify .AddPlaylistItems (context .Background (), mutationPlaylistID , []string {validURI }, nil )
275313 assertMutationUncertain (t , err , http .MethodPost , ErrInvalidResponse )
276314 if calls != 1 {
@@ -293,6 +331,17 @@ func assertMutationUncertain(t *testing.T, err error, method string, cause error
293331 }
294332}
295333
334+ func assertMutationRejected (t * testing.T , err error , method string , statusCode int , cause error ) {
335+ t .Helper ()
336+ var rejected * MutationRejectedError
337+ var uncertain * MutationOutcomeUncertainError
338+ if ! errors .As (err , & rejected ) || errors .As (err , & uncertain ) || rejected .Method != method ||
339+ rejected .Path != "/playlists/" + mutationPlaylistID + "/items" || rejected .StatusCode != statusCode ||
340+ ! errors .Is (err , cause ) || err .Error () != cause .Error () {
341+ t .Fatalf ("rejected=%+v error=%v" , rejected , err )
342+ }
343+ }
344+
296345type truncatedMutationBody struct { sent bool }
297346
298347func (body * truncatedMutationBody ) Read (value []byte ) (int , error ) {
0 commit comments