diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000000..972baf10bc --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,106 @@ +package db + +import ( + "database/sql" + "errors" + "fmt" + "os" + "strings" + + "github.com/golang-migrate/migrate/v4" + "github.com/jinzhu/gorm" + "github.com/ory/dockertest" +) + +type DbType int + +const ( + UnknownDB DbType = 0 + Postgres DbType = 1 +) + +func (db DbType) String() string { + return [...]string{ + "unknown", + "postgres", + }[db] +} + +// Open a database connection which is long-lived. +// You need to call Close() on the returned gorm.DB +func Open(dbType DbType, connectionUrl string) (*gorm.DB, error) { + db, err := gorm.Open(dbType.String(), connectionUrl) + if err != nil { + return nil, fmt.Errorf("unable to open database: %w", err) + } + return db, nil +} + +// Migrate a database schema +func Migrate(connectionUrl string, migrationsDirectory string) error { + if connectionUrl == "" { + return errors.New("connection url is unset") + } + if _, err := os.Stat(migrationsDirectory); os.IsNotExist(err) { + return errors.New("error migrations directory does not exist") + } + // run migrations + m, err := migrate.New(fmt.Sprintf("file://%s", migrationsDirectory), connectionUrl) + if err != nil { + return fmt.Errorf("unable to create migrations: %w", err) + } + if err := m.Up(); err != nil && err != migrate.ErrNoChange { + return fmt.Errorf("unable to run migrations: %w", err) + } + return nil +} + +// StartDbInDocker +func StartDbInDocker() (cleanup func() error, retURL string, err error) { + pool, err := dockertest.NewPool("") + if err != nil { + return func() error { return nil }, "", fmt.Errorf("could not connect to docker: %w", err) + } + + resource, err := pool.Run("postgres", "latest", []string{"POSTGRES_PASSWORD=secret", "POSTGRES_DB=watchtower"}) + if err != nil { + return func() error { return nil }, "", fmt.Errorf("could not start resource: %w", err) + } + + c := func() error { + return cleanupDockerResource(pool, resource) + } + + url := fmt.Sprintf("postgres://postgres:secret@localhost:%s?sslmode=disable", resource.GetPort("5432/tcp")) + + if err := pool.Retry(func() error { + db, err := sql.Open("postgres", url) + if err != nil { + return fmt.Errorf("error opening postgres dev container: %w", err) + } + + if err := db.Ping(); err != nil { + return err + } + defer db.Close() + return nil + }); err != nil { + return func() error { return nil }, "", fmt.Errorf("could not connect to docker: %w", err) + } + return c, url, nil +} + +// cleanupDockerResource will clean up the dockertest resources (postgres) +func cleanupDockerResource(pool *dockertest.Pool, resource *dockertest.Resource) error { + var err error + for i := 0; i < 10; i++ { + err = pool.Purge(resource) + if err == nil { + return nil + } + } + if strings.Contains(err.Error(), "No such container") { + return nil + } + return fmt.Errorf("Failed to cleanup local container: %s", err) +} diff --git a/internal/db/db_test.go b/internal/db/db_test.go new file mode 100644 index 0000000000..273577d5d6 --- /dev/null +++ b/internal/db/db_test.go @@ -0,0 +1,105 @@ +package db + +import ( + "testing" +) + +func TestOpen(t *testing.T) { + cleanup, url, err := StartDbInDocker() + if err != nil { + t.Fatal(err) + } + defer cleanup() + type args struct { + dbType DbType + connectionUrl string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "valid", + args: args{ + dbType: Postgres, + connectionUrl: url, + }, + wantErr: false, + }, + { + name: "invalid", + args: args{ + dbType: Postgres, + connectionUrl: "", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Open(tt.args.dbType, tt.args.connectionUrl) + defer func() { + if err == nil { + got.Close() + } + }() + if (err != nil) != tt.wantErr { + t.Errorf("Open() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr && got != nil { + t.Error("Open() wanted error and got != nil") + } + }) + } +} + +func TestMigrate(t *testing.T) { + cleanup, url, err := StartDbInDocker() + if err != nil { + t.Fatal(err) + } + defer cleanup() + type args struct { + connectionUrl string + migrationsDirectory string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "valid", + args: args{ + connectionUrl: url, + migrationsDirectory: "migrations/postgres", + }, + wantErr: false, + }, + { + name: "bad-url", + args: args{ + connectionUrl: "", + migrationsDirectory: "migrations/postgres", + }, + wantErr: true, + }, + { + name: "bad-dir", + args: args{ + connectionUrl: url, + migrationsDirectory: "", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := Migrate(tt.args.connectionUrl, tt.args.migrationsDirectory); (err != nil) != tt.wantErr { + t.Errorf("Migrate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/db/testing.go b/internal/db/testing.go index 8d8686962d..02af8666a3 100644 --- a/internal/db/testing.go +++ b/internal/db/testing.go @@ -2,10 +2,8 @@ package db import ( "crypto/rand" - "database/sql" "fmt" "os" - "strings" "testing" "github.com/golang-migrate/migrate/v4" @@ -14,16 +12,15 @@ import ( wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-kms-wrapping/wrappers/aead" "github.com/jinzhu/gorm" - "github.com/ory/dockertest/v3" ) // setup the tests (initialize the database one-time and intialized testDatabaseURL) -func TestSetup(t *testing.T, migrationsDirectory string) (func(), *gorm.DB) { +func TestSetup(t *testing.T, migrationsDirectory string) (func() error, *gorm.DB) { if _, err := os.Stat(migrationsDirectory); os.IsNotExist(err) { t.Fatal("error migrationsDirectory does not exist") } - cleanup := func() {} + cleanup := func() error { return nil } var url string var err error cleanup, url, err = initDbInDocker(t, migrationsDirectory) @@ -55,47 +52,21 @@ func TestWrapper(t *testing.T) wrapping.Wrapper { } // initDbInDocker initializes postgres within dockertest for the unit tests -func initDbInDocker(t *testing.T, migrationsDirectory string) (cleanup func(), retURL string, err error) { +func initDbInDocker(t *testing.T, migrationsDirectory string) (cleanup func() error, retURL string, err error) { if os.Getenv("PG_URL") != "" { - TestInitStore(t, func() {}, os.Getenv("PG_URL"), migrationsDirectory) - return func() {}, os.Getenv("PG_URL"), nil + TestInitStore(t, func() error { return nil }, os.Getenv("PG_URL"), migrationsDirectory) + return func() error { return nil }, os.Getenv("PG_URL"), nil } - pool, err := dockertest.NewPool("") + c, url, err := StartDbInDocker() if err != nil { - return func() {}, "", fmt.Errorf("could not connect to docker: %w", err) - } - - resource, err := pool.Run("postgres", "latest", []string{"POSTGRES_PASSWORD=secret", "POSTGRES_DB=watchtower"}) - if err != nil { - return func() {}, "", fmt.Errorf("could not start resource: %w", err) - } - - c := func() { - cleanupResource(t, pool, resource) - } - - url := fmt.Sprintf("postgres://postgres:secret@localhost:%s?sslmode=disable", resource.GetPort("5432/tcp")) - - if err := pool.Retry(func() error { - db, err := sql.Open("postgres", url) - if err != nil { - return fmt.Errorf("error opening postgres dev container: %w", err) - } - - if err := db.Ping(); err != nil { - return err - } - defer db.Close() - return nil - }); err != nil { - return func() {}, "", fmt.Errorf("could not connect to docker: %w", err) + return func() error { return nil }, "", fmt.Errorf("could not start docker: %w", err) } TestInitStore(t, c, url, migrationsDirectory) return c, url, nil } // TestInitStore will execute the migrations needed to initialize the store for tests -func TestInitStore(t *testing.T, cleanup func(), url string, migrationsDirectory string) { +func TestInitStore(t *testing.T, cleanup func() error, url string, migrationsDirectory string) { // run migrations m, err := migrate.New(fmt.Sprintf("file://%s", migrationsDirectory), url) if err != nil { @@ -107,19 +78,3 @@ func TestInitStore(t *testing.T, cleanup func(), url string, migrationsDirectory t.Fatalf("Error running migrations: %s", err) } } - -// cleanupResource will clean up the dockertest resources (postgres) -func cleanupResource(t *testing.T, pool *dockertest.Pool, resource *dockertest.Resource) { - var err error - for i := 0; i < 10; i++ { - err = pool.Purge(resource) - if err == nil { - return - } - } - - if strings.Contains(err.Error(), "No such container") { - return - } - t.Fatalf("Failed to cleanup local container: %s", err) -}