Alternative names (#32): - PUT /api/v1/soundtracks/{uuid}/alternative-names replaces a soundtrack's alternative names (trimmed, deduplicated, without the soundtrack's own name). - alternative_names is in the soundtrack and statistics responses. - /find matches them too and shows which name matched. Covers (#12), stored in COVERS_PATH and off when it is not set: - GET, PUT (upload) and DELETE /api/v1/soundtracks/{uuid}/cover, and PUT .../cover/source to pick a SteamGridDB game or AniList anime by ID. Uploaded and picked covers are never replaced automatically. - With STEAMGRIDDB_API_KEY, a background job searches SteamGridDB, then AniList, by name, alternative names and the name without bracketed parts, at startup, after a sync and after names change. Search results are ranked by matching words, so "Civilization IV" gets "Sid Meier's Civilization IV". - cover_modified (Unix seconds) is in the soundtrack and statistics responses, so clients can cache covers. The new columns are added to migration 000006, which is not released yet. Closes #12, closes #32 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
395 lines
16 KiB
Go
395 lines
16 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"music-server/internal/backend"
|
|
"music-server/internal/config"
|
|
"music-server/internal/db/repository"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestMusicRandomAndQueue(t *testing.T) {
|
|
e := newTestEnv(t)
|
|
e.addSong("Game A", "Song", "0123456789")
|
|
|
|
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, "/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, "/api/v1/music/info").Body.Bytes(), &info))
|
|
assert.Equal(t, "Game A", info.Soundtrack)
|
|
assert.Equal(t, "Song", info.Song)
|
|
|
|
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, "/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 TestMusicSongsAreNotCached(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")
|
|
for _, path := range []string{"/api/v1/music/rand", "/api/v1/music/rand/low", "/api/v1/music/rand/classic",
|
|
"/api/v1/music/next", "/api/v1/music/previous", "/api/v1/music?song=0", "/api/v1/music/soundTest"} {
|
|
rec := e.do(http.MethodGet, path)
|
|
require.Equal(t, http.StatusOK, rec.Code, path)
|
|
assert.Equal(t, "no-store", rec.Header().Get("Cache-Control"), path)
|
|
}
|
|
}
|
|
|
|
func TestMusicRangeRequest(t *testing.T) {
|
|
e := newTestEnv(t)
|
|
e.addSong("Game A", "Song", "0123456789")
|
|
|
|
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())
|
|
assert.Equal(t, "audio/mpeg", rec.Header().Get("Content-Type"))
|
|
}
|
|
|
|
func TestMusicEmptyStates(t *testing.T) {
|
|
e := newTestEnv(t)
|
|
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, "/api/v1/music?song=abc").Code)
|
|
assert.Equal(t, http.StatusUnprocessableEntity, e.do(http.MethodGet, "/api/v1/music").Code)
|
|
|
|
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, "/api/v1/music/soundTest")
|
|
assert.Equal(t, http.StatusOK, rec.Code)
|
|
assert.Equal(t, "sound check", rec.Body.String())
|
|
}
|
|
|
|
func TestMusicSoundtrackLists(t *testing.T) {
|
|
e := newTestEnv(t)
|
|
e.addSong("B Game", "Song", "b")
|
|
e.addSong("A Game", "Song", "a")
|
|
|
|
var ordered []string
|
|
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, "/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, "/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, "/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)
|
|
|
|
var details []struct {
|
|
Name string `json:"name"`
|
|
Modified int64 `json:"modified"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/character/details").Body.Bytes(), &details))
|
|
require.Len(t, details, 2)
|
|
assert.Equal(t, "Adol.png", details[0].Name)
|
|
assert.Equal(t, "Aloy.jpg", details[1].Name)
|
|
assert.Positive(t, details[0].Modified)
|
|
|
|
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"))
|
|
|
|
rec = e.do(http.MethodGet, "/api/v1/character?name=Aloy.jpg")
|
|
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, "/api/v1/character?name=Missing.png").Code)
|
|
}
|
|
|
|
func TestCharacterPathTraversal(t *testing.T) {
|
|
e := newTestEnv(t)
|
|
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, "/api/v1/character?name="+name)
|
|
assert.Equal(t, http.StatusBadRequest, rec.Code, name)
|
|
assert.NotContains(t, rec.Body.String(), "secret", name)
|
|
}
|
|
}
|
|
|
|
func newGitea(t *testing.T, status int) string {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(status)
|
|
w.Write([]byte(`{"id":1,"name":"v2.0.0","assets":[{"name":"Player.exe","browser_download_url":"https://example.com/Player.exe"}]}`))
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
return server.URL
|
|
}
|
|
|
|
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, "/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, "/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, "/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{"/api/v1/download", "/api/v1/download/list", "/api/v1/download/windows"} {
|
|
assert.Equal(t, http.StatusBadGateway, e.do(http.MethodGet, path).Code, path)
|
|
}
|
|
}
|
|
|
|
func withAuth(password string) func(*config.Config) {
|
|
return func(c *config.Config) {
|
|
c.DisableSessionAuth = false
|
|
c.APIPassword = password
|
|
}
|
|
}
|
|
|
|
func (e *testEnv) createToken(password string) string {
|
|
e.t.Helper()
|
|
rec := e.doBody(http.MethodPost, "/api/v1/token", `{"password":"`+password+`"}`)
|
|
require.Equal(e.t, http.StatusOK, rec.Code, rec.Body.String())
|
|
var resp TokenResponse
|
|
require.NoError(e.t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
assert.Equal(e.t, "web", resp.ClientType)
|
|
assert.WithinDuration(e.t, time.Now().Add(tokenLifetime), resp.ExpiresAt, time.Minute)
|
|
return resp.Token
|
|
}
|
|
|
|
func TestTokenAuth(t *testing.T) {
|
|
e := newTestEnv(t, withAuth("secret"))
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/version").Code)
|
|
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/version", "Authorization", "Basic abc").Code)
|
|
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/version", "Authorization", "Bearer unknown").Code)
|
|
assert.Equal(t, http.StatusUnauthorized, e.doBody(http.MethodPost, "/api/v1/token", `{"password":"wrong"}`).Code)
|
|
assert.Equal(t, http.StatusBadRequest, e.doBody(http.MethodPost, "/api/v1/token", `{not json`).Code)
|
|
|
|
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.MethodDelete, "/api/v1/token", auth...).Code)
|
|
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/version", auth...).Code)
|
|
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodDelete, "/api/v1/token").Code)
|
|
}
|
|
|
|
func TestTokenOpenAccessWithoutPassword(t *testing.T) {
|
|
e := newTestEnv(t, withAuth(""))
|
|
token := e.createToken("anything")
|
|
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/api/v1/version", "Authorization", "Bearer "+token).Code)
|
|
}
|
|
|
|
func TestTokenCleanupRequiresAuth(t *testing.T) {
|
|
e := newTestEnv(t, withAuth("secret"))
|
|
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodPost, "/api/v1/token/cleanup").Code)
|
|
|
|
token := e.createToken("secret")
|
|
e.store.Sessions["old"] = repository.Session{Token: "old", ExpiresAt: pgtype.Timestamptz{Time: time.Now().Add(-time.Hour), Valid: true}}
|
|
assert.Equal(t, http.StatusOK, e.do(http.MethodPost, "/api/v1/token/cleanup", "Authorization", "Bearer "+token).Code)
|
|
assert.False(t, e.store.HasSession("old"))
|
|
assert.True(t, e.store.HasSession(token))
|
|
}
|
|
|
|
func TestExpiredTokenIsDeleted(t *testing.T) {
|
|
e := newTestEnv(t, withAuth("secret"))
|
|
_, err := e.store.CreateSession(context.Background(), repository.CreateSessionParams{
|
|
Token: "expired",
|
|
ExpiresAt: pgtype.Timestamptz{Time: time.Now().Add(-time.Minute), Valid: true},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
rec := e.do(http.MethodGet, "/api/v1/version", "Authorization", "Bearer expired")
|
|
assert.Equal(t, http.StatusUnauthorized, rec.Code)
|
|
assert.Contains(t, rec.Body.String(), "Token expired")
|
|
// The fake refuses to delete with a cancelled context, like a real database would.
|
|
assert.Eventually(t, func() bool { return !e.store.HasSession("expired") }, time.Second, 10*time.Millisecond)
|
|
}
|
|
|
|
func TestTokenRateLimit(t *testing.T) {
|
|
e := newTestEnv(t)
|
|
for range 10 {
|
|
assert.Equal(t, http.StatusOK, e.doBody(http.MethodPost, "/api/v1/token", `{}`).Code)
|
|
}
|
|
rec := e.doBody(http.MethodPost, "/api/v1/token", `{}`)
|
|
assert.Equal(t, http.StatusTooManyRequests, rec.Code)
|
|
assert.Contains(t, rec.Body.String(), "Rate limit exceeded")
|
|
}
|
|
|
|
var (
|
|
statsSoundtrackUUID = uuid.New()
|
|
statsSongUUID = uuid.New()
|
|
)
|
|
|
|
// statsStore returns canned statistics.
|
|
type statsStore struct {
|
|
repository.Querier
|
|
err error
|
|
}
|
|
|
|
func (s statsStore) GetMostPlayedSoundtracksWithSongs(_ context.Context, limit int32) ([]repository.GetMostPlayedSoundtracksWithSongsRow, error) {
|
|
return []repository.GetMostPlayedSoundtracksWithSongsRow{{
|
|
SoundtrackID: int32(limit),
|
|
SoundtrackUuid: statsSoundtrackUUID,
|
|
SoundtrackName: "Game A",
|
|
Songs: []byte(`[{"song_uuid":"` + statsSongUUID.String() + `","song_name":"Song","times_played":3}]`),
|
|
}}, s.err
|
|
}
|
|
|
|
func (s statsStore) GetAllSoundtracksWithSongs(context.Context) ([]repository.GetAllSoundtracksWithSongsRow, error) {
|
|
return []repository.GetAllSoundtracksWithSongsRow{{
|
|
SoundtrackUuid: statsSoundtrackUUID,
|
|
SoundtrackName: "Game A",
|
|
AlternativeNames: []string{"GA"},
|
|
CoverModified: pgtype.Timestamptz{Time: time.Unix(1730000000, 0), Valid: true},
|
|
SoundtrackPlayed: 4,
|
|
Songs: []byte(`[{"song_uuid":"` + statsSongUUID.String() + `","song_name":"Song","times_played":3}]`),
|
|
}, {
|
|
SoundtrackName: "Game B",
|
|
}}, s.err
|
|
}
|
|
|
|
func (s statsStore) GetStatisticsSummary(context.Context) (repository.GetStatisticsSummaryRow, error) {
|
|
return repository.GetStatisticsSummaryRow{TotalSoundtracks: 5, AvgSoundtrackPlays: 1.5}, s.err
|
|
}
|
|
|
|
func statsService() *backend.StatisticsService {
|
|
return backend.NewStatisticsService(statsStore{})
|
|
}
|
|
|
|
func TestStatisticsEndpoints(t *testing.T) {
|
|
e := newTestEnv(t)
|
|
e.server.stats = backend.NewStatisticsService(statsStore{})
|
|
e.handler = e.server.RegisterRoutes()
|
|
|
|
var soundtracks []backend.SoundtrackWithSongs
|
|
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/statistics/soundtracks/most-played").Body.Bytes(), &soundtracks))
|
|
require.Len(t, soundtracks, 1)
|
|
assert.Equal(t, int32(10), soundtracks[0].SoundtrackID, "default limit")
|
|
assert.Equal(t, "Song", soundtracks[0].Songs[0].SongName)
|
|
assert.Equal(t, statsSoundtrackUUID, soundtracks[0].SoundtrackUuid)
|
|
assert.Equal(t, statsSongUUID, soundtracks[0].Songs[0].SongUuid)
|
|
|
|
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/statistics/soundtracks/most-played?limit=500").Body.Bytes(), &soundtracks))
|
|
assert.Equal(t, int32(100), soundtracks[0].SoundtrackID, "limit is capped")
|
|
|
|
for _, limit := range []string{"0", "-1", "abc"} {
|
|
assert.Equal(t, http.StatusUnprocessableEntity, e.do(http.MethodGet, "/api/v1/statistics/soundtracks/most-played?limit="+limit).Code, limit)
|
|
}
|
|
|
|
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/statistics/soundtracks").Body.Bytes(), &soundtracks))
|
|
require.Len(t, soundtracks, 2, "every soundtrack, no limit")
|
|
assert.Equal(t, "Game A", soundtracks[0].SoundtrackName)
|
|
assert.Equal(t, int32(4), soundtracks[0].SoundtrackPlayed)
|
|
assert.Equal(t, "Song", soundtracks[0].Songs[0].SongName)
|
|
assert.Empty(t, soundtracks[1].Songs, "no songs is an empty list")
|
|
assert.Equal(t, []string{"GA"}, soundtracks[0].AlternativeNames)
|
|
require.NotNil(t, soundtracks[0].CoverModified)
|
|
assert.Equal(t, int64(1730000000), *soundtracks[0].CoverModified)
|
|
assert.Equal(t, []string{}, soundtracks[1].AlternativeNames, "no names is an empty list")
|
|
assert.Nil(t, soundtracks[1].CoverModified)
|
|
|
|
var summary backend.StatisticsSummary
|
|
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/statistics/summary").Body.Bytes(), &summary))
|
|
assert.Equal(t, backend.StatisticsSummary{TotalSoundtracks: 5, AvgSoundtrackPlays: 1.5}, summary)
|
|
|
|
e.server.stats = backend.NewStatisticsService(statsStore{err: errors.New("db down")})
|
|
e.handler = e.server.RegisterRoutes()
|
|
assert.Equal(t, http.StatusInternalServerError, e.do(http.MethodGet, "/api/v1/statistics/summary").Code)
|
|
assert.Equal(t, http.StatusInternalServerError, e.do(http.MethodGet, "/api/v1/statistics/soundtracks").Code)
|
|
}
|