Skip to content
Snippets Groups Projects
store_test.go 18.93 KiB
//
//  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
}