github.com/derat/nup@v0.0.0-20230418113745-15592ba7c620/test/e2e/e2e_test.go (about) 1 // Copyright 2020 Daniel Erat. 2 // All rights reserved. 3 4 // Package e2e contains end-to-end tests between the server and command-line tools. 5 package e2e 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "flag" 11 "fmt" 12 "io" 13 "log" 14 "net/http" 15 "net/url" 16 "os" 17 "path/filepath" 18 "reflect" 19 "sort" 20 "strconv" 21 "strings" 22 "testing" 23 "time" 24 25 "github.com/derat/nup/server/config" 26 "github.com/derat/nup/server/db" 27 "github.com/derat/nup/server/query" 28 "github.com/derat/nup/test" 29 30 "golang.org/x/sys/unix" 31 ) 32 33 const ( 34 coverBucket = "cover-bucket" 35 36 guestUsername = "guest" 37 guestPassword = "guestpw" 38 maxGuestRequests = 3 39 ) 40 41 var ( 42 // Pull some stuff into our namespace for convenience. 43 Song0s = test.Song0s 44 Song0sUpdated = test.Song0sUpdated 45 Song1s = test.Song1s 46 Song5s = test.Song5s 47 Song10s = test.Song10s 48 LegacySong1 = test.LegacySong1 49 LegacySong2 = test.LegacySong2 50 51 appURL string // URL of App Engine app 52 outDir string // base directory for temp files and logs 53 54 guestExcludedTags = []string{"rock"} 55 guestPresets = []config.SearchPreset{{Name: "custom", MinRating: 4}} 56 ) 57 58 func TestMain(m *testing.M) { 59 // Do everything in a function so that deferred calls run on failure. 60 code, err := runTests(m) 61 if err != nil { 62 log.Print("Failed running tests: ", err) 63 } 64 os.Exit(code) 65 } 66 67 func runTests(m *testing.M) (res int, err error) { 68 createIndexes := flag.Bool("create-indexes", false, "Update datastore indexes in index.yaml") 69 flag.Parse() 70 71 test.HandleSignals([]os.Signal{unix.SIGINT, unix.SIGTERM}, nil) 72 73 var keepOutDir bool 74 if outDir, keepOutDir, err = test.OutputDir("e2e_test"); err != nil { 75 return -1, err 76 } 77 defer func() { 78 if res == 0 && !keepOutDir { 79 log.Print("Removing ", outDir) 80 os.RemoveAll(outDir) 81 } 82 }() 83 log.Print("Writing files to ", outDir) 84 85 appLog, err := os.Create(filepath.Join(outDir, "app.log")) 86 if err != nil { 87 return -1, err 88 } 89 defer appLog.Close() 90 91 // Serve the test/data/songs directory so the server's /song endpoint will work. 92 // This just serves the checked-in data, so it won't necessarily match the songs 93 // that a given test has imported into the server. 94 songsDir, err := test.SongsDir() 95 if err != nil { 96 return -1, err 97 } 98 songsSrv := test.ServeFiles(songsDir) 99 defer songsSrv.Close() 100 101 cfg := &config.Config{ 102 Users: []config.User{ 103 {Username: test.Username, Password: test.Password, Admin: true}, 104 { 105 Username: guestUsername, 106 Password: guestPassword, 107 Guest: true, 108 Presets: guestPresets, 109 ExcludedTags: guestExcludedTags, 110 }, 111 }, 112 SongBaseURL: songsSrv.URL, 113 CoverBaseURL: songsSrv.URL, // bogus, but no tests request covers 114 MaxGuestSongRequestsPerHour: maxGuestRequests, 115 } 116 storageDir := filepath.Join(outDir, "app_storage") 117 srv, err := test.NewDevAppserver(cfg, storageDir, appLog, test.DevAppserverCreateIndexes(*createIndexes)) 118 if err != nil { 119 return -1, fmt.Errorf("dev_appserver: %v", err) 120 } 121 defer os.RemoveAll(storageDir) 122 defer srv.Close() 123 appURL = srv.URL() 124 log.Print("dev_appserver is listening at ", appURL) 125 126 res = m.Run() 127 return res, nil 128 } 129 130 func initTest(t *testing.T) (*test.Tester, func()) { 131 tmpDir := filepath.Join(outDir, "tester."+t.Name()) 132 tester := test.NewTester(t, appURL, tmpDir, test.TesterConfig{}) 133 tester.PingServer() 134 log.Print("Clearing ", appURL) 135 tester.ClearData() 136 tester.FlushCache(test.FlushAll) 137 138 // Remove the test-specific temp dir since it often ends up holding music files. 139 return tester, func() { os.RemoveAll(tmpDir) } 140 } 141 142 func compareQueryResults(expected, actual []db.Song, order test.OrderPolicy) error { 143 expectedCleaned := make([]db.Song, len(expected)) 144 for i := range expected { 145 s := expected[i] 146 query.CleanSong(&s, 0) 147 148 // Change some stuff back to match the expected values. 149 s.SongID = "" 150 if len(s.Tags) == 0 { 151 s.Tags = nil 152 } 153 154 expectedCleaned[i] = s 155 } 156 157 actualCleaned := make([]db.Song, len(actual)) 158 for i := range actual { 159 s := actual[i] 160 161 if len(s.SongID) == 0 { 162 return fmt.Errorf("song %v (%v) has no ID", i, s.Filename) 163 } 164 s.SongID = "" 165 166 if len(s.Tags) == 0 { 167 s.Tags = nil 168 } 169 170 actualCleaned[i] = s 171 } 172 173 return test.CompareSongs(expectedCleaned, actualCleaned, order) 174 } 175 176 func timeToSeconds(t time.Time) float64 { 177 return float64(t.UnixNano()) / float64(time.Second/time.Nanosecond) 178 } 179 180 func TestUpdate(tt *testing.T) { 181 t, done := initTest(tt) 182 defer done() 183 184 log.Print("Importing songs from music dir") 185 test.Must(tt, test.CopySongs(t.MusicDir, Song0s.Filename, Song1s.Filename)) 186 t.UpdateSongs() 187 if err := test.CompareSongs([]db.Song{Song0s, Song1s}, 188 t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 189 tt.Error("Bad songs after import: ", err) 190 } 191 192 log.Print("Importing another song") 193 test.Must(tt, test.CopySongs(t.MusicDir, Song5s.Filename)) 194 t.UpdateSongs() 195 if err := test.CompareSongs([]db.Song{Song0s, Song1s, Song5s}, 196 t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 197 tt.Error("Bad songs after second import: ", err) 198 } 199 200 log.Print("Updating a song") 201 test.Must(tt, test.DeleteSongs(t.MusicDir, Song0s.Filename)) 202 test.Must(tt, test.CopySongs(t.MusicDir, Song0sUpdated.Filename)) 203 t.UpdateSongs() 204 if err := test.CompareSongs([]db.Song{Song0sUpdated, Song1s, Song5s}, 205 t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 206 tt.Error("Bad songs after update: ", err) 207 } 208 209 gs5 := Song5s 210 gs5.TrackGain = -6.3 211 gs5.AlbumGain = -7.1 212 gs5.PeakAmp = 0.9 213 214 // If we pass a glob, only the matched file should be updated. 215 // Change the song's gain info (by getting it from a dump) so we can 216 // verify that it worked as expected. 217 log.Print("Importing dumped gain with glob") 218 glob := strings.TrimSuffix(gs5.Filename, ".mp3") + ".*" 219 dumpPath, err := test.WriteSongsToJSONFile(tt.TempDir(), gs5) 220 if err != nil { 221 tt.Fatal("Failed writing JSON file: ", err) 222 } 223 t.UpdateSongs(test.ForceGlobFlag(glob), test.DumpedGainsFlag(dumpPath)) 224 if err := test.CompareSongs([]db.Song{Song0sUpdated, Song1s, gs5}, 225 t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 226 tt.Error("Bad songs after glob import: ", err) 227 } 228 } 229 230 func TestUserData(tt *testing.T) { 231 t, done := initTest(tt) 232 defer done() 233 234 log.Print("Importing a song") 235 test.Must(tt, test.CopySongs(t.MusicDir, Song0s.Filename)) 236 t.UpdateSongs() 237 id := t.SongID(Song0s.SHA1) 238 239 log.Print("Rating and tagging") 240 s := Song0s 241 s.Rating = 4 242 s.Tags = []string{"electronic", "instrumental"} 243 t.RateAndTag(id, s.Rating, s.Tags) 244 if err := test.CompareSongs([]db.Song{s}, t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 245 tt.Fatal("Bad songs after rating and tagging: ", err) 246 } 247 248 log.Print("Reporting play") 249 s.Plays = []db.Play{ 250 db.NewPlay(test.Date(2014, 9, 15, 2, 5, 18), "127.0.0.1"), 251 db.NewPlay(test.Date(2014, 9, 15, 2, 8, 43), "127.0.0.1"), 252 db.NewPlay(test.Date(2014, 9, 15, 2, 13, 4), "127.0.0.1"), 253 } 254 for i, p := range s.Plays { 255 if i < len(s.Plays)-1 { 256 t.ReportPlayed(id, p.StartTime) // RFC 3339 257 } else { 258 t.ReportPlayedUnix(id, p.StartTime) // seconds since epoch 259 } 260 } 261 if err := test.CompareSongs([]db.Song{s}, t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 262 tt.Fatal("Bad songs after reporting play: ", err) 263 } 264 265 log.Print("Updating song and checking that user data is preserved") 266 test.Must(tt, test.DeleteSongs(t.MusicDir, s.Filename)) 267 us := Song0sUpdated 268 us.Rating = s.Rating 269 us.Tags = s.Tags 270 us.Plays = s.Plays 271 test.Must(tt, test.CopySongs(t.MusicDir, us.Filename)) 272 t.UpdateSongs() 273 if err := test.CompareSongs([]db.Song{us}, t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 274 tt.Error("Bad songs after updating song: ", err) 275 } 276 277 log.Print("Checking that duplicate plays are ignored") 278 t.ReportPlayed(id, s.Plays[len(us.Plays)-1].StartTime) 279 if err := test.CompareSongs([]db.Song{us}, t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 280 tt.Fatal("Bad songs after duplicate play: ", err) 281 } 282 283 log.Print("Checking that duplicate tags are ignored") 284 us.Tags = []string{"electronic", "rock"} 285 t.RateAndTag(id, -1, []string{"electronic", "electronic", "rock", "electronic"}) 286 if err := test.CompareSongs([]db.Song{us}, t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 287 tt.Fatal("Bad songs after duplicate tags: ", err) 288 } 289 290 log.Print("Clearing tags") 291 us.Tags = nil 292 t.RateAndTag(id, -1, []string{}) 293 if err := test.CompareSongs([]db.Song{us}, t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 294 tt.Fatal("Bad songs after clearing tags: ", err) 295 } 296 297 plays := us.Plays 298 sort.Sort(db.PlayArray(plays)) 299 300 log.Print("Checking first-played queries") 301 firstPlay := plays[0].StartTime 302 query := "minFirstPlayed=" + firstPlay.Add(-10*time.Second).Format(time.RFC3339) 303 if err := compareQueryResults([]db.Song{us}, t.QuerySongs(query), test.IgnoreOrder); err != nil { 304 tt.Errorf("Bad results for %q: %v", query, err) 305 } 306 query = "minFirstPlayed=" + firstPlay.Add(10*time.Second).Format(time.RFC3339) 307 if err := compareQueryResults([]db.Song{}, t.QuerySongs(query), test.IgnoreOrder); err != nil { 308 tt.Errorf("Bad results for %q: %v", query, err) 309 } 310 311 log.Print("Checking last-played queries") 312 lastPlay := plays[len(plays)-1].StartTime 313 query = "maxLastPlayed=" + lastPlay.Add(-10*time.Second).Format(time.RFC3339) 314 if err := compareQueryResults([]db.Song{}, t.QuerySongs(query), test.IgnoreOrder); err != nil { 315 tt.Errorf("Bad results for %q: %v", query, err) 316 } 317 query = "maxLastPlayed=" + lastPlay.Add(10*time.Second).Format(time.RFC3339) 318 if err := compareQueryResults([]db.Song{us}, t.QuerySongs(query), test.IgnoreOrder); err != nil { 319 tt.Errorf("Bad results for %q: %v", query, err) 320 } 321 322 log.Print("Checking that play stats were updated") 323 for i := 0; i < 3; i++ { 324 query = "maxPlays=" + strconv.Itoa(i) 325 if err := compareQueryResults([]db.Song{}, 326 t.QuerySongs(query), test.IgnoreOrder); err != nil { 327 tt.Errorf("Bad results for %q: %v", query, err) 328 } 329 } 330 query = "maxPlays=3" 331 if err := compareQueryResults([]db.Song{us}, t.QuerySongs(query), test.IgnoreOrder); err != nil { 332 tt.Errorf("Bad results for %q: %v", query, err) 333 } 334 } 335 336 func TestUpdateUseFilenames(tt *testing.T) { 337 t, done := initTest(tt) 338 defer done() 339 340 mv := func(oldFn, newFn string) { 341 if err := os.Rename(filepath.Join(t.MusicDir, oldFn), filepath.Join(t.MusicDir, newFn)); err != nil { 342 tt.Error("Failed renaming song: ", err) 343 } 344 } 345 346 const ( 347 oldFn = "old.mp3" 348 newFn = "new.mp3" 349 rating = 4 350 ) 351 352 log.Print("Importing song from music dir") 353 test.Must(tt, test.CopySongs(t.MusicDir, Song0s.Filename)) 354 mv(Song0s.Filename, oldFn) 355 song := Song0s 356 song.Filename = oldFn 357 t.UpdateSongs() 358 if err := test.CompareSongs([]db.Song{song}, t.DumpSongs(test.StripIDs), 359 test.IgnoreOrder); err != nil { 360 tt.Error("Bad songs after import: ", err) 361 } 362 363 log.Print("Rating song") 364 song.Rating = rating 365 t.RateAndTag(t.SongID(song.SHA1), song.Rating, nil) 366 367 // If we just rename the file, its hash will remain the same, so the existing 368 // datastore entity should be reused. 369 log.Print("Renaming song and checking that rating is preserved") 370 mv(oldFn, newFn) 371 song.Filename = newFn 372 t.UpdateSongs() 373 if err := test.CompareSongs([]db.Song{song}, t.DumpSongs(test.StripIDs), 374 test.IgnoreOrder); err != nil { 375 tt.Error("Bad songs after renaming song: ", err) 376 } 377 378 // If we replace the file (changing its hash) but pass -use-filenames, 379 // the datastore entity should be looked up by filename rather than by hash, 380 // so we should still update the existing entity. 381 test.Must(tt, test.CopySongs(t.MusicDir, Song5s.Filename)) 382 mv(Song5s.Filename, newFn) 383 song = Song5s 384 song.Filename = newFn 385 song.Rating = rating 386 t.UpdateSongs(test.UseFilenamesFlag) 387 if err := test.CompareSongs([]db.Song{song}, t.DumpSongs(test.StripIDs), 388 test.IgnoreOrder); err != nil { 389 tt.Error("Bad songs after replacing song: ", err) 390 } 391 } 392 393 func TestUpdateCompare(tt *testing.T) { 394 t, done := initTest(tt) 395 defer done() 396 397 test.Must(tt, test.CopySongs(t.MusicDir, Song0s.Filename, Song1s.Filename, Song5s.Filename)) 398 399 // Dump 0s (with changed user data) and 1s (with changed metadata) to a file. 400 s0 := Song0s 401 s0.Rating = 3 402 s0.Tags = []string{"instrumental"} 403 s1 := Song1s 404 s1.Artist = s1.Artist + " (old)" 405 dump, err := test.WriteSongsToJSONFile(tt.TempDir(), s0, s1) 406 if err != nil { 407 tt.Fatal("Failed writing songs: ", err) 408 } 409 410 // The update should send 1s (since the actual metadata differs from the dump) 411 // and 5s (since it wasn't present in the dump). 412 t.UpdateSongs(test.CompareDumpFileFlag(dump)) 413 if err := test.CompareSongs([]db.Song{Song1s, Song5s}, t.DumpSongs(test.StripIDs), 414 test.IgnoreOrder); err != nil { 415 tt.Error("Bad songs after update: ", err) 416 } 417 } 418 419 func TestQueries(tt *testing.T) { 420 t, done := initTest(tt) 421 defer done() 422 423 log.Print("Posting some songs") 424 t.PostSongs([]db.Song{LegacySong1, LegacySong2}, true, 0) 425 t.PostSongs([]db.Song{Song0s, Song1s, Song5s}, false, 0) 426 427 // Also post a song with Unicode characters that should be normalized. 428 s10s := test.Song10s 429 s10s.Artist = "µ-Ziq" // U+00B5 (MICRO SIGN) 430 s10s.Title = "Mañana" 431 s10s.Album = "Two²" 432 s10s.Rating = 5 433 s10s.DiscSubtitle = "Just a Subtitle" 434 t.PostSongs([]db.Song{s10s}, true, 0) 435 436 const ( 437 // Flags for test cases. 438 noIndex = 1 << iota // fallback mode is allowed for query 439 ignoreOrder // don't check result order 440 ) 441 442 for _, tc := range []struct { 443 params string 444 flags uint32 445 want []db.Song 446 }{ 447 {"artist=AROVANE", 0, []db.Song{LegacySong1}}, 448 {"title=thaem+nue", 0, []db.Song{LegacySong1}}, 449 {"album=ATOL+scrap", 0, []db.Song{LegacySong1}}, 450 {"albumId=" + Song0s.AlbumID, 0, []db.Song{Song0s, Song1s}}, 451 {"album=" + url.QueryEscape(Song5s.Album) + "&albumId=" + Song5s.AlbumID, 0, []db.Song{Song5s}}, 452 {"keywords=arovane+thaem+atol", 0, []db.Song{LegacySong1}}, 453 {"keywords=arovane+foo", 0, []db.Song{}}, 454 {"keywords=second+artist", 0, []db.Song{Song1s}}, // track artist 455 {"keywords=remixer", 0, []db.Song{Song1s}}, // album artist 456 {"keywords=just+subtitle", 0, []db.Song{s10s}}, // disc subtitle 457 // Don't bother checking the result order when checking rating filters. 458 {"rating=1", ignoreOrder, []db.Song{}}, 459 {"rating=2", ignoreOrder, []db.Song{}}, 460 {"rating=3", ignoreOrder, []db.Song{LegacySong2}}, 461 {"rating=4", ignoreOrder, []db.Song{LegacySong1}}, 462 {"rating=5", ignoreOrder, []db.Song{s10s}}, 463 {"minRating=1", ignoreOrder, []db.Song{LegacySong1, LegacySong2, s10s}}, 464 {"minRating=2", ignoreOrder, []db.Song{LegacySong1, LegacySong2, s10s}}, 465 {"minRating=3", ignoreOrder, []db.Song{LegacySong1, LegacySong2, s10s}}, 466 {"minRating=4", ignoreOrder, []db.Song{LegacySong1, s10s}}, 467 {"minRating=5", ignoreOrder, []db.Song{s10s}}, 468 {"maxRating=1", ignoreOrder, []db.Song{}}, 469 {"maxRating=2", ignoreOrder, []db.Song{}}, 470 {"maxRating=3", ignoreOrder, []db.Song{LegacySong2}}, 471 {"maxRating=4", ignoreOrder, []db.Song{LegacySong1, LegacySong2}}, 472 {"maxRating=5", ignoreOrder, []db.Song{s10s, LegacySong1, LegacySong2}}, 473 {"unrated=1", 0, []db.Song{Song0s, Song1s, Song5s}}, 474 {"tags=instrumental", 0, []db.Song{LegacySong2, LegacySong1}}, 475 {"tags=electronic+instrumental", 0, []db.Song{LegacySong1}}, 476 {"tags=-electronic+instrumental", 0, []db.Song{LegacySong2}}, 477 {"tags=instrumental&minRating=4", 0, []db.Song{LegacySong1}}, 478 {"tags=instrumental&minRating=4&maxPlays=1", noIndex, []db.Song{}}, 479 {"tags=instrumental&minRating=4&maxPlays=2", noIndex, []db.Song{LegacySong1}}, 480 {"firstTrack=1", 0, []db.Song{Song0s, LegacySong1}}, 481 {"artist=" + url.QueryEscape("µ-Ziq"), 0, []db.Song{s10s}}, // U+00B5 (MICRO SIGN) 482 {"artist=" + url.QueryEscape("μ-Ziq"), 0, []db.Song{s10s}}, // U+03BC (GREEK SMALL LETTER MU) 483 {"title=manana", 0, []db.Song{s10s}}, 484 {"title=" + url.QueryEscape("mánanä"), 0, []db.Song{s10s}}, 485 {"album=two2", 0, []db.Song{s10s}}, 486 {"filename=" + url.QueryEscape(Song5s.Filename), 0, []db.Song{Song5s}}, 487 {"minDate=2000-01-01T00:00:00Z", 0, []db.Song{Song1s, Song5s}}, 488 {"maxDate=2000-01-01T00:00:00Z", 0, []db.Song{Song0s}}, 489 {"minDate=2000-01-01T00:00:00Z&maxDate=2010-01-01T00:00:00Z", 0, []db.Song{Song1s}}, 490 // Ensure that Datastore indexes exist to satisfy various queries (or if not, that the 491 // server's fallback mode is still able to handle them). 492 {"tags=-bogus&minRating=5&shuffle=1&orderByLastPlayed=1", ignoreOrder, []db.Song{s10s}}, 493 {"tags=instrumental&minRating=4&shuffle=1&orderByLastPlayed=1", ignoreOrder, []db.Song{LegacySong1}}, 494 {"tags=instrumental+-bogus&minRating=4&shuffle=1&orderByLastPlayed=1", ignoreOrder, []db.Song{LegacySong1}}, 495 {"minRating=4&shuffle=1&orderByLastPlayed=1", ignoreOrder, []db.Song{s10s, LegacySong1}}, // old songs 496 {"minRating=4&maxPlays=1&shuffle=1", ignoreOrder, []db.Song{s10s}}, 497 {"minRating=2&orderByLastPlayed=1", noIndex, []db.Song{s10s, LegacySong1, LegacySong2}}, 498 {"tags=instrumental&minRating=4&shuffle=1&maxLastPlayed=2022-04-06T14:41:14Z", ignoreOrder, []db.Song{LegacySong1}}, 499 {"tags=instrumental&minRating=4&shuffle=1&maxPlays=1", noIndex | ignoreOrder, []db.Song{}}, 500 {"tags=instrumental&maxLastPlayed=2022-04-06T14:41:14Z", noIndex, []db.Song{LegacySong2, LegacySong1}}, 501 {"firstTrack=1&minFirstPlayed=2010-06-09T04:19:30Z", 0, []db.Song{LegacySong1}}, // new albums 502 {"firstTrack=1&minFirstPlayed=2010-06-09T04:19:30Z&maxPlays=1", noIndex, []db.Song{}}, 503 {"keywords=arovane&minRating=4", 0, []db.Song{LegacySong1}}, 504 {"keywords=arovane&minRating=4&maxPlays=1", noIndex, []db.Song{}}, 505 {"keywords=arovane&firstTrack=1", 0, []db.Song{LegacySong1}}, 506 {"keywords=arovane&tags=instrumental&minRating=4&shuffle=1", ignoreOrder, []db.Song{LegacySong1}}, 507 {"artist=arovane&firstTrack=1", 0, []db.Song{LegacySong1}}, 508 {"artist=arovane&minRating=4", 0, []db.Song{LegacySong1}}, 509 {"artist=arovane&minRating=4&maxPlays=1", noIndex, []db.Song{}}, 510 {"orderByLastPlayed=1&minFirstPlayed=2010-06-09T04:19:30Z&maxLastPlayed=2022-04-06T14:41:14Z", 511 noIndex, []db.Song{LegacySong1, LegacySong2}}, 512 {"orderByLastPlayed=1&maxPlays=1&minFirstPlayed=2010-06-09T04:19:30Z&maxLastPlayed=2022-04-06T14:41:14Z", 513 noIndex, []db.Song{LegacySong2}}, 514 {"orderByLastPlayed=1&maxPlays=1&minFirstPlayed=1276057160&maxLastPlayed=1649256074", 515 noIndex, []db.Song{LegacySong2}}, // pass Unix timestamps 516 } { 517 order := test.CompareOrder 518 if tc.flags&ignoreOrder != 0 { 519 order = test.IgnoreOrder 520 } 521 522 suffixes := []string{""} 523 if tc.flags&noIndex == 0 { 524 // If we should have an index, also verify that the query works both without 525 // falling back and (for good measure) when only using the fallback path. 526 suffixes = append(suffixes, "&fallback=never", "&fallback=force") 527 } 528 for _, suf := range suffixes { 529 query := tc.params + suf 530 log.Printf("Doing query %q", query) 531 if err := compareQueryResults(tc.want, t.QuerySongs(query), order); err != nil { 532 tt.Errorf("%v: %v", query, err) 533 } 534 } 535 } 536 } 537 538 func TestCaching(tt *testing.T) { 539 t, done := initTest(tt) 540 defer done() 541 542 log.Print("Posting and querying a song") 543 const cacheParam = "cacheOnly=1" 544 s1 := LegacySong1 545 t.PostSongs([]db.Song{s1}, true, 0) 546 if err := compareQueryResults([]db.Song{s1}, t.QuerySongs(), test.IgnoreOrder); err != nil { 547 tt.Error("Bad results when querying from cache: ", err) 548 } 549 550 // After rating the song, the query results should still be served from the cache. 551 log.Print("Rating and re-querying") 552 id1 := t.SongID(s1.SHA1) 553 s1.Rating = 5 554 t.RateAndTag(id1, s1.Rating, nil) 555 if err := compareQueryResults([]db.Song{s1}, t.QuerySongs(cacheParam), test.IgnoreOrder); err != nil { 556 tt.Error("Bad results after rating: ", err) 557 } 558 559 // After updating metadata, the updated song should be returned (indicating 560 // that the cached results were dropped). 561 log.Print("Updating and re-querying") 562 s1.Artist = "The Artist Formerly Known As " + s1.Artist 563 t.PostSongs([]db.Song{s1}, false, 0) 564 if err := compareQueryResults([]db.Song{s1}, t.QuerySongs(), test.IgnoreOrder); err != nil { 565 tt.Error("Bad results after updating: ", err) 566 } 567 568 log.Print("Checking that time-based queries aren't cached") 569 timeParam := "maxLastPlayed=" + s1.Plays[1].StartTime.Add(time.Second).Format(time.RFC3339) 570 if err := compareQueryResults([]db.Song{s1}, t.QuerySongs(timeParam), test.IgnoreOrder); err != nil { 571 tt.Errorf("Bad results for %q without cache: %v", timeParam, err) 572 } 573 if err := compareQueryResults([]db.Song{}, t.QuerySongs(timeParam, cacheParam), test.IgnoreOrder); err != nil { 574 tt.Errorf("Bad results for %q from cache: %v", timeParam, err) 575 } 576 577 log.Print("Checking that play-count-based queries aren't cached") 578 playParam := "maxPlays=10" 579 if err := compareQueryResults([]db.Song{s1}, t.QuerySongs(playParam), test.IgnoreOrder); err != nil { 580 tt.Errorf("Bad results for %q: %v", playParam, err) 581 } 582 if err := compareQueryResults([]db.Song{}, t.QuerySongs(playParam, cacheParam), test.IgnoreOrder); err != nil { 583 tt.Errorf("Bad results for %q from cache: %v", playParam, err) 584 } 585 586 log.Print("Checking that datastore cache is used after memcache miss") 587 if err := compareQueryResults([]db.Song{s1}, t.QuerySongs(), test.IgnoreOrder); err != nil { 588 tt.Error("Bad results before flushing memcache: ", err) 589 } 590 t.FlushCache(test.FlushMemcache) 591 if err := compareQueryResults([]db.Song{s1}, t.QuerySongs(cacheParam), test.IgnoreOrder); err != nil { 592 tt.Error("Bad results after flushing memcache: ", err) 593 } 594 595 log.Print("Checking that posting a song drops cached queries") 596 s2 := LegacySong2 597 t.PostSongs([]db.Song{s2}, true, 0) 598 if err := compareQueryResults([]db.Song{s1, s2}, t.QuerySongs(), test.IgnoreOrder); err != nil { 599 tt.Error("Bad results after posting song: ", err) 600 } 601 602 log.Print("Checking that deleting a song drops cached queries") 603 if err := compareQueryResults([]db.Song{s2}, 604 t.QuerySongs("album="+url.QueryEscape(s2.Album)), test.IgnoreOrder); err != nil { 605 tt.Error("Bad results before deleting song: ", err) 606 } 607 id2 := t.SongID(s2.SHA1) 608 t.DeleteSong(id2) 609 if err := compareQueryResults([]db.Song{}, 610 t.QuerySongs("album="+url.QueryEscape(s2.Album)), test.IgnoreOrder); err != nil { 611 tt.Error("Bad results after deleting song: ", err) 612 } 613 } 614 615 func TestAndroid(tt *testing.T) { 616 t, done := initTest(tt) 617 defer done() 618 619 log.Print("Posting songs") 620 now := t.GetNowFromServer() 621 t.PostSongs([]db.Song{LegacySong1, LegacySong2}, true, 0) 622 if err := compareQueryResults([]db.Song{LegacySong1, LegacySong2}, 623 t.GetSongsForAndroid(time.Time{}, test.GetRegularSongs), test.IgnoreOrder); err != nil { 624 tt.Error("Bad results with empty time: ", err) 625 } 626 if err := compareQueryResults([]db.Song{LegacySong1, LegacySong2}, 627 t.GetSongsForAndroid(now, test.GetRegularSongs), test.IgnoreOrder); err != nil { 628 tt.Error("Bad results with old time: ", err) 629 } 630 if err := compareQueryResults([]db.Song{}, 631 t.GetSongsForAndroid(t.GetNowFromServer(), test.GetRegularSongs), test.IgnoreOrder); err != nil { 632 tt.Error("Bad results with now: ", err) 633 } 634 635 log.Print("Rating a song") 636 id := t.SongID(LegacySong1.SHA1) 637 updatedLegacySong1 := LegacySong1 638 updatedLegacySong1.Rating = 5 639 now = t.GetNowFromServer() 640 t.RateAndTag(id, updatedLegacySong1.Rating, nil) 641 if err := compareQueryResults([]db.Song{updatedLegacySong1}, 642 t.GetSongsForAndroid(now, test.GetRegularSongs), test.IgnoreOrder); err != nil { 643 tt.Error("Bad results after rating and tagging: ", err) 644 } 645 646 // Reporting a play shouldn't update the song's last-modified time. 647 log.Print("Reporting play") 648 p := db.NewPlay(test.Date(2014, 9, 15, 2, 5, 18), "127.0.0.1") 649 updatedLegacySong1.Plays = append(updatedLegacySong1.Plays, p) 650 now = t.GetNowFromServer() 651 t.ReportPlayed(id, p.StartTime) 652 if err := compareQueryResults([]db.Song{}, 653 t.GetSongsForAndroid(now, test.GetRegularSongs), test.IgnoreOrder); err != nil { 654 tt.Error("Bad results after reporting play: ", err) 655 } 656 } 657 658 func TestTags(tt *testing.T) { 659 t, done := initTest(tt) 660 defer done() 661 662 log.Print("Getting hopefully-empty tag list") 663 if tags := t.GetTags(false); len(tags) > 0 { 664 tt.Errorf("got unexpected tags %q", tags) 665 } 666 667 log.Print("Posting song and getting tags") 668 t.PostSongs([]db.Song{LegacySong1}, true, 0) 669 if tags := t.GetTags(false); tags != "electronic,instrumental" { 670 tt.Errorf("got tags %q", tags) 671 } 672 673 log.Print("Posting another song and getting tags") 674 t.PostSongs([]db.Song{LegacySong2}, true, 0) 675 if tags := t.GetTags(false); tags != "electronic,instrumental,rock" { 676 tt.Errorf("got tags %q", tags) 677 } 678 679 log.Print("Checking that tags are cached") 680 if tags := t.GetTags(true); tags != "electronic,instrumental,rock" { 681 tt.Errorf("got tags %q", tags) 682 } 683 684 log.Print("Checking that datastore cache is used after memcache miss") 685 t.FlushCache(test.FlushMemcache) 686 if tags := t.GetTags(true); tags != "electronic,instrumental,rock" { 687 tt.Errorf("got tags %q", tags) 688 } 689 690 log.Print("Adding tags and checking that they're returned") 691 id := t.SongID(LegacySong1.SHA1) 692 t.RateAndTag(id, -1, []string{"electronic", "instrumental", "drums", "idm"}) 693 if tags := t.GetTags(false); tags != "drums,electronic,idm,instrumental,rock" { 694 tt.Errorf("got tags %q", tags) 695 } 696 } 697 698 func TestCovers(tt *testing.T) { 699 t, done := initTest(tt) 700 defer done() 701 702 createCover := func(fn string) { 703 f, err := os.Create(filepath.Join(t.CoverDir, fn)) 704 if err != nil { 705 tt.Fatal("Failed creating cover: ", err) 706 } 707 if err := f.Close(); err != nil { 708 tt.Fatal("Failed closing cover: ", err) 709 } 710 } 711 712 log.Print("Writing cover and importing songs") 713 test.Must(tt, test.CopySongs(t.MusicDir, Song0s.Filename, Song5s.Filename)) 714 s5 := Song5s 715 s5.CoverFilename = fmt.Sprintf("%s.jpg", s5.AlbumID) 716 createCover(s5.CoverFilename) 717 t.UpdateSongs() 718 if err := compareQueryResults([]db.Song{Song0s, s5}, t.QuerySongs(), test.IgnoreOrder); err != nil { 719 tt.Error("Bad results after importing songs: ", err) 720 } 721 722 log.Print("Writing another cover and updating") 723 test.Must(tt, test.DeleteSongs(t.MusicDir, Song0s.Filename)) 724 test.Must(tt, test.CopySongs(t.MusicDir, Song0sUpdated.Filename)) 725 s0 := Song0sUpdated 726 s0.CoverFilename = fmt.Sprintf("%s.jpg", s0.AlbumID) 727 createCover(s0.CoverFilename) 728 t.UpdateSongs() 729 if err := compareQueryResults([]db.Song{s0, s5}, t.QuerySongs(), test.IgnoreOrder); err != nil { 730 tt.Error("Bad results after updating songs: ", err) 731 } 732 733 log.Print("Writing cover named after recording ID") 734 test.Must(tt, test.CopySongs(t.MusicDir, Song1s.Filename)) 735 s1 := Song1s 736 s1.CoverFilename = fmt.Sprintf("%s.jpg", s1.RecordingID) 737 createCover(s1.CoverFilename) 738 test.Must(tt, test.DeleteSongs(t.CoverDir, s0.CoverFilename)) 739 t.UpdateSongs() 740 if err := compareQueryResults([]db.Song{s0, s1, s5}, t.QuerySongs(), test.IgnoreOrder); err != nil { 741 tt.Error("Bad results after using recording ID: ", err) 742 } 743 744 log.Print("Checking that covers are dumped") 745 if err := test.CompareSongs([]db.Song{s0, s1, s5}, 746 t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 747 tt.Error("Bad songs when dumping covers: ", err) 748 } 749 if err := test.CompareSongs([]db.Song{s0, s1, s5}, 750 t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 751 tt.Error("Bad songs when dumping covers: ", err) 752 } 753 } 754 755 func TestJSONImport(tt *testing.T) { 756 t, done := initTest(tt) 757 defer done() 758 759 log.Print("Importing songs from JSON") 760 t.ImportSongsFromJSONFile([]db.Song{LegacySong1, LegacySong2}) 761 if err := test.CompareSongs([]db.Song{LegacySong1, LegacySong2}, 762 t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 763 tt.Error("Bad songs after importing from JSON: ", err) 764 } 765 766 log.Print("Updating song from JSON") 767 us := LegacySong1 768 us.Artist += " bogus" 769 us.Title += " bogus" 770 us.Album += " bogus" 771 us.Track += 1 772 us.Disc += 1 773 us.Length *= 2 774 us.Rating = 2 775 us.Plays = us.Plays[0:1] 776 us.Tags = []string{"bogus"} 777 t.ImportSongsFromJSONFile([]db.Song{us, LegacySong2}) 778 if err := test.CompareSongs([]db.Song{us, LegacySong2}, 779 t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 780 tt.Error("Bad songs after updating from JSON: ", err) 781 } 782 783 log.Print("Reporting play") 784 id := t.SongID(us.SHA1) 785 st := test.Date(2014, 9, 15, 2, 5, 18) 786 t.ReportPlayed(id, st) 787 us.Plays = append(us.Plays, db.NewPlay(st, "127.0.0.1")) 788 if err := test.CompareSongs([]db.Song{us, LegacySong2}, 789 t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 790 tt.Error("Bad songs after reporting play: ", err) 791 } 792 793 log.Print("Updating song from JSON but preserving user data") 794 t.ImportSongsFromJSONFile([]db.Song{LegacySong1, LegacySong2}, test.KeepUserDataFlag) 795 us2 := LegacySong1 796 us2.Rating = us.Rating 797 us2.Tags = us.Tags 798 us2.Plays = us.Plays 799 if err := test.CompareSongs([]db.Song{us2, LegacySong2}, 800 t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 801 tt.Error("Bad songs after updating from JSON with preserved user data: ", err) 802 } 803 } 804 805 func TestUpdateList(tt *testing.T) { 806 t, done := initTest(tt) 807 defer done() 808 809 test.Must(tt, test.CopySongs(t.MusicDir, Song0s.Filename, Song1s.Filename, Song5s.Filename)) 810 tempDir := tt.TempDir() 811 listPath, err := test.WriteSongPathsFile(tempDir, Song0s.Filename, Song5s.Filename) 812 if err != nil { 813 tt.Fatal("Failed writing song paths: ", err) 814 } 815 816 gs0 := Song0s 817 gs0.TrackGain = -8.4 818 gs0.AlbumGain = -7.6 819 gs0.PeakAmp = 1.2 820 821 gs5 := Song5s 822 gs5.TrackGain = -6.3 823 gs5.AlbumGain = -7.1 824 gs5.PeakAmp = 0.9 825 826 log.Print("Updating songs from list") 827 t.UpdateSongsFromList(listPath) 828 if err := test.CompareSongs([]db.Song{Song0s, Song5s}, 829 t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 830 tt.Error("Bad songs after updating from list: ", err) 831 } 832 833 // When a dump file is passed, its gain info should be sent to the server. 834 log.Print("Updating songs from list with dumped gains") 835 dumpPath, err := test.WriteSongsToJSONFile(tempDir, gs0, gs5) 836 if err != nil { 837 tt.Fatal("Failed writing JSON file: ", err) 838 } 839 t.UpdateSongsFromList(listPath, test.DumpedGainsFlag(dumpPath)) 840 if err := test.CompareSongs([]db.Song{gs0, gs5}, 841 t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 842 tt.Error("Bad songs after updating from list with dumped gains: ", err) 843 } 844 } 845 846 func TestSorting(tt *testing.T) { 847 t, done := initTest(tt) 848 defer done() 849 850 songs := make([]db.Song, 0) 851 for _, s := range []struct { 852 Artist string 853 Album string 854 AlbumID string 855 Disc int 856 Track int 857 }{ 858 // Sorting should be [Artist, Date (unset), Album, Disc, Track]. 859 // Sorting is tested more rigorously in the server/query package. 860 {"a", "album1", "56", 1, 1}, 861 {"a", "album1", "56", 1, 2}, 862 {"a", "album1", "56", 1, 3}, 863 {"b", "album1", "23", 1, 1}, 864 {"b", "album1", "23", 1, 2}, 865 {"b", "album1", "23", 2, 1}, 866 {"b", "album1", "23", 2, 2}, 867 {"b", "album1", "23", 2, 3}, 868 {"b", "album2", "10", 1, 1}, 869 } { 870 id := fmt.Sprintf("%s-%s-%d-%d", s.Artist, s.Album, s.Disc, s.Track) 871 songs = append(songs, db.Song{ 872 SHA1: id, 873 Filename: id + ".mp3", 874 Artist: s.Artist, 875 Title: fmt.Sprintf("Track %d", s.Track), 876 Album: s.Album, 877 AlbumID: s.AlbumID, 878 Track: s.Track, 879 Disc: s.Disc, 880 }) 881 } 882 883 log.Print("Importing songs and checking sort order") 884 t.ImportSongsFromJSONFile(songs) 885 if err := compareQueryResults(songs, t.QuerySongs(), test.CompareOrder); err != nil { 886 tt.Error("Bad results: ", err) 887 } 888 } 889 890 func TestDeleteSong(tt *testing.T) { 891 t, done := initTest(tt) 892 defer done() 893 894 log.Print("Posting songs and deleting first song") 895 postTime := t.GetNowFromServer() 896 t.PostSongs([]db.Song{LegacySong1, LegacySong2}, true, 0) 897 id1 := t.SongID(LegacySong1.SHA1) 898 t.DeleteSong(id1) 899 900 log.Print("Checking non-deleted song") 901 if err := compareQueryResults([]db.Song{LegacySong2}, t.QuerySongs(), test.IgnoreOrder); err != nil { 902 tt.Error("Bad results for non-deleted song: ", err) 903 } 904 if err := compareQueryResults([]db.Song{LegacySong2}, 905 t.GetSongsForAndroid(time.Time{}, test.GetRegularSongs), test.IgnoreOrder); err != nil { 906 tt.Error("Bad Android results for non-deleted song with empty time: ", err) 907 } 908 if err := compareQueryResults([]db.Song{LegacySong2}, 909 t.GetSongsForAndroid(postTime, test.GetRegularSongs), test.IgnoreOrder); err != nil { 910 tt.Error("Bad Android results for non-deleted song with later time: ", err) 911 } 912 if err := test.CompareSongs([]db.Song{LegacySong2}, 913 t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 914 tt.Error("Bad songs after deletion: ", err) 915 } 916 917 log.Print("Checking that deleted song is in Android query") 918 deletedSongs := t.GetSongsForAndroid(postTime, test.GetDeletedSongs) 919 if err := compareQueryResults([]db.Song{LegacySong1}, deletedSongs, test.IgnoreOrder); err != nil { 920 tt.Error("Bad Android results for deleted song: ", err) 921 } 922 if deletedSongs[0].SongID != id1 { 923 tt.Errorf("Deleted song's ID (%v) didn't match original id (%v)", 924 deletedSongs[0].SongID, id1) 925 } 926 927 log.Print("Deleting second song") 928 laterTime := t.GetNowFromServer() 929 id2 := t.SongID(LegacySong2.SHA1) 930 t.DeleteSong(id2) 931 932 log.Print("Checking no non-deleted songs") 933 if err := compareQueryResults([]db.Song{}, t.QuerySongs(), test.IgnoreOrder); err != nil { 934 tt.Error("Bad results for empty non-deleted songs: ", err) 935 } 936 if err := compareQueryResults([]db.Song{}, 937 t.GetSongsForAndroid(time.Time{}, test.GetRegularSongs), test.IgnoreOrder); err != nil { 938 tt.Error("Bad Android results for empty non-deleted songs: ", err) 939 } 940 if err := test.CompareSongs([]db.Song{}, t.DumpSongs(test.StripIDs), 941 test.IgnoreOrder); err != nil { 942 tt.Error("Bad songs after deleting second song: ", err) 943 } 944 945 log.Print("Checking that both deleted songs are in Android query") 946 deletedSongs = t.GetSongsForAndroid(laterTime, test.GetDeletedSongs) 947 if err := compareQueryResults([]db.Song{LegacySong2}, deletedSongs, test.IgnoreOrder); err != nil { 948 tt.Error("Bad deleted songs for Android: ", err) 949 } 950 if deletedSongs[0].SongID != id2 { 951 tt.Errorf("Deleted song's ID (%v) didn't match original id (%v)", 952 deletedSongs[0].SongID, id2) 953 } 954 } 955 956 func TestMergeSongs(tt *testing.T) { 957 t, done := initTest(tt) 958 defer done() 959 960 log.Print("Posting songs") 961 s1 := Song0s 962 s1.Rating = 4 963 s1.Tags = []string{"guitar", "instrumental"} 964 s1.Plays = []db.Play{ 965 db.NewPlay(test.Date(2014, 9, 15, 2, 5, 18), "127.0.0.1"), 966 } 967 s2 := Song1s 968 s2.Rating = 2 969 s2.Tags = []string{"drums", "guitar", "rock"} 970 s2.Plays = []db.Play{ 971 db.NewPlay(test.Date(2014, 9, 15, 2, 8, 43), "127.0.0.1"), 972 db.NewPlay(test.Date(2014, 9, 15, 2, 13, 4), "127.0.0.1"), 973 } 974 t.PostSongs([]db.Song{s1, s2}, true, 0) 975 976 log.Print("Merging songs") 977 t.MergeSongs(t.SongID(s1.SHA1), t.SongID(s2.SHA1)) 978 979 log.Print("Checking that songs were merged") 980 s2.Rating = s1.Rating 981 s2.Tags = []string{"drums", "guitar", "instrumental", "rock"} 982 s2.Plays = append(s2.Plays, s1.Plays...) 983 sort.Sort(db.PlayArray(s2.Plays)) 984 if err := test.CompareSongs([]db.Song{s1, s2}, t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 985 tt.Fatal("Bad songs after merging: ", err) 986 } 987 988 log.Print("Merging songs again") 989 t.MergeSongs(t.SongID(s1.SHA1), t.SongID(s2.SHA1), test.DeleteAfterMergeFlag) 990 991 log.Print("Checking that first song was deleted") 992 if err := test.CompareSongs([]db.Song{s2}, t.DumpSongs(test.StripIDs), test.IgnoreOrder); err != nil { 993 tt.Fatal("Bad songs after merging again: ", err) 994 } 995 } 996 997 func TestReindexSongs(tt *testing.T) { 998 t, done := initTest(tt) 999 defer done() 1000 1001 log.Print("Posting song") 1002 s := Song0s 1003 s.Rating = 4 1004 s.Tags = []string{"guitar", "instrumental"} 1005 s.Plays = []db.Play{ 1006 db.NewPlay(test.Date(2014, 9, 15, 2, 5, 18), "127.0.0.1"), 1007 } 1008 t.PostSongs([]db.Song{s}, true, 0) 1009 1010 log.Print("Reindexing") 1011 t.ReindexSongs() 1012 1013 // This doesn't actually check that we reindex, but it at least verifies that the server isn't 1014 // dropping user data. 1015 log.Print("Querying after reindex") 1016 if err := compareQueryResults([]db.Song{s}, t.QuerySongs("minRating=1"), test.IgnoreOrder); err != nil { 1017 tt.Error("Bad results for query: ", err) 1018 } 1019 } 1020 1021 func TestStats(tt *testing.T) { 1022 t, done := initTest(tt) 1023 defer done() 1024 1025 log.Print("Posting songs") 1026 s1 := Song1s 1027 s1.Rating = 4 1028 s1.Tags = []string{"guitar", "instrumental"} 1029 s1.Plays = []db.Play{db.NewPlay(test.Date(2014, 9, 15, 2, 5, 18), "127.0.0.1")} 1030 s2 := Song5s 1031 s2.Rating = 5 1032 s2.Tags = []string{"guitar", "vocals"} 1033 s2.Plays = []db.Play{ 1034 db.NewPlay(test.Date(2013, 9, 15, 2, 5, 18), "127.0.0.1"), 1035 db.NewPlay(test.Date(2014, 9, 15, 2, 5, 18), "127.0.0.1"), 1036 } 1037 s3 := Song10s 1038 t.PostSongs([]db.Song{s1, s2, s3}, true, 0) 1039 1040 log.Print("Updating stats") 1041 t.UpdateStats() 1042 1043 log.Print("Checking stats") 1044 got := t.GetStats() 1045 if got.UpdateTime.IsZero() { 1046 tt.Error("Stats update time is zero") 1047 } 1048 want := db.Stats{ 1049 Songs: 3, 1050 Albums: 3, 1051 TotalSec: s1.Length + s2.Length + s3.Length, 1052 Ratings: map[int]int{0: 1, 4: 1, 5: 1}, 1053 SongDecades: map[int]int{0: 1, 2000: 1, 2010: 1}, 1054 Tags: map[string]int{"guitar": 2, "instrumental": 1, "vocals": 1}, 1055 Years: map[int]db.PlayStats{ 1056 2013: {Plays: 1, TotalSec: s2.Length, FirstPlays: 1}, 1057 2014: {Plays: 2, TotalSec: s1.Length + s2.Length, FirstPlays: 1, LastPlays: 2}, 1058 }, 1059 UpdateTime: got.UpdateTime, // checked for non-zero earlier 1060 } 1061 if !reflect.DeepEqual(got, want) { 1062 tt.Errorf("Got %+v, want %+v", got, want) 1063 } 1064 } 1065 1066 func TestUpdateError(tt *testing.T) { 1067 t, done := initTest(tt) 1068 defer done() 1069 1070 // Write a file with a header with a bogus ID3v2 version number (which should cause an error in 1071 // taglib-go) and no trailing ID3v1 tag (so we can't fall back to it). 1072 f, err := os.Create(filepath.Join(t.MusicDir, "song.mp3")) 1073 if err != nil { 1074 tt.Fatal("Failed creating file: ", err) 1075 } 1076 if _, err := io.WriteString(f, "ID301"+strings.Repeat("\x00", 1024)); err != nil { 1077 tt.Fatal("Failed writing file: ", err) 1078 } 1079 if err := f.Close(); err != nil { 1080 tt.Fatal("Failed closing file: ", err) 1081 } 1082 want := filepath.Base(f.Name()) + ": taglib: format not supported" 1083 1084 log.Print("Importing malformed song") 1085 if _, stderr, err := t.UpdateSongsRaw(); err == nil { 1086 tt.Error("Update unexpectedly succeeded with bad file\nstderr:\n" + stderr) 1087 } else if !strings.Contains(stderr, want) { 1088 tt.Errorf("Output doesn't include %q\nstderr:\n%s", want, stderr) 1089 } 1090 1091 // We shouldn't have written the last update time, so a second attempt should also fail. 1092 log.Print("Importing malformed song again") 1093 if _, stderr, err := t.UpdateSongsRaw(); err == nil { 1094 tt.Error("Repeated update attempt unexpectedly succeeded\nstderr:\n" + stderr) 1095 } 1096 } 1097 1098 func TestGuestUser(tt *testing.T) { 1099 t, done := initTest(tt) 1100 defer done() 1101 1102 log.Print("Posting song and updating stats") 1103 t.PostSongs([]db.Song{Song0s}, false, 0) 1104 t.UpdateStats() 1105 songID := t.SongID(Song0s.SHA1) 1106 1107 send := func(method, path, user, pass string) (int, []byte) { 1108 req := t.NewRequest(method, path, nil) 1109 req.SetBasicAuth(user, pass) 1110 resp, err := http.DefaultClient.Do(req) 1111 if err != nil { 1112 tt.Fatalf("%v request for %v from %q failed: %v", method, path, user, err) 1113 } 1114 defer resp.Body.Close() 1115 body, err := io.ReadAll(resp.Body) 1116 if err != nil { 1117 tt.Fatalf("Failed reading body from %v request for %v from %q failed: %v", method, path, user, err) 1118 } 1119 return resp.StatusCode, body 1120 } 1121 1122 // Normal (or admin) users should be able to call /rate_and_tag, but guest users shouldn't. 1123 log.Print("Checking /rate_and_tag access") 1124 ratePath := "rate_and_tag?songId=" + songID + "&rating=5&tags=drums+guitar+rock" 1125 if code, _ := send("POST", ratePath, test.Username, test.Password); code != http.StatusOK { 1126 tt.Fatalf("Normal request for /%v returned %v; want %v", ratePath, code, http.StatusOK) 1127 } 1128 if code, _ := send("POST", ratePath, guestUsername, guestPassword); code != http.StatusForbidden { 1129 tt.Fatalf("Guest request for /%v returned %v; want %v", ratePath, code, http.StatusForbidden) 1130 } 1131 1132 // Ditto for /played. 1133 log.Print("Checking /played access") 1134 playedPath := "played?songId=" + songID + "&startTime=2006-01-02T15:04:05Z" 1135 if code, _ := send("POST", playedPath, test.Username, test.Password); code != http.StatusOK { 1136 tt.Fatalf("Normal request for /%v returned %v; want %v", playedPath, code, http.StatusOK) 1137 } 1138 if code, _ := send("POST", playedPath, guestUsername, guestPassword); code != http.StatusForbidden { 1139 tt.Fatalf("Guest request for /%v returned %v; want %v", playedPath, code, http.StatusForbidden) 1140 } 1141 1142 // The /user endpoint should return information about the requesting user. 1143 log.Print("Checking /user") 1144 if code, got := send("GET", "user", test.Username, test.Password); code != http.StatusOK { 1145 tt.Fatalf("Normal request for /user returned %v; want %v", code, http.StatusOK) 1146 } else if want, err := json.Marshal(config.User{Username: test.Username, Admin: true}); err != nil { 1147 tt.Fatal("Failed marshaling:", err) 1148 } else if !bytes.Equal(got, want) { 1149 tt.Fatalf("Normal request for /user gave %q; want %q", got, want) 1150 } 1151 if code, got := send("GET", "user", guestUsername, guestPassword); code != http.StatusOK { 1152 tt.Fatalf("Guest request for /user returned %v; want %v", code, http.StatusOK) 1153 } else if want, err := json.Marshal( 1154 config.User{ 1155 Username: guestUsername, 1156 Guest: true, 1157 Presets: guestPresets, 1158 ExcludedTags: guestExcludedTags, 1159 }); err != nil { 1160 tt.Fatal("Failed marshaling:", err) 1161 } else if !bytes.Equal(got, want) { 1162 tt.Fatalf("Guest request for /user gave %q; want %q", got, want) 1163 } 1164 1165 // Guest users should be able to fetch /stats, but not update them via /stats?update=1. 1166 log.Print("Checking /stats access") 1167 if code, _ := send("GET", "stats", guestUsername, guestPassword); code != http.StatusOK { 1168 tt.Fatalf("Guest request for /stats returned %v; want %v", code, http.StatusOK) 1169 } 1170 if code, _ := send("GET", "stats?update=1", guestUsername, guestPassword); code != http.StatusForbidden { 1171 tt.Fatalf("Guest request for /stats?update=1 returned %v; want %v", code, http.StatusForbidden) 1172 } 1173 1174 log.Print("Checking that tags are excluded from /tags") 1175 var tags []string 1176 if code, body := send("GET", "tags", guestUsername, guestPassword); code != http.StatusOK { 1177 tt.Fatalf("Guest request for /tags returned %v; want %v", code, http.StatusOK) 1178 } else if err := json.Unmarshal(body, &tags); err != nil { 1179 tt.Fatal("Failed unmarshaling:", err) 1180 } else if want := []string{"drums", "guitar"}; !reflect.DeepEqual(tags, want) { 1181 tt.Fatalf("Guest request for /tags returned %q; want %q", tags, want) 1182 } 1183 1184 log.Print("Checking that excluded tags are added to /query") 1185 if code, got := send("GET", "query", guestUsername, guestPassword); code != http.StatusOK { 1186 tt.Fatalf("Guest request for /query returned %v; want %v", code, http.StatusOK) 1187 } else if want := "[]"; string(got) != want { 1188 tt.Fatalf("Guest request for /query returned %q; want %q", got, want) 1189 } 1190 1191 // Normal (or admin) users should be able to go above the guest rate limit for /song. 1192 log.Print("Checking /song rate-limiting") 1193 songPath := "song?filename=" + url.QueryEscape(Song0s.Filename) 1194 for i := 0; i <= maxGuestRequests; i++ { 1195 if code, _ := send("GET", songPath, test.Username, test.Password); code != http.StatusOK { 1196 tt.Fatalf("Normal request %v for /%v returned %v; want %v", i, songPath, code, http.StatusOK) 1197 } 1198 } 1199 // The guest user should get an error when they exceed the limit. 1200 for i := 0; i <= maxGuestRequests; i++ { 1201 want := http.StatusOK 1202 if i == maxGuestRequests { 1203 want = http.StatusTooManyRequests 1204 } 1205 if code, _ := send("GET", songPath, guestUsername, guestPassword); code != want { 1206 tt.Fatalf("Guest request %v for /%v returned %v; want %v", i, songPath, code, want) 1207 } 1208 } 1209 }