You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Koito/engine/long_test.go

893 lines
28 KiB

package engine_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"path"
"strings"
"sync"
"testing"
"time"
"github.com/gabehf/koito/engine/handlers"
"github.com/gabehf/koito/internal/cfg"
"github.com/gabehf/koito/internal/db"
"github.com/gabehf/koito/internal/models"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var session string
var apikey string
var loginOnce sync.Once
var apikeyOnce sync.Once
func login(t *testing.T) {
loginOnce.Do(func() {
formdata := url.Values{}
formdata.Set("username", cfg.DefaultUsername())
formdata.Set("password", cfg.DefaultPassword())
encoded := formdata.Encode()
resp, err := http.DefaultClient.Post(host()+"/apis/web/v1/login", "application/x-www-form-urlencoded", strings.NewReader(encoded))
respBytes, _ := io.ReadAll(resp.Body)
t.Logf("Login request response: %s - %s", resp.Status, respBytes)
require.NoError(t, err)
require.Len(t, resp.Cookies(), 1)
session = resp.Cookies()[0].Value
require.NotEmpty(t, session)
})
}
func makeAuthRequest(t *testing.T, session, method, endpoint string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequest(method, host()+endpoint, body)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "koito_session",
Value: session,
})
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
t.Logf("Making request to %s with session: %s", endpoint, session)
return http.DefaultClient.Do(req)
}
// Expects a valid session
func getApiKey(t *testing.T, session string) {
apikeyOnce.Do(func() {
resp, err := makeAuthRequest(t, session, "GET", "/apis/web/v1/user/apikeys", nil)
require.NoError(t, err)
var keys []models.ApiKey
err = json.NewDecoder(resp.Body).Decode(&keys)
require.NoError(t, err)
require.GreaterOrEqual(t, len(keys), 1)
apikey = keys[0].Key
})
}
func truncateTestData(t *testing.T) {
err := store.Exec(context.Background(),
`TRUNCATE
artists,
artist_aliases,
tracks,
artist_tracks,
releases,
artist_releases,
release_aliases,
listens
RESTART IDENTITY CASCADE`)
require.NoError(t, err)
}
func doSubmitListens(t *testing.T) {
login(t)
getApiKey(t, session)
truncateTestData(t)
bodies := []string{fmt.Sprintf(`{
"listen_type": "single",
"payload": [
{
"listened_at": %d,
"track_metadata": {
"additional_info": {
"artist_mbids": [
"efc787f0-046f-4a60-beff-77b398c8cdf4"
],
"artist_names": [
"さユり"
],
"duration_ms": 275960,
"recording_mbid": "21524d55-b1f8-45d1-b172-976cba447199",
"release_group_mbid": "3281e0d9-fa44-4337-a8ce-6f264beeae16",
"release_mbid": "eb790e90-0065-4852-b47d-bbeede4aa9fc",
"submission_client": "navidrome",
"submission_client_version": "0.56.1 (fa2cf362)"
},
"artist_name": "さユり",
"release_name": "酸欠少女",
"track_name": "花の塔"
}
}
]
}`, time.Now().Add(-2*time.Hour).Unix()), // yesterday
fmt.Sprintf(`{
"listen_type": "single",
"payload": [
{
"listened_at": %d,
"track_metadata": {
"additional_info": {
"artist_mbids": [
"80b3cb83-b7a3-4f79-ad42-8325cefb3626"
],
"artist_names": [
"キタニタツヤ"
],
"duration_ms": 197270,
"recording_mbid": "4e909c21-e7a8-404d-b75a-0c8c2926efb0",
"release_group_mbid": "89069d92-e495-462c-b189-3431551868ed",
"release_mbid": "e16a49d6-77f3-4d73-b93c-cac855ce6ad5",
"submission_client": "navidrome",
"submission_client_version": "0.56.1 (fa2cf362)"
},
"artist_name": "キタニタツヤ",
"release_name": "Where Our Blue Is",
"track_name": "Where Our Blue Is"
}
}
]
}`, time.Now().Unix()),
fmt.Sprintf(`{
"listen_type": "single",
"payload": [
{
"listened_at": %d,
"track_metadata": {
"additional_info": {
"artist_mbids": [
"1262ab85-308b-46e7-b0b5-91fef8e46b62"
],
"artist_names": [
"ネクライトーキー"
],
"duration_ms": 241560,
"recording_mbid": "8eec4f3f-a059-4217-aad1-fbf82e33e756",
"release_group_mbid": "14f1aff0-dd19-4b42-82dd-720386b6d4c1",
"release_mbid": "7762d7af-7b6c-454f-977e-1b261743e265",
"submission_client": "navidrome",
"submission_client_version": "0.56.1 (fa2cf362)"
},
"artist_name": "ネクライトーキー",
"release_name": "ONE!",
"track_name": "こんがらがった!"
}
}
]
}`, time.Now().Add(-1*time.Hour).Unix())}
for _, body := range bodies {
req, err := http.NewRequest("POST", host()+"/apis/listenbrainz/1/submit-listens", strings.NewReader(body))
require.NoError(t, err)
req.Header.Add("Authorization", fmt.Sprintf("Token %s", apikey))
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
respBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, `{"status": "ok"}`, string(respBytes))
}
}
func TestGetters(t *testing.T) {
t.Run("Submit Listens", doSubmitListens)
// Artist was saved
resp, err := http.DefaultClient.Get(host() + "/apis/web/v1/artist?id=1")
assert.NoError(t, err)
var artist models.Artist
err = json.NewDecoder(resp.Body).Decode(&artist)
require.NoError(t, err)
assert.Equal(t, "さユり", artist.Name)
// Album was saved
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/album?id=1")
assert.NoError(t, err)
var album models.Album
err = json.NewDecoder(resp.Body).Decode(&album)
require.NoError(t, err)
assert.Equal(t, "酸欠少女", album.Title)
// Track was saved
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/track?id=1")
assert.NoError(t, err)
var track models.Track
err = json.NewDecoder(resp.Body).Decode(&track)
require.NoError(t, err)
assert.Equal(t, "花の塔", track.Title)
// Listen was saved
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/listens")
assert.NoError(t, err)
var listens db.PaginatedResponse[models.Listen]
err = json.NewDecoder(resp.Body).Decode(&listens)
require.NoError(t, err)
require.Len(t, listens.Items, 3)
assert.EqualValues(t, 2, listens.Items[0].Track.ID)
assert.Equal(t, "Where Our Blue Is", listens.Items[0].Track.Title)
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/top-artists")
assert.NoError(t, err)
var artists db.PaginatedResponse[models.Artist]
err = json.NewDecoder(resp.Body).Decode(&artists)
require.NoError(t, err)
require.Len(t, artists.Items, 3)
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/top-albums")
assert.NoError(t, err)
var albums db.PaginatedResponse[models.Album]
err = json.NewDecoder(resp.Body).Decode(&albums)
require.NoError(t, err)
require.Len(t, albums.Items, 3)
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/top-tracks")
assert.NoError(t, err)
var tracks db.PaginatedResponse[models.Track]
err = json.NewDecoder(resp.Body).Decode(&tracks)
require.NoError(t, err)
require.Len(t, tracks.Items, 3)
truncateTestData(t)
}
func TestMerge(t *testing.T) {
t.Run("Submit Listens", doSubmitListens)
resp, err := makeAuthRequest(t, session, "POST", "/apis/web/v1/merge/tracks?from_id=1&to_id=2", nil)
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/track?id=2")
require.NoError(t, err)
var track models.Track
err = json.NewDecoder(resp.Body).Decode(&track)
require.NoError(t, err)
assert.EqualValues(t, 2, track.ListenCount)
truncateTestData(t)
t.Run("Submit Listens", doSubmitListens)
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/merge/artists?from_id=1&to_id=2", nil)
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/artist?id=2")
require.NoError(t, err)
var artist models.Artist
err = json.NewDecoder(resp.Body).Decode(&artist)
require.NoError(t, err)
assert.EqualValues(t, 2, artist.ListenCount)
truncateTestData(t)
t.Run("Submit Listens", doSubmitListens)
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/merge/albums?from_id=1&to_id=2", nil)
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/album?id=2")
require.NoError(t, err)
var album models.Album
err = json.NewDecoder(resp.Body).Decode(&album)
require.NoError(t, err)
assert.EqualValues(t, 2, album.ListenCount)
truncateTestData(t)
}
func TestValidateToken(t *testing.T) {
login(t)
getApiKey(t, session)
req, err := http.NewRequest("GET", host()+"/apis/listenbrainz/1/validate-token", nil)
require.NoError(t, err)
req.Header.Add("Authorization", fmt.Sprintf("Token %s", apikey))
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
var actual handlers.LbzValidateResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&actual))
t.Log(actual)
var expected handlers.LbzValidateResponse
expected.Code = 200
expected.Message = "Token valid."
expected.Valid = true
expected.UserName = "test"
assert.True(t, assert.ObjectsAreEqual(expected, actual))
req, err = http.NewRequest("GET", host()+"/apis/listenbrainz/1/validate-token", nil)
require.NoError(t, err)
req.Header.Add("Authorization", "Token thisisasuperinvalidtoken")
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
req, err = http.NewRequest("GET", host()+"/apis/listenbrainz/1/validate-token", nil)
require.NoError(t, err)
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
}
func TestDelete(t *testing.T) {
t.Run("Submit Listens", doSubmitListens)
resp, err := makeAuthRequest(t, session, "DELETE", "/apis/web/v1/artist?id=1", nil)
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/artist?id=1")
require.NoError(t, err)
require.Equal(t, 404, resp.StatusCode)
resp, err = makeAuthRequest(t, session, "DELETE", "/apis/web/v1/album?id=1", nil)
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/album?id=1")
require.NoError(t, err)
require.Equal(t, 404, resp.StatusCode)
resp, err = makeAuthRequest(t, session, "DELETE", "/apis/web/v1/track?id=1", nil)
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/track?id=1")
require.NoError(t, err)
require.Equal(t, 404, resp.StatusCode)
truncateTestData(t)
}
func TestAliasesAndSearch(t *testing.T) {
t.Run("Submit Listens", doSubmitListens)
resp, err := makeAuthRequest(t, session, "POST", "/apis/web/v1/aliases?artist_id=1&alias=Sayuri", nil)
require.NoError(t, err)
require.Equal(t, 201, resp.StatusCode)
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/aliases?artist_id=1")
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
var actual []models.Alias
require.NoError(t, json.NewDecoder(resp.Body).Decode(&actual))
require.Len(t, actual, 2)
assert.Equal(t, actual[0].Alias, "さユり")
assert.Equal(t, actual[0].Source, "Canonical")
assert.Equal(t, actual[1].Alias, "Sayuri")
assert.Equal(t, actual[1].Source, "Manual")
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/aliases?album_id=1&alias=Sanketsu+Girl", nil)
require.NoError(t, err)
require.Equal(t, 201, resp.StatusCode)
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/aliases?album_id=1")
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
actual = nil
require.NoError(t, json.NewDecoder(resp.Body).Decode(&actual))
require.Len(t, actual, 2)
assert.Equal(t, actual[0].Alias, "酸欠少女")
assert.Equal(t, actual[0].Source, "Canonical")
assert.Equal(t, actual[1].Alias, "Sanketsu Girl")
assert.Equal(t, actual[1].Source, "Manual")
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/aliases?track_id=1&alias=Tower+of+Flower", nil)
require.NoError(t, err)
require.Equal(t, 201, resp.StatusCode)
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/aliases/primary?track_id=1&alias=Tower+of+Flower", nil)
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/track?id=1")
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
var track models.Track
require.NoError(t, json.NewDecoder(resp.Body).Decode(&track))
require.Len(t, actual, 2)
assert.Equal(t, track.Title, "Tower of Flower")
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/aliases/primary?artist_id=1&alias=Sayuri", nil)
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
// make sure searching works with aliases
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/search?q=Sanketsu")
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
var results handlers.SearchResults
require.NoError(t, json.NewDecoder(resp.Body).Decode(&results))
require.Len(t, results.Albums, 1)
assert.Equal(t, results.Albums[0].Title, "酸欠少女")
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/search?q=Sayuri")
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
results = handlers.SearchResults{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&results))
require.Len(t, results.Artists, 1)
assert.Equal(t, results.Artists[0].Name, "Sayuri") // reflects the new primary alias
truncateTestData(t)
}
func TestStats(t *testing.T) {
// zeroes
resp, err := http.DefaultClient.Get(host() + "/apis/web/v1/stats")
t.Log(resp)
require.NoError(t, err)
t.Run("Submit Listens", doSubmitListens)
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/stats")
t.Log(resp)
require.NoError(t, err)
var actual handlers.StatsResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&actual))
assert.EqualValues(t, 3, actual.ListenCount)
assert.EqualValues(t, 3, actual.TrackCount)
assert.EqualValues(t, 3, actual.AlbumCount)
assert.EqualValues(t, 3, actual.ArtistCount)
assert.EqualValues(t, 11, actual.MinutesListened)
}
func TestListenActivity(t *testing.T) {
// this test fails when run a bit after midnight
// i'll figure out a better test later
// t.Run("Submit Listens", doSubmitListens)
// resp, err := http.DefaultClient.Get(host() + "/apis/web/v1/listen-activity?range=3")
// t.Log(resp)
// require.NoError(t, err)
// var actual []db.ListenActivityItem
// require.NoError(t, json.NewDecoder(resp.Body).Decode(&actual))
// t.Log(actual)
// require.Len(t, actual, 3)
// assert.EqualValues(t, 3, actual[2].Listens)
}
func TestAuth(t *testing.T) {
// logs in a new session
formdata := url.Values{}
formdata.Set("username", cfg.DefaultUsername())
formdata.Set("password", cfg.DefaultPassword())
encoded := formdata.Encode()
resp, err := http.DefaultClient.Post(host()+"/apis/web/v1/login", "application/x-www-form-urlencoded", strings.NewReader(encoded))
respBytes, _ := io.ReadAll(resp.Body)
t.Logf("Login request response: %s - %s", resp.Status, respBytes)
require.NoError(t, err)
require.Len(t, resp.Cookies(), 1)
s := resp.Cookies()[0].Value
require.NotEmpty(t, s)
// test update user
req, err := http.NewRequest("PATCH", host()+"/apis/web/v1/user?username=new&password=supersecret", nil)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "koito_session",
Value: s,
})
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
// test /me with updated info
req, err = http.NewRequest("GET", host()+"/apis/web/v1/user/me", nil)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "koito_session",
Value: s,
})
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
var me models.User
require.NoError(t, json.NewDecoder(resp.Body).Decode(&me))
require.Equal(t, "new", me.Username)
// login with old password fails
formdata = url.Values{}
formdata.Set("username", cfg.DefaultUsername())
formdata.Set("password", cfg.DefaultPassword())
encoded = formdata.Encode()
resp, err = http.DefaultClient.Post(host()+"/apis/web/v1/login", "application/x-www-form-urlencoded", strings.NewReader(encoded))
require.NoError(t, err)
require.Equal(t, 401, resp.StatusCode)
// reset update so other tests dont fail
req, err = http.NewRequest("PATCH", host()+fmt.Sprintf("/apis/web/v1/user?username=%s&password=%s", cfg.DefaultUsername(), cfg.DefaultPassword()), nil)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "koito_session",
Value: s,
})
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
// creates api key
req, err = http.NewRequest("POST", host()+"/apis/web/v1/user/apikeys?label=testing", nil)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "koito_session",
Value: s,
})
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, 201, resp.StatusCode)
var response struct {
Key string `json:"key"`
}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&response))
require.NotEmpty(t, response.Key)
// validates api key
req, err = http.NewRequest("GET", host()+"/apis/listenbrainz/1/validate-token", nil)
require.NoError(t, err)
req.Header.Add("Authorization", fmt.Sprintf("Token %s", response.Key))
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
var actual handlers.LbzValidateResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&actual))
var expected handlers.LbzValidateResponse
expected.Code = 200
expected.Message = "Token valid."
expected.Valid = true
expected.UserName = "test"
assert.True(t, assert.ObjectsAreEqual(expected, actual))
// changes api key label
login(t) // i dont care about using the new session anymore
resp, err = makeAuthRequest(t, s, "PATCH", "/apis/web/v1/user/apikeys?id=2&label=well+tested", nil)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
resp, err = makeAuthRequest(t, s, "GET", "/apis/web/v1/user/apikeys", nil)
require.NoError(t, err)
var keys []models.ApiKey
err = json.NewDecoder(resp.Body).Decode(&keys)
require.NoError(t, err)
require.GreaterOrEqual(t, len(keys), 2)
require.NotNil(t, keys[1].Label)
assert.Equal(t, "well tested", keys[1].Label)
// logs out
req, err = http.NewRequest("POST", host()+"/apis/web/v1/logout", nil)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "koito_session",
Value: s,
})
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
// attempts to create an api key - unauthorized
formdata = url.Values{}
formdata.Set("label", "testing")
encoded = formdata.Encode()
req, err = http.NewRequest("POST", host()+"/apis/web/v1/user/apikeys", strings.NewReader(encoded))
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "koito_session",
Value: s,
})
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, 401, resp.StatusCode)
}
func TestDeleteListen(t *testing.T) {
login(t)
getApiKey(t, session)
truncateTestData(t)
body := `{
"listen_type": "single",
"payload": [
{
"listened_at": 1749475719,
"track_metadata": {
"additional_info": {
"artist_mbids": [
"80b3cb83-b7a3-4f79-ad42-8325cefb3626"
],
"artist_names": [
"キタニタツヤ"
],
"duration_ms": 197270,
"recording_mbid": "4e909c21-e7a8-404d-b75a-0c8c2926efb0",
"release_group_mbid": "89069d92-e495-462c-b189-3431551868ed",
"release_mbid": "e16a49d6-77f3-4d73-b93c-cac855ce6ad5",
"submission_client": "navidrome",
"submission_client_version": "0.56.1 (fa2cf362)"
},
"artist_name": "キタニタツヤ",
"release_name": "Where Our Blue Is",
"track_name": "Where Our Blue Is"
}
}
]
}`
req, err := http.NewRequest("POST", host()+"/apis/listenbrainz/1/submit-listens", strings.NewReader(body))
require.NoError(t, err)
req.Header.Add("Authorization", fmt.Sprintf("Token %s", apikey))
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
respBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, `{"status": "ok"}`, string(respBytes))
resp, err = makeAuthRequest(t, session, "DELETE", "/apis/web/v1/listen?track_id=1&unix=1749475719", nil)
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
// deletes are idempotent
resp, err = makeAuthRequest(t, session, "DELETE", "/apis/web/v1/listen?track_id=1&unix=1749475719", nil)
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
// listen is deleted
resp, err = http.DefaultClient.Get(host() + "/apis/web/v1/track?id=1")
require.NoError(t, err)
var track models.Track
err = json.NewDecoder(resp.Body).Decode(&track)
require.NoError(t, err)
assert.EqualValues(t, 0, track.ListenCount)
}
func TestArtistReplaceImage(t *testing.T) {
t.Run("Submit Listens", doSubmitListens)
buf := &bytes.Buffer{}
mpw := multipart.NewWriter(buf)
mpw.WriteField("artist_id", "1")
w, err := mpw.CreateFormFile("image", path.Join("..", "test_assets", "yuu.jpg"))
require.NoError(t, err)
f, err := os.Open(path.Join("..", "test_assets", "yuu.jpg"))
require.NoError(t, err)
defer f.Close()
_, err = io.Copy(w, f)
require.NoError(t, err)
require.NoError(t, mpw.Close())
req, err := http.NewRequest("POST", host()+"/apis/web/v1/replace-image", buf)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "koito_session",
Value: session,
})
req.Header.Add("Content-Type", mpw.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
response := new(handlers.ReplaceImageResponse)
require.NoError(t, json.NewDecoder(resp.Body).Decode(response))
require.NotEmpty(t, response.Image)
newid, err := uuid.Parse(response.Image)
require.NoError(t, err)
a, err := store.GetArtist(context.Background(), db.GetArtistOpts{ID: 1})
require.NoError(t, err)
assert.NotNil(t, a.Image)
assert.Equal(t, newid, *a.Image)
}
func TestAlbumReplaceImage(t *testing.T) {
t.Run("Submit Listens", doSubmitListens)
buf := &bytes.Buffer{}
mpw := multipart.NewWriter(buf)
mpw.WriteField("album_id", "1")
w, err := mpw.CreateFormFile("image", path.Join("..", "test_assets", "yuu.jpg"))
require.NoError(t, err)
f, err := os.Open(path.Join("..", "test_assets", "yuu.jpg"))
require.NoError(t, err)
defer f.Close()
_, err = io.Copy(w, f)
require.NoError(t, err)
require.NoError(t, mpw.Close())
req, err := http.NewRequest("POST", host()+"/apis/web/v1/replace-image", buf)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "koito_session",
Value: session,
})
req.Header.Add("Content-Type", mpw.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
response := new(handlers.ReplaceImageResponse)
require.NoError(t, json.NewDecoder(resp.Body).Decode(response))
require.NotEmpty(t, response.Image)
newid, err := uuid.Parse(response.Image)
require.NoError(t, err)
a, err := store.GetAlbum(context.Background(), db.GetAlbumOpts{ID: 1})
require.NoError(t, err)
assert.NotNil(t, a.Image)
assert.Equal(t, newid, *a.Image)
}
func TestSetPrimaryArtist(t *testing.T) {
t.Run("Submit Listens", doSubmitListens)
ctx := context.Background()
// set and unset track primary artist
formdata := url.Values{}
formdata.Set("artist_id", "1")
formdata.Set("track_id", "1")
formdata.Set("is_primary", "false")
body := formdata.Encode()
resp, err := makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
exists, err := store.RowExists(ctx, `
SELECT EXISTS (
SELECT 1 FROM artist_tracks
WHERE track_id = $1 AND artist_id = $2 AND is_primary = $3
)`, 1, 1, false)
require.NoError(t, err)
assert.True(t, exists, "expected artist is_primary to be false")
formdata = url.Values{}
formdata.Set("artist_id", "1")
formdata.Set("track_id", "1")
formdata.Set("is_primary", "true")
body = formdata.Encode()
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
exists, err = store.RowExists(ctx, `
SELECT EXISTS (
SELECT 1 FROM artist_tracks
WHERE track_id = $1 AND artist_id = $2 AND is_primary = $3
)`, 1, 1, true)
require.NoError(t, err)
assert.True(t, exists, "expected artist is_primary to be true")
// set and unset album primary artist
formdata = url.Values{}
formdata.Set("artist_id", "1")
formdata.Set("album_id", "1")
formdata.Set("is_primary", "false")
body = formdata.Encode()
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
exists, err = store.RowExists(ctx, `
SELECT EXISTS (
SELECT 1 FROM artist_releases
WHERE release_id = $1 AND artist_id = $2 AND is_primary = $3
)`, 1, 1, false)
require.NoError(t, err)
assert.True(t, exists, "expected artist is_primary to be false")
formdata = url.Values{}
formdata.Set("artist_id", "1")
formdata.Set("album_id", "1")
formdata.Set("is_primary", "true")
body = formdata.Encode()
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
exists, err = store.RowExists(ctx, `
SELECT EXISTS (
SELECT 1 FROM artist_releases
WHERE release_id = $1 AND artist_id = $2 AND is_primary = $3
)`, 1, 1, true)
require.NoError(t, err)
assert.True(t, exists, "expected artist is_primary to be true")
// create a new track with multiple artists to make sure only one is primary at a time
listenBody := `{
"listen_type": "single",
"payload": [
{
"listened_at": 1749475719,
"track_metadata": {
"additional_info": {
"artist_names": [
"Rat Tally",
"Madeline Kenney"
],
"duration_ms": 197270,
"submission_client": "navidrome",
"submission_client_version": "0.56.1 (fa2cf362)"
},
"artist_name": "Rat Tally feat. Madeline Kenney",
"release_name": "In My Car",
"track_name": "In My Car"
}
}
]
}`
req, err := http.NewRequest("POST", host()+"/apis/listenbrainz/1/submit-listens", strings.NewReader(listenBody))
require.NoError(t, err)
req.Header.Add("Authorization", fmt.Sprintf("Token %s", apikey))
req.Header.Add("Content-Type", "application/json")
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
respBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, `{"status": "ok"}`, string(respBytes))
// set both artists as primary
formdata = url.Values{}
formdata.Set("artist_id", "4")
formdata.Set("album_id", "4")
formdata.Set("is_primary", "true")
body = formdata.Encode()
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
formdata = url.Values{}
formdata.Set("artist_id", "5")
formdata.Set("album_id", "4")
formdata.Set("is_primary", "true")
body = formdata.Encode()
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
formdata = url.Values{}
formdata.Set("artist_id", "4")
formdata.Set("track_id", "4")
formdata.Set("is_primary", "true")
body = formdata.Encode()
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
formdata = url.Values{}
formdata.Set("artist_id", "5")
formdata.Set("track_id", "4")
formdata.Set("is_primary", "true")
body = formdata.Encode()
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
count, err := store.Count(ctx, `SELECT COUNT(*) FROM artist_releases WHERE release_id = $1 AND is_primary = $2`, 4, true)
require.NoError(t, err)
assert.EqualValues(t, 1, count, "expected only one primary artist for release")
count, err = store.Count(ctx, `SELECT COUNT(*) FROM artist_tracks WHERE track_id = $1 AND is_primary = $2`, 4, true)
require.NoError(t, err)
assert.EqualValues(t, 1, count, "expected only one primary artist for track")
}