// // tank // // Import and Playlist Daemon for autoradio project // // // Copyright (C) 2017-2018 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, publicGroupName) testDBType = "mysql" testDBConnection = "tank:aura@tcp(127.0.0.1:3306)/tank?charset=utf8&parseTime=True&loc=Local" // testDBType = "postgres" // testDBConnection = "host=127.0.0.1 port=5432 user=tank dbname=tank password=aura sslmode=disable" testGroup1 = "test1" testGroup2 = "test2" testUser1 = "user1" testUser2 = "user2" testSourceURI1 = "attachment://test1.mp3" testSourceURI2 = "attachment://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 newTestStore(t *testing.T) *Store { cfg := Config{} cfg.BasePath = testBasePath cfg.DB.Type = testDBType cfg.DB.Connection = testDBConnection // 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, publicGroupName) // testDBConnection = filepath.Join(testBasePath, ".db.sqlite") os.Exit(m.Run()) } // // Testing // func TestOpen(t *testing.T) { // base-path is non-existing directory cfg := Config{} cfg.BasePath = "/nonexistend/" cfg.DB.Type = testDBType cfg.DB.Connection = testDBConnection 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) } /// exisiting but non-database file (only possible with sqlite...) // if f, err := os.Create(testDBConnection); err != nil { // t.Fatalf("unexpected error: %v", err) // } else { // io.WriteString(f, "this is not a sqlite db.") // f.Close() // } // if _, err := NewStore(cfg); err == nil { // t.Fatalf("opening store using a invalid database should throw an error") // } /// create new db and reopen it // os.Remove(testDBConnection) 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 group 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.Type = testDBType cfg.DB.Connection = testDBConnection // 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() != "201806161853" { t.Fatalf("for now new databases should have revision %q: got %q", "201806161853", 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 groups (name) VALUES ($1)", testGroup1) //// mysql: db.Exec("INSERT INTO groups (name) VALUES (?)", testGroup1) //// //// 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 groups (name) VALUES (?)", testGroup1) // if res.Error != nil { // t.Fatalf("adding group '%s' shouldn't fail: %v", testGroup1, res.Error) // } // if res = db.Exec("INSERT INTO groups (name) VALUES (?)", testGroup1); res.Error == nil { // t.Fatalf("re-adding the same group should fail") // } // if res = db.Exec("INSERT INTO files (group_name, size) VALUES (?, ?)", testGroup1, 500); res.Error != nil { // t.Fatalf("re-adding the same group should fail") // } // } // Groups // func checkGroups(t *testing.T, groups Groups, expected []string) { // if len(groups) != len(expected) { // t.Fatalf("expected %d groups in store but got %d: %v", len(expected), len(groups), groups) // } for _, gname := range expected { found := false for _, g := range groups { if gname == g.Name { found = true break } } if !found { t.Fatalf("expected group '%s' to be in store but got: %v", gname, groups) } if st, err := os.Stat(filepath.Join(testBasePath, gname)); err != nil { t.Fatalf("can't open group directory for group '%s': %v", gname, err) } else if !st.IsDir() { t.Fatalf("path of group '%s' is not a directory", gname) } } } func TestGroups(t *testing.T) { store := newTestStore(t) groups, err := store.ListGroups() if err != nil { t.Fatalf("listing groups failed: %v", err) } checkGroups(t, groups, []string{publicGroupName}) if _, err = store.CreateGroup(testGroup1); err != nil { t.Fatalf("creating group failed: %v", err) } if _, err = store.CreateGroup(testGroup2); err != nil { t.Fatalf("creating group failed: %v", err) } groups, err = store.ListGroups() if err != nil { t.Fatalf("unexpected error: %v", err) } checkGroups(t, groups, []string{publicGroupName, testGroup1, testGroup2}) if err = store.DeleteGroup(testGroup1); err != nil { t.Fatalf("deleting group '%s' failed: %v", testGroup1, err) } groups, err = store.ListGroups() if err != nil { t.Fatalf("unexpected error: %v", err) } checkGroups(t, groups, []string{publicGroupName, testGroup2}) if err = store.DeleteGroup(testGroup2); err != nil { t.Fatalf("deleteing group '%s' failed: %v", testGroup2, err) } checkGroups(t, groups, []string{publicGroupName}) } // Files // func TestFilesListCreateDelete(t *testing.T) { store := newTestStore(t) files, err := store.ListFiles(publicGroupName) if err != nil { t.Fatalf("listing files of public group failed: %v", err) } if len(files) != 0 { t.Fatalf("a newly created store should contain no files in public group but ListFiles returned: %v", files) } files, err = store.ListFiles("notexistend") if err != nil { t.Fatalf("listing files of not existing group shouldn't throw an error but returned: %v", err) } if len(files) != 0 { t.Fatalf("listing files of not existing group should return and empty list but ListFiles returned: %v", files) } publicFile, err := store.CreateFile(publicGroupName, File{}) if err != nil { t.Fatalf("creating file in public group failed: %v", err) } files, err = store.ListFiles(publicGroupName) 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(testGroup1, File{Size: 17}) if err != nil { t.Fatalf("creating file in not existing group shouldn't throw an error but CreateFile returned: %v", err) } _, err = store.CreateFile(testGroup1, File{Size: 23}) if err != nil { t.Fatalf("creating file in not existing group shouldn't throw an error but CreateFile returned: %v", err) } groups, err := store.ListGroups() if err != nil { t.Fatalf("unexpected error: %v", err) } checkGroups(t, groups, []string{publicGroupName, testGroup1}) files, err = store.ListFiles(testGroup1) 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.DeleteGroup(testGroup1); err != nil { t.Fatalf("unexpected error: %v", err) } if err = store.DeleteFile(publicGroupName, publicFile.ID); err != nil { t.Fatalf("deleteing file %d of group '%s' failed: %v", publicFile.ID, publicGroupName, err) } checkGroups(t, groups, []string{publicGroupName}) } 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(testGroup1, 0); err != ErrNotFound { t.Fatalf("getting file in not-existing group 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(testGroup1, file1) if err != nil { t.Fatalf("unexpected error: %v", err) } out1, err := store.GetFile(testGroup1, 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(testGroup1, 0); err != ErrNotFound { t.Fatalf("getting not-existing file in existing group 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(testGroup1, file2) if err != nil { t.Fatalf("unexpected error: %v", err) } out2, err := store.GetFile(testGroup1, 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(testGroup1) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(files) != 2 { t.Fatalf("group '%s' should contain 2 files but ListFiles returned: %+v", testGroup1, 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("group '%s' should only contain files %d and %d but ListFiles returned: %+v", testGroup1, 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.DeleteGroup(testGroup1); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestFilesUpdate(t *testing.T) { store := newTestStore(t) if _, err := store.UpdateFile(publicGroupName, 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(testGroup2, file) if err != nil { t.Fatalf("unexpected error: %v", err) } out, err := store.GetFile(testGroup2, 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(testGroup2, in.ID, *out); err != nil { t.Fatalf("updateting an existing file shouldn't fail but UpdateFile returned: %v", err) } files, err := store.ListFiles(testGroup2) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(files) != 1 { t.Fatalf("group '%s' should contain 1 file but ListFiles returned: %+v", testGroup2, 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.DeleteGroup(testGroup2); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestFilesDelete(t *testing.T) { store := newTestStore(t) if err := store.DeleteFile(testGroup1, 0); err != ErrNotFound { t.Fatalf("deleting not-existing file should return ErrNotFound, but DeleteFile returned: %v", err) } file, err := store.CreateFile(testGroup1, File{Size: 12345}) if err != nil { t.Fatalf("unexpected error: %v", err) } if err = store.DeleteFile(testGroup1, file.ID); err != nil { t.Fatalf("deleting file failed, DeleteFile returned: %v", err) } if err = store.DeleteFile(testGroup1, 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.DeleteGroup(testGroup1); 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(publicGroupName) if err != nil { t.Fatalf("listing playlists of public group failed: %v", err) } if len(playlists) != 0 { t.Fatalf("a newly created store should contain no playlists in public group but ListPlaylists returned: %v", playlists) } playlists, err = store.ListPlaylists("notexistend") if err != nil { t.Fatalf("listing playlists of not existing group shouldn't throw an error but returned: %v", err) } if len(playlists) != 0 { t.Fatalf("listing playlists of not existing group should return and empty list but ListPlaylists returned: %v", playlists) } in := generateTestPlaylist("audioin://1", "http://stream.example.com/live.mp") publicPlaylist, err := store.CreatePlaylist(publicGroupName, in) if err != nil { t.Fatalf("creating playlist in public group failed: %v", err) } playlists, err = store.ListPlaylists(publicGroupName) 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(testGroup1, in1) if err != nil { t.Fatalf("creating playlist in not existing group shouldn't throw an error but CreatePlaylist returned: %v", err) } in2 := generateTestPlaylist("https://stream.example.com/other.ogg", "audioin://2") _, err = store.CreatePlaylist(testGroup1, in2) if err != nil { t.Fatalf("creating playlist in not existing group shouldn't throw an error but CreatePlaylist returned: %v", err) } groups, err := store.ListGroups() if err != nil { t.Fatalf("unexpected error: %v", err) } checkGroups(t, groups, []string{publicGroupName, testGroup1}) playlists, err = store.ListPlaylists(testGroup1) 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.DeleteGroup(testGroup1); err != nil { t.Fatalf("unexpected error: %v", err) } if err = store.DeletePlaylist(publicGroupName, publicPlaylist.ID); err != nil { t.Fatalf("deleteing playlist %d of group '%s' failed: %v", publicPlaylist.ID, publicGroupName, 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(testGroup1, f) if err != nil { t.Fatalf("unexpected error: %v", err) } if _, err := store.GetPlaylist(testGroup1, 0); err != ErrNotFound { t.Fatalf("getting playlist in not-existing group should return ErrNotFound, but GetPlaylist returned: %v", err) } p := generateTestPlaylist("http://stream.example.com/stream.mp3") p.Entries = append(p.Entries, PlaylistEntry{File: &File{GroupName: file1.GroupName, ID: file1.ID}}) list1, err := store.CreatePlaylist(testGroup1, p) if err != nil { t.Fatalf("unexpected error: %v", err) } if _, err := store.GetPlaylist(testGroup1, 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.GroupName, file1.ID)) if _, err = store.CreatePlaylist(testGroup1, p); err != nil { t.Fatalf("unexpected error: %v", err) } playlists, err := store.ListPlaylists(testGroup1) 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 }