mirror of https://github.com/gabehf/Koito.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
375 lines
12 KiB
375 lines
12 KiB
package catalog_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"regexp"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gabehf/koito/internal/catalog"
|
|
"github.com/gabehf/koito/internal/cfg"
|
|
"github.com/gabehf/koito/internal/db/psql"
|
|
"github.com/gabehf/koito/internal/mbz"
|
|
"github.com/gabehf/koito/internal/utils"
|
|
_ "github.com/gabehf/koito/testing_init"
|
|
"github.com/google/uuid"
|
|
"github.com/ory/dockertest/v3"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var (
|
|
mbzArtistData = map[uuid.UUID]*mbz.MusicBrainzArtist{
|
|
uuid.MustParse("00000000-0000-0000-0000-000000000001"): {
|
|
Name: "ATARASHII GAKKO!",
|
|
SortName: "Atarashii Gakko",
|
|
Aliases: []mbz.MusicBrainzArtistAlias{
|
|
{
|
|
Name: "新しい学校のリーダーズ",
|
|
Type: "Artist name",
|
|
Primary: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
mbzReleaseGroupData = map[uuid.UUID]*mbz.MusicBrainzReleaseGroup{
|
|
uuid.MustParse("00000000-0000-0000-0000-000000000011"): {
|
|
Title: "AG! Calling",
|
|
Type: "Album",
|
|
ArtistCredit: []mbz.MusicBrainzArtistCredit{
|
|
{
|
|
Artist: mbz.MusicBrainzArtist{
|
|
Name: "ATARASHII GAKKO!",
|
|
Aliases: []mbz.MusicBrainzArtistAlias{
|
|
{
|
|
Name: "新しい学校のリーダーズ",
|
|
Type: "Artist name",
|
|
Primary: true,
|
|
},
|
|
},
|
|
},
|
|
Name: "ATARASHII GAKKO!",
|
|
},
|
|
},
|
|
Releases: []mbz.MusicBrainzRelease{
|
|
{
|
|
Title: "AG! Calling",
|
|
ID: "00000000-0000-0000-0000-000000000101",
|
|
ArtistCredit: []mbz.MusicBrainzArtistCredit{
|
|
{
|
|
Artist: mbz.MusicBrainzArtist{
|
|
Name: "ATARASHII GAKKO!",
|
|
Aliases: []mbz.MusicBrainzArtistAlias{
|
|
{
|
|
Name: "ATARASHII GAKKO!",
|
|
Type: "Artist name",
|
|
Primary: true,
|
|
},
|
|
},
|
|
},
|
|
Name: "ATARASHII GAKKO!",
|
|
},
|
|
},
|
|
Status: "Official",
|
|
},
|
|
{
|
|
Title: "AG! Calling - Alt Title",
|
|
ID: "00000000-0000-0000-0000-000000000102",
|
|
ArtistCredit: []mbz.MusicBrainzArtistCredit{
|
|
{
|
|
Artist: mbz.MusicBrainzArtist{
|
|
Name: "ATARASHII GAKKO!",
|
|
Aliases: []mbz.MusicBrainzArtistAlias{
|
|
{
|
|
Name: "ATARASHII GAKKO!",
|
|
Type: "Artist name",
|
|
Primary: true,
|
|
},
|
|
},
|
|
},
|
|
Name: "ATARASHII GAKKO!",
|
|
},
|
|
},
|
|
Status: "Official",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
mbzReleaseData = map[uuid.UUID]*mbz.MusicBrainzRelease{
|
|
uuid.MustParse("00000000-0000-0000-0000-000000000101"): {
|
|
Title: "AG! Calling",
|
|
ID: "00000000-0000-0000-0000-000000000101",
|
|
ArtistCredit: []mbz.MusicBrainzArtistCredit{
|
|
{
|
|
Artist: mbz.MusicBrainzArtist{
|
|
Name: "ATARASHII GAKKO!",
|
|
Aliases: []mbz.MusicBrainzArtistAlias{
|
|
{
|
|
Name: "新しい学校のリーダーズ",
|
|
Type: "Artist name",
|
|
Primary: true,
|
|
},
|
|
},
|
|
},
|
|
Name: "ATARASHII GAKKO!",
|
|
},
|
|
},
|
|
Status: "Official",
|
|
},
|
|
uuid.MustParse("00000000-0000-0000-0000-000000000202"): {
|
|
Title: "EVANGELION FINALLY",
|
|
ID: "00000000-0000-0000-0000-000000000202",
|
|
ArtistCredit: []mbz.MusicBrainzArtistCredit{
|
|
{
|
|
Artist: mbz.MusicBrainzArtist{
|
|
Name: "Various Artists",
|
|
},
|
|
Name: "Various Artists",
|
|
},
|
|
},
|
|
Status: "Official",
|
|
},
|
|
}
|
|
mbzTrackData = map[uuid.UUID]*mbz.MusicBrainzTrack{
|
|
uuid.MustParse("00000000-0000-0000-0000-000000001001"): {
|
|
Title: "Tokyo Calling",
|
|
LengthMs: 191000,
|
|
},
|
|
}
|
|
)
|
|
|
|
var store *psql.Psql
|
|
|
|
func getTestGetenv(resource *dockertest.Resource) func(string) string {
|
|
dir, err := utils.GenerateRandomString(8)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return func(env string) string {
|
|
switch env {
|
|
case cfg.ENABLE_STRUCTURED_LOGGING_ENV:
|
|
return "true"
|
|
case cfg.LOG_LEVEL_ENV:
|
|
return "debug"
|
|
case cfg.DATABASE_URL_ENV:
|
|
return fmt.Sprintf("postgres://postgres:secret@localhost:%s", resource.GetPort("5432/tcp"))
|
|
case cfg.CONFIG_DIR_ENV:
|
|
return dir
|
|
case cfg.DISABLE_DEEZER_ENV, cfg.DISABLE_COVER_ART_ARCHIVE_ENV, cfg.DISABLE_MUSICBRAINZ_ENV, cfg.ENABLE_FULL_IMAGE_CACHE_ENV:
|
|
return "true"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
}
|
|
|
|
func truncateTestData(t *testing.T) {
|
|
err := store.Exec(context.Background(),
|
|
`TRUNCATE
|
|
artists,
|
|
artist_aliases,
|
|
tracks,
|
|
artist_tracks,
|
|
releases,
|
|
artist_releases,
|
|
release_aliases,
|
|
listens
|
|
RESTART IDENTITY CASCADE`)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func setupTestDataWithMbzIDs(t *testing.T) {
|
|
truncateTestData(t)
|
|
|
|
err := store.Exec(context.Background(),
|
|
`INSERT INTO artists (musicbrainz_id)
|
|
VALUES ('00000000-0000-0000-0000-000000000001')`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO artist_aliases (artist_id, alias, source, is_primary)
|
|
VALUES (1, 'ATARASHII GAKKO!', 'Testing', true)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO releases (musicbrainz_id)
|
|
VALUES ('00000000-0000-0000-0000-000000000101')`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO release_aliases (release_id, alias, source, is_primary)
|
|
VALUES (1, 'AG! Calling', 'Testing', true)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO artist_releases (artist_id, release_id)
|
|
VALUES (1, 1)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO tracks (release_id, musicbrainz_id)
|
|
VALUES (1, '00000000-0000-0000-0000-000000001001')`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO track_aliases (track_id, alias, source, is_primary)
|
|
VALUES (1, 'Tokyo Calling', 'Testing', true)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO artist_tracks (artist_id, track_id)
|
|
VALUES (1, 1)`)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func setupTestDataSansMbzIDs(t *testing.T) {
|
|
truncateTestData(t)
|
|
|
|
err := store.Exec(context.Background(),
|
|
`INSERT INTO artists (musicbrainz_id)
|
|
VALUES (NULL)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO artist_aliases (artist_id, alias, source, is_primary)
|
|
VALUES (1, 'ATARASHII GAKKO!', 'Testing', true)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO releases (musicbrainz_id)
|
|
VALUES (NULL)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO release_aliases (release_id, alias, source, is_primary)
|
|
VALUES (1, 'AG! Calling', 'Testing', true)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO artist_releases (artist_id, release_id)
|
|
VALUES (1, 1)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO tracks (release_id, musicbrainz_id)
|
|
VALUES (1, NULL)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO track_aliases (track_id, alias, source, is_primary)
|
|
VALUES (1, 'Tokyo Calling', 'Testing', true)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO artist_tracks (artist_id, track_id)
|
|
VALUES (1, 1)`)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
pool, err := dockertest.NewPool("")
|
|
if err != nil {
|
|
log.Fatalf("Could not construct pool: %s", err)
|
|
}
|
|
|
|
if err := pool.Client.Ping(); err != nil {
|
|
log.Fatalf("Could not connect to Docker: %s", err)
|
|
}
|
|
|
|
resource, err := pool.Run("postgres", "latest", []string{"POSTGRES_PASSWORD=secret"})
|
|
if err != nil {
|
|
log.Fatalf("Could not start resource: %s", err)
|
|
}
|
|
|
|
err = cfg.Load(getTestGetenv(resource), "test")
|
|
if err != nil {
|
|
log.Fatalf("Could not load cfg: %s", err)
|
|
}
|
|
|
|
if err := pool.Retry(func() error {
|
|
var err error
|
|
store, err = psql.New()
|
|
if err != nil {
|
|
log.Println("Failed to connect to test database, retrying...")
|
|
return err
|
|
}
|
|
return store.Ping(context.Background())
|
|
}); err != nil {
|
|
log.Fatalf("Could not connect to database: %s", err)
|
|
}
|
|
|
|
// insert a user into the db with id 1 to use for tests
|
|
err = store.Exec(context.Background(), `INSERT INTO users (username, password) VALUES ('test', DECODE('abc123', 'hex'))`)
|
|
if err != nil {
|
|
log.Fatalf("Failed to insert test user: %v", err)
|
|
}
|
|
|
|
code := m.Run()
|
|
|
|
// You can't defer this because os.Exit doesn't care for defer
|
|
if err := pool.Purge(resource); err != nil {
|
|
log.Fatalf("Could not purge resource: %s", err)
|
|
}
|
|
|
|
err = os.RemoveAll(cfg.ConfigDir())
|
|
if err != nil {
|
|
log.Fatalf("Could not remove temporary config dir: %v", err)
|
|
}
|
|
|
|
os.Exit(code)
|
|
}
|
|
|
|
// From: https://brandur.org/fragments/go-equal-time
|
|
// EqualTime compares two times in a way that's safer and with better fail
|
|
// output than a call to `require.Equal` would produce.
|
|
//
|
|
// It takes care to:
|
|
//
|
|
// - Strip off monotonic portions of timestamps so they aren't considered for
|
|
// purposes of comparison.
|
|
//
|
|
// - Truncate nanoseconds in a functionally equivalent way to how pgx would do
|
|
// it so that times that have round-tripped from Postgres can still be
|
|
// compared. Postgres only stores times to the microsecond level.
|
|
//
|
|
// - Use formatted, human-friendly time outputs so that in case of a failure,
|
|
// the discrepancy is easier to pick out.
|
|
func EqualTime(t testing.TB, t1, t2 time.Time) {
|
|
// Note that leaving off the nanosecond portion will have the effect of
|
|
// truncating it rather than rounding to the nearest microsecond, which
|
|
// functionally matches pgx's behavior while persisting.
|
|
const rfc3339Micro = "2006-01-02T15:04:05.999999Z07:00"
|
|
|
|
require.Equal(t,
|
|
t1.Format(rfc3339Micro),
|
|
t2.Format(rfc3339Micro),
|
|
)
|
|
}
|
|
|
|
func TestArtistStringParse(t *testing.T) {
|
|
type input struct {
|
|
Name string
|
|
Title string
|
|
}
|
|
cases := map[input][]string{
|
|
// only one artist
|
|
{"NELKE", ""}: {"NELKE"},
|
|
{"The Brook & The Bluff", ""}: {"The Brook & The Bluff"},
|
|
{"half·alive", ""}: {"half·alive"},
|
|
// Earth, Wind, & Fire
|
|
{"Earth, Wind & Fire", "The Very Best of Earth, Wind & Fire"}: {"Earth, Wind & Fire"},
|
|
// only artists in artist string
|
|
{"Carly Rae Jepsen feat. Rufus Wainwright", ""}: {"Carly Rae Jepsen", "Rufus Wainwright"},
|
|
{"Mimi (feat. HATSUNE MIKU & KAFU)", ""}: {"Mimi", "HATSUNE MIKU", "KAFU"},
|
|
{"Magnify Tokyo · Kanade Ishihara", ""}: {"Magnify Tokyo", "Kanade Ishihara"},
|
|
{"Daft Punk [feat. Paul Williams]", ""}: {"Daft Punk", "Paul Williams"},
|
|
// primary artist in artist string, features in title
|
|
{"Tyler, The Creator", "CA (feat. Alice Smith, Leon Ware & Clem Creevy)"}: {"Tyler, The Creator", "Alice Smith", "Leon Ware", "Clem Creevy"},
|
|
{"ONE OK ROCK", "C.U.R.I.O.S.I.T.Y. (feat. Paledusk and CHICO CARLITO)"}: {"ONE OK ROCK", "Paledusk", "CHICO CARLITO"},
|
|
{"Rat Tally", "In My Car feat. Madeline Kenney"}: {"Rat Tally", "Madeline Kenney"},
|
|
// artists in both
|
|
{"Daft Punk feat. Julian Casablancas", "Instant Crush (feat. Julian Casablancas)"}: {"Daft Punk", "Julian Casablancas"},
|
|
{"Paramore (feat. Joy Williams)", "Hate to See Your Heart Break feat. Joy Williams"}: {"Paramore", "Joy Williams"},
|
|
{"MINSU", "오해 금지 (Feat. BIG Naughty)"}: {"MINSU", "BIG Naughty"},
|
|
{"MINSU", "오해 금지 [Feat. BIG Naughty]"}: {"MINSU", "BIG Naughty"},
|
|
{"MINSU", "오해 금지 Feat. BIG Naughty"}: {"MINSU", "BIG Naughty"},
|
|
|
|
// custom separator
|
|
{"MIMiNARI//楠木ともり", "眠れない"}: {"MIMiNARI", "楠木ともり"},
|
|
}
|
|
|
|
for in, out := range cases {
|
|
artists := catalog.ParseArtists(in.Name, in.Title, []*regexp.Regexp{regexp.MustCompile(`\s*//\s*`), regexp.MustCompile(`\s+·\s+`)})
|
|
assert.ElementsMatch(t, out, artists)
|
|
}
|
|
}
|