update
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
This commit is contained in:
19
tests/fake_http_client.go
Normal file
19
tests/fake_http_client.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package tests
|
||||
|
||||
import "net/http"
|
||||
|
||||
type FakeHttpClient struct {
|
||||
Res http.Response
|
||||
Err error
|
||||
SavedRequest *http.Request
|
||||
RequestCount int
|
||||
}
|
||||
|
||||
func (c *FakeHttpClient) Do(req *http.Request) (*http.Response, error) {
|
||||
c.RequestCount++
|
||||
c.SavedRequest = req
|
||||
if c.Err != nil {
|
||||
return nil, c.Err
|
||||
}
|
||||
return &c.Res, nil
|
||||
}
|
||||
0
tests/fixtures/#snapshot/.gitkeep
vendored
Normal file
0
tests/fixtures/#snapshot/.gitkeep
vendored
Normal file
0
tests/fixtures/$Recycle.Bin/.gitkeep
vendored
Normal file
0
tests/fixtures/$Recycle.Bin/.gitkeep
vendored
Normal file
0
tests/fixtures/...unhidden_folder/.gitkeep
vendored
Normal file
0
tests/fixtures/...unhidden_folder/.gitkeep
vendored
Normal file
0
tests/fixtures/._02 Invisible.mp3
vendored
Normal file
0
tests/fixtures/._02 Invisible.mp3
vendored
Normal file
0
tests/fixtures/.hidden_folder/.gitkeep
vendored
Normal file
0
tests/fixtures/.hidden_folder/.gitkeep
vendored
Normal file
BIN
tests/fixtures/01 Invisible (RED) Edit Version.m4a
vendored
Normal file
BIN
tests/fixtures/01 Invisible (RED) Edit Version.m4a
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/01 Invisible (RED) Edit Version.mp3
vendored
Normal file
BIN
tests/fixtures/01 Invisible (RED) Edit Version.mp3
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/artist/an-album/artist.png
vendored
Normal file
BIN
tests/fixtures/artist/an-album/artist.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
BIN
tests/fixtures/artist/an-album/cover.jpg
vendored
Normal file
BIN
tests/fixtures/artist/an-album/cover.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
tests/fixtures/artist/an-album/front.png
vendored
Normal file
BIN
tests/fixtures/artist/an-album/front.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
BIN
tests/fixtures/artist/an-album/test.mp3
vendored
Normal file
BIN
tests/fixtures/artist/an-album/test.mp3
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/artist/artist.jpg
vendored
Normal file
BIN
tests/fixtures/artist/artist.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
4
tests/fixtures/bom-test.lrc
vendored
Normal file
4
tests/fixtures/bom-test.lrc
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[00:00.00] 作曲 : 柏大輔
|
||||
NOTE: This file intentionally contains a UTF-8 BOM (Byte Order Mark) at byte 0.
|
||||
This tests BOM handling in lyrics parsing (GitHub issue #4631).
|
||||
The BOM bytes are: 0xEF 0xBB 0xBF
|
||||
BIN
tests/fixtures/bom-utf16-test.lrc
vendored
Normal file
BIN
tests/fixtures/bom-utf16-test.lrc
vendored
Normal file
Binary file not shown.
9
tests/fixtures/deezer.artist.bio.json
vendored
Normal file
9
tests/fixtures/deezer.artist.bio.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"data": {
|
||||
"artist": {
|
||||
"bio": {
|
||||
"full": "<p>Schoolmates Thomas and Guy-Manuel began their career in 1992 with the indie rock trio Darlin' (named after The Beach Boys song) but were scathingly dismissed by Melody Maker magazine as \"daft punk.\" Turning to house-inspired electronica, they used the put down as a name for their DJ-ing partnership and became a hugely successful and influential dance act.</p>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
tests/fixtures/deezer.artist.related.json
vendored
Normal file
1
tests/fixtures/deezer.artist.related.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/fixtures/deezer.artist.top.json
vendored
Normal file
1
tests/fixtures/deezer.artist.top.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/fixtures/deezer.search.artist.json
vendored
Normal file
1
tests/fixtures/deezer.search.artist.json
vendored
Normal file
File diff suppressed because one or more lines are too long
0
tests/fixtures/empty.txt
vendored
Normal file
0
tests/fixtures/empty.txt
vendored
Normal file
0
tests/fixtures/empty_folder/not_an_audio_file.txt
vendored
Normal file
0
tests/fixtures/empty_folder/not_an_audio_file.txt
vendored
Normal file
0
tests/fixtures/ignored_folder/.ndignore
vendored
Normal file
0
tests/fixtures/ignored_folder/.ndignore
vendored
Normal file
15
tests/fixtures/index.html
vendored
Normal file
15
tests/fixtures/index.html
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta
|
||||
name="description"
|
||||
content="Navidrome Music Server - {{.Version}}"
|
||||
/>
|
||||
<title>Navidrome</title>
|
||||
<!-- The line below has to match the exact format of the equivalent line in ui/build/index.html -->
|
||||
<script>window.__APP_CONFIG__={{ .AppConfig }};</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
BIN
tests/fixtures/invalid-files/test-invalid-frame.mp3
vendored
Normal file
BIN
tests/fixtures/invalid-files/test-invalid-frame.mp3
vendored
Normal file
Binary file not shown.
1909
tests/fixtures/itunes-library.xml
vendored
Normal file
1909
tests/fixtures/itunes-library.xml
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
tests/fixtures/lastfm.album.getinfo.empty_urls.json
vendored
Normal file
1
tests/fixtures/lastfm.album.getinfo.empty_urls.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"album":{"artist":"The Jesus and Mary Chain","listeners":"2","image":[{"size":"small","#text":""},{"size":"medium","#text":""},{"size":"large","#text":""},{"size":"extralarge","#text":""},{"size":"mega","#text":""},{"size":"","#text":""}],"mbid":"","tags":"","name":"The Definitive Less Damage And More Joy","playcount":"2","url":"https:\/\/www.last.fm\/music\/The+Jesus+and+Mary+Chain\/The+Definitive+Less+Damage+And+More+Joy"}}
|
||||
1
tests/fixtures/lastfm.album.getinfo.json
vendored
Normal file
1
tests/fixtures/lastfm.album.getinfo.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/fixtures/lastfm.artist.getinfo.json
vendored
Normal file
1
tests/fixtures/lastfm.artist.getinfo.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/fixtures/lastfm.artist.getinfo.unknown.json
vendored
Normal file
1
tests/fixtures/lastfm.artist.getinfo.unknown.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"artist":{"name":"[unknown]","mbid":"5dfdca28-9ddc-4853-933c-8bc97d87beec","url":"https://www.last.fm/music/%5Bunknown%5D","image":[{"#text":"https://lastfm.freetls.fastly.net/i/u/34s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"small"},{"#text":"https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"medium"},{"#text":"https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"large"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":"extralarge"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":"mega"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":""}],"streamable":"0","ontour":"0","stats":{"listeners":"2774837","playcount":"137106224"},"similar":{"artist":[]},"tags":{"tag":[{"name":"mysterious","url":"https://www.last.fm/tag/mysterious"},{"name":"mistagged artist","url":"https://www.last.fm/tag/mistagged+artist"},{"name":"unknown","url":"https://www.last.fm/tag/unknown"},{"name":"rock","url":"https://www.last.fm/tag/rock"},{"name":"Soundtrack","url":"https://www.last.fm/tag/Soundtrack"}]},"bio":{"links":{"link":{"#text":"","rel":"original","href":"https://last.fm/music/%5Bunknown%5D/+wiki"}},"published":"10 Feb 2006, 20:25","summary":"[unknown] is a standard artist name used at MusicBrainz for indicating where an artist name is lacking or not provided.\n\n--\n\nFor the short-lived visual-kei band, see *\n\n--\n\nThere are other artists with this or a similar spelling, usually their scrobbles will be filtered when submitted unless they are whitelisted. <a href=\"https://www.last.fm/music/%5Bunknown%5D\">Read more on Last.fm</a>","content":"[unknown] is a standard artist name used at MusicBrainz for indicating where an artist name is lacking or not provided.\n\n--\n\nFor the short-lived visual-kei band, see *\n\n--\n\nThere are other artists with this or a similar spelling, usually their scrobbles will be filtered when submitted unless they are whitelisted. <a href=\"https://www.last.fm/music/%5Bunknown%5D\">Read more on Last.fm</a>. User-contributed text is available under the Creative Commons By-SA License; additional terms may apply."}}}
|
||||
1
tests/fixtures/lastfm.artist.getsimilar.json
vendored
Normal file
1
tests/fixtures/lastfm.artist.getsimilar.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"similarartists":{"artist":[{"name":"Passengers","mbid":"e110c11f-1c94-4471-a350-c38f46b29389","match":"1","url":"https://www.last.fm/music/Passengers","image":[{"#text":"https://lastfm.freetls.fastly.net/i/u/34s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"small"},{"#text":"https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"medium"},{"#text":"https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"large"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":"extralarge"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":"mega"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":""}],"streamable":"0"},{"name":"INXS","mbid":"481bf5f9-2e7c-4c44-b08a-05b32bc7c00d","match":"0.511468","url":"https://www.last.fm/music/INXS","image":[{"#text":"https://lastfm.freetls.fastly.net/i/u/34s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"small"},{"#text":"https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"medium"},{"#text":"https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"large"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":"extralarge"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":"mega"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":""}],"streamable":"0"}],"@attr":{"artist":"U2"}}}
|
||||
1
tests/fixtures/lastfm.artist.getsimilar.unknown.json
vendored
Normal file
1
tests/fixtures/lastfm.artist.getsimilar.unknown.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"similarartists":{"artist":[],"@attr":{"artist":"[unknown]"}}}
|
||||
1
tests/fixtures/lastfm.artist.gettoptracks.json
vendored
Normal file
1
tests/fixtures/lastfm.artist.gettoptracks.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"toptracks":{"track":[{"name":"Beautiful Day","playcount":"6309776","listeners":"1037970","mbid":"f7f264d0-a89b-4682-9cd7-a4e7c37637af","url":"https://www.last.fm/music/U2/_/Beautiful+Day","streamable":"0","artist":{"name":"U2","mbid":"a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432","url":"https://www.last.fm/music/U2"},"image":[{"#text":"https://lastfm.freetls.fastly.net/i/u/34s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"small"},{"#text":"https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"medium"},{"#text":"https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"large"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":"extralarge"}],"@attr":{"rank":"1"}},{"name":"With or Without You","playcount":"6779665","listeners":"1022929","mbid":"6b9a509f-6907-4a6e-9345-2f12da09ba4b","url":"https://www.last.fm/music/U2/_/With+or+Without+You","streamable":"0","artist":{"name":"U2","mbid":"a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432","url":"https://www.last.fm/music/U2"},"image":[{"#text":"https://lastfm.freetls.fastly.net/i/u/34s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"small"},{"#text":"https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"medium"},{"#text":"https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"large"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":"extralarge"}],"@attr":{"rank":"2"}}],"@attr":{"artist":"U2","page":"1","perPage":"2","totalPages":"166117","total":"332234"}}}
|
||||
1
tests/fixtures/lastfm.artist.gettoptracks.unknown.json
vendored
Normal file
1
tests/fixtures/lastfm.artist.gettoptracks.unknown.json
vendored
Normal file
File diff suppressed because one or more lines are too long
7
tests/fixtures/lastfm.artist.page.html
vendored
Normal file
7
tests/fixtures/lastfm.artist.page.html
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta property="og:image" content="https://lastfm.freetls.fastly.net/i/u/ar0/818148bf682d429dc21b59a73ef6f68e.png" />
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
7
tests/fixtures/lastfm.artist.page.ignored.html
vendored
Normal file
7
tests/fixtures/lastfm.artist.page.ignored.html
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta property="og:image" content="https://lastfm.freetls.fastly.net/i/u/ar0/2a96cbd8b46e442fc41c2b86b821562f.png" />
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
6
tests/fixtures/lastfm.artist.page.no_meta.html
vendored
Normal file
6
tests/fixtures/lastfm.artist.page.no_meta.html
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
24
tests/fixtures/listenbrainz.nowplaying.request.json
vendored
Normal file
24
tests/fixtures/listenbrainz.nowplaying.request.json
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"listen_type": "playing_now",
|
||||
"payload": [
|
||||
{
|
||||
"track_metadata": {
|
||||
"artist_name": "Track Artist",
|
||||
"track_name": "Track Title",
|
||||
"release_name": "Track Album",
|
||||
"additional_info": {
|
||||
"tracknumber": 1,
|
||||
"recording_mbid": "mbz-123",
|
||||
"artist_names": [
|
||||
"Artist 1", "Artist 2"
|
||||
],
|
||||
"artist_mbids": [
|
||||
"mbz-789", "mbz-012"
|
||||
],
|
||||
"release_mbid": "mbz-456",
|
||||
"duration_ms": 142200
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
25
tests/fixtures/listenbrainz.scrobble.request.json
vendored
Normal file
25
tests/fixtures/listenbrainz.scrobble.request.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"listen_type": "single",
|
||||
"payload": [
|
||||
{
|
||||
"listened_at": 1635000000,
|
||||
"track_metadata": {
|
||||
"artist_name": "Track Artist",
|
||||
"track_name": "Track Title",
|
||||
"release_name": "Track Album",
|
||||
"additional_info": {
|
||||
"tracknumber": 1,
|
||||
"recording_mbid": "mbz-123",
|
||||
"artist_names": [
|
||||
"Artist 1", "Artist 2"
|
||||
],
|
||||
"artist_mbids": [
|
||||
"mbz-789", "mbz-012"
|
||||
],
|
||||
"release_mbid": "mbz-456",
|
||||
"duration_ms": 142200
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
tests/fixtures/mixed-lyrics.flac
vendored
Normal file
BIN
tests/fixtures/mixed-lyrics.flac
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/no_replaygain.mp3
vendored
Normal file
BIN
tests/fixtures/no_replaygain.mp3
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/playlists/bom-test-utf16.m3u
vendored
Normal file
BIN
tests/fixtures/playlists/bom-test-utf16.m3u
vendored
Normal file
Binary file not shown.
6
tests/fixtures/playlists/bom-test.m3u
vendored
Normal file
6
tests/fixtures/playlists/bom-test.m3u
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
#EXTM3U
|
||||
# NOTE: This file intentionally contains a UTF-8 BOM (Byte Order Mark) at the beginning
|
||||
# (bytes 0xEF 0xBB 0xBF) to test BOM handling in playlist parsing.
|
||||
#PLAYLIST:Test Playlist
|
||||
#EXTINF:123,Test Artist - Test Song
|
||||
test.mp3
|
||||
1
tests/fixtures/playlists/cr-ended.m3u
vendored
Normal file
1
tests/fixtures/playlists/cr-ended.m3u
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# This is a comment
|
||||
42
tests/fixtures/playlists/invalid_json.nsp
vendored
Normal file
42
tests/fixtures/playlists/invalid_json.nsp
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"all": [
|
||||
{"is": {"loved": true}},
|
||||
{"isNot": {"genre": "Hip-Hop"}},
|
||||
{"isNot": {"genre": "Hip Hop"}},
|
||||
{"isNot": {"genre": "Rap"}},
|
||||
{"isNot": {"genre": "Alternative Hip Hop"}},
|
||||
{"isNot": {"genre": "Deutsch-Rap"}},
|
||||
{"isNot": {"genre": "Deutsche Musik"}},
|
||||
{"isNot": {"genre": "Uk Hip Hop"}},
|
||||
{"isNot": {"genre": "UK Rap"}},
|
||||
{"isNot": {"genre": "Boom Bap"}},
|
||||
{"isNot": {"genre": "Lo-Fi Hip Hop"}},
|
||||
{"isNot": {"genre": "Jazzy Hip-Hop"}},
|
||||
{"isNot": {"genre": "Jazz Rap"}},
|
||||
{"isNot": {"genre": "Jazz Rap"}},
|
||||
{"isNot": {"genre": "Southern Hip Hop"}},
|
||||
{"isNot": {"genre": "Alternative Hip Hop}},
|
||||
{"isNot": {"genre": "Underground"}},
|
||||
{"isNot": {"genre": "Trap"}},
|
||||
{"isNot": {"genre": "Mixtape"}},
|
||||
{"isNot": {"genre": "Boom-Bap"}},
|
||||
{"isNot": {"genre": "Conscious"}},
|
||||
{"isNot": {"genre": "Turntablism"}},
|
||||
{"isNot": {"genre": "Pop Rap"}},
|
||||
{"isNot": {"genre": "Aussie"}},
|
||||
{"isNot": {"genre": "Horror-Core"}},
|
||||
{"isNot": {"genre": "Pop Rap"}},
|
||||
{"isNot": {"genre": "Female-Rap"}},
|
||||
{"isNot": {"genre": "Female Rap"}},
|
||||
{"isNot": {"genre": "East Coast"}},
|
||||
{"isNot": {"genre": "East Coast Hip Hop"}},
|
||||
{"isNot": {"genre": "West Coast"}},
|
||||
{"isNot": {"genre": "Gangsta Rap"}},
|
||||
{"isNot": {"genre": "Cloudrap"}},
|
||||
{"isNot": {"genre": "Hardcore Hip Hop"}},
|
||||
{"isNot": {"genre": "Mixtape"}},
|
||||
{"isNot": {"genre": "Deutschrap"}}
|
||||
],
|
||||
"sort": "dateLoved",
|
||||
"order": "desc"
|
||||
}
|
||||
3
tests/fixtures/playlists/lf-ended.m3u
vendored
Normal file
3
tests/fixtures/playlists/lf-ended.m3u
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# This is a comment
|
||||
abc.mp3
|
||||
def.mp3
|
||||
4
tests/fixtures/playlists/pls-with-name.m3u
vendored
Normal file
4
tests/fixtures/playlists/pls-with-name.m3u
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
#PLAYLIST:playlist 1
|
||||
tests/fixtures/test.mp3
|
||||
tests/fixtures/test.ogg
|
||||
file:///tests/fixtures/01%20Invisible%20(RED)%20Edit%20Version.mp3
|
||||
3
tests/fixtures/playlists/pls-without-name.m3u
vendored
Normal file
3
tests/fixtures/playlists/pls-without-name.m3u
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
tests/fixtures/test.mp3
|
||||
tests/fixtures/test.ogg
|
||||
file:///tests/fixtures/01%20Invisible%20(RED)%20Edit%20Version.mp3
|
||||
2
tests/fixtures/playlists/pls1.m3u
vendored
Normal file
2
tests/fixtures/playlists/pls1.m3u
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
test.mp3
|
||||
test.ogg
|
||||
14
tests/fixtures/playlists/recently_played.nsp
vendored
Normal file
14
tests/fixtures/playlists/recently_played.nsp
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
Top Level Comment
|
||||
*/
|
||||
{
|
||||
"name": "Recently Played",
|
||||
"comment": "Recently played tracks",
|
||||
"all": [
|
||||
// This is an inline comment
|
||||
{"inTheLast": {"lastPlayed": 30}}
|
||||
],
|
||||
"sort": "lastPlayed",
|
||||
"order": "desc",
|
||||
"limit": 100
|
||||
}
|
||||
2
tests/fixtures/playlists/subfolder1/.hidden_playlist1.m3u
vendored
Normal file
2
tests/fixtures/playlists/subfolder1/.hidden_playlist1.m3u
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
test.mp3
|
||||
test.ogg
|
||||
2
tests/fixtures/playlists/subfolder1/pls1.m3u
vendored
Normal file
2
tests/fixtures/playlists/subfolder1/pls1.m3u
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
test.mp3
|
||||
test.ogg
|
||||
2
tests/fixtures/playlists/subfolder2/.hidden_playlist2.m3u
vendored
Normal file
2
tests/fixtures/playlists/subfolder2/.hidden_playlist2.m3u
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
test.mp3
|
||||
test.ogg
|
||||
4
tests/fixtures/playlists/subfolder2/pls2.m3u
vendored
Normal file
4
tests/fixtures/playlists/subfolder2/pls2.m3u
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
../test.mp3
|
||||
../test.ogg
|
||||
/tests/fixtures/01%20Invisible%20(RED)%20Edit%20Version.mp3
|
||||
/invalid/path/xyz.mp3
|
||||
4
tests/fixtures/robots.txt
vendored
Normal file
4
tests/fixtures/robots.txt
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
User-agent: bingbot
|
||||
Disallow: /manifest.webmanifest
|
||||
|
||||
User-agent: *
|
||||
638
tests/fixtures/spotify.search.artist.json
vendored
Normal file
638
tests/fixtures/spotify.search.artist.json
vendored
Normal file
@@ -0,0 +1,638 @@
|
||||
{
|
||||
"artists": {
|
||||
"href": "https://api.spotify.com/v1/search?query=U2&type=artist&offset=0&limit=20",
|
||||
"items": [
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/51Blml2LZPmy7TTiAg47vQ"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 7369641
|
||||
},
|
||||
"genres": [
|
||||
"irish rock",
|
||||
"permanent wave",
|
||||
"rock"
|
||||
],
|
||||
"href": "https://api.spotify.com/v1/artists/51Blml2LZPmy7TTiAg47vQ",
|
||||
"id": "51Blml2LZPmy7TTiAg47vQ",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/e22d5c0c8139b8439440a69854ed66efae91112d",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 320,
|
||||
"url": "https://i.scdn.co/image/40d6c5c14355cfc127b70da221233315497ec91d",
|
||||
"width": 320
|
||||
},
|
||||
{
|
||||
"height": 160,
|
||||
"url": "https://i.scdn.co/image/7293d6752ae8a64e34adee5086858e408185b534",
|
||||
"width": 160
|
||||
}
|
||||
],
|
||||
"name": "U2",
|
||||
"popularity": 82,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:51Blml2LZPmy7TTiAg47vQ"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/6Yi6ndhYVLUaYu7rEqUCPT"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 1008
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/6Yi6ndhYVLUaYu7rEqUCPT",
|
||||
"id": "6Yi6ndhYVLUaYu7rEqUCPT",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/ab67616d0000b2734dc59f13a52e236c404b8abf",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 300,
|
||||
"url": "https://i.scdn.co/image/ab67616d00001e024dc59f13a52e236c404b8abf",
|
||||
"width": 300
|
||||
},
|
||||
{
|
||||
"height": 64,
|
||||
"url": "https://i.scdn.co/image/ab67616d000048514dc59f13a52e236c404b8abf",
|
||||
"width": 64
|
||||
}
|
||||
],
|
||||
"name": "U2R",
|
||||
"popularity": 1,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:6Yi6ndhYVLUaYu7rEqUCPT"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/5TucOfYYQ8HPdDdvsQZAZe"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 658
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/5TucOfYYQ8HPdDdvsQZAZe",
|
||||
"id": "5TucOfYYQ8HPdDdvsQZAZe",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/ab67616d0000b273931ae74e023fcb999dc423a5",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 300,
|
||||
"url": "https://i.scdn.co/image/ab67616d00001e02931ae74e023fcb999dc423a5",
|
||||
"width": 300
|
||||
},
|
||||
{
|
||||
"height": 64,
|
||||
"url": "https://i.scdn.co/image/ab67616d00004851931ae74e023fcb999dc423a5",
|
||||
"width": 64
|
||||
}
|
||||
],
|
||||
"name": "U2KUSHI",
|
||||
"popularity": 2,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:5TucOfYYQ8HPdDdvsQZAZe"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/5s3rOzCczqCQrvueHRCZOx"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 44
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/5s3rOzCczqCQrvueHRCZOx",
|
||||
"id": "5s3rOzCczqCQrvueHRCZOx",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/c474959b393e2cf05bec6deb83643b65b12cf258",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 320,
|
||||
"url": "https://i.scdn.co/image/36ea6b9246b8dfe59288f826cfeaf9cf641e7316",
|
||||
"width": 320
|
||||
},
|
||||
{
|
||||
"height": 160,
|
||||
"url": "https://i.scdn.co/image/709c8f24166781c1a0695046e757e1f4f6e1ac34",
|
||||
"width": 160
|
||||
}
|
||||
],
|
||||
"name": "U2funnyTJ",
|
||||
"popularity": 6,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:5s3rOzCczqCQrvueHRCZOx"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/4CWC85PCLJ0yzPeJYXnQOG"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 908
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/4CWC85PCLJ0yzPeJYXnQOG",
|
||||
"id": "4CWC85PCLJ0yzPeJYXnQOG",
|
||||
"images": [],
|
||||
"name": "U2 Rocks",
|
||||
"popularity": 0,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:4CWC85PCLJ0yzPeJYXnQOG"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/21114frei5NgrkMuLn6AOz"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 0
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/21114frei5NgrkMuLn6AOz",
|
||||
"id": "21114frei5NgrkMuLn6AOz",
|
||||
"images": [],
|
||||
"name": "U2A9F",
|
||||
"popularity": 0,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:21114frei5NgrkMuLn6AOz"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/3dhoDqkI6atVLE43nkx8VZ"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 878
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/3dhoDqkI6atVLE43nkx8VZ",
|
||||
"id": "3dhoDqkI6atVLE43nkx8VZ",
|
||||
"images": [],
|
||||
"name": "LMC vs U2",
|
||||
"popularity": 14,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:3dhoDqkI6atVLE43nkx8VZ"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/5bi7xpKp2mDDSnFfQkBEjR"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 989
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/5bi7xpKp2mDDSnFfQkBEjR",
|
||||
"id": "5bi7xpKp2mDDSnFfQkBEjR",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/ab67616d0000b2735931f4613d57703ef50ff0e4",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 300,
|
||||
"url": "https://i.scdn.co/image/ab67616d00001e025931f4613d57703ef50ff0e4",
|
||||
"width": 300
|
||||
},
|
||||
{
|
||||
"height": 64,
|
||||
"url": "https://i.scdn.co/image/ab67616d000048515931f4613d57703ef50ff0e4",
|
||||
"width": 64
|
||||
}
|
||||
],
|
||||
"name": "U21",
|
||||
"popularity": 0,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:5bi7xpKp2mDDSnFfQkBEjR"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/3H7E05uiFuqgwBQrXFaQIm"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 18
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/3H7E05uiFuqgwBQrXFaQIm",
|
||||
"id": "3H7E05uiFuqgwBQrXFaQIm",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/ab67616d0000b27366ca114acb03e008d141f28b",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 300,
|
||||
"url": "https://i.scdn.co/image/ab67616d00001e0266ca114acb03e008d141f28b",
|
||||
"width": 300
|
||||
},
|
||||
{
|
||||
"height": 64,
|
||||
"url": "https://i.scdn.co/image/ab67616d0000485166ca114acb03e008d141f28b",
|
||||
"width": 64
|
||||
}
|
||||
],
|
||||
"name": "U2M JR",
|
||||
"popularity": 1,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:3H7E05uiFuqgwBQrXFaQIm"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/6BMzJXRYmy28QVMZc09rGB"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 13
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/6BMzJXRYmy28QVMZc09rGB",
|
||||
"id": "6BMzJXRYmy28QVMZc09rGB",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/ab67616d0000b273bd26433a01cf571413cbb1ec",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 300,
|
||||
"url": "https://i.scdn.co/image/ab67616d00001e02bd26433a01cf571413cbb1ec",
|
||||
"width": 300
|
||||
},
|
||||
{
|
||||
"height": 64,
|
||||
"url": "https://i.scdn.co/image/ab67616d00004851bd26433a01cf571413cbb1ec",
|
||||
"width": 64
|
||||
}
|
||||
],
|
||||
"name": "U2oh",
|
||||
"popularity": 0,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:6BMzJXRYmy28QVMZc09rGB"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/4MtRKC7apgAyAd5uUjN3L4"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 64
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/4MtRKC7apgAyAd5uUjN3L4",
|
||||
"id": "4MtRKC7apgAyAd5uUjN3L4",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/ab67616d0000b273b8ca9830e6849d80b41ef109",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 300,
|
||||
"url": "https://i.scdn.co/image/ab67616d00001e02b8ca9830e6849d80b41ef109",
|
||||
"width": 300
|
||||
},
|
||||
{
|
||||
"height": 64,
|
||||
"url": "https://i.scdn.co/image/ab67616d00004851b8ca9830e6849d80b41ef109",
|
||||
"width": 64
|
||||
}
|
||||
],
|
||||
"name": "Zürcher Jugendblasorchester U25",
|
||||
"popularity": 1,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:4MtRKC7apgAyAd5uUjN3L4"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/18JD8DVlD1fakDAw7E9LFC"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 137412
|
||||
},
|
||||
"genres": [
|
||||
"bubblegum dance",
|
||||
"eurodance",
|
||||
"europop",
|
||||
"hip house"
|
||||
],
|
||||
"href": "https://api.spotify.com/v1/artists/18JD8DVlD1fakDAw7E9LFC",
|
||||
"id": "18JD8DVlD1fakDAw7E9LFC",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/c4fdb52d1be39038a8001116929044415fbd8962",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 320,
|
||||
"url": "https://i.scdn.co/image/54a2ea5b22f2966c5d30ba2aa5d5589adfe023ef",
|
||||
"width": 320
|
||||
},
|
||||
{
|
||||
"height": 160,
|
||||
"url": "https://i.scdn.co/image/c11fdfd488dcf99e8b88975bba88205998ee7012",
|
||||
"width": 160
|
||||
}
|
||||
],
|
||||
"name": "2 Unlimited",
|
||||
"popularity": 59,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:18JD8DVlD1fakDAw7E9LFC"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/0goZ9x7MGZF5rlaJOFrj1F"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 10
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/0goZ9x7MGZF5rlaJOFrj1F",
|
||||
"id": "0goZ9x7MGZF5rlaJOFrj1F",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/195ebaebab44986c53d8423155299b47d16652db",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 320,
|
||||
"url": "https://i.scdn.co/image/00bc3410ab6f5065625f10d8a1c7a4c4f922e95e",
|
||||
"width": 320
|
||||
},
|
||||
{
|
||||
"height": 160,
|
||||
"url": "https://i.scdn.co/image/c0c6bae7ea925c370fb91b2e27f4aa89182f8b3f",
|
||||
"width": 160
|
||||
}
|
||||
],
|
||||
"name": "24U",
|
||||
"popularity": 42,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:0goZ9x7MGZF5rlaJOFrj1F"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/76pyFXpXITp0aRz4j3SyGJ"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 318
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/76pyFXpXITp0aRz4j3SyGJ",
|
||||
"id": "76pyFXpXITp0aRz4j3SyGJ",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/9c72fe64128e7d01d8bae4275401e37a12562b43",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 320,
|
||||
"url": "https://i.scdn.co/image/d3a313ef8e07f8ae5bf4a1800690065a7d1001b8",
|
||||
"width": 320
|
||||
},
|
||||
{
|
||||
"height": 160,
|
||||
"url": "https://i.scdn.co/image/3430c976f100fc5065b96fb588b3341d568c4f42",
|
||||
"width": 160
|
||||
}
|
||||
],
|
||||
"name": "L2U",
|
||||
"popularity": 21,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:76pyFXpXITp0aRz4j3SyGJ"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/0j5kVHxvTgUN4nBIPKCLRJ"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 9504
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/0j5kVHxvTgUN4nBIPKCLRJ",
|
||||
"id": "0j5kVHxvTgUN4nBIPKCLRJ",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/827f2b45917c1cc7bdc750a86b4f075c85fa615d",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 320,
|
||||
"url": "https://i.scdn.co/image/3a16f063bc027a66e29343156be2c206575c773b",
|
||||
"width": 320
|
||||
},
|
||||
{
|
||||
"height": 160,
|
||||
"url": "https://i.scdn.co/image/f39027c69b58a49f13a07c778c215cdd592935b9",
|
||||
"width": 160
|
||||
}
|
||||
],
|
||||
"name": "Never Get Used To People",
|
||||
"popularity": 46,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:0j5kVHxvTgUN4nBIPKCLRJ"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/1TxfUEM21kYVWinDMOqWwb"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 121
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/1TxfUEM21kYVWinDMOqWwb",
|
||||
"id": "1TxfUEM21kYVWinDMOqWwb",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/ab67616d0000b27387b97641acd320159865afea",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 300,
|
||||
"url": "https://i.scdn.co/image/ab67616d00001e0287b97641acd320159865afea",
|
||||
"width": 300
|
||||
},
|
||||
{
|
||||
"height": 64,
|
||||
"url": "https://i.scdn.co/image/ab67616d0000485187b97641acd320159865afea",
|
||||
"width": 64
|
||||
}
|
||||
],
|
||||
"name": "2f U-Flow",
|
||||
"popularity": 29,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:1TxfUEM21kYVWinDMOqWwb"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/0iwKcRbay1SnKY1IH8MNL8"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 2
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/0iwKcRbay1SnKY1IH8MNL8",
|
||||
"id": "0iwKcRbay1SnKY1IH8MNL8",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/ab67616d0000b2739664d2726b29a5e642003027",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 300,
|
||||
"url": "https://i.scdn.co/image/ab67616d00001e029664d2726b29a5e642003027",
|
||||
"width": 300
|
||||
},
|
||||
{
|
||||
"height": 64,
|
||||
"url": "https://i.scdn.co/image/ab67616d000048519664d2726b29a5e642003027",
|
||||
"width": 64
|
||||
}
|
||||
],
|
||||
"name": "y27uri",
|
||||
"popularity": 30,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:0iwKcRbay1SnKY1IH8MNL8"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/2wTNs9AmIOv5Fjs66HK1tV"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 15791
|
||||
},
|
||||
"genres": [
|
||||
"rhythm game"
|
||||
],
|
||||
"href": "https://api.spotify.com/v1/artists/2wTNs9AmIOv5Fjs66HK1tV",
|
||||
"id": "2wTNs9AmIOv5Fjs66HK1tV",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/d4d7e6f174ee5be4c1099ccbe61220fcae904953",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 320,
|
||||
"url": "https://i.scdn.co/image/57a8c4bf2c20aece32d765ce9fc69330dd3cd18f",
|
||||
"width": 320
|
||||
},
|
||||
{
|
||||
"height": 160,
|
||||
"url": "https://i.scdn.co/image/1d9e1c6d0aa5f080dbca4fc7d2b5457b5d5d8011",
|
||||
"width": 160
|
||||
}
|
||||
],
|
||||
"name": "M2U",
|
||||
"popularity": 41,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:2wTNs9AmIOv5Fjs66HK1tV"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/1oUiDfTNWZCprR1GeRPs0i"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 15485
|
||||
},
|
||||
"genres": [
|
||||
"j-pixie",
|
||||
"japanese math rock"
|
||||
],
|
||||
"href": "https://api.spotify.com/v1/artists/1oUiDfTNWZCprR1GeRPs0i",
|
||||
"id": "1oUiDfTNWZCprR1GeRPs0i",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/5d40c50ce833008a578fa0c7d92fc65d0f222c54",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 320,
|
||||
"url": "https://i.scdn.co/image/0b346e14627b90cd25e9020443122bc32681baed",
|
||||
"width": 320
|
||||
},
|
||||
{
|
||||
"height": 160,
|
||||
"url": "https://i.scdn.co/image/f80ef7a4ec092e5bf054bc245b014963561639e5",
|
||||
"width": 160
|
||||
}
|
||||
],
|
||||
"name": "Lie and a Chameleon",
|
||||
"popularity": 41,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:1oUiDfTNWZCprR1GeRPs0i"
|
||||
},
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/6diA719p2OaW6zQnXCbRO9"
|
||||
},
|
||||
"followers": {
|
||||
"href": null,
|
||||
"total": 236
|
||||
},
|
||||
"genres": [],
|
||||
"href": "https://api.spotify.com/v1/artists/6diA719p2OaW6zQnXCbRO9",
|
||||
"id": "6diA719p2OaW6zQnXCbRO9",
|
||||
"images": [
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/ab67616d0000b2737934fbc7e0876496ee772792",
|
||||
"width": 640
|
||||
},
|
||||
{
|
||||
"height": 300,
|
||||
"url": "https://i.scdn.co/image/ab67616d00001e027934fbc7e0876496ee772792",
|
||||
"width": 300
|
||||
},
|
||||
{
|
||||
"height": 64,
|
||||
"url": "https://i.scdn.co/image/ab67616d000048517934fbc7e0876496ee772792",
|
||||
"width": 64
|
||||
}
|
||||
],
|
||||
"name": "US Two",
|
||||
"popularity": 32,
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:6diA719p2OaW6zQnXCbRO9"
|
||||
}
|
||||
],
|
||||
"limit": 20,
|
||||
"next": "https://api.spotify.com/v1/search?query=U2&type=artist&offset=20&limit=20",
|
||||
"offset": 0,
|
||||
"previous": null,
|
||||
"total": 922
|
||||
}
|
||||
}
|
||||
1
tests/fixtures/symlink
vendored
Symbolic link
1
tests/fixtures/symlink
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
index.html
|
||||
1
tests/fixtures/symlink2dir
vendored
Symbolic link
1
tests/fixtures/symlink2dir
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
empty_folder
|
||||
BIN
tests/fixtures/test.aiff
vendored
Normal file
BIN
tests/fixtures/test.aiff
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/test.flac
vendored
Normal file
BIN
tests/fixtures/test.flac
vendored
Normal file
Binary file not shown.
6
tests/fixtures/test.lrc
vendored
Normal file
6
tests/fixtures/test.lrc
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
[ar:Rick Astley]
|
||||
[ti:That one song]
|
||||
[offset:-100]
|
||||
[lang:eng]
|
||||
[00:18.80]We're no strangers to love
|
||||
[00:22.801]You know the rules and so do I
|
||||
BIN
tests/fixtures/test.m4a
vendored
Normal file
BIN
tests/fixtures/test.m4a
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/test.mp3
vendored
Normal file
BIN
tests/fixtures/test.mp3
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/test.ogg
vendored
Normal file
BIN
tests/fixtures/test.ogg
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/test.tak
vendored
Normal file
BIN
tests/fixtures/test.tak
vendored
Normal file
Binary file not shown.
2
tests/fixtures/test.txt
vendored
Normal file
2
tests/fixtures/test.txt
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
We're no strangers to love
|
||||
You know the rules and so do I
|
||||
BIN
tests/fixtures/test.wav
vendored
Normal file
BIN
tests/fixtures/test.wav
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/test.wma
vendored
Normal file
BIN
tests/fixtures/test.wma
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/test.wv
vendored
Normal file
BIN
tests/fixtures/test.wv
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/zero_replaygain.mp3
vendored
Normal file
BIN
tests/fixtures/zero_replaygain.mp3
vendored
Normal file
Binary file not shown.
33
tests/init_tests.go
Normal file
33
tests/init_tests.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
)
|
||||
|
||||
var once sync.Once
|
||||
|
||||
func Init(t *testing.T, skipOnShort bool) {
|
||||
if skipOnShort && testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
once.Do(func() {
|
||||
_, file, _, _ := runtime.Caller(0)
|
||||
appPath, _ := filepath.Abs(filepath.Join(filepath.Dir(file), ".."))
|
||||
confPath, _ := filepath.Abs(filepath.Join(appPath, "tests", "navidrome-test.toml"))
|
||||
println("Loading test configuration file from " + confPath)
|
||||
_ = os.Chdir(appPath)
|
||||
conf.LoadFromFile(confPath)
|
||||
|
||||
noLog := os.Getenv("NOLOG")
|
||||
if noLog != "" {
|
||||
log.SetLevel(log.LevelError)
|
||||
}
|
||||
})
|
||||
}
|
||||
161
tests/mock_album_repo.go
Normal file
161
tests/mock_album_repo.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/id"
|
||||
)
|
||||
|
||||
func CreateMockAlbumRepo() *MockAlbumRepo {
|
||||
return &MockAlbumRepo{
|
||||
Data: make(map[string]*model.Album),
|
||||
}
|
||||
}
|
||||
|
||||
type MockAlbumRepo struct {
|
||||
model.AlbumRepository
|
||||
Data map[string]*model.Album
|
||||
All model.Albums
|
||||
Err bool
|
||||
Options model.QueryOptions
|
||||
ReassignAnnotationCalls map[string]string // prevID -> newID
|
||||
}
|
||||
|
||||
func (m *MockAlbumRepo) SetError(err bool) {
|
||||
m.Err = err
|
||||
}
|
||||
|
||||
func (m *MockAlbumRepo) SetData(albums model.Albums) {
|
||||
m.Data = make(map[string]*model.Album, len(albums))
|
||||
m.All = albums
|
||||
for i, a := range m.All {
|
||||
m.Data[a.ID] = &m.All[i]
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockAlbumRepo) Exists(id string) (bool, error) {
|
||||
if m.Err {
|
||||
return false, errors.New("unexpected error")
|
||||
}
|
||||
_, found := m.Data[id]
|
||||
return found, nil
|
||||
}
|
||||
|
||||
func (m *MockAlbumRepo) Get(id string) (*model.Album, error) {
|
||||
if m.Err {
|
||||
return nil, errors.New("unexpected error")
|
||||
}
|
||||
if d, ok := m.Data[id]; ok {
|
||||
return d, nil
|
||||
}
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *MockAlbumRepo) Put(al *model.Album) error {
|
||||
if m.Err {
|
||||
return errors.New("unexpected error")
|
||||
}
|
||||
if al.ID == "" {
|
||||
al.ID = id.NewRandom()
|
||||
}
|
||||
m.Data[al.ID] = al
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockAlbumRepo) GetAll(qo ...model.QueryOptions) (model.Albums, error) {
|
||||
if len(qo) > 0 {
|
||||
m.Options = qo[0]
|
||||
}
|
||||
if m.Err {
|
||||
return nil, errors.New("unexpected error")
|
||||
}
|
||||
return m.All, nil
|
||||
}
|
||||
|
||||
func (m *MockAlbumRepo) IncPlayCount(id string, timestamp time.Time) error {
|
||||
if m.Err {
|
||||
return errors.New("unexpected error")
|
||||
}
|
||||
if d, ok := m.Data[id]; ok {
|
||||
d.PlayCount++
|
||||
d.PlayDate = ×tamp
|
||||
return nil
|
||||
}
|
||||
return model.ErrNotFound
|
||||
}
|
||||
func (m *MockAlbumRepo) CountAll(...model.QueryOptions) (int64, error) {
|
||||
return int64(len(m.All)), nil
|
||||
}
|
||||
|
||||
func (m *MockAlbumRepo) GetTouchedAlbums(libID int) (model.AlbumCursor, error) {
|
||||
if m.Err {
|
||||
return nil, errors.New("unexpected error")
|
||||
}
|
||||
return func(yield func(model.Album, error) bool) {
|
||||
for _, a := range m.Data {
|
||||
if a.ID == "error" {
|
||||
if !yield(*a, errors.New("error")) {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
if a.LibraryID != libID {
|
||||
continue
|
||||
}
|
||||
if !yield(*a, nil) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockAlbumRepo) UpdateExternalInfo(album *model.Album) error {
|
||||
if m.Err {
|
||||
return errors.New("unexpected error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockAlbumRepo) Search(q string, offset int, size int, options ...model.QueryOptions) (model.Albums, error) {
|
||||
if len(options) > 0 {
|
||||
m.Options = options[0]
|
||||
}
|
||||
if m.Err {
|
||||
return nil, errors.New("unexpected error")
|
||||
}
|
||||
// Simple mock implementation - just return all albums for testing
|
||||
return m.All, nil
|
||||
}
|
||||
|
||||
// ReassignAnnotation reassigns annotations from one album to another
|
||||
func (m *MockAlbumRepo) ReassignAnnotation(prevID string, newID string) error {
|
||||
if m.Err {
|
||||
return errors.New("unexpected error")
|
||||
}
|
||||
// Mock implementation - track the reassignment calls
|
||||
if m.ReassignAnnotationCalls == nil {
|
||||
m.ReassignAnnotationCalls = make(map[string]string)
|
||||
}
|
||||
m.ReassignAnnotationCalls[prevID] = newID
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetRating sets the rating for an album
|
||||
func (m *MockAlbumRepo) SetRating(rating int, itemID string) error {
|
||||
if m.Err {
|
||||
return errors.New("unexpected error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetStar sets the starred status for albums
|
||||
func (m *MockAlbumRepo) SetStar(starred bool, itemIDs ...string) error {
|
||||
if m.Err {
|
||||
return errors.New("unexpected error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ model.AlbumRepository = (*MockAlbumRepo)(nil)
|
||||
160
tests/mock_artist_repo.go
Normal file
160
tests/mock_artist_repo.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/id"
|
||||
)
|
||||
|
||||
func CreateMockArtistRepo() *MockArtistRepo {
|
||||
return &MockArtistRepo{
|
||||
Data: make(map[string]*model.Artist),
|
||||
}
|
||||
}
|
||||
|
||||
type MockArtistRepo struct {
|
||||
model.ArtistRepository
|
||||
Data map[string]*model.Artist
|
||||
Err bool
|
||||
Options model.QueryOptions
|
||||
}
|
||||
|
||||
func (m *MockArtistRepo) SetError(err bool) {
|
||||
m.Err = err
|
||||
}
|
||||
|
||||
func (m *MockArtistRepo) SetData(artists model.Artists) {
|
||||
m.Data = make(map[string]*model.Artist)
|
||||
for i, a := range artists {
|
||||
m.Data[a.ID] = &artists[i]
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockArtistRepo) Exists(id string) (bool, error) {
|
||||
if m.Err {
|
||||
return false, errors.New("Error!")
|
||||
}
|
||||
_, found := m.Data[id]
|
||||
return found, nil
|
||||
}
|
||||
|
||||
func (m *MockArtistRepo) Get(id string) (*model.Artist, error) {
|
||||
if m.Err {
|
||||
return nil, errors.New("Error!")
|
||||
}
|
||||
if d, ok := m.Data[id]; ok {
|
||||
return d, nil
|
||||
}
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *MockArtistRepo) Put(ar *model.Artist, columsToUpdate ...string) error {
|
||||
if m.Err {
|
||||
return errors.New("error")
|
||||
}
|
||||
if ar.ID == "" {
|
||||
ar.ID = id.NewRandom()
|
||||
}
|
||||
m.Data[ar.ID] = ar
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockArtistRepo) IncPlayCount(id string, timestamp time.Time) error {
|
||||
if m.Err {
|
||||
return errors.New("error")
|
||||
}
|
||||
if d, ok := m.Data[id]; ok {
|
||||
d.PlayCount++
|
||||
d.PlayDate = ×tamp
|
||||
return nil
|
||||
}
|
||||
return model.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *MockArtistRepo) GetAll(options ...model.QueryOptions) (model.Artists, error) {
|
||||
if len(options) > 0 {
|
||||
m.Options = options[0]
|
||||
}
|
||||
if m.Err {
|
||||
return nil, errors.New("mock repo error")
|
||||
}
|
||||
var allArtists model.Artists
|
||||
for _, artist := range m.Data {
|
||||
allArtists = append(allArtists, *artist)
|
||||
}
|
||||
// Apply Max=1 if present (simple simulation for findArtistByName)
|
||||
if len(options) > 0 && options[0].Max == 1 && len(allArtists) > 0 {
|
||||
return allArtists[:1], nil
|
||||
}
|
||||
return allArtists, nil
|
||||
}
|
||||
|
||||
func (m *MockArtistRepo) UpdateExternalInfo(artist *model.Artist) error {
|
||||
if m.Err {
|
||||
return errors.New("mock repo error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockArtistRepo) RefreshStats(allArtists bool) (int64, error) {
|
||||
if m.Err {
|
||||
return 0, errors.New("mock repo error")
|
||||
}
|
||||
return int64(len(m.Data)), nil
|
||||
}
|
||||
|
||||
func (m *MockArtistRepo) RefreshPlayCounts() (int64, error) {
|
||||
if m.Err {
|
||||
return 0, errors.New("mock repo error")
|
||||
}
|
||||
return int64(len(m.Data)), nil
|
||||
}
|
||||
|
||||
func (m *MockArtistRepo) GetIndex(includeMissing bool, libraryIds []int, roles ...model.Role) (model.ArtistIndexes, error) {
|
||||
if m.Err {
|
||||
return nil, errors.New("mock repo error")
|
||||
}
|
||||
|
||||
artists, err := m.GetAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For mock purposes, if no artists available, return empty result
|
||||
if len(artists) == 0 {
|
||||
return model.ArtistIndexes{}, nil
|
||||
}
|
||||
|
||||
// Simple index grouping by first letter (simplified implementation for mocks)
|
||||
indexMap := make(map[string]model.Artists)
|
||||
for _, artist := range artists {
|
||||
key := "#"
|
||||
if len(artist.Name) > 0 {
|
||||
key = string(artist.Name[0])
|
||||
}
|
||||
indexMap[key] = append(indexMap[key], artist)
|
||||
}
|
||||
|
||||
var result model.ArtistIndexes
|
||||
for k, artists := range indexMap {
|
||||
result = append(result, model.ArtistIndex{ID: k, Artists: artists})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *MockArtistRepo) Search(q string, offset int, size int, options ...model.QueryOptions) (model.Artists, error) {
|
||||
if len(options) > 0 {
|
||||
m.Options = options[0]
|
||||
}
|
||||
if m.Err {
|
||||
return nil, errors.New("unexpected error")
|
||||
}
|
||||
// Simple mock implementation - just return all artists for testing
|
||||
allArtists, err := m.GetAll()
|
||||
return allArtists, err
|
||||
}
|
||||
|
||||
var _ model.ArtistRepository = (*MockArtistRepo)(nil)
|
||||
287
tests/mock_data_store.go
Normal file
287
tests/mock_data_store.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
)
|
||||
|
||||
type MockDataStore struct {
|
||||
RealDS model.DataStore
|
||||
MockedLibrary model.LibraryRepository
|
||||
MockedFolder model.FolderRepository
|
||||
MockedGenre model.GenreRepository
|
||||
MockedAlbum model.AlbumRepository
|
||||
MockedArtist model.ArtistRepository
|
||||
MockedMediaFile model.MediaFileRepository
|
||||
MockedTag model.TagRepository
|
||||
MockedUser model.UserRepository
|
||||
MockedProperty model.PropertyRepository
|
||||
MockedPlayer model.PlayerRepository
|
||||
MockedPlaylist model.PlaylistRepository
|
||||
MockedPlayQueue model.PlayQueueRepository
|
||||
MockedShare model.ShareRepository
|
||||
MockedTranscoding model.TranscodingRepository
|
||||
MockedUserProps model.UserPropsRepository
|
||||
MockedScrobbleBuffer model.ScrobbleBufferRepository
|
||||
MockedScrobble model.ScrobbleRepository
|
||||
MockedRadio model.RadioRepository
|
||||
scrobbleBufferMu sync.Mutex
|
||||
repoMu sync.Mutex
|
||||
|
||||
// GC tracking
|
||||
GCCalled bool
|
||||
GCError error
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Library(ctx context.Context) model.LibraryRepository {
|
||||
if db.MockedLibrary == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedLibrary = db.RealDS.Library(ctx)
|
||||
} else {
|
||||
db.MockedLibrary = &MockLibraryRepo{}
|
||||
}
|
||||
}
|
||||
return db.MockedLibrary
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Folder(ctx context.Context) model.FolderRepository {
|
||||
if db.MockedFolder == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedFolder = db.RealDS.Folder(ctx)
|
||||
} else {
|
||||
db.MockedFolder = struct{ model.FolderRepository }{}
|
||||
}
|
||||
}
|
||||
return db.MockedFolder
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Tag(ctx context.Context) model.TagRepository {
|
||||
if db.MockedTag == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedTag = db.RealDS.Tag(ctx)
|
||||
} else {
|
||||
db.MockedTag = struct{ model.TagRepository }{}
|
||||
}
|
||||
}
|
||||
return db.MockedTag
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Album(ctx context.Context) model.AlbumRepository {
|
||||
if db.MockedAlbum == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedAlbum = db.RealDS.Album(ctx)
|
||||
} else {
|
||||
db.MockedAlbum = CreateMockAlbumRepo()
|
||||
}
|
||||
}
|
||||
return db.MockedAlbum
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Artist(ctx context.Context) model.ArtistRepository {
|
||||
if db.MockedArtist == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedArtist = db.RealDS.Artist(ctx)
|
||||
} else {
|
||||
db.MockedArtist = CreateMockArtistRepo()
|
||||
}
|
||||
}
|
||||
return db.MockedArtist
|
||||
}
|
||||
|
||||
func (db *MockDataStore) MediaFile(ctx context.Context) model.MediaFileRepository {
|
||||
db.repoMu.Lock()
|
||||
defer db.repoMu.Unlock()
|
||||
if db.MockedMediaFile == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedMediaFile = db.RealDS.MediaFile(ctx)
|
||||
} else {
|
||||
db.MockedMediaFile = CreateMockMediaFileRepo()
|
||||
}
|
||||
}
|
||||
return db.MockedMediaFile
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Genre(ctx context.Context) model.GenreRepository {
|
||||
if db.MockedGenre == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedGenre = db.RealDS.Genre(ctx)
|
||||
} else {
|
||||
db.MockedGenre = &MockedGenreRepo{}
|
||||
}
|
||||
}
|
||||
return db.MockedGenre
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Playlist(ctx context.Context) model.PlaylistRepository {
|
||||
if db.MockedPlaylist == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedPlaylist = db.RealDS.Playlist(ctx)
|
||||
} else {
|
||||
db.MockedPlaylist = &MockPlaylistRepo{}
|
||||
}
|
||||
}
|
||||
return db.MockedPlaylist
|
||||
}
|
||||
|
||||
func (db *MockDataStore) PlayQueue(ctx context.Context) model.PlayQueueRepository {
|
||||
if db.MockedPlayQueue == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedPlayQueue = db.RealDS.PlayQueue(ctx)
|
||||
} else {
|
||||
db.MockedPlayQueue = &MockPlayQueueRepo{}
|
||||
}
|
||||
}
|
||||
return db.MockedPlayQueue
|
||||
}
|
||||
|
||||
func (db *MockDataStore) UserProps(ctx context.Context) model.UserPropsRepository {
|
||||
if db.MockedUserProps == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedUserProps = db.RealDS.UserProps(ctx)
|
||||
} else {
|
||||
db.MockedUserProps = &MockedUserPropsRepo{}
|
||||
}
|
||||
}
|
||||
return db.MockedUserProps
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Property(ctx context.Context) model.PropertyRepository {
|
||||
if db.MockedProperty == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedProperty = db.RealDS.Property(ctx)
|
||||
} else {
|
||||
db.MockedProperty = &MockedPropertyRepo{}
|
||||
}
|
||||
}
|
||||
return db.MockedProperty
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Share(ctx context.Context) model.ShareRepository {
|
||||
if db.MockedShare == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedShare = db.RealDS.Share(ctx)
|
||||
} else {
|
||||
db.MockedShare = &MockShareRepo{}
|
||||
}
|
||||
}
|
||||
return db.MockedShare
|
||||
}
|
||||
|
||||
func (db *MockDataStore) User(ctx context.Context) model.UserRepository {
|
||||
if db.MockedUser == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedUser = db.RealDS.User(ctx)
|
||||
} else {
|
||||
db.MockedUser = CreateMockUserRepo()
|
||||
}
|
||||
}
|
||||
return db.MockedUser
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Transcoding(ctx context.Context) model.TranscodingRepository {
|
||||
if db.MockedTranscoding == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedTranscoding = db.RealDS.Transcoding(ctx)
|
||||
} else {
|
||||
db.MockedTranscoding = struct{ model.TranscodingRepository }{}
|
||||
}
|
||||
}
|
||||
return db.MockedTranscoding
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Player(ctx context.Context) model.PlayerRepository {
|
||||
if db.MockedPlayer == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedPlayer = db.RealDS.Player(ctx)
|
||||
} else {
|
||||
db.MockedPlayer = struct{ model.PlayerRepository }{}
|
||||
}
|
||||
}
|
||||
return db.MockedPlayer
|
||||
}
|
||||
|
||||
func (db *MockDataStore) ScrobbleBuffer(ctx context.Context) model.ScrobbleBufferRepository {
|
||||
db.scrobbleBufferMu.Lock()
|
||||
defer db.scrobbleBufferMu.Unlock()
|
||||
if db.MockedScrobbleBuffer == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedScrobbleBuffer = db.RealDS.ScrobbleBuffer(ctx)
|
||||
} else {
|
||||
db.MockedScrobbleBuffer = &MockedScrobbleBufferRepo{}
|
||||
}
|
||||
}
|
||||
return db.MockedScrobbleBuffer
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Scrobble(ctx context.Context) model.ScrobbleRepository {
|
||||
if db.MockedScrobble == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedScrobble = db.RealDS.Scrobble(ctx)
|
||||
} else {
|
||||
db.MockedScrobble = &MockScrobbleRepo{ctx: ctx}
|
||||
}
|
||||
}
|
||||
return db.MockedScrobble
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Radio(ctx context.Context) model.RadioRepository {
|
||||
if db.MockedRadio == nil {
|
||||
if db.RealDS != nil {
|
||||
db.MockedRadio = db.RealDS.Radio(ctx)
|
||||
} else {
|
||||
db.MockedRadio = CreateMockedRadioRepo()
|
||||
}
|
||||
}
|
||||
return db.MockedRadio
|
||||
}
|
||||
|
||||
func (db *MockDataStore) WithTx(block func(tx model.DataStore) error, label ...string) error {
|
||||
return block(db)
|
||||
}
|
||||
|
||||
func (db *MockDataStore) WithTxImmediate(block func(tx model.DataStore) error, label ...string) error {
|
||||
return block(db)
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Resource(ctx context.Context, m any) model.ResourceRepository {
|
||||
switch m.(type) {
|
||||
case model.MediaFile, *model.MediaFile:
|
||||
return db.MediaFile(ctx).(model.ResourceRepository)
|
||||
case model.Album, *model.Album:
|
||||
return db.Album(ctx).(model.ResourceRepository)
|
||||
case model.Artist, *model.Artist:
|
||||
return db.Artist(ctx).(model.ResourceRepository)
|
||||
case model.User, *model.User:
|
||||
return db.User(ctx).(model.ResourceRepository)
|
||||
case model.Playlist, *model.Playlist:
|
||||
return db.Playlist(ctx).(model.ResourceRepository)
|
||||
case model.Radio, *model.Radio:
|
||||
return db.Radio(ctx).(model.ResourceRepository)
|
||||
case model.Share, *model.Share:
|
||||
return db.Share(ctx).(model.ResourceRepository)
|
||||
case model.Genre, *model.Genre:
|
||||
return db.Genre(ctx).(model.ResourceRepository)
|
||||
case model.Tag, *model.Tag:
|
||||
return db.Tag(ctx).(model.ResourceRepository)
|
||||
case model.Transcoding, *model.Transcoding:
|
||||
return db.Transcoding(ctx).(model.ResourceRepository)
|
||||
case model.Player, *model.Player:
|
||||
return db.Player(ctx).(model.ResourceRepository)
|
||||
default:
|
||||
return struct{ model.ResourceRepository }{}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *MockDataStore) GC(context.Context, ...int) error {
|
||||
db.GCCalled = true
|
||||
if db.GCError != nil {
|
||||
return db.GCError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *MockDataStore) ReindexAll(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
70
tests/mock_ffmpeg.go
Normal file
70
tests/mock_ffmpeg.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
func NewMockFFmpeg(data string) *MockFFmpeg {
|
||||
return &MockFFmpeg{Reader: strings.NewReader(data)}
|
||||
}
|
||||
|
||||
type MockFFmpeg struct {
|
||||
io.Reader
|
||||
lock sync.Mutex
|
||||
closed atomic.Bool
|
||||
Error error
|
||||
}
|
||||
|
||||
func (ff *MockFFmpeg) IsAvailable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ff *MockFFmpeg) Transcode(context.Context, string, string, int, int) (io.ReadCloser, error) {
|
||||
if ff.Error != nil {
|
||||
return nil, ff.Error
|
||||
}
|
||||
return ff, nil
|
||||
}
|
||||
|
||||
func (ff *MockFFmpeg) ExtractImage(context.Context, string) (io.ReadCloser, error) {
|
||||
if ff.Error != nil {
|
||||
return nil, ff.Error
|
||||
}
|
||||
return ff, nil
|
||||
}
|
||||
|
||||
func (ff *MockFFmpeg) Probe(context.Context, []string) (string, error) {
|
||||
if ff.Error != nil {
|
||||
return "", ff.Error
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
func (ff *MockFFmpeg) CmdPath() (string, error) {
|
||||
if ff.Error != nil {
|
||||
return "", ff.Error
|
||||
}
|
||||
return "ffmpeg", nil
|
||||
}
|
||||
|
||||
func (ff *MockFFmpeg) Version() string {
|
||||
return "1.0"
|
||||
}
|
||||
|
||||
func (ff *MockFFmpeg) Read(p []byte) (n int, err error) {
|
||||
ff.lock.Lock()
|
||||
defer ff.lock.Unlock()
|
||||
return ff.Reader.Read(p)
|
||||
}
|
||||
|
||||
func (ff *MockFFmpeg) Close() error {
|
||||
ff.closed.Store(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ff *MockFFmpeg) IsClosed() bool {
|
||||
return ff.closed.Load()
|
||||
}
|
||||
38
tests/mock_genre_repo.go
Normal file
38
tests/mock_genre_repo.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"github.com/navidrome/navidrome/model"
|
||||
)
|
||||
|
||||
type MockedGenreRepo struct {
|
||||
Error error
|
||||
Data map[string]model.Genre
|
||||
}
|
||||
|
||||
func (r *MockedGenreRepo) init() {
|
||||
if r.Data == nil {
|
||||
r.Data = make(map[string]model.Genre)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MockedGenreRepo) GetAll(...model.QueryOptions) (model.Genres, error) {
|
||||
if r.Error != nil {
|
||||
return nil, r.Error
|
||||
}
|
||||
r.init()
|
||||
|
||||
var all model.Genres
|
||||
for _, g := range r.Data {
|
||||
all = append(all, g)
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
|
||||
func (r *MockedGenreRepo) Put(g *model.Genre) error {
|
||||
if r.Error != nil {
|
||||
return r.Error
|
||||
}
|
||||
r.init()
|
||||
r.Data[g.ID] = *g
|
||||
return nil
|
||||
}
|
||||
312
tests/mock_library_repo.go
Normal file
312
tests/mock_library_repo.go
Normal file
@@ -0,0 +1,312 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/deluan/rest"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
)
|
||||
|
||||
type MockLibraryRepo struct {
|
||||
model.LibraryRepository
|
||||
Data map[int]model.Library
|
||||
Err error
|
||||
PutFn func(*model.Library) error // Allow custom Put behavior for testing
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) SetData(data model.Libraries) {
|
||||
m.Data = make(map[int]model.Library)
|
||||
for _, d := range data {
|
||||
m.Data[d.ID] = d
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) GetAll(...model.QueryOptions) (model.Libraries, error) {
|
||||
if m.Err != nil {
|
||||
return nil, m.Err
|
||||
}
|
||||
var libraries model.Libraries
|
||||
for _, lib := range m.Data {
|
||||
libraries = append(libraries, lib)
|
||||
}
|
||||
// Sort by ID for predictable order
|
||||
slices.SortFunc(libraries, func(a, b model.Library) int {
|
||||
return a.ID - b.ID
|
||||
})
|
||||
return libraries, nil
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) CountAll(qo ...model.QueryOptions) (int64, error) {
|
||||
if m.Err != nil {
|
||||
return 0, m.Err
|
||||
}
|
||||
|
||||
// If no query options, return total count
|
||||
if len(qo) == 0 || qo[0].Filters == nil {
|
||||
return int64(len(m.Data)), nil
|
||||
}
|
||||
|
||||
// Handle squirrel.Eq filter for ID validation
|
||||
if eq, ok := qo[0].Filters.(squirrel.Eq); ok {
|
||||
if idFilter, exists := eq["id"]; exists {
|
||||
if ids, isSlice := idFilter.([]int); isSlice {
|
||||
count := 0
|
||||
for _, id := range ids {
|
||||
if _, exists := m.Data[id]; exists {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return int64(count), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to total count for other filters
|
||||
return int64(len(m.Data)), nil
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) Get(id int) (*model.Library, error) {
|
||||
if m.Err != nil {
|
||||
return nil, m.Err
|
||||
}
|
||||
if lib, ok := m.Data[id]; ok {
|
||||
return &lib, nil
|
||||
}
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) GetPath(id int) (string, error) {
|
||||
if m.Err != nil {
|
||||
return "", m.Err
|
||||
}
|
||||
if lib, ok := m.Data[id]; ok {
|
||||
return lib.Path, nil
|
||||
}
|
||||
return "", model.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) Put(library *model.Library) error {
|
||||
if m.PutFn != nil {
|
||||
return m.PutFn(library)
|
||||
}
|
||||
if m.Err != nil {
|
||||
return m.Err
|
||||
}
|
||||
if m.Data == nil {
|
||||
m.Data = make(map[int]model.Library)
|
||||
}
|
||||
m.Data[library.ID] = *library
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) Delete(id int) error {
|
||||
if m.Err != nil {
|
||||
return m.Err
|
||||
}
|
||||
if _, ok := m.Data[id]; !ok {
|
||||
return model.ErrNotFound
|
||||
}
|
||||
delete(m.Data, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) StoreMusicFolder() error {
|
||||
if m.Err != nil {
|
||||
return m.Err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) AddArtist(id int, artistID string) error {
|
||||
if m.Err != nil {
|
||||
return m.Err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) ScanBegin(id int, fullScan bool) error {
|
||||
if m.Err != nil {
|
||||
return m.Err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) ScanEnd(id int) error {
|
||||
if m.Err != nil {
|
||||
return m.Err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) ScanInProgress() (bool, error) {
|
||||
if m.Err != nil {
|
||||
return false, m.Err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) RefreshStats(id int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// User-library association methods - mock implementations
|
||||
|
||||
func (m *MockLibraryRepo) GetUsersWithLibraryAccess(libraryID int) (model.Users, error) {
|
||||
if m.Err != nil {
|
||||
return nil, m.Err
|
||||
}
|
||||
// Mock: return empty users for now
|
||||
return model.Users{}, nil
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) Count(options ...rest.QueryOptions) (int64, error) {
|
||||
return m.CountAll()
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) Read(id string) (interface{}, error) {
|
||||
idInt, _ := strconv.Atoi(id)
|
||||
mf, err := m.Get(idInt)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil, rest.ErrNotFound
|
||||
}
|
||||
return mf, err
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) ReadAll(options ...rest.QueryOptions) (interface{}, error) {
|
||||
return m.GetAll()
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) EntityName() string {
|
||||
return "library"
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) NewInstance() interface{} {
|
||||
return &model.Library{}
|
||||
}
|
||||
|
||||
// REST Repository methods (string-based IDs)
|
||||
|
||||
func (m *MockLibraryRepo) Save(entity interface{}) (string, error) {
|
||||
lib := entity.(*model.Library)
|
||||
if m.Err != nil {
|
||||
return "", m.Err
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if lib.Name == "" {
|
||||
return "", &rest.ValidationError{Errors: map[string]string{"name": "library name is required"}}
|
||||
}
|
||||
if lib.Path == "" {
|
||||
return "", &rest.ValidationError{Errors: map[string]string{"path": "library path is required"}}
|
||||
}
|
||||
|
||||
// Generate ID if not set
|
||||
if lib.ID == 0 {
|
||||
lib.ID = len(m.Data) + 1
|
||||
}
|
||||
if m.Data == nil {
|
||||
m.Data = make(map[int]model.Library)
|
||||
}
|
||||
m.Data[lib.ID] = *lib
|
||||
return strconv.Itoa(lib.ID), nil
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) Update(id string, entity interface{}, cols ...string) error {
|
||||
lib := entity.(*model.Library)
|
||||
if m.Err != nil {
|
||||
return m.Err
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if lib.Name == "" {
|
||||
return &rest.ValidationError{Errors: map[string]string{"name": "library name is required"}}
|
||||
}
|
||||
if lib.Path == "" {
|
||||
return &rest.ValidationError{Errors: map[string]string{"path": "library path is required"}}
|
||||
}
|
||||
|
||||
idInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return errors.New("invalid ID format")
|
||||
}
|
||||
if _, exists := m.Data[idInt]; !exists {
|
||||
return rest.ErrNotFound
|
||||
}
|
||||
lib.ID = idInt
|
||||
m.Data[idInt] = *lib
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) DeleteByStringID(id string) error {
|
||||
if m.Err != nil {
|
||||
return m.Err
|
||||
}
|
||||
idInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return errors.New("invalid ID format")
|
||||
}
|
||||
if _, exists := m.Data[idInt]; !exists {
|
||||
return rest.ErrNotFound
|
||||
}
|
||||
delete(m.Data, idInt)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Service-level methods for core.Library interface
|
||||
|
||||
func (m *MockLibraryRepo) GetUserLibraries(ctx context.Context, userID string) (model.Libraries, error) {
|
||||
if m.Err != nil {
|
||||
return nil, m.Err
|
||||
}
|
||||
if userID == "non-existent" {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
// Convert map to slice for return
|
||||
var libraries model.Libraries
|
||||
for _, lib := range m.Data {
|
||||
libraries = append(libraries, lib)
|
||||
}
|
||||
// Sort by ID for predictable order
|
||||
slices.SortFunc(libraries, func(a, b model.Library) int {
|
||||
return a.ID - b.ID
|
||||
})
|
||||
return libraries, nil
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) SetUserLibraries(ctx context.Context, userID string, libraryIDs []int) error {
|
||||
if m.Err != nil {
|
||||
return m.Err
|
||||
}
|
||||
if userID == "non-existent" {
|
||||
return model.ErrNotFound
|
||||
}
|
||||
if userID == "admin-1" {
|
||||
return fmt.Errorf("%w: cannot manually assign libraries to admin users", model.ErrValidation)
|
||||
}
|
||||
if len(libraryIDs) == 0 {
|
||||
return fmt.Errorf("%w: at least one library must be assigned to non-admin users", model.ErrValidation)
|
||||
}
|
||||
// Validate all library IDs exist
|
||||
for _, id := range libraryIDs {
|
||||
if _, exists := m.Data[id]; !exists {
|
||||
return fmt.Errorf("%w: library ID %d does not exist", model.ErrValidation, id)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockLibraryRepo) ValidateLibraryAccess(ctx context.Context, userID string, libraryID int) error {
|
||||
if m.Err != nil {
|
||||
return m.Err
|
||||
}
|
||||
// For testing purposes, allow access to all libraries
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ model.LibraryRepository = (*MockLibraryRepo)(nil)
|
||||
var _ model.ResourceRepository = (*MockLibraryRepo)(nil)
|
||||
299
tests/mock_mediafile_repo.go
Normal file
299
tests/mock_mediafile_repo.go
Normal file
@@ -0,0 +1,299 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"maps"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/deluan/rest"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/id"
|
||||
"github.com/navidrome/navidrome/utils/slice"
|
||||
)
|
||||
|
||||
func CreateMockMediaFileRepo() *MockMediaFileRepo {
|
||||
return &MockMediaFileRepo{
|
||||
Data: make(map[string]*model.MediaFile),
|
||||
}
|
||||
}
|
||||
|
||||
type MockMediaFileRepo struct {
|
||||
model.MediaFileRepository
|
||||
Data map[string]*model.MediaFile
|
||||
Err bool
|
||||
// Add fields and methods for controlling CountAll and DeleteAllMissing in tests
|
||||
CountAllValue int64
|
||||
CountAllOptions model.QueryOptions
|
||||
DeleteAllMissingValue int64
|
||||
Options model.QueryOptions
|
||||
// Add fields for cross-library move detection tests
|
||||
FindRecentFilesByMBZTrackIDFunc func(missing model.MediaFile, since time.Time) (model.MediaFiles, error)
|
||||
FindRecentFilesByPropertiesFunc func(missing model.MediaFile, since time.Time) (model.MediaFiles, error)
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) SetError(err bool) {
|
||||
m.Err = err
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) SetData(mfs model.MediaFiles) {
|
||||
m.Data = make(map[string]*model.MediaFile)
|
||||
for i, mf := range mfs {
|
||||
m.Data[mf.ID] = &mfs[i]
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) Exists(id string) (bool, error) {
|
||||
if m.Err {
|
||||
return false, errors.New("error")
|
||||
}
|
||||
_, found := m.Data[id]
|
||||
return found, nil
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) Get(id string) (*model.MediaFile, error) {
|
||||
if m.Err {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
if d, ok := m.Data[id]; ok {
|
||||
// Intentionally clone the file and remove participants. This should
|
||||
// catch any caller that actually means to call GetWithParticipants
|
||||
res := *d
|
||||
res.Participants = model.Participants{}
|
||||
return &res, nil
|
||||
}
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) GetWithParticipants(id string) (*model.MediaFile, error) {
|
||||
if m.Err {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
if d, ok := m.Data[id]; ok {
|
||||
return d, nil
|
||||
}
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) GetAll(qo ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
if len(qo) > 0 {
|
||||
m.Options = qo[0]
|
||||
}
|
||||
if m.Err {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
values := slices.Collect(maps.Values(m.Data))
|
||||
result := slice.Map(values, func(p *model.MediaFile) model.MediaFile {
|
||||
return *p
|
||||
})
|
||||
// Sort by ID to ensure deterministic ordering for tests
|
||||
slices.SortFunc(result, func(a, b model.MediaFile) int {
|
||||
return cmp.Compare(a.ID, b.ID)
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) Put(mf *model.MediaFile) error {
|
||||
if m.Err {
|
||||
return errors.New("error")
|
||||
}
|
||||
if mf.ID == "" {
|
||||
mf.ID = id.NewRandom()
|
||||
}
|
||||
m.Data[mf.ID] = mf
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) Delete(id string) error {
|
||||
if m.Err {
|
||||
return errors.New("error")
|
||||
}
|
||||
if _, ok := m.Data[id]; !ok {
|
||||
return model.ErrNotFound
|
||||
}
|
||||
delete(m.Data, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) IncPlayCount(id string, timestamp time.Time) error {
|
||||
if m.Err {
|
||||
return errors.New("error")
|
||||
}
|
||||
if d, ok := m.Data[id]; ok {
|
||||
d.PlayCount++
|
||||
d.PlayDate = ×tamp
|
||||
return nil
|
||||
}
|
||||
return model.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) FindByAlbum(artistId string) (model.MediaFiles, error) {
|
||||
if m.Err {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
var res = make(model.MediaFiles, len(m.Data))
|
||||
i := 0
|
||||
for _, a := range m.Data {
|
||||
if a.AlbumID == artistId {
|
||||
res[i] = *a
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) GetMissingAndMatching(libId int) (model.MediaFileCursor, error) {
|
||||
if m.Err {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
var res model.MediaFiles
|
||||
for _, a := range m.Data {
|
||||
if a.LibraryID == libId && a.Missing {
|
||||
res = append(res, *a)
|
||||
}
|
||||
}
|
||||
|
||||
for _, a := range m.Data {
|
||||
if a.LibraryID == libId && !(*a).Missing && slices.IndexFunc(res, func(mediaFile model.MediaFile) bool {
|
||||
return mediaFile.PID == a.PID
|
||||
}) != -1 {
|
||||
res = append(res, *a)
|
||||
}
|
||||
}
|
||||
slices.SortFunc(res, func(i, j model.MediaFile) int {
|
||||
return cmp.Or(
|
||||
cmp.Compare(i.PID, j.PID),
|
||||
cmp.Compare(i.ID, j.ID),
|
||||
)
|
||||
})
|
||||
|
||||
return func(yield func(model.MediaFile, error) bool) {
|
||||
for _, a := range res {
|
||||
if !yield(a, nil) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) CountAll(opts ...model.QueryOptions) (int64, error) {
|
||||
if m.Err {
|
||||
return 0, errors.New("error")
|
||||
}
|
||||
if m.CountAllValue != 0 {
|
||||
if len(opts) > 0 {
|
||||
m.CountAllOptions = opts[0]
|
||||
}
|
||||
return m.CountAllValue, nil
|
||||
}
|
||||
return int64(len(m.Data)), nil
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) DeleteAllMissing() (int64, error) {
|
||||
if m.Err {
|
||||
return 0, errors.New("error")
|
||||
}
|
||||
if m.DeleteAllMissingValue != 0 {
|
||||
return m.DeleteAllMissingValue, nil
|
||||
}
|
||||
// Remove all missing files from Data
|
||||
var count int64
|
||||
for id, mf := range m.Data {
|
||||
if mf.Missing {
|
||||
delete(m.Data, id)
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// ResourceRepository methods
|
||||
func (m *MockMediaFileRepo) Count(...rest.QueryOptions) (int64, error) {
|
||||
return m.CountAll()
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) Read(id string) (interface{}, error) {
|
||||
mf, err := m.Get(id)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil, rest.ErrNotFound
|
||||
}
|
||||
return mf, err
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) ReadAll(...rest.QueryOptions) (interface{}, error) {
|
||||
return m.GetAll()
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) EntityName() string {
|
||||
return "mediafile"
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) NewInstance() interface{} {
|
||||
return &model.MediaFile{}
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) Search(q string, offset int, size int, options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
if len(options) > 0 {
|
||||
m.Options = options[0]
|
||||
}
|
||||
if m.Err {
|
||||
return nil, errors.New("unexpected error")
|
||||
}
|
||||
// Simple mock implementation - just return all media files for testing
|
||||
allFiles, err := m.GetAll()
|
||||
return allFiles, err
|
||||
}
|
||||
|
||||
// Cross-library move detection mock methods
|
||||
func (m *MockMediaFileRepo) FindRecentFilesByMBZTrackID(missing model.MediaFile, since time.Time) (model.MediaFiles, error) {
|
||||
if m.Err {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
if m.FindRecentFilesByMBZTrackIDFunc != nil {
|
||||
return m.FindRecentFilesByMBZTrackIDFunc(missing, since)
|
||||
}
|
||||
// Default implementation: find files with same MBZ Track ID in other libraries
|
||||
var result model.MediaFiles
|
||||
for _, mf := range m.Data {
|
||||
if mf.LibraryID != missing.LibraryID &&
|
||||
mf.MbzReleaseTrackID == missing.MbzReleaseTrackID &&
|
||||
mf.MbzReleaseTrackID != "" &&
|
||||
mf.Suffix == missing.Suffix &&
|
||||
mf.CreatedAt.After(since) &&
|
||||
!mf.Missing {
|
||||
result = append(result, *mf)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *MockMediaFileRepo) FindRecentFilesByProperties(missing model.MediaFile, since time.Time) (model.MediaFiles, error) {
|
||||
if m.Err {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
if m.FindRecentFilesByPropertiesFunc != nil {
|
||||
return m.FindRecentFilesByPropertiesFunc(missing, since)
|
||||
}
|
||||
// Default implementation: find files with same properties in other libraries
|
||||
var result model.MediaFiles
|
||||
for _, mf := range m.Data {
|
||||
if mf.LibraryID != missing.LibraryID &&
|
||||
mf.Title == missing.Title &&
|
||||
mf.Size == missing.Size &&
|
||||
mf.Suffix == missing.Suffix &&
|
||||
mf.DiscNumber == missing.DiscNumber &&
|
||||
mf.TrackNumber == missing.TrackNumber &&
|
||||
mf.Album == missing.Album &&
|
||||
mf.MbzReleaseTrackID == "" && // Exclude files with MBZ Track ID
|
||||
mf.CreatedAt.After(since) &&
|
||||
!mf.Missing {
|
||||
result = append(result, *mf)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var _ model.MediaFileRepository = (*MockMediaFileRepo)(nil)
|
||||
var _ model.ResourceRepository = (*MockMediaFileRepo)(nil)
|
||||
33
tests/mock_playlist_repo.go
Normal file
33
tests/mock_playlist_repo.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"github.com/deluan/rest"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
)
|
||||
|
||||
type MockPlaylistRepo struct {
|
||||
model.PlaylistRepository
|
||||
|
||||
Entity *model.Playlist
|
||||
Error error
|
||||
}
|
||||
|
||||
func (m *MockPlaylistRepo) Get(_ string) (*model.Playlist, error) {
|
||||
if m.Error != nil {
|
||||
return nil, m.Error
|
||||
}
|
||||
if m.Entity == nil {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
return m.Entity, nil
|
||||
}
|
||||
|
||||
func (m *MockPlaylistRepo) Count(_ ...rest.QueryOptions) (int64, error) {
|
||||
if m.Error != nil {
|
||||
return 0, m.Error
|
||||
}
|
||||
if m.Entity == nil {
|
||||
return 0, nil
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
65
tests/mock_playqueue_repo.go
Normal file
65
tests/mock_playqueue_repo.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
)
|
||||
|
||||
type MockPlayQueueRepo struct {
|
||||
model.PlayQueueRepository
|
||||
Queue *model.PlayQueue
|
||||
Err bool
|
||||
LastCols []string
|
||||
}
|
||||
|
||||
func (m *MockPlayQueueRepo) Store(q *model.PlayQueue, cols ...string) error {
|
||||
if m.Err {
|
||||
return errors.New("error")
|
||||
}
|
||||
copyItems := make(model.MediaFiles, len(q.Items))
|
||||
copy(copyItems, q.Items)
|
||||
qCopy := *q
|
||||
qCopy.Items = copyItems
|
||||
m.Queue = &qCopy
|
||||
m.LastCols = cols
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPlayQueueRepo) RetrieveWithMediaFiles(userId string) (*model.PlayQueue, error) {
|
||||
if m.Err {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
if m.Queue == nil || m.Queue.UserID != userId {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
copyItems := make(model.MediaFiles, len(m.Queue.Items))
|
||||
copy(copyItems, m.Queue.Items)
|
||||
qCopy := *m.Queue
|
||||
qCopy.Items = copyItems
|
||||
return &qCopy, nil
|
||||
}
|
||||
|
||||
func (m *MockPlayQueueRepo) Retrieve(userId string) (*model.PlayQueue, error) {
|
||||
if m.Err {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
if m.Queue == nil || m.Queue.UserID != userId {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
copyItems := make(model.MediaFiles, len(m.Queue.Items))
|
||||
for i, t := range m.Queue.Items {
|
||||
copyItems[i] = model.MediaFile{ID: t.ID}
|
||||
}
|
||||
qCopy := *m.Queue
|
||||
qCopy.Items = copyItems
|
||||
return &qCopy, nil
|
||||
}
|
||||
|
||||
func (m *MockPlayQueueRepo) Clear(userId string) error {
|
||||
if m.Err {
|
||||
return errors.New("error")
|
||||
}
|
||||
m.Queue = nil
|
||||
return nil
|
||||
}
|
||||
59
tests/mock_property_repo.go
Normal file
59
tests/mock_property_repo.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package tests
|
||||
|
||||
import "github.com/navidrome/navidrome/model"
|
||||
|
||||
type MockedPropertyRepo struct {
|
||||
model.PropertyRepository
|
||||
Error error
|
||||
Data map[string]string
|
||||
}
|
||||
|
||||
func (p *MockedPropertyRepo) init() {
|
||||
if p.Data == nil {
|
||||
p.Data = make(map[string]string)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MockedPropertyRepo) Put(id string, value string) error {
|
||||
if p.Error != nil {
|
||||
return p.Error
|
||||
}
|
||||
p.init()
|
||||
p.Data[id] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MockedPropertyRepo) Get(id string) (string, error) {
|
||||
if p.Error != nil {
|
||||
return "", p.Error
|
||||
}
|
||||
p.init()
|
||||
if v, ok := p.Data[id]; ok {
|
||||
return v, nil
|
||||
}
|
||||
return "", model.ErrNotFound
|
||||
}
|
||||
|
||||
func (p *MockedPropertyRepo) Delete(id string) error {
|
||||
if p.Error != nil {
|
||||
return p.Error
|
||||
}
|
||||
p.init()
|
||||
if _, ok := p.Data[id]; ok {
|
||||
delete(p.Data, id)
|
||||
return nil
|
||||
}
|
||||
return model.ErrNotFound
|
||||
}
|
||||
|
||||
func (p *MockedPropertyRepo) DefaultGet(id string, defaultValue string) (string, error) {
|
||||
if p.Error != nil {
|
||||
return "", p.Error
|
||||
}
|
||||
p.init()
|
||||
v, err := p.Get(id)
|
||||
if err != nil {
|
||||
return defaultValue, nil //nolint:nilerr
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
85
tests/mock_radio_repository.go
Normal file
85
tests/mock_radio_repository.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/id"
|
||||
)
|
||||
|
||||
type MockedRadioRepo struct {
|
||||
model.RadioRepository
|
||||
Data map[string]*model.Radio
|
||||
All model.Radios
|
||||
Err bool
|
||||
Options model.QueryOptions
|
||||
}
|
||||
|
||||
func CreateMockedRadioRepo() *MockedRadioRepo {
|
||||
return &MockedRadioRepo{}
|
||||
}
|
||||
|
||||
func (m *MockedRadioRepo) SetError(err bool) {
|
||||
m.Err = err
|
||||
}
|
||||
|
||||
func (m *MockedRadioRepo) CountAll(options ...model.QueryOptions) (int64, error) {
|
||||
if m.Err {
|
||||
return 0, errors.New("error")
|
||||
}
|
||||
return int64(len(m.Data)), nil
|
||||
}
|
||||
|
||||
func (m *MockedRadioRepo) Delete(id string) error {
|
||||
if m.Err {
|
||||
return errors.New("Error!")
|
||||
}
|
||||
|
||||
_, found := m.Data[id]
|
||||
|
||||
if !found {
|
||||
return errors.New("not found")
|
||||
}
|
||||
|
||||
delete(m.Data, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockedRadioRepo) Exists(id string) (bool, error) {
|
||||
if m.Err {
|
||||
return false, errors.New("Error!")
|
||||
}
|
||||
_, found := m.Data[id]
|
||||
return found, nil
|
||||
}
|
||||
|
||||
func (m *MockedRadioRepo) Get(id string) (*model.Radio, error) {
|
||||
if m.Err {
|
||||
return nil, errors.New("Error!")
|
||||
}
|
||||
if d, ok := m.Data[id]; ok {
|
||||
return d, nil
|
||||
}
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *MockedRadioRepo) GetAll(qo ...model.QueryOptions) (model.Radios, error) {
|
||||
if len(qo) > 0 {
|
||||
m.Options = qo[0]
|
||||
}
|
||||
if m.Err {
|
||||
return nil, errors.New("Error!")
|
||||
}
|
||||
return m.All, nil
|
||||
}
|
||||
|
||||
func (m *MockedRadioRepo) Put(radio *model.Radio) error {
|
||||
if m.Err {
|
||||
return errors.New("error")
|
||||
}
|
||||
if radio.ID == "" {
|
||||
radio.ID = id.NewRandom()
|
||||
}
|
||||
m.Data[radio.ID] = radio
|
||||
return nil
|
||||
}
|
||||
120
tests/mock_scanner.go
Normal file
120
tests/mock_scanner.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
)
|
||||
|
||||
// MockScanner implements scanner.Scanner for testing with proper synchronization
|
||||
type MockScanner struct {
|
||||
mu sync.Mutex
|
||||
scanAllCalls []ScanAllCall
|
||||
scanFoldersCalls []ScanFoldersCall
|
||||
scanningStatus bool
|
||||
statusResponse *model.ScannerStatus
|
||||
}
|
||||
|
||||
type ScanAllCall struct {
|
||||
FullScan bool
|
||||
}
|
||||
|
||||
type ScanFoldersCall struct {
|
||||
FullScan bool
|
||||
Targets []model.ScanTarget
|
||||
}
|
||||
|
||||
func NewMockScanner() *MockScanner {
|
||||
return &MockScanner{
|
||||
scanAllCalls: make([]ScanAllCall, 0),
|
||||
scanFoldersCalls: make([]ScanFoldersCall, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockScanner) ScanAll(_ context.Context, fullScan bool) ([]string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.scanAllCalls = append(m.scanAllCalls, ScanAllCall{FullScan: fullScan})
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockScanner) ScanFolders(_ context.Context, fullScan bool, targets []model.ScanTarget) ([]string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Make a copy of targets to avoid race conditions
|
||||
targetsCopy := make([]model.ScanTarget, len(targets))
|
||||
copy(targetsCopy, targets)
|
||||
|
||||
m.scanFoldersCalls = append(m.scanFoldersCalls, ScanFoldersCall{
|
||||
FullScan: fullScan,
|
||||
Targets: targetsCopy,
|
||||
})
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockScanner) Status(_ context.Context) (*model.ScannerStatus, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.statusResponse != nil {
|
||||
return m.statusResponse, nil
|
||||
}
|
||||
|
||||
return &model.ScannerStatus{
|
||||
Scanning: m.scanningStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockScanner) GetScanAllCallCount() int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return len(m.scanAllCalls)
|
||||
}
|
||||
|
||||
func (m *MockScanner) GetScanAllCalls() []ScanAllCall {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
// Return a copy to avoid race conditions
|
||||
calls := make([]ScanAllCall, len(m.scanAllCalls))
|
||||
copy(calls, m.scanAllCalls)
|
||||
return calls
|
||||
}
|
||||
|
||||
func (m *MockScanner) GetScanFoldersCallCount() int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return len(m.scanFoldersCalls)
|
||||
}
|
||||
|
||||
func (m *MockScanner) GetScanFoldersCalls() []ScanFoldersCall {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
// Return a copy to avoid race conditions
|
||||
calls := make([]ScanFoldersCall, len(m.scanFoldersCalls))
|
||||
copy(calls, m.scanFoldersCalls)
|
||||
return calls
|
||||
}
|
||||
|
||||
func (m *MockScanner) Reset() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.scanAllCalls = make([]ScanAllCall, 0)
|
||||
m.scanFoldersCalls = make([]ScanFoldersCall, 0)
|
||||
}
|
||||
|
||||
func (m *MockScanner) SetScanning(scanning bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.scanningStatus = scanning
|
||||
}
|
||||
|
||||
func (m *MockScanner) SetStatusResponse(status *model.ScannerStatus) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.statusResponse = status
|
||||
}
|
||||
93
tests/mock_scrobble_buffer_repo.go
Normal file
93
tests/mock_scrobble_buffer_repo.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
)
|
||||
|
||||
type MockedScrobbleBufferRepo struct {
|
||||
Error error
|
||||
Data model.ScrobbleEntries
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func CreateMockedScrobbleBufferRepo() *MockedScrobbleBufferRepo {
|
||||
return &MockedScrobbleBufferRepo{}
|
||||
}
|
||||
|
||||
func (m *MockedScrobbleBufferRepo) UserIDs(service string) ([]string, error) {
|
||||
if m.Error != nil {
|
||||
return nil, m.Error
|
||||
}
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
userIds := make(map[string]struct{})
|
||||
for _, e := range m.Data {
|
||||
if e.Service == service {
|
||||
userIds[e.UserID] = struct{}{}
|
||||
}
|
||||
}
|
||||
var result []string
|
||||
for uid := range userIds {
|
||||
result = append(result, uid)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *MockedScrobbleBufferRepo) Enqueue(service, userId, mediaFileId string, playTime time.Time) error {
|
||||
if m.Error != nil {
|
||||
return m.Error
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.Data = append(m.Data, model.ScrobbleEntry{
|
||||
MediaFile: model.MediaFile{ID: mediaFileId},
|
||||
Service: service,
|
||||
UserID: userId,
|
||||
PlayTime: playTime,
|
||||
EnqueueTime: time.Now(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockedScrobbleBufferRepo) Next(service, userId string) (*model.ScrobbleEntry, error) {
|
||||
if m.Error != nil {
|
||||
return nil, m.Error
|
||||
}
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for _, e := range m.Data {
|
||||
if e.Service == service && e.UserID == userId {
|
||||
return &e, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockedScrobbleBufferRepo) Dequeue(entry *model.ScrobbleEntry) error {
|
||||
if m.Error != nil {
|
||||
return m.Error
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
newData := model.ScrobbleEntries{}
|
||||
for _, e := range m.Data {
|
||||
if e.Service == entry.Service && e.UserID == entry.UserID && e.PlayTime == entry.PlayTime && e.MediaFile.ID == entry.MediaFile.ID {
|
||||
continue
|
||||
}
|
||||
newData = append(newData, e)
|
||||
}
|
||||
m.Data = newData
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockedScrobbleBufferRepo) Length() (int64, error) {
|
||||
if m.Error != nil {
|
||||
return 0, m.Error
|
||||
}
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return int64(len(m.Data)), nil
|
||||
}
|
||||
24
tests/mock_scrobble_repo.go
Normal file
24
tests/mock_scrobble_repo.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
)
|
||||
|
||||
type MockScrobbleRepo struct {
|
||||
RecordedScrobbles []model.Scrobble
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (m *MockScrobbleRepo) RecordScrobble(fileID string, submissionTime time.Time) error {
|
||||
user, _ := request.UserFrom(m.ctx)
|
||||
m.RecordedScrobbles = append(m.RecordedScrobbles, model.Scrobble{
|
||||
MediaFileID: fileID,
|
||||
UserID: user.ID,
|
||||
SubmissionTime: submissionTime,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
46
tests/mock_share_repo.go
Normal file
46
tests/mock_share_repo.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"github.com/deluan/rest"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
)
|
||||
|
||||
type MockShareRepo struct {
|
||||
model.ShareRepository
|
||||
rest.Repository
|
||||
rest.Persistable
|
||||
|
||||
Entity interface{}
|
||||
ID string
|
||||
Cols []string
|
||||
Error error
|
||||
}
|
||||
|
||||
func (m *MockShareRepo) Save(entity interface{}) (string, error) {
|
||||
if m.Error != nil {
|
||||
return "", m.Error
|
||||
}
|
||||
s := entity.(*model.Share)
|
||||
if s.ID == "" {
|
||||
s.ID = "id"
|
||||
}
|
||||
m.Entity = s
|
||||
return s.ID, nil
|
||||
}
|
||||
|
||||
func (m *MockShareRepo) Update(id string, entity interface{}, cols ...string) error {
|
||||
if m.Error != nil {
|
||||
return m.Error
|
||||
}
|
||||
m.ID = id
|
||||
m.Entity = entity
|
||||
m.Cols = cols
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockShareRepo) Exists(id string) (bool, error) {
|
||||
if m.Error != nil {
|
||||
return false, m.Error
|
||||
}
|
||||
return id == m.ID, nil
|
||||
}
|
||||
24
tests/mock_transcoding_repo.go
Normal file
24
tests/mock_transcoding_repo.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package tests
|
||||
|
||||
import "github.com/navidrome/navidrome/model"
|
||||
|
||||
type MockTranscodingRepo struct {
|
||||
model.TranscodingRepository
|
||||
}
|
||||
|
||||
func (m *MockTranscodingRepo) Get(id string) (*model.Transcoding, error) {
|
||||
return &model.Transcoding{ID: id, TargetFormat: "mp3", DefaultBitRate: 160}, nil
|
||||
}
|
||||
|
||||
func (m *MockTranscodingRepo) FindByFormat(format string) (*model.Transcoding, error) {
|
||||
switch format {
|
||||
case "mp3":
|
||||
return &model.Transcoding{ID: "mp31", TargetFormat: "mp3", DefaultBitRate: 160}, nil
|
||||
case "oga":
|
||||
return &model.Transcoding{ID: "oga1", TargetFormat: "oga", DefaultBitRate: 128}, nil
|
||||
case "opus":
|
||||
return &model.Transcoding{ID: "opus1", TargetFormat: "opus", DefaultBitRate: 96}, nil
|
||||
default:
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
}
|
||||
59
tests/mock_user_props_repo.go
Normal file
59
tests/mock_user_props_repo.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package tests
|
||||
|
||||
import "github.com/navidrome/navidrome/model"
|
||||
|
||||
type MockedUserPropsRepo struct {
|
||||
model.UserPropsRepository
|
||||
Error error
|
||||
Data map[string]string
|
||||
}
|
||||
|
||||
func (p *MockedUserPropsRepo) init() {
|
||||
if p.Data == nil {
|
||||
p.Data = make(map[string]string)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MockedUserPropsRepo) Put(userId, key string, value string) error {
|
||||
if p.Error != nil {
|
||||
return p.Error
|
||||
}
|
||||
p.init()
|
||||
p.Data[userId+key] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MockedUserPropsRepo) Get(userId, key string) (string, error) {
|
||||
if p.Error != nil {
|
||||
return "", p.Error
|
||||
}
|
||||
p.init()
|
||||
if v, ok := p.Data[userId+key]; ok {
|
||||
return v, nil
|
||||
}
|
||||
return "", model.ErrNotFound
|
||||
}
|
||||
|
||||
func (p *MockedUserPropsRepo) Delete(userId, key string) error {
|
||||
if p.Error != nil {
|
||||
return p.Error
|
||||
}
|
||||
p.init()
|
||||
if _, ok := p.Data[userId+key]; ok {
|
||||
delete(p.Data, userId+key)
|
||||
return nil
|
||||
}
|
||||
return model.ErrNotFound
|
||||
}
|
||||
|
||||
func (p *MockedUserPropsRepo) DefaultGet(userId, key string, defaultValue string) (string, error) {
|
||||
if p.Error != nil {
|
||||
return "", p.Error
|
||||
}
|
||||
p.init()
|
||||
v, err := p.Get(userId, key)
|
||||
if err != nil {
|
||||
return defaultValue, nil //nolint:nilerr
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
125
tests/mock_user_repo.go
Normal file
125
tests/mock_user_repo.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils/gg"
|
||||
)
|
||||
|
||||
func CreateMockUserRepo() *MockedUserRepo {
|
||||
return &MockedUserRepo{
|
||||
Data: map[string]*model.User{},
|
||||
UserLibraries: map[string][]int{},
|
||||
}
|
||||
}
|
||||
|
||||
type MockedUserRepo struct {
|
||||
model.UserRepository
|
||||
Error error
|
||||
Data map[string]*model.User
|
||||
UserLibraries map[string][]int // userID -> libraryIDs
|
||||
}
|
||||
|
||||
func (u *MockedUserRepo) CountAll(qo ...model.QueryOptions) (int64, error) {
|
||||
if u.Error != nil {
|
||||
return 0, u.Error
|
||||
}
|
||||
return int64(len(u.Data)), nil
|
||||
}
|
||||
|
||||
func (u *MockedUserRepo) Put(usr *model.User) error {
|
||||
if u.Error != nil {
|
||||
return u.Error
|
||||
}
|
||||
if usr.ID == "" {
|
||||
usr.ID = base64.StdEncoding.EncodeToString([]byte(usr.UserName))
|
||||
}
|
||||
usr.Password = usr.NewPassword
|
||||
u.Data[strings.ToLower(usr.UserName)] = usr
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *MockedUserRepo) FindByUsername(username string) (*model.User, error) {
|
||||
if u.Error != nil {
|
||||
return nil, u.Error
|
||||
}
|
||||
usr, ok := u.Data[strings.ToLower(username)]
|
||||
if !ok {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
return usr, nil
|
||||
}
|
||||
|
||||
func (u *MockedUserRepo) FindByUsernameWithPassword(username string) (*model.User, error) {
|
||||
return u.FindByUsername(username)
|
||||
}
|
||||
|
||||
func (u *MockedUserRepo) Get(id string) (*model.User, error) {
|
||||
if u.Error != nil {
|
||||
return nil, u.Error
|
||||
}
|
||||
for _, usr := range u.Data {
|
||||
if usr.ID == id {
|
||||
return usr, nil
|
||||
}
|
||||
}
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
func (u *MockedUserRepo) UpdateLastLoginAt(id string) error {
|
||||
for _, usr := range u.Data {
|
||||
if usr.ID == id {
|
||||
usr.LastLoginAt = gg.P(time.Now())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return u.Error
|
||||
}
|
||||
|
||||
func (u *MockedUserRepo) UpdateLastAccessAt(id string) error {
|
||||
for _, usr := range u.Data {
|
||||
if usr.ID == id {
|
||||
usr.LastAccessAt = gg.P(time.Now())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return u.Error
|
||||
}
|
||||
|
||||
// Library association methods - mock implementations
|
||||
|
||||
func (u *MockedUserRepo) GetUserLibraries(userID string) (model.Libraries, error) {
|
||||
if u.Error != nil {
|
||||
return nil, u.Error
|
||||
}
|
||||
libraryIDs, exists := u.UserLibraries[userID]
|
||||
if !exists {
|
||||
return model.Libraries{}, nil
|
||||
}
|
||||
|
||||
// Mock: Create libraries based on IDs
|
||||
var libraries model.Libraries
|
||||
for _, id := range libraryIDs {
|
||||
libraries = append(libraries, model.Library{
|
||||
ID: id,
|
||||
Name: fmt.Sprintf("Test Library %d", id),
|
||||
Path: fmt.Sprintf("/music/library%d", id),
|
||||
})
|
||||
}
|
||||
return libraries, nil
|
||||
}
|
||||
|
||||
func (u *MockedUserRepo) SetUserLibraries(userID string, libraryIDs []int) error {
|
||||
if u.Error != nil {
|
||||
return u.Error
|
||||
}
|
||||
if u.UserLibraries == nil {
|
||||
u.UserLibraries = make(map[string][]int)
|
||||
}
|
||||
u.UserLibraries[userID] = libraryIDs
|
||||
return nil
|
||||
}
|
||||
60
tests/test_helpers.go
Normal file
60
tests/test_helpers.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/navidrome/navidrome/db"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model/id"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
)
|
||||
|
||||
type testingT interface {
|
||||
TempDir() string
|
||||
}
|
||||
|
||||
func TempFileName(t testingT, prefix, suffix string) string {
|
||||
return filepath.Join(t.TempDir(), prefix+id.NewRandom()+suffix)
|
||||
}
|
||||
|
||||
func TempFile(t testingT, prefix, suffix string) (*os.File, string, error) {
|
||||
name := TempFileName(t, prefix, suffix)
|
||||
f, err := os.Create(name)
|
||||
return f, name, err
|
||||
}
|
||||
|
||||
// ClearDB deletes all tables and data from the database
|
||||
// https://stackoverflow.com/questions/525512/drop-all-tables-command
|
||||
func ClearDB() error {
|
||||
_, err := db.Db().ExecContext(context.Background(), `
|
||||
PRAGMA writable_schema = 1;
|
||||
DELETE FROM sqlite_master;
|
||||
PRAGMA writable_schema = 0;
|
||||
VACUUM;
|
||||
PRAGMA integrity_check;
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
// LogHook sets up a logrus test hook and configures the default logger to use it.
|
||||
// It returns the hook and a cleanup function to restore the default logger.
|
||||
// Example usage:
|
||||
//
|
||||
// hook, cleanup := LogHook()
|
||||
// defer cleanup()
|
||||
// // ... perform logging operations ...
|
||||
// Expect(hook.LastEntry()).ToNot(BeNil())
|
||||
// Expect(hook.LastEntry().Level).To(Equal(logrus.WarnLevel))
|
||||
// Expect(hook.LastEntry().Message).To(Equal("log message"))
|
||||
func LogHook() (*test.Hook, func()) {
|
||||
l, hook := test.NewNullLogger()
|
||||
log.SetLevel(log.LevelWarn)
|
||||
log.SetDefaultLogger(l)
|
||||
return hook, func() {
|
||||
// Restore default logger after test
|
||||
log.SetDefaultLogger(logrus.New())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user