diff --git a/go.mod b/go.mod index a36c080..a1c2b3a 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/sirupsen/logrus v1.9.3 go.uber.org/mock v0.4.0 gorm.io/driver/mysql v1.5.6 + gorm.io/driver/sqlite v1.5.5 gorm.io/gorm v1.25.7 ) @@ -16,5 +17,6 @@ require ( github.com/go-sql-driver/mysql v1.8.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect golang.org/x/sys v0.18.0 // indirect ) diff --git a/go.sum b/go.sum index 0ce45df..05eb758 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= @@ -31,5 +33,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= +gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= diff --git a/storage/database_test.go b/storage/database_test.go new file mode 100644 index 0000000..c452732 --- /dev/null +++ b/storage/database_test.go @@ -0,0 +1,200 @@ +package storage + +import ( + "io" + "os" + "reflect" + "testing" + "todolist/core" + + log "github.com/sirupsen/logrus" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +func TestMain(m *testing.M) { + // So that we don't see log messages during tests. + log.SetOutput(io.Discard) + code := m.Run() + os.Exit(code) +} + +// NOTE: Errors on the database panics because it means the test setup is incorrect. + +func (dba *DatabaseAccessor) initTestDb() { + var err error + // NOTE: Using the in-memory SQLite database for testing purposes. + dba.db, err = gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{ + Logger: logger.Discard, + }) + if err != nil { + panic(err) + } + err = dba.db.AutoMigrate(&TodoItemModel{}) + if err != nil { + panic(err) + } +} + +func (dba *DatabaseAccessor) closeTestDb() { + err := dba.db.Migrator().DropTable(&TodoItemModel{}) + if err != nil { + panic(err) + } + dba.db = nil +} + +// TestCreate Given a todo item, when Create is called, then the todo item should be created in the database and the id should be set and returned. +func TestCreate(t *testing.T) { + // arrange + dba := DatabaseAccessor{} + dba.initTestDb() + defer dba.closeTestDb() + + // act + todo := core.TodoItem{Description: "Test description", Completed: false} + id, err := dba.Create(&todo) + + // assert + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if todo.Id != id { + t.Errorf("Id not set on todo item correctly, expected %v, got %v", id, todo.Id) + } + want := []TodoItemModel{ + {Id: id, Description: "Test description", Completed: false}, + } + todosInDb := []TodoItemModel{} + dba.db.Find(&todosInDb) + if !reflect.DeepEqual(todosInDb, want) { + t.Errorf("want: %v, got: %v", want, todosInDb) + } +} + +// TestRead Given some todo items in the database, when Read is called with a where clause that matches on the description of a todo item, then the todo item should be returned. +func TestRead(t *testing.T) { + // arrange + dba := DatabaseAccessor{} + dba.initTestDb() + defer dba.closeTestDb() + match := "Test description 1" + dba.db.Create(&[]TodoItemModel{ + {Id: 1, Description: match, Completed: false}, + {Id: 2, Description: "Test description 2", Completed: true}, + }) + + // act + want := core.TodoItem{Id: 1, Description: "Test description 1", Completed: false} + got := dba.Read(func(item core.TodoItem) bool { return item.Description == match }) + + // assert + if len(got) != 1 { + t.Fatalf("expected 1 item, got %v", len(got)) + } + if !reflect.DeepEqual(got[0], want) { + t.Errorf("want: %v, got: %v", want, got[0]) + } +} + +// TestUpdate Given some todo items in the database, when Update is called with the id of a todo item, then the todo item should be updated. +func TestUpdate(t *testing.T) { + // arrange + dba := DatabaseAccessor{} + dba.initTestDb() + defer dba.closeTestDb() + targetId := 2 + dba.db.Create(&[]TodoItemModel{ + {Id: 1, Description: "Test description 1", Completed: false}, + {Id: targetId, Description: "Test description 2", Completed: true}, + }) + + // act + updatedTodo := core.TodoItem{Id: targetId, Description: "Updated description", Completed: false} + err := dba.Update(updatedTodo) + + // assert + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + want := []TodoItemModel{ + {Id: 1, Description: "Test description 1", Completed: false}, + {Id: targetId, Description: updatedTodo.Description, Completed: updatedTodo.Completed}, + } + todosInDb := []TodoItemModel{} + dba.db.Find(&todosInDb) + if !reflect.DeepEqual(todosInDb, want) { + t.Errorf("want: %v, got: %v", want, todosInDb) + } +} + +// TestUpdateNotFound Given some todo items in the database, when Update is called with an id that does not exist, then an error should be returned. +func TestUpdateNotFound(t *testing.T) { + // arrange + dba := DatabaseAccessor{} + dba.initTestDb() + defer dba.closeTestDb() + dba.db.Create(&[]TodoItemModel{ + {Id: 1, Description: "Test description 1", Completed: false}, + {Id: 2, Description: "Test description 2", Completed: true}, + }) + + // act + nonExistentTodo := core.TodoItem{Id: 3, Description: "Updated description", Completed: false} + err := dba.Update(nonExistentTodo) + + // assert + if err == nil { + t.Fatalf("expected error, got nil") + } +} + +// TestDelete Given some todo items in the database, when Delete is called with the id of a todo item, then the todo item should be deleted. +func TestDelete(t *testing.T) { + // arrange + dba := DatabaseAccessor{} + dba.initTestDb() + defer dba.closeTestDb() + dba.db.Create(&[]TodoItemModel{ + {Id: 1, Description: "Test description 1", Completed: false}, + {Id: 2, Description: "Test description 2", Completed: true}, + }) + + // act + err := dba.Delete(1) + + // assert + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + want := []TodoItemModel{ + {Id: 2, Description: "Test description 2", Completed: true}, + } + todosInDb := []TodoItemModel{} + dba.db.Find(&todosInDb) + if !reflect.DeepEqual(todosInDb, want) { + t.Errorf("want: %v, got: %v", want, todosInDb) + } +} + +// TestDeleteNotFound Given some todo items in the database, when Delete is called with an id that does not exist, then an error should be returned. +func TestDeleteNotFound(t *testing.T) { + // arrange + dba := DatabaseAccessor{} + dba.initTestDb() + defer dba.closeTestDb() + items := []TodoItemModel{ + {Id: 1, Description: "Test description 1", Completed: false}, + {Id: 2, Description: "Test description 2", Completed: true}, + } + dba.db.Create(&items) + + // act + err := dba.Delete(3) + + // assert + if err == nil { + t.Fatalf("expected error, got nil") + } +}