Store each client's play queue in the database
The queue lived in memory and was shared by all clients, so a restart or a finished sync cleared it. Every client now has its own queue in the new queue and queue_song tables (added to the undeployed migration 000006). The music endpoints return the queue ID in the X-Queue-Id header and expect it back. A request without an ID, or with an ended one, starts a new queue. /music/reset ends the queue and returns a new ID, and a sync ends all running queues. Ended queues are kept as history. The deprecated legacy routes without the /api/v1 prefix are removed.
This commit is contained in:
1 parent
b5dc42b654
commit
a705d7b476
28 files changed
+1103
-320
No files matched your search
@@ -23,32 +23,80 @@ func TestMusicRandomAndQueue(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
e.addSong("Game A", "Song", "0123456789")
|
||||
|
||||
rec := e.do(http.MethodGet, "/music/rand")
|
||||
rec := e.do(http.MethodGet, "/api/v1/music/rand")
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Equal(t, "audio/mpeg", rec.Header().Get("Content-Type"))
|
||||
assert.Equal(t, "0123456789", rec.Body.String())
|
||||
|
||||
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/music/addQue").Code)
|
||||
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/music/addPlayed").Code)
|
||||
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/api/v1/music/addQue").Code)
|
||||
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/api/v1/music/addPlayed").Code)
|
||||
|
||||
var info backend.SongInfo
|
||||
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/music/info").Body.Bytes(), &info))
|
||||
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/music/info").Body.Bytes(), &info))
|
||||
assert.Equal(t, "Game A", info.Soundtrack)
|
||||
assert.Equal(t, "Song", info.Song)
|
||||
|
||||
for _, path := range []string{"/music/next", "/music/previous", "/music?song=0", "/api/v1/music?song=0"} {
|
||||
for _, path := range []string{"/api/v1/music/next", "/api/v1/music/previous", "/api/v1/music?song=0", "/api/v1/music?song=0"} {
|
||||
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, path).Code, path)
|
||||
}
|
||||
assert.Equal(t, http.StatusOK, e.do(http.MethodPut, "/music/played?song=0").Code)
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, e.do(http.MethodPut, "/music/played?song=x").Code)
|
||||
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/music/reset").Code)
|
||||
assert.Equal(t, http.StatusOK, e.do(http.MethodPut, "/api/v1/music/played?song=0").Code)
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, e.do(http.MethodPut, "/api/v1/music/played?song=x").Code)
|
||||
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/api/v1/music/reset").Code)
|
||||
}
|
||||
|
||||
func TestMusicQueueID(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
e.addSong("Game A", "Song", "x")
|
||||
|
||||
rec := e.do(http.MethodGet, "/api/v1/music/rand")
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
first := rec.Header().Get("X-Queue-Id")
|
||||
require.NotEmpty(t, first, "a request without a queue ID gets a new queue")
|
||||
require.Equal(t, http.StatusOK, e.do(http.MethodGet, "/api/v1/music/addQue").Code)
|
||||
rec = e.do(http.MethodGet, "/api/v1/music/list")
|
||||
assert.Equal(t, first, rec.Header().Get("X-Queue-Id"), "the sent queue is kept")
|
||||
assert.Contains(t, rec.Body.String(), `"Song":"Song"`)
|
||||
|
||||
// Another client has its own queue.
|
||||
other := e.do(http.MethodGet, "/api/v1/music/list", "X-Queue-Id", "")
|
||||
assert.NotEqual(t, first, other.Header().Get("X-Queue-Id"))
|
||||
assert.JSONEq(t, "[]", other.Body.String())
|
||||
e.queueID = first
|
||||
|
||||
rec = e.do(http.MethodGet, "/api/v1/music/reset")
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
second := rec.Header().Get("X-Queue-Id")
|
||||
assert.NotEqual(t, first, second, "reset starts a new queue")
|
||||
assert.JSONEq(t, "[]", e.do(http.MethodGet, "/api/v1/music/list").Body.String())
|
||||
|
||||
// The old ID has ended, so it gets a new queue too.
|
||||
rec = e.do(http.MethodGet, "/api/v1/music/list", "X-Queue-Id", first)
|
||||
assert.NotEqual(t, first, rec.Header().Get("X-Queue-Id"))
|
||||
|
||||
assert.Empty(t, e.do(http.MethodGet, "/api/v1/music/all", "X-Queue-Id", "").Header().Get("X-Queue-Id"),
|
||||
"endpoints that do not use the queue do not start one")
|
||||
}
|
||||
|
||||
func TestMusicQueueEndsAfterSync(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
e.addSong("Game A", "Song", "x")
|
||||
e.do(http.MethodGet, "/api/v1/music/rand")
|
||||
e.do(http.MethodGet, "/api/v1/music/addQue")
|
||||
before := e.queueID
|
||||
|
||||
require.Equal(t, http.StatusOK, e.do(http.MethodPost, "/api/v1/sync").Code)
|
||||
e.server.sync.Wait()
|
||||
|
||||
rec := e.do(http.MethodGet, "/api/v1/music/list")
|
||||
assert.NotEqual(t, before, rec.Header().Get("X-Queue-Id"))
|
||||
assert.JSONEq(t, "[]", rec.Body.String())
|
||||
}
|
||||
|
||||
func TestMusicRangeRequest(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
e.addSong("Game A", "Song", "0123456789")
|
||||
|
||||
rec := e.do(http.MethodGet, "/music/rand", "Range", "bytes=2-5")
|
||||
rec := e.do(http.MethodGet, "/api/v1/music/rand", "Range", "bytes=2-5")
|
||||
assert.Equal(t, http.StatusPartialContent, rec.Code)
|
||||
assert.Equal(t, "bytes 2-5/10", rec.Header().Get("Content-Range"))
|
||||
assert.Equal(t, "2345", rec.Body.String())
|
||||
@@ -57,21 +105,21 @@ func TestMusicRangeRequest(t *testing.T) {
|
||||
|
||||
func TestMusicEmptyStates(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
for _, path := range []string{"/music/next", "/music/previous", "/music?song=0", "/music/rand", "/music/rand/low", "/music/rand/classic"} {
|
||||
for _, path := range []string{"/api/v1/music/next", "/api/v1/music/previous", "/api/v1/music?song=0", "/api/v1/music/rand", "/api/v1/music/rand/low", "/api/v1/music/rand/classic"} {
|
||||
assert.Equal(t, http.StatusNotFound, e.do(http.MethodGet, path).Code, path)
|
||||
}
|
||||
// Invalid or missing parameters fail validation.
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, e.do(http.MethodGet, "/music?song=abc").Code)
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, e.do(http.MethodGet, "/music").Code)
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, e.do(http.MethodGet, "/api/v1/music?song=abc").Code)
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, e.do(http.MethodGet, "/api/v1/music").Code)
|
||||
|
||||
rec := e.do(http.MethodGet, "/music/list")
|
||||
rec := e.do(http.MethodGet, "/api/v1/music/list")
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.JSONEq(t, "[]", rec.Body.String())
|
||||
}
|
||||
|
||||
func TestMusicSoundCheck(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
rec := e.do(http.MethodGet, "/music/soundTest")
|
||||
rec := e.do(http.MethodGet, "/api/v1/music/soundTest")
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Equal(t, "sound check", rec.Body.String())
|
||||
}
|
||||
@@ -82,31 +130,31 @@ func TestMusicSoundtrackLists(t *testing.T) {
|
||||
e.addSong("A Game", "Song", "a")
|
||||
|
||||
var ordered []string
|
||||
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/music/all/order").Body.Bytes(), &ordered))
|
||||
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/music/all/order").Body.Bytes(), &ordered))
|
||||
assert.Equal(t, []string{"A Game", "B Game"}, ordered)
|
||||
|
||||
var random []string
|
||||
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/music/all").Body.Bytes(), &random))
|
||||
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/music/all").Body.Bytes(), &random))
|
||||
assert.ElementsMatch(t, ordered, random)
|
||||
}
|
||||
|
||||
func TestMusicDatabaseError(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
e.store.Err = errors.New("db down")
|
||||
assert.Equal(t, http.StatusInternalServerError, e.do(http.MethodGet, "/music/rand").Code)
|
||||
assert.Equal(t, http.StatusInternalServerError, e.do(http.MethodGet, "/music/all/order").Code)
|
||||
assert.Equal(t, http.StatusInternalServerError, e.do(http.MethodGet, "/api/v1/music/rand").Code)
|
||||
assert.Equal(t, http.StatusInternalServerError, e.do(http.MethodGet, "/api/v1/music/all/order").Code)
|
||||
}
|
||||
|
||||
func TestCharacterEndpoints(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
|
||||
var list []string
|
||||
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/characters").Body.Bytes(), &list))
|
||||
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/character/list").Body.Bytes(), &list))
|
||||
assert.Equal(t, []string{"Adol.png", "Aloy.jpg"}, list)
|
||||
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/character/list").Body.Bytes(), &list))
|
||||
assert.Equal(t, []string{"Adol.png", "Aloy.jpg"}, list)
|
||||
|
||||
rec := e.do(http.MethodGet, "/character?name=Adol.png")
|
||||
rec := e.do(http.MethodGet, "/api/v1/character?name=Adol.png")
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Equal(t, "image/png", rec.Header().Get("Content-Type"))
|
||||
|
||||
@@ -114,14 +162,14 @@ func TestCharacterEndpoints(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Equal(t, "image/jpeg", rec.Header().Get("Content-Type"))
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, e.do(http.MethodGet, "/character?name=Missing.png").Code)
|
||||
assert.Equal(t, http.StatusNotFound, e.do(http.MethodGet, "/api/v1/character?name=Missing.png").Code)
|
||||
}
|
||||
|
||||
func TestCharacterPathTraversal(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, e.do(http.MethodGet, "/character?name=").Code, "name is required")
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, e.do(http.MethodGet, "/api/v1/character?name=").Code, "name is required")
|
||||
for _, name := range []string{"../secret.png", "..%2Fsecret.png", "%2Fetc%2Fpasswd", "../../etc/passwd"} {
|
||||
rec := e.do(http.MethodGet, "/character?name="+name)
|
||||
rec := e.do(http.MethodGet, "/api/v1/character?name="+name)
|
||||
assert.Equal(t, http.StatusBadRequest, rec.Code, name)
|
||||
assert.NotContains(t, rec.Body.String(), "secret", name)
|
||||
}
|
||||
@@ -140,23 +188,23 @@ func TestDownloadEndpoints(t *testing.T) {
|
||||
url := newGitea(t, http.StatusOK)
|
||||
e := newTestEnv(t, func(c *config.Config) { c.ReleasesURL = url })
|
||||
|
||||
rec := e.do(http.MethodGet, "/download")
|
||||
rec := e.do(http.MethodGet, "/api/v1/download")
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.JSONEq(t, `"v2.0.0"`, rec.Body.String())
|
||||
|
||||
rec = e.do(http.MethodGet, "/api/v1/download/list")
|
||||
assert.JSONEq(t, `["Player.exe"]`, rec.Body.String())
|
||||
|
||||
rec = e.do(http.MethodGet, "/download/windows")
|
||||
rec = e.do(http.MethodGet, "/api/v1/download/windows")
|
||||
assert.Equal(t, http.StatusFound, rec.Code)
|
||||
assert.Equal(t, "https://example.com/Player.exe", rec.Header().Get("Location"))
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, e.do(http.MethodGet, "/download/linux").Code)
|
||||
assert.Equal(t, http.StatusNotFound, e.do(http.MethodGet, "/api/v1/download/linux").Code)
|
||||
}
|
||||
|
||||
func TestDownloadUpstreamDown(t *testing.T) {
|
||||
e := newTestEnv(t, func(c *config.Config) { c.ReleasesURL = newGitea(t, http.StatusServiceUnavailable) })
|
||||
for _, path := range []string{"/download", "/download/list", "/download/windows"} {
|
||||
for _, path := range []string{"/api/v1/download", "/api/v1/download/list", "/api/v1/download/windows"} {
|
||||
assert.Equal(t, http.StatusBadGateway, e.do(http.MethodGet, path).Code, path)
|
||||
}
|
||||
}
|
||||
@@ -191,7 +239,6 @@ func TestTokenAuth(t *testing.T) {
|
||||
token := e.createToken("secret")
|
||||
auth := []string{"Authorization", "Bearer " + token}
|
||||
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/api/v1/version", auth...).Code)
|
||||
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/version").Code, "legacy routes need no token")
|
||||
|
||||
assert.Equal(t, http.StatusOK, e.do(http.MethodDelete, "/api/v1/token", auth...).Code)
|
||||
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/version", auth...).Code)
|
||||
|
||||
Reference in new issue
Block a user