Skip to content
Snippets Groups Projects
store_test.go 24 KiB
Newer Older
  • Learn to ignore specific revisions
  • Christian Pointner's avatar
    Christian Pointner committed
    //
    
    Christian Pointner's avatar
    Christian Pointner committed
    //  tank, Import and Playlist Daemon for Aura project
    //  Copyright (C) 2017-2020 Christian Pointner <equinox@helsinki.at>
    
    Christian Pointner's avatar
    Christian Pointner committed
    //
    
    Christian Pointner's avatar
    Christian Pointner committed
    //  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.
    
    Christian Pointner's avatar
    Christian Pointner committed
    //
    
    Christian Pointner's avatar
    Christian Pointner committed
    //  This program is distributed in the hope that it will be useful,
    
    Christian Pointner's avatar
    Christian Pointner committed
    //  but WITHOUT ANY WARRANTY; without even the implied warranty of
    //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    
    Christian Pointner's avatar
    Christian Pointner committed
    //  GNU Affero General Public License for more details.
    
    Christian Pointner's avatar
    Christian Pointner committed
    //  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/>.
    
    Christian Pointner's avatar
    Christian Pointner committed
    //
    
    package store
    
    import (
    	"fmt"
    
    Christian Pointner's avatar
    Christian Pointner committed
    	"os"
    	"os/user"
    
    	"path/filepath"
    
    Christian Pointner's avatar
    Christian Pointner committed
    	"testing"
    
    Christian Pointner's avatar
    Christian Pointner committed
    	//"github.com/jinzhu/gorm"
    
    	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"
    
    Christian Pointner's avatar
    Christian Pointner committed
    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"
    	}
    
    
    Christian Pointner's avatar
    Christian Pointner committed
    	cfg.Port = testDBPort
    	cfg.Username = testDBUsername
    	cfg.Password = testDBPassword
    	cfg.DB = testDBDB
    	return
    }
    
    
    func newTestStore(t *testing.T) *Store {
    	cfg := Config{}
    	cfg.BasePath = testBasePath
    
    Christian Pointner's avatar
    Christian Pointner committed
    	cfg.DB = testDBConfig()
    
    	// TODO: drop database (all tables and constrains)
    
    
    	store, err := NewStore(cfg)
    	if err != nil {
    		t.Fatalf("unexpected error: %v", err)
    
    Christian Pointner's avatar
    Christian Pointner committed
    	}
    
    Christian Pointner's avatar
    Christian Pointner committed
    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)
    
    Christian Pointner's avatar
    Christian Pointner committed
    	os.Exit(m.Run())
    }
    
    //
    // Testing
    //
    
    
    func TestOpen(t *testing.T) {
    	// base-path is non-existing directory
    
    Christian Pointner's avatar
    Christian Pointner committed
    	cfg := Config{}
    
    	cfg.BasePath = "/nonexistend/"
    
    Christian Pointner's avatar
    Christian Pointner committed
    	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) {
    
    Christian Pointner's avatar
    Christian Pointner committed
    	cfg := Config{}
    
    	cfg.BasePath = testBasePath
    
    Christian Pointner's avatar
    Christian Pointner committed
    	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")
    
    Christian Pointner's avatar
    Christian Pointner committed
    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
    
    			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()
    
    Christian Pointner's avatar
    Christian Pointner committed
    	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()
    
    Christian Pointner's avatar
    Christian Pointner committed
    	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{})
    
    func TestFilesListCreateDelete(t *testing.T) {
    	store := newTestStore(t)
    
    	files, err := store.ListFiles(testShowName, -1, -1)
    
    		t.Fatalf("listing files of test show failed: %v", err)
    
    		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)
    
    		t.Fatalf("listing files of not existing show shouldn't throw an error but returned: %v", err)
    
    		t.Fatalf("listing files of not existing show should return and empty list but ListFiles returned: %v", files)
    
    	testFile, err := store.CreateFile(testShowName, File{})
    
    		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})
    
    		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})
    
    		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)
    	}
    }
    
    
    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)
    	}
    }