// // tank, Import and Playlist Daemon for Aura project // Copyright (C) 2017-2020 Christian Pointner <equinox@helsinki.at> // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. // package store import ( "fmt" "io" "os" "os/user" "path/filepath" "reflect" "testing" "time" //"github.com/jinzhu/gorm" ) var ( testBasePath = "run/aura-tank_testing" testShowName = "foo" testTestDirPath = filepath.Join(testBasePath, testShowName) testDBPort = uint16(0) testDBUsername = "tank" testDBPassword = "aura" testDBDB = "tank" testShow1 = "test1" testShow2 = "test2" testUser1 = "user1" testUser2 = "user2" testSourceURI1 = "upload://test1.mp3" testSourceURI2 = "upload://test2.ogg" testFileArtist1 = "solo artist" testFileArtist2 = "band of 2" testFileAlbum1 = "first album" testFileAlbum2 = "second album" testFileTitle1 = "this is one title" testFileTitle2 = "this is not two title's" ) func testDBConfig() (cfg DBConfig) { cfg.Type = os.Getenv("AURA_TANK_TEST_DB_TYPE") if cfg.Type == "" { cfg.Type = "mysql" } cfg.Host = os.Getenv("AURA_TANK_TEST_DB_HOST") if cfg.Host == "" { cfg.Host = "127.0.0.1" } switch cfg.Type { case "mysql": cfg.TLS = "false" case "postgres": cfg.TLS = "disable" } cfg.Port = testDBPort cfg.Username = testDBUsername cfg.Password = testDBPassword cfg.DB = testDBDB return } func newTestStore(t *testing.T) *Store { cfg := Config{} cfg.BasePath = testBasePath cfg.DB = testDBConfig() // TODO: drop database (all tables and constrains) store, err := NewStore(cfg) if err != nil { t.Fatalf("unexpected error: %v", err) } return store } func TestMain(m *testing.M) { u, err := user.Current() if err != nil { os.Exit(-1) } testBasePath = fmt.Sprintf("/run/user/%s/aura-tank_testing", u.Uid) os.RemoveAll(testBasePath) testTestDirPath = filepath.Join(testBasePath, testShowName) os.Exit(m.Run()) } // // Testing // func TestOpen(t *testing.T) { // base-path is non-existing directory cfg := Config{} cfg.BasePath = "/nonexistend/" cfg.DB = testDBConfig() if _, err := NewStore(cfg); err == nil { t.Fatalf("opening store in nonexisting directory should throw an error") } cfg.BasePath = testBasePath if err := os.MkdirAll(testBasePath, 0700); err != nil { t.Fatalf("unexpected error: %v", err) } store, err := NewStore(cfg) if err != nil { t.Fatalf("creating new store failed: %v", err) } store.Close() if store, err = NewStore(cfg); err != nil { t.Fatalf("re-opening existing store failed: %v", err) } store.Close() if err := os.RemoveAll(testTestDirPath); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestMigrations(t *testing.T) { cfg := Config{} cfg.BasePath = testBasePath cfg.DB = testDBConfig() // os.Remove(testDBConnection) store, err := NewStore(cfg) if err != nil { t.Fatalf("creating new store failed: %v", err) } expected := dbMigrations[len(dbMigrations)-1].ID if store.GetRevision() != expected { t.Fatalf("for now new databases should have revision %q: got %q", expected, store.GetRevision()) } store.Close() // TODO: needs more testing!!!! } //// Arrrrgh!!!!! //// both database/sql and gorm are incredibly stupid!!!! //// using database/sql alone it is needlessly hard to write portable sql queries: //// postgres: db.Exec("INSERT INTO shows (name) VALUES ($1)", testShow1) //// mysql: db.Exec("INSERT INTO shows (name) VALUES (?)", testShow1) //// //// using gorm db.Exec() will deal with that but i couldn't find a good way to get //// the last inserted id without using the model code of gorm and we specifically //// don't want to use the model code since it is not clear whether the constraints //// are actually enforced by the DBMS or by some gorm callbacks... //// // func TestDBConstraints(t *testing.T) { // // we don't want to use the model but hand written SQL commands here since we want to check // // whether the constraints are really enforced by the DBMS and not by some magic bullshit in gorm! // // This is even worse since gorm.AutoUpdate will not deal with foreign key constraints and we // // need do it ourselves at the migration scripts. It would be really nice to be able to check // // whether the migrations do the right thing!!! // db, err := gorm.Open(testDBType, testDBConnection) // if err != nil { // t.Fatalf("unexpected error: %v", err) // } // if err = db.DB().Ping(); err != nil { // t.Fatalf("unexpected error: %v", err) // } // res := db.Exec("INSERT INTO shows (name) VALUES (?)", testShow1) // if res.Error != nil { // t.Fatalf("adding show '%s' shouldn't fail: %v", testShow1, res.Error) // } // if res = db.Exec("INSERT INTO shows (name) VALUES (?)", testShow1); res.Error == nil { // t.Fatalf("re-adding the same show should fail") // } // if res = db.Exec("INSERT INTO files (show_name, size) VALUES (?, ?)", testShow1, 500); res.Error != nil { // t.Fatalf("re-adding the same show should fail") // } // } // Shows // func checkShows(t *testing.T, shows []Show, expected []string) { // if len(shows) != len(expected) { // t.Fatalf("expected %d shows in store but got %d: %v", len(expected), len(shows), shows) // } for _, sname := range expected { found := false for _, g := range shows { if sname == g.Name { found = true break } } if !found { t.Fatalf("expected show '%s' to be in store but got: %v", sname, shows) } if st, err := os.Stat(filepath.Join(testBasePath, sname)); err != nil { t.Fatalf("can't open show directory for show '%s': %v", sname, err) } else if !st.IsDir() { t.Fatalf("path of show '%s' is not a directory", sname) } } } func TestShows(t *testing.T) { store := newTestStore(t) shows, err := store.ListShows() if err != nil { t.Fatalf("listing shows failed: %v", err) } checkShows(t, shows, []string{}) if f, err := os.Create(testTestDirPath); err != nil { t.Fatalf("unexpected error: %v", err) } else { io.WriteString(f, "this is not a directory") f.Close() } if _, err = store.CreateShow(testShowName); err == nil { t.Fatalf("creating a show where dir exists but is not a directory should throw an error") } if err := os.RemoveAll(testTestDirPath); err != nil { t.Fatalf("unexpected error: %v", err) } if _, err = store.CreateShow(testShow1); err != nil { t.Fatalf("creating show failed: %v", err) } if _, err = store.CreateShow(testShow2); err != nil { t.Fatalf("creating show failed: %v", err) } shows, err = store.ListShows() if err != nil { t.Fatalf("unexpected error: %v", err) } checkShows(t, shows, []string{testShow1, testShow2}) if err = store.DeleteShow(testShow1); err != nil { t.Fatalf("deleting show '%s' failed: %v", testShow1, err) } shows, err = store.ListShows() if err != nil { t.Fatalf("unexpected error: %v", err) } checkShows(t, shows, []string{testShow2}) if err = store.DeleteShow(testShow2); err != nil { t.Fatalf("deleteing show '%s' failed: %v", testShow2, err) } checkShows(t, shows, []string{}) } // Files // func TestFilesListCreateDelete(t *testing.T) { store := newTestStore(t) files, err := store.ListFiles(testShowName, -1, -1) if err != nil { t.Fatalf("listing files of test show failed: %v", err) } if len(files) != 0 { t.Fatalf("a newly created store should contain no files in test show but ListFiles returned: %v", files) } files, err = store.ListFiles("notexistend", -1, -1) if err != nil { t.Fatalf("listing files of not existing show shouldn't throw an error but returned: %v", err) } if len(files) != 0 { t.Fatalf("listing files of not existing show should return and empty list but ListFiles returned: %v", files) } testFile, err := store.CreateFile(testShowName, File{}) if err != nil { t.Fatalf("creating file in test show failed: %v", err) } files, err = store.ListFiles(testShowName, -1, -1) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(files) != 1 { t.Fatalf("ListFiles should return a single file but returned: %v", files) } _, err = store.CreateFile(testShow1, File{Size: 17}) if err != nil { t.Fatalf("creating file in not existing show shouldn't throw an error but CreateFile returned: %v", err) } _, err = store.CreateFile(testShow1, File{Size: 23}) if err != nil { t.Fatalf("creating file in not existing show shouldn't throw an error but CreateFile returned: %v", err) } shows, err := store.ListShows() if err != nil { t.Fatalf("unexpected error: %v", err) } checkShows(t, shows, []string{testShowName, testShow1}) files, err = store.ListFiles(testShow1, -1, -1) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(files) != 2 { t.Fatalf("ListFiles should return a two files but returned: %v", files) } // clean up so next test can run with a clean DB, TODO: remove as soon as newTestStore() can re-init the DB if err = store.DeleteShow(testShow1); err != nil { t.Fatalf("unexpected error: %v", err) } if err = store.DeleteFile(testShowName, testFile.ID); err != nil { t.Fatalf("deleteing file %d of show '%s' failed: %v", testFile.ID, testShowName, err) } checkShows(t, shows, []string{testShowName}) } func fileEqual(a, b *File) bool { // TODO: comparing the whole file struct using DeepEqual does not work because it does not use time.Equal when comparing timestamps... return (a.Size == b.Size || reflect.DeepEqual(a.Source, b.Source) || reflect.DeepEqual(a.Metadata, b.Metadata)) } func TestFilesCreateAndGet(t *testing.T) { store := newTestStore(t) if _, err := store.GetFile(testShow1, 0); err != ErrNotFound { t.Fatalf("getting file in not-existing show should return ErrNotFound, but GetFile returned: %v", err) } file1 := File{Size: 12345} file1.Source.URI = testSourceURI1 file1.Metadata.Artist = testFileArtist1 file1.Metadata.Album = testFileAlbum1 file1.Metadata.Title = testFileTitle1 in1, err := store.CreateFile(testShow1, file1) if err != nil { t.Fatalf("unexpected error: %v", err) } out1, err := store.GetFile(testShow1, in1.ID) if err != nil { t.Fatalf("getting existing file from store shouldn't return an error, but GetFile returned: %v", err) } if !fileEqual(in1, out1) { t.Fatalf("GetFile returned different file than expected. Got %+v, expected %+v", out1, in1) } if _, err = store.GetFile(testShow1, 0); err != ErrNotFound { t.Fatalf("getting not-existing file in existing show should return ErrNotFound, but GetFile returned: %v", err) } file2 := File{Size: 54321} file2.Source.URI = testSourceURI2 file2.Metadata.Artist = testFileArtist2 file2.Metadata.Album = testFileAlbum2 file2.Metadata.Title = testFileTitle2 in2, err := store.CreateFile(testShow1, file2) if err != nil { t.Fatalf("unexpected error: %v", err) } out2, err := store.GetFile(testShow1, in2.ID) if err != nil { t.Fatalf("getting existing file from store shouldn't return an error, but GetFile returned: %v", err) } if !fileEqual(in2, out2) { t.Fatalf("GetFile returned different file than expected. Got %+v, expected %+v", out2, in2) } files, err := store.ListFiles(testShow1, -1, -1) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(files) != 2 { t.Fatalf("show '%s' should contain 2 files but ListFiles returned: %+v", testShow1, files) } for _, file := range files { if file.ID == in1.ID { if !fileEqual(in1, out1) { t.Fatalf("ListFile returned different file than expected. Got %+v, expected %+v", out1, in1) } } else if file.ID == in2.ID { if !fileEqual(in2, out2) { t.Fatalf("ListFile returned different file than expected. Got %+v, expected %+v", out2, in2) } } else { t.Fatalf("show '%s' should only contain files %d and %d but ListFiles returned: %+v", testShow1, in1.ID, in2.ID, files) } } // clean up so next test can run with a clean DB, TODO: remove as soon as newTestStore() can re-init the DB if err = store.DeleteShow(testShow1); err != nil { t.Fatalf("unexpected error: %v", err) } } // TODO: add test for pagination in store.ListeFiles() func TestFilesUpdate(t *testing.T) { store := newTestStore(t) if _, err := store.UpdateFile(testShowName, 0, File{}); err != ErrNotFound { t.Fatalf("updateting not-existing file hould return ErrNotFound, but UpdateFile returned: %v", err) } file := File{Size: 12345} file.Source.URI = testSourceURI1 file.Metadata.Artist = testFileArtist1 file.Metadata.Album = testFileAlbum1 file.Metadata.Title = testFileTitle1 in, err := store.CreateFile(testShow2, file) if err != nil { t.Fatalf("unexpected error: %v", err) } out, err := store.GetFile(testShow2, in.ID) if err != nil { t.Fatalf("unexpected error: %v", err) } if !fileEqual(in, out) { t.Fatalf("GetFile returned different file than expected. Got %+v, expected %+v", out, in) } out.Size = 54321 out.Source.URI = testSourceURI2 out.Metadata.Artist = testFileArtist2 out.Metadata.Album = testFileAlbum2 out.Metadata.Title = testFileTitle2 if _, err = store.UpdateFile(testShow2, in.ID, *out); err != nil { t.Fatalf("updateting an existing file shouldn't fail but UpdateFile returned: %v", err) } files, err := store.ListFiles(testShow2, -1, -1) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(files) != 1 { t.Fatalf("show '%s' should contain 1 file but ListFiles returned: %+v", testShow2, files) } if files[0].ID != in.ID || !fileEqual(out, &(files[0])) { t.Fatalf("ListFile returned different file than expected. Got %+v, expected %+v", out, files[0]) } // clean up so next test can run with a clean DB, TODO: remove as soon as newTestStore() can re-init the DB if err = store.DeleteShow(testShow2); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestFilesDelete(t *testing.T) { store := newTestStore(t) if err := store.DeleteFile(testShow1, 0); err != ErrNotFound { t.Fatalf("deleting not-existing file should return ErrNotFound, but DeleteFile returned: %v", err) } file, err := store.CreateFile(testShow1, File{Size: 12345}) if err != nil { t.Fatalf("unexpected error: %v", err) } if err = store.DeleteFile(testShow1, file.ID); err != nil { t.Fatalf("deleting file failed, DeleteFile returned: %v", err) } if err = store.DeleteFile(testShow1, file.ID); err != ErrNotFound { t.Fatalf("repeated deletion of file should return ErrNotFound, but DeleteFile returned: %v", err) } // clean up so next test can run with a clean DB, TODO: remove as soon as newTestStore() can re-init the DB if err = store.DeleteShow(testShow1); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestFilesImportState(t *testing.T) { store := newTestStore(t) file, err := store.CreateFile(testShow1, File{}) if err != nil { t.Fatalf("unexpected error: %v", err) } if file.Source.Import.State != ImportNew { t.Fatalf("newly created file should have state %q but has %q", ImportNew, file.Source.Import.State) } if _, err = store.SetFileImportStateInitializing(file.ShowName, file.ID); err != nil { t.Fatalf("unexpected error: %v", err) } if _, err = store.SetFileImportStateInitializing(file.ShowName, file.ID); err != ErrFileNotNew { t.Fatalf("setting file import state to %q for file that is not new must fail with ErrFileNotNew but got: %v", ImportInitializing, err) } if file, err = store.SetFileImportStatePending(file.ShowName, file.ID); err != nil { t.Fatalf("unexpected error: %v", err) } if file.Source.Import.State != ImportPending { t.Fatalf("file should now have state %q but has %q", ImportPending, file.Source.Import.State) } if file, err = store.SetFileImportStateRunning(file.ShowName, file.ID); err != nil { t.Fatalf("unexpected error: %v", err) } if file.Source.Import.State != ImportRunning { t.Fatalf("file should now have state %q but has %q", ImportRunning, file.Source.Import.State) } // TODO: testing done needs some extra work since the store will try to read the metadata from the file if file, err = store.SetFileImportStateAborted(file.ShowName, file.ID, "computer says noooo..."); err != nil { t.Fatalf("unexpected error: %v", err) } if file.Source.Import.State != ImportAborted { t.Fatalf("file should now have state %q but has %q", ImportAborted, file.Source.Import.State) } if file.Source.Import.Error != "computer says noooo..." { t.Fatalf("file import error is wrong: %q", file.Source.Import.Error) } } func TestFilesSourceHash(t *testing.T) { store := newTestStore(t) file, err := store.CreateFile(testShow1, File{}) if err != nil { t.Fatalf("unexpected error: %v", err) } if file.Source.Hash != "" { t.Fatalf("new file shoud have no hash set but has: %q", file.Source.Hash) } hash := "this-is-the-output-of-a-hash-function" if file, err = store.UpdateFileSourceHash(file.ShowName, file.ID, hash); err != nil { t.Fatalf("unexpected error: %v", err) } if file.Source.Hash != hash { t.Fatalf("file should now have hash set to %q but has %q", hash, file.Source.Hash) } } // Playlists // type playlistTestEntry struct { uri string duration *time.Duration } func generateTestPlaylist(entries ...playlistTestEntry) (p Playlist) { for _, entry := range entries { e := PlaylistEntry{URI: entry.uri, Duration: entry.duration} p.Entries = append(p.Entries, e) } return p } func TestPlaylistsListCreateDelete(t *testing.T) { store := newTestStore(t) playlists, err := store.ListPlaylists(testShowName, -1, -1) if err != nil { t.Fatalf("listing playlists of test show failed: %v", err) } if len(playlists) != 0 { t.Fatalf("a newly created store should contain no playlists in test show but ListPlaylists returned: %v", playlists) } playlists, err = store.ListPlaylists("notexistend", -1, -1) if err != nil { t.Fatalf("listing playlists of not existing show shouldn't throw an error but returned: %v", err) } if len(playlists) != 0 { t.Fatalf("listing playlists of not existing show should return and empty list but ListPlaylists returned: %v", playlists) } in := generateTestPlaylist(playlistTestEntry{"audioin://1", nil}, playlistTestEntry{"http://stream.example.com/live.mp", nil}) testPlaylist, err := store.CreatePlaylist(testShowName, in) if err != ErrPlaylistHasMultipleNullDurationEntries { t.Fatalf("creating playlist with more than one non-file entry without duration should fail with specific error but returned: %v", err) } testDuration := time.Second in = generateTestPlaylist(playlistTestEntry{"audioin://1", &testDuration}, playlistTestEntry{"http://stream.example.com/live.mp", nil}) testPlaylist, err = store.CreatePlaylist(testShowName, in) if err != nil { t.Fatalf("creating playlist in test show failed: %v", err) } playlists, err = store.ListPlaylists(testShowName, -1, -1) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(playlists) != 1 { t.Fatalf("ListPlaylists should return a single playlist but returned: %v", playlists) } in1 := generateTestPlaylist(playlistTestEntry{"audioin://1", &testDuration}, playlistTestEntry{"http://stream.example.com/live.mp3", nil}) _, err = store.CreatePlaylist(testShow1, in1) if err != nil { t.Fatalf("creating playlist in not existing show shouldn't throw an error but CreatePlaylist returned: %v", err) } in2 := generateTestPlaylist(playlistTestEntry{"https://stream.example.com/other.ogg", &testDuration}, playlistTestEntry{"audioin://2", nil}) _, err = store.CreatePlaylist(testShow1, in2) if err != nil { t.Fatalf("creating playlist in not existing show shouldn't throw an error but CreatePlaylist returned: %v", err) } shows, err := store.ListShows() if err != nil { t.Fatalf("unexpected error: %v", err) } checkShows(t, shows, []string{testShowName, testShow1}) playlists, err = store.ListPlaylists(testShow1, -1, -1) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(playlists) != 2 { t.Fatalf("ListPlaylists should return two playlists but returned: %v", playlists) } // clean up so next test can run with a clean DB, TODO: remove as soon as newTestStore() can re-init the DB if err = store.DeleteShow(testShow1); err != nil { t.Fatalf("unexpected error: %v", err) } if err = store.DeletePlaylist(testShowName, testPlaylist.ID); err != nil { t.Fatalf("deleteing playlist %d of show '%s' failed: %v", testPlaylist.ID, testShowName, err) } } func TestPlaylistsCreateAndGet(t *testing.T) { store := newTestStore(t) f := File{Size: 12345} f.Source.URI = testSourceURI1 f.Metadata.Artist = testFileArtist1 f.Metadata.Album = testFileAlbum1 f.Metadata.Title = testFileTitle1 file1, err := store.CreateFile(testShow1, f) if err != nil { t.Fatalf("unexpected error: %v", err) } if _, err := store.GetPlaylist(testShow1, 0); err != ErrNotFound { t.Fatalf("getting playlist in not-existing show should return ErrNotFound, but GetPlaylist returned: %v", err) } p := generateTestPlaylist(playlistTestEntry{"http://stream.example.com/stream.mp3", nil}) p.Entries = append(p.Entries, PlaylistEntry{File: &File{ShowName: file1.ShowName, ID: file1.ID}}) list1, err := store.CreatePlaylist(testShow1, p) if err != nil { t.Fatalf("unexpected error: %v", err) } if _, err := store.GetPlaylist(testShow1, list1.ID); err != nil { t.Fatalf("getting existing playlist from store shouldn't return an error, but GetPlaylist returned: %v", err) } // TODO: check if playlists are equal p = generateTestPlaylist(playlistTestEntry{"http://stream.example.com/other.mp3", nil}, playlistTestEntry{fmt.Sprintf("file://%s/%d", file1.ShowName, file1.ID), nil}) if _, err = store.CreatePlaylist(testShow1, p); err != nil { t.Fatalf("unexpected error: %v", err) } playlists, err := store.ListPlaylists(testShow1, -1, -1) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(playlists) != 2 { t.Fatalf("ListPlaylists should return two playlists but returned: %v", playlists) } // TODO: check playlists contains both lists } // TODO: add test for pagination in store.ListePlaylists() // File usage // func TestFileUsage(t *testing.T) { store := newTestStore(t) file := &File{Size: 12345} file.Source.URI = testSourceURI1 file.Metadata.Artist = testFileArtist1 file.Metadata.Album = testFileAlbum1 file.Metadata.Title = testFileTitle1 file, err := store.CreateFile(testShow1, *file) if err != nil { t.Fatalf("unexpected error: %v", err) } lists, err := store.GetFileUsage(file.ShowName, file.ID) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(lists) != 0 { t.Fatalf("file should be in use by any playlist but got %d entries in usage list", len(lists)) } p := generateTestPlaylist(playlistTestEntry{"http://stream.example.com/stream.mp3", nil}) p.Entries = append(p.Entries, PlaylistEntry{File: &File{ShowName: file.ShowName, ID: file.ID}}) list, err := store.CreatePlaylist(testShow1, p) if err != nil { t.Fatalf("unexpected error: %v", err) } lists, err = store.GetFileUsage(file.ShowName, file.ID) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(lists) != 1 { t.Fatalf("file should be used by exactly one playlist but got %d entries in usage list", len(lists)) } if lists[0].ID != list.ID { t.Fatalf("file should be in use by playlist %d but entries of usage list only contains: %d", list.ID, lists[0].ID) } err = store.DeleteFile(file.ShowName, file.ID) errInUse, ok := err.(*ErrFileInUse) if !ok { t.Fatalf("deleting file that is in use should return an error of type ErrFileInUs, but returned: %v (type: %t)", err, err) } if len(errInUse.Playlists) != 1 || errInUse.Playlists[0].ID != list.ID { t.Fatalf("usage reports invalid playlist-list: %+v", errInUse.Playlists) } if err = store.DeletePlaylist(list.ShowName, list.ID); err != nil { t.Fatalf("unexpected error: %v", err) } if err = store.DeleteFile(file.ShowName, file.ID); err != nil { t.Fatalf("unexpected error: %v", err) } }