// // tank // // Import and Playlist Daemon for autoradio project // // // Copyright (C) 2017-2019 Christian Pointner <equinox@helsinki.at> // // This file is part of tank. // // tank is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. // // tank 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 General Public License for more details. // // You should have received a copy of the GNU General Public License // along with tank. If not, see <http://www.gnu.org/licenses/>. // package store import ( "fmt" "io" "os" "os/user" "path/filepath" "reflect" "testing" //"github.com/jinzhu/gorm" ) var ( testBasePath = "run/aura-tank_testing" testPublicDirPath = filepath.Join(testBasePath, publicShowName) 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) testPublicDirPath = filepath.Join(testBasePath, publicShowName) 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(testPublicDirPath); err != nil { t.Fatalf("unexpected error: %v", err) } if f, err := os.Create(testPublicDirPath); err != nil { t.Fatalf("unexpected error: %v", err) } else { io.WriteString(f, "this is not a directory") f.Close() } if _, err = NewStore(cfg); err == nil { t.Fatalf("opening store where path to public show dir is not a directory should throw an error") } if err := os.RemoveAll(testPublicDirPath); 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) } // TODO: This is only true for now!! if store.GetRevision() != "201903131716" { t.Fatalf("for now new databases should have revision %q: got %q", "201903131716", 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 Shows, expected []string) { // if len(shows) != len(expected) { // t.Fatalf("expected %d shows in store but got %d: %v", len(expected), len(shows), shows) // } for _, gname := range expected { found := false for _, g := range shows { if gname == g.Name { found = true break } } if !found { t.Fatalf("expected show '%s' to be in store but got: %v", gname, shows) } if st, err := os.Stat(filepath.Join(testBasePath, gname)); err != nil { t.Fatalf("can't open show directory for show '%s': %v", gname, err) } else if !st.IsDir() { t.Fatalf("path of show '%s' is not a directory", gname) } } } 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{publicShowName}) 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{publicShowName, 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{publicShowName, testShow2}) if err = store.DeleteShow(testShow2); err != nil { t.Fatalf("deleteing show '%s' failed: %v", testShow2, err) } checkShows(t, shows, []string{publicShowName}) } // Files // func TestFilesListCreateDelete(t *testing.T) { store := newTestStore(t) files, err := store.ListFiles(publicShowName) if err != nil { t.Fatalf("listing files of public show failed: %v", err) } if len(files) != 0 { t.Fatalf("a newly created store should contain no files in public show but ListFiles returned: %v", files) } files, err = store.ListFiles("notexistend") 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) } publicFile, err := store.CreateFile(publicShowName, File{}) if err != nil { t.Fatalf("creating file in public show failed: %v", err) } files, err = store.ListFiles(publicShowName) 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{publicShowName, testShow1}) files, err = store.ListFiles(testShow1) 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(publicShowName, publicFile.ID); err != nil { t.Fatalf("deleteing file %d of show '%s' failed: %v", publicFile.ID, publicShowName, err) } checkShows(t, shows, []string{publicShowName}) } 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) 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) } } func TestFilesUpdate(t *testing.T) { store := newTestStore(t) if _, err := store.UpdateFile(publicShowName, 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) 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) } } // Playlists // func generateTestPlaylist(uris ...string) (p Playlist) { for _, u := range uris { e := PlaylistEntry{URI: u} p.Entries = append(p.Entries, e) } return p } func TestPlaylistsListCreateDelete(t *testing.T) { store := newTestStore(t) playlists, err := store.ListPlaylists(publicShowName) if err != nil { t.Fatalf("listing playlists of public show failed: %v", err) } if len(playlists) != 0 { t.Fatalf("a newly created store should contain no playlists in public show but ListPlaylists returned: %v", playlists) } playlists, err = store.ListPlaylists("notexistend") 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("audioin://1", "http://stream.example.com/live.mp") publicPlaylist, err := store.CreatePlaylist(publicShowName, in) if err != nil { t.Fatalf("creating playlist in public show failed: %v", err) } playlists, err = store.ListPlaylists(publicShowName) 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("audioin://1", "http://stream.example.com/live.mp3") _, 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("https://stream.example.com/other.ogg", "audioin://2") _, 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{publicShowName, testShow1}) playlists, err = store.ListPlaylists(testShow1) 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(publicShowName, publicPlaylist.ID); err != nil { t.Fatalf("deleteing playlist %d of show '%s' failed: %v", publicPlaylist.ID, publicShowName, 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("http://stream.example.com/stream.mp3") 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("http://stream.example.com/other.mp3", fmt.Sprintf("file://%s/%d", file1.ShowName, file1.ID)) if _, err = store.CreatePlaylist(testShow1, p); err != nil { t.Fatalf("unexpected error: %v", err) } playlists, err := store.ListPlaylists(testShow1) 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 }