diff --git a/Makefile b/Makefile index 1353523..c04feba 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,10 @@ test: go test $(FLAGS) ./... PHONEY: test +test-short: + go test -short $(FLAGS) ./... +PHONEY: test-short + # make test-int ARGS=no-cache test-int: go test $(FLAGS) ./cmd/... diff --git a/go.mod b/go.mod index 9e780ff..fede3f3 100644 --- a/go.mod +++ b/go.mod @@ -2,12 +2,18 @@ module git.runcible.io/learning/ratchet go 1.23.3 -require github.com/mattn/go-sqlite3 v1.14.24 +require ( + github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885 + github.com/alexedwards/scs/v2 v2.8.0 + github.com/go-playground/form/v4 v4.2.1 + github.com/golang-migrate/migrate/v4 v4.18.1 + github.com/justinas/nosurf v1.1.1 + github.com/mattn/go-sqlite3 v1.14.24 + golang.org/x/crypto v0.32.0 +) require ( - github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885 // indirect - github.com/alexedwards/scs/v2 v2.8.0 // indirect - github.com/go-playground/form/v4 v4.2.1 // indirect - github.com/justinas/nosurf v1.1.1 // indirect - golang.org/x/crypto v0.32.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + go.uber.org/atomic v1.7.0 // indirect ) diff --git a/go.sum b/go.sum index ef2985e..213e3f3 100644 --- a/go.sum +++ b/go.sum @@ -2,13 +2,36 @@ github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885 h1:+D github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss= github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/form/v4 v4.2.1 h1:HjdRDKO0fftVMU5epjPW2SOREcZ6/wLUzEobqUGJuPw= github.com/go-playground/form/v4 v4.2.1/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U= +github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= +github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk= github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/assert/assert.go b/internal/assert/assert.go index d9a7019..3cc90d5 100644 --- a/internal/assert/assert.go +++ b/internal/assert/assert.go @@ -28,3 +28,11 @@ func StringContains(t *testing.T, actual, expectedSubstring string) { t.Errorf("got: %q; expected to contain %q", actual, expectedSubstring) } } + +func NilError(t *testing.T, actual error) { + t.Helper() + + if actual != nil { + t.Errorf("got: %v; expected: nil", actual) + } +} diff --git a/internal/model/integration/testdata/seed.sql b/internal/model/integration/testdata/seed.sql new file mode 100644 index 0000000..ef8149a --- /dev/null +++ b/internal/model/integration/testdata/seed.sql @@ -0,0 +1,9 @@ +INSERT INTO users VALUES( + 1337, + 'tester', + 'tester@example.com', + /* thisisinsecure */ + '$2a$12$M51w5lWkveAOhwoanoCxO.hJe3s1m8qJuCzbzdETt0SThjpq4BPRq', + '2025-02-25 18:58:44', + '2025-02-25 18:58:44' +); \ No newline at end of file diff --git a/internal/model/integration/testutils_test.go b/internal/model/integration/testutils_test.go new file mode 100644 index 0000000..dd2cd4c --- /dev/null +++ b/internal/model/integration/testutils_test.go @@ -0,0 +1,52 @@ +package integration + +import ( + "database/sql" + "os" + "testing" + + "git.runcible.io/learning/ratchet/migrations" +) + +const testerPasswd = "thisisinsecure" + +func newTestDB(t *testing.T) *sql.DB { + dbFile, err := os.CreateTemp("", "ratchet-*.db") + if err != nil { + t.Fatal(err) + } + + db, err := sql.Open("sqlite3", dbFile.Name()) + if err != nil { + db.Close() + t.Fatal(err) + } + + err = migrations.Migrate(db) + if err != nil { + db.Close() + t.Fatal(err) + } + + script, err := os.ReadFile("./testdata/seed.sql") + if err != nil { + db.Close() + t.Fatal(err) + } + + _, err = db.Exec(string(script)) + if err != nil { + db.Close() + t.Fatal(err) + } + + t.Cleanup(func() { + db.Close() + err := os.Remove(dbFile.Name()) + if err != nil { + t.Fatal(err) + } + }) + + return db +} diff --git a/internal/model/integration/user_integration_test.go b/internal/model/integration/user_integration_test.go new file mode 100644 index 0000000..0a6a1b4 --- /dev/null +++ b/internal/model/integration/user_integration_test.go @@ -0,0 +1,51 @@ +package integration + +import ( + "testing" + + "git.runcible.io/learning/ratchet/internal/assert" + "git.runcible.io/learning/ratchet/internal/model" +) + +func TestUserModelExists(t *testing.T) { + // Skip the test if the "-short" flag is provided when running the test. + if testing.Short() { + t.Skip("models: skipping model integration test") + } + + tests := []struct { + name string + userID int + want bool + }{{ + name: "Valid ID", + userID: 1337, + want: true, + }, + { + name: "Zero ID", + userID: 0, + want: false, + }, + { + name: "Zero ID", + userID: 2, + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + db := newTestDB(t) + + userService := model.UserService{db} + + exists, err := userService.Exists(test.userID) + + assert.Equal(t, exists, test.want) + assert.NilError(t, err) + + }) + } + +} diff --git a/migrations/migrate.go b/migrations/migrate.go new file mode 100644 index 0000000..a15c445 --- /dev/null +++ b/migrations/migrate.go @@ -0,0 +1,48 @@ +package migrations + +import ( + "database/sql" + "embed" + "fmt" + + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/sqlite3" + _ "github.com/golang-migrate/migrate/v4/source/file" + "github.com/golang-migrate/migrate/v4/source/iofs" +) + +//go:embed *.sql +var migrationFiles embed.FS + +func Migrate(db *sql.DB) error { + // Create a database driver for the specific database type + driver, err := sqlite3.WithInstance(db, &sqlite3.Config{}) + if err != nil { + return fmt.Errorf("failed to create database driver: %w", err) + } + + // Create an IFS source from the embedded files + source, err := iofs.New(migrationFiles, ".") + if err != nil { + return fmt.Errorf("failed to create migration source: %w", err) + } + + // Create a new migrate instance + m, err := migrate.NewWithInstance("iofs", source, "sqlite3", driver) + if err != nil { + return fmt.Errorf("failed to create migrate instance: %w", err) + } + + // Run migrations + if err := m.Up(); err != nil && err != migrate.ErrNoChange { + return fmt.Errorf("failed to run migrations: %w", err) + } + + // set WAL mode + _, err = db.Exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;") + if err != nil { + return fmt.Errorf("failed to set wall mode: %w", err) + } + + return nil +}