From 558c4a276d553cc720bd7093e356f6539d421282 Mon Sep 17 00:00:00 2001 From: Peter Downs Date: Thu, 28 Mar 2024 11:05:08 -0400 Subject: [PATCH 1/6] cleanup(nix): upgrade golang tooling --- flake.lock | 18 +++++++++--------- flake.nix | 6 ++++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/flake.lock b/flake.lock index 1d7e1e6..42b88a7 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", "owner": "edolstra", "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", "type": "github" }, "original": { @@ -21,11 +21,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1681202837, - "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "cfacdce06f30d2b68473a46042957675eebb3401", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "type": "github" }, "original": { @@ -36,11 +36,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1684619786, - "narHash": "sha256-fU7O8ANlhquvU+HJ51bRv5sjyGv1MrfKENYeNXIdwvU=", + "lastModified": 1711636205, + "narHash": "sha256-zgPXBiuTukDgls/GHB3enwmtG0swGy/suqtLXS7J0ok=", "owner": "nixos", "repo": "nixpkgs", - "rev": "e2ae06bfa852f741325414b45f4b53621442d6bf", + "rev": "7f85233f89d71929964be7ccb9d2c9dd37cde0b9", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 492e4f6..f561dc1 100644 --- a/flake.nix +++ b/flake.nix @@ -29,14 +29,16 @@ packages = with pkgs; [ # Go delve - go-outline go + go-outline golangci-lint + gomodifytags gopkgs gopls + gotests gotools + impl # Nix - rnix-lsp nixpkgs-fmt # Other just From 552746edc6e88b966a852f651dafa5219b8254cc Mon Sep 17 00:00:00 2001 From: Peter Downs Date: Thu, 28 Mar 2024 11:20:23 -0400 Subject: [PATCH 2/6] cleanup(lint): remove depguard, newer versions are broken and useless. --- .golangci.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 50ee870..777ae39 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -6,20 +6,12 @@ linters-settings: enable-all: true disable: - fieldalignment - depguard: - list-type: blacklist - include-go-root: true - include-go-std-lib: true - packages: - - "github.com/stretchr/testify/require" - - "github.com/stretchr/testify/assert" exhaustive: default-signifies-exhaustive: true nolintlint: allow-unused: false allow-leading-space: false allow-no-explanation: - - depguard - gochecknoglobals - gochecknoinits require-explanation: true @@ -100,7 +92,6 @@ linters: disable-all: true enable: - asciicheck - - depguard - errcheck - exhaustive - gocritic From 461cb0b92aa639e62b33569c4aba58aa4126f317 Mon Sep 17 00:00:00 2001 From: Peter Downs Date: Thu, 28 Mar 2024 11:22:31 -0400 Subject: [PATCH 3/6] bug(tests): use connection with session lock when running migrations in test migrator. --- testdb_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testdb_test.go b/testdb_test.go index 0ea8c80..848365a 100644 --- a/testdb_test.go +++ b/testdb_test.go @@ -322,7 +322,7 @@ func (s *sqlMigrator) Hash() (string, error) { func (s *sqlMigrator) Migrate(ctx context.Context, db *sql.DB, _ pgtestdb.Config) error { return sessionlock.With(ctx, db, "test-sql-migrator", func(conn *sql.Conn) error { for _, migration := range s.migrations { - if _, err := db.ExecContext(ctx, migration); err != nil { + if _, err := conn.ExecContext(ctx, migration); err != nil { return err } } @@ -333,7 +333,7 @@ func (s *sqlMigrator) Migrate(ctx context.Context, db *sql.DB, _ pgtestdb.Config func (s *sqlMigrator) Prepare(ctx context.Context, db *sql.DB, _ pgtestdb.Config) error { return sessionlock.With(ctx, db, "test-sql-migrator", func(conn *sql.Conn) error { for _, migration := range s.preparations { - if _, err := db.ExecContext(ctx, migration); err != nil { + if _, err := conn.ExecContext(ctx, migration); err != nil { return err } } From 1a31ad5218ec47f2964f8f2f6bf2608a9f22afbe Mon Sep 17 00:00:00 2001 From: Peter Downs Date: Thu, 28 Mar 2024 11:26:39 -0400 Subject: [PATCH 4/6] fix(justfile): allow passing -count=1 when running all tests --- Justfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Justfile b/Justfile index e42aa96..bde87ca 100644 --- a/Justfile +++ b/Justfile @@ -16,9 +16,9 @@ test *args='./...': go test -race "$@" # test pgtestdb + migrators -test-all: +test-all *args='': #!/usr/bin/env bash - go test -race github.com/peterldowns/pgtestdb/... + go test -race github.com/peterldowns/pgtestdb/... "$@" # lint pgtestdb lint *args: From b58cc21b80a18df7303ba886e6d87b1582276873 Mon Sep 17 00:00:00 2001 From: Peter Downs Date: Fri, 29 Mar 2024 11:42:46 -0400 Subject: [PATCH 5/6] feat: support custom roles for superuser-only extensions like postgis --- .github/workflows/golang.yaml | 2 +- README.md | 94 ++++++++++++++++---- docker-compose.yml | 6 +- testdb.go | 157 ++++++++++++++++++++++++++-------- testdb_test.go | 88 +++++++++++++++++++ 5 files changed, 292 insertions(+), 55 deletions(-) diff --git a/.github/workflows/golang.yaml b/.github/workflows/golang.yaml index e600088..bfa298a 100644 --- a/.github/workflows/golang.yaml +++ b/.github/workflows/golang.yaml @@ -8,7 +8,7 @@ jobs: test: services: postgres: - image: postgres:15 + image: postgis/postgis:15-master env: POSTGRES_PASSWORD: password # TODO: unable to turn off fsync easily, see diff --git a/README.md b/README.md index e39cb9a..f61867f 100644 --- a/README.md +++ b/README.md @@ -12,23 +12,26 @@ everything. # Documentation -- [This page, https://github.com/peterldowns/pgtestdb](https://github.com/peterldowns/pgtestdb) +- [The github README, https://github.com/peterldowns/pgtestdb](https://github.com/peterldowns/pgtestdb) - [The go.dev docs, pkg.go.dev/github.com/peterldowns/pgtestdb](https://pkg.go.dev/github.com/peterldowns/pgtestdb) -This page is the primary source for documentation. The code itself is supposed -to be well-organized, and each function has a meaningful docstring, so you -should be able to explore it quite easily using an LSP plugin, reading the +The github README is the primary source for documentation. The code itself is +supposed to be well-organized, and each function has a meaningful docstring, so +you should be able to explore it quite easily using an LSP plugin, reading the code, or clicking through the go.dev docs. ## How does it work? -Each time a test asks for a fresh database by calling `pgtestdb.New`, pgtestdb will +Each time one of your tests asks for a fresh database by calling `pgtestdb.New`, pgtestdb will check to see if a template database already exists. If not, it creates a new -database, runs your migrations on it, and then marks it as a template. Once the -template exists, it is _very_ fast to create a new database from that template. +database, runs your migrations on it, and then marks it as a template. Once the +template exists, it then creates a test-specific database from that template. -pgtestdb only runs migrations one time when your migrations change. The marginal -cost of a new test that uses the database is just the time to create a clone -from the template, which is now basically free. +Creating a new database from a template is _very_ fast, on the order of 10s of +milliseconds. And because pgtestdb uses advisory locks and hashes your +migrations to determine which template database to use, your migrations only +end up being run one time, regardless of how many tests or separate packages +you have. This is true even across test runs --- pgtestdb will only run your +migrations again if you change them in some way. When a test succeeds, the database it used is automatically deleted. When a test fails, the database it used is left alive, and the test logs will @@ -334,13 +337,18 @@ config (see below.) ```go // Config contains the details needed to connect to a postgres server/database. type Config struct { - DriverName string // "pgx" (pgx) or "postgres" (lib/pq) - Host string // "localhost" - Port string // "5433" - User string // "postgres" - Password string // "password" - Database string // "postgres" - Options string // "sslmode=disable&anotherSetting=value" + DriverName string // the name of a driver to use when calling sql.Open() to connect to a database, "pgx" (pgx) or "postgres" (lib/pq) + Host string // the host of the database, "localhost" + Port string // the port of the database, "5433" + User string // the user to connect as, "postgres" + Password string // the password to connect with, "password" + Database string // the database to connect to, "postgres" + Options string // URL-formatted additional options to pass in the connection string, "sslmode=disable&something=value" + // TestRole is the role used to create and connect to the template database + // and each test database. If not provided, defaults to [DefaultRole]. The + // capabilities of this role should match the capabilities of the role that + // your application uses to connect to its database and run migrations. + TestRole *Role } // URL returns a postgres connection string in the format @@ -357,6 +365,58 @@ new databases and roles. Most likely you want to connect as the default `postgres` user, since you'll be connecting to a dedicated testing-only Postgres server as described earlier. +### `pgtestdb.Role` +A dedicated Postgres role (user) is used to create the template database and each test database. pgtestdb will create this role for you with sane defaults, but you can control the username, password, and capabilities of this role if desired. + +```go +const ( + // DefaultRoleUsername is the default name for the role that is created and + // used to create and connect to each test database. + DefaultRoleUsername = "pgtdbuser" + // DefaultRolePassword is the default password for the role that is created and + // used to create and connect to each test database. + DefaultRolePassword = "pgtdbpass" + // DefaultRoleCapabilities is the default set of capabilities for the role + // that is created and used to create and conect to each test database. + // This is locked down by default, and will not allow the creation of + // extensions. + DefaultRoleCapabilities = "NOSUPERUSER NOCREATEDB NOCREATEROLE" +) + +// DefaultRole returns the default Role used to create and connect to the +// template database and each test database. It is a function, not a struct, to +// prevent accidental overriding. +func DefaultRole() Role { + return Role{ + Username: DefaultRoleUsername, + Password: DefaultRolePassword, + Capabilities: DefaultRoleCapabilities, + } +} + +// Role contains the details of a postgres role (user) that will be used +// when creating and connecting to the template and test databases. +type Role struct { + // The username for the role, defaults to [DefaultRoleUsername]. + Username string + // The password for the role, defaults to [DefaultRolePassword]. + Password string + // The capabilities that will be granted to the role, defaults to + // [DefaultRoleCapabilities]. + Capabilities string +} +``` + +Because this role is used to connect to each template and each test database +and run the migrations, its capabilities should match those of your production +application. For instance, if in production your application connects as a +superuser, you will want to pass a custom `Role` whthat includes the +`SUPERUSER` capability so that your migrations will run the same in both +envproduction and tests. + +This is a common case for many applications that install or activate extensions +like [Postgis](https://postgis.net/), which require activation via a superuser. + ### `pgtestdb.Migrator` The `Migrator` interface contains all of the logic needed to prepare a template diff --git a/docker-compose.yml b/docker-compose.yml index 3b7411e..32931d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,11 @@ version: "3.6" services: testdb: - image: postgres:15 + # We're using postgis so that we can test the creation of the postgis + # extension, which requires superuser extensions. + # + # To use the equivalent in plain postgres, use `postgres:15` + image: postgis/postgis:15-master environment: POSTGRES_PASSWORD: password restart: unless-stopped diff --git a/testdb.go b/testdb.go index f1819f8..2168ba2 100644 --- a/testdb.go +++ b/testdb.go @@ -7,28 +7,67 @@ import ( "database/sql" "encoding/hex" "fmt" - "testing" "github.com/peterldowns/pgtestdb/internal/once" "github.com/peterldowns/pgtestdb/internal/sessionlock" + "github.com/peterldowns/pgtestdb/migrators/common" ) const ( - // TestUser is the username for connecting to each test database. - TestUser = "pgtdbuser" - // TestPassword is the password for connecting to each test database. - TestPassword = "pgtdbpass" + // DefaultRoleUsername is the default name for the role that is created and + // used to create and connect to each test database. + DefaultRoleUsername = "pgtdbuser" + // DefaultRolePassword is the default password for the role that is created and + // used to create and connect to each test database. + DefaultRolePassword = "pgtdbpass" + // DefaultRoleCapabilities is the default set of capabilities for the role + // that is created and used to create and conect to each test database. + // This is locked down by default, and will not allow the creation of + // extensions. + DefaultRoleCapabilities = "NOSUPERUSER NOCREATEDB NOCREATEROLE" + // Deprecated: prefer [DefaultRoleUsername]. + TestUser = DefaultRoleUsername + // Deprecated: prefer [DefaultRolePassword]. + TestPassword = DefaultRolePassword ) +// DefaultRole returns the default Role used to create and connect to the +// template database and each test database. It is a function, not a struct, to +// prevent accidental overriding. +func DefaultRole() Role { + return Role{ + Username: DefaultRoleUsername, + Password: DefaultRolePassword, + Capabilities: DefaultRoleCapabilities, + } +} + // Config contains the details needed to connect to a postgres server/database. type Config struct { - DriverName string // the name of a driver to use when calling sql.Open() to connect to a database + DriverName string // the name of a driver to use when calling sql.Open() to connect to a database, "pgx" (pgx) or "postgres" (lib/pq) Host string // the host of the database, "localhost" Port string // the port of the database, "5433" User string // the user to connect as, "postgres" Password string // the password to connect with, "password" Database string // the database to connect to, "postgres" Options string // URL-formatted additional options to pass in the connection string, "sslmode=disable&something=value" + // TestRole is the role used to create and connect to the template database + // and each test database. If not provided, defaults to [DefaultRole]. The + // capabilities of this role should match the capabilities of the role that + // your application uses to connect to its database and run migrations. + TestRole *Role +} + +// Role contains the details of a postgres role (user) that will be used +// when creating and connecting to the template and test databases. +type Role struct { + // The username for the role, defaults to [DefaultRoleUsername]. + Username string + // The password for the role, defaults to [DefaultRolePassword]. + Password string + // The capabilities that will be granted to the role, defaults to + // [DefaultRoleCapabilities]. + Capabilities string } // URL returns a postgres connection string in the format @@ -87,7 +126,7 @@ type Migrator interface { // database instance. This database is prepared and migrated by the given // migrator, by get-or-creating a template database and then cloning it. This is // a concurrency-safe primitive. If there is an error creating the database, the -// test will be immediately failed with `t.Fatal()`. +// test will be immediately failed with `t.Fatalf()`. // // If this method succeeds, it will `t.Log()` the connection string to the // created database, so that if your test fails, you can connect to the database @@ -96,20 +135,32 @@ type Migrator interface { // If this method succeeds and your test succeeds, the database will be removed // as part of the test cleanup process. // -// `testing.TB` is the common testing interface -// implemented by `*testing.T`, `*testing.B`, and `*testing.F`, so you can use -// pgtestdb to get a database for tests, benchmarks, and fuzzes. -func New(t testing.TB, conf Config, migrator Migrator) *sql.DB { +// `TB` is a subset of the `testing.TB` testing interface implemented by +// `*testing.T`, `*testing.B`, and `*testing.F`, so you can use pgtestdb to get +// a database for tests, benchmarks, and fuzzes. +func New(t TB, conf Config, migrator Migrator) *sql.DB { t.Helper() _, db := create(t, conf, migrator) return db } +// TB is a subset of the `testing.TB` testing interface implemented by +// `*testing.T`, `*testing.B`, and `*testing.F`, so you can use pgtestdb to get +// a database for tests, benchmarks, and fuzzes. It contains only the methods +// actually needed by pgtestdb, defined so that we can more easily mock it. +type TB interface { + Cleanup(func()) + Failed() bool + Fatalf(format string, args ...any) + Helper() + Logf(format string, args ...any) +} + // Custom is like [New] but after creating the new database instance, it closes // any connections and returns the configuration details of that database so // that you can connect to it explicitly, potentially via a different SQL // interface. -func Custom(t testing.TB, conf Config, migrator Migrator) *Config { +func Custom(t TB, conf Config, migrator Migrator) *Config { t.Helper() config, db := create(t, conf, migrator) // Close `*sql.DB` connection that was opened during the creation process so @@ -122,7 +173,14 @@ func Custom(t testing.TB, conf Config, migrator Migrator) *Config { return config } -func create(t testing.TB, conf Config, migrator Migrator) (*Config, *sql.DB) { +// Helper +// Fatalf +// Fatal +// Logf +// Cleanup +// Failed + +func create(t TB, conf Config, migrator Migrator) (*Config, *sql.DB) { t.Helper() ctx := context.Background() baseDB, err := conf.Connect() @@ -131,13 +189,21 @@ func create(t testing.TB, conf Config, migrator Migrator) (*Config, *sql.DB) { return nil, nil // unreachable } - if err := ensureUser(ctx, baseDB); err != nil { + // From this point onward, all functions assume that `conf.TestRole` is not nil. + // We default to the + if conf.TestRole == nil { + role := DefaultRole() + conf.TestRole = &role + } + if err := ensureUser(ctx, baseDB, conf); err != nil { t.Fatalf("could not create pgtestdb user: %s", err) + return nil, nil // unreachable } template, err := getOrCreateTemplate(ctx, baseDB, conf, migrator) if err != nil { - t.Fatal(err) + t.Fatalf("%s", err) + return nil, nil // unreachable } instance, err := createInstance(ctx, baseDB, *template) @@ -167,6 +233,7 @@ func create(t testing.TB, conf Config, migrator Migrator) (*Config, *sql.DB) { query := fmt.Sprintf(`DROP DATABASE IF EXISTS "%s"`, instance.Database) if _, err := baseDB.ExecContext(ctx, query); err != nil { t.Fatalf("could not drop test database '%s': %s", instance.Database, err) + return // unreachable } }) @@ -179,43 +246,50 @@ func create(t testing.TB, conf Config, migrator Migrator) (*Config, *sql.DB) { // Assumption: verification is >>> faster than performing the migrations, // and is therefore safe to run at the beginning of each test. if err := migrator.Verify(ctx, db, *instance); err != nil { - t.Fatal(fmt.Errorf("test database failed verification %s: %w", instance.Database, err)) + t.Fatalf("test database failed verification %s: %w", instance.Database, err) + return nil, nil // unreachable } return instance, db } -// user is used to guarantee that the testdb user/role is only get-or-created at -// most once per program. -var user once.Var[any] = once.NewVar[any]() //nolint:gochecknoglobals +// user is used to guarantee that each testdb user/role is only get-or-created +// at most once per program. Different calls to pgtestdb can specify different +// roles, but each will be get-or-created at most one time per program, and will +// be created only once no matter how many different programs or test suites run +// at once, thanks to the use of session locks. +var users once.Map[string, any] = once.NewMap[string, any]() //nolint:gochecknoglobals func ensureUser( ctx context.Context, baseDB *sql.DB, + conf Config, ) error { - _, err := user.Set(func() (*any, error) { - return nil, sessionlock.With(ctx, baseDB, "testdb-user", func(conn *sql.Conn) error { + username := conf.TestRole.Username + _, err := users.Set(username, func() (*any, error) { + return nil, sessionlock.With(ctx, baseDB, username, func(conn *sql.Conn) error { // Get-or-create a role/user dedicated to connecting to these test databases. var roleExists bool query := "SELECT EXISTS (SELECT from pg_catalog.pg_roles WHERE rolname = $1)" - if err := conn.QueryRowContext(ctx, query, TestUser).Scan(&roleExists); err != nil { - return fmt.Errorf("failed to detect if role %s exists: %w", TestUser, err) + if err := conn.QueryRowContext(ctx, query, username).Scan(&roleExists); err != nil { + return fmt.Errorf("failed to detect if role %s exists: %w", username, err) } if roleExists { return nil } if !roleExists { - query = fmt.Sprintf(`CREATE ROLE "%s"`, TestUser) + query = fmt.Sprintf(`CREATE ROLE "%s"`, username) if _, err := conn.ExecContext(ctx, query); err != nil { - return fmt.Errorf("failed to create role %s: %w", TestUser, err) + return fmt.Errorf("failed to create role %s: %w", username, err) } query = fmt.Sprintf( - `ALTER ROLE "%s" WITH LOGIN PASSWORD '%s' NOSUPERUSER NOCREATEDB NOCREATEROLE`, - TestUser, - TestPassword, + `ALTER ROLE "%s" WITH LOGIN PASSWORD '%s' %s`, + username, + conf.TestRole.Password, + conf.TestRole.Capabilities, ) if _, err := conn.ExecContext(ctx, query); err != nil { - return fmt.Errorf("failed to alter role and set password for %s: %w", TestUser, err) + return fmt.Errorf("failed to set password and capabilities for '%s': %w", username, err) } } return nil @@ -231,7 +305,7 @@ type templateState struct { hash string } -var states once.Map[string, templateState] = once.NewMap[string, templateState]() //nolint:gochecknoglobals +var templates once.Map[string, templateState] = once.NewMap[string, templateState]() //nolint:gochecknoglobals // getOrCreateTemplate will get-or-create a template, synchronizing at // the golang level (with the states map, so that each template is @@ -252,11 +326,21 @@ func getOrCreateTemplate( dbconf Config, migrator Migrator, ) (*templateState, error) { - hash, err := migrator.Hash() + mhash, err := migrator.Hash() if err != nil { return nil, fmt.Errorf("failed to calculate template hash: %w", err) } - return states.Set(hash, func() (*templateState, error) { + // The migrator Hash() implementation is included, along with the role + // details, so that if the user runs tests in parallel with different role + // information, they each get their own database. + hash := common.NewRecursiveHash( + common.Field("Username", dbconf.TestRole.Username), + common.Field("Password", dbconf.TestRole.Password), + common.Field("Capabilities", dbconf.TestRole.Capabilities), + common.Field("MigratorHash", mhash), + ).String() + + return templates.Set(hash, func() (*templateState, error) { // This function runs once per program, but only synchronizes access // within a single program. When running larger test suites, each // package's tests may run in parallel, which means this does not @@ -264,13 +348,14 @@ func getOrCreateTemplate( state := templateState{} state.hash = hash state.conf = dbconf - state.conf.User = TestUser - state.conf.Password = TestPassword + state.conf.TestRole = dbconf.TestRole + state.conf.User = dbconf.TestRole.Username + state.conf.Password = dbconf.TestRole.Password state.conf.Database = fmt.Sprintf("testdb_tpl_%s", hash) // sessionlock synchronizes the creation of the template with a // session-scoped advisory lock. err := sessionlock.With(ctx, baseDB, state.conf.Database, func(conn *sql.Conn) error { - return ensureTemplate(ctx, conn, migrator, &state) + return ensureTemplate(ctx, conn, migrator, state) }) if err != nil { return nil, err @@ -288,7 +373,7 @@ func ensureTemplate( ctx context.Context, conn *sql.Conn, migrator Migrator, - state *templateState, + state templateState, ) error { // If the template database already exists, and is marked as a template, // there is no more work to be done. diff --git a/testdb_test.go b/testdb_test.go index 848365a..2fe3e90 100644 --- a/testdb_test.go +++ b/testdb_test.go @@ -238,6 +238,61 @@ func TestMigrationWithConcurrentCreate(t *testing.T) { } } +func TestDefaultRolePreventsPostgis(t *testing.T) { + t.Parallel() + config := pgtestdb.Config{ + DriverName: "pgx", + User: "postgres", + Password: "password", + Host: "localhost", + Port: "5433", + Options: "sslmode=disable", + } + migrator := &sqlMigrator{ + migrations: []string{ + // This requires SUPERUSER permissions, but by default they're + // not enabled, so it should fail. + "CREATE EXTENSION postgis;", + }, + } + tt := &MockT{} + _ = pgtestdb.New(tt, config, migrator) + tt.DoCleanup() + assert.True(t, tt.Failed()) +} + +func TestCustomRoleAllowsPostgis(t *testing.T) { + t.Parallel() + config := pgtestdb.Config{ + DriverName: "pgx", + User: "postgres", + Password: "password", + Host: "localhost", + Port: "5433", + Options: "sslmode=disable", + TestRole: &pgtestdb.Role{ + // Must use a distinct name or it will collide with other tests that + // use the default username, but have non-SUPERUSER capabilities. + // TODO: figure out some way to detect a difference in capabilities + // and fail + warn the user about the collision. + // Or, TODO: figure out a way to include a hash of the capabilities + // on to the basename if there are custom capabilities? But then + // it's a pain and confusing. Blargh. + Username: "pgtestdb-superuser", + Password: pgtestdb.DefaultRolePassword, + Capabilities: "SUPERUSER", + }, + } + migrator := &sqlMigrator{ + migrations: []string{ + // This will work since the migrations will be run with a role that + // has SUPERUSER permissions. + "CREATE EXTENSION postgis;", + }, + } + _ = pgtestdb.New(t, config, migrator) +} + // pgtestdb.New should be able to connect with either lib/pq or pgx/stdlib. func TestWithLibPqAndPgxStdlibDrivers(t *testing.T) { t.Parallel() @@ -349,3 +404,36 @@ func (s *sqlMigrator) Verify(ctx context.Context, db *sql.DB, _ pgtestdb.Config) } return nil } + +// MockT implements the `TB“ interface so that we can check to see if a test +// "would have failed". +type MockT struct { + failed bool + cleanups []func() +} + +func (t *MockT) Fatalf(string, ...any) { + t.failed = true +} + +func (*MockT) Logf(string, ...any) { + // no-op +} + +func (*MockT) Helper() { + // no-op +} + +func (t *MockT) Cleanup(f func()) { + t.cleanups = append(t.cleanups, f) +} + +func (t *MockT) DoCleanup() { + for _, f := range t.cleanups { + f() + } +} + +func (t *MockT) Failed() bool { + return t.failed +} From 3368d993f1c4ad32056ead581c13a7195590902f Mon Sep 17 00:00:00 2001 From: Peter Downs Date: Wed, 3 Apr 2024 17:12:55 -0400 Subject: [PATCH 6/6] version: v0.0.12 -> v0.0.13 --- README.md | 2 +- VERSION | 2 +- migrators/atlasmigrator/go.mod | 2 +- migrators/dbmatemigrator/go.mod | 2 +- migrators/golangmigrator/go.mod | 2 +- migrators/goosemigrator/go.mod | 2 +- migrators/pgmigrator/go.mod | 2 +- migrators/sqlmigrator/go.mod | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f61867f..7e2574e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🧪 pgtestdb -![Latest Version](https://badgers.space/badge/latest%20version/v0.0.12/blueviolet?corner_radius=m) +![Latest Version](https://badgers.space/badge/latest%20version/v0.0.13/blueviolet?corner_radius=m) ![Golang](https://badgers.space/badge/golang/1.18+/blue?corner_radius=m) pgtestdb makes it cheap and easy to create ephemeral Postgres databases for your diff --git a/VERSION b/VERSION index f252462..03ac640 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.0.12 +v0.0.13 diff --git a/migrators/atlasmigrator/go.mod b/migrators/atlasmigrator/go.mod index f796737..9cbd99a 100644 --- a/migrators/atlasmigrator/go.mod +++ b/migrators/atlasmigrator/go.mod @@ -6,7 +6,7 @@ replace github.com/peterldowns/pgtestdb => ../../ require ( github.com/jackc/pgx/v5 v5.5.1 - github.com/peterldowns/pgtestdb v0.0.12 + github.com/peterldowns/pgtestdb v0.0.13 github.com/peterldowns/testy v0.0.1 ) diff --git a/migrators/dbmatemigrator/go.mod b/migrators/dbmatemigrator/go.mod index 609a173..651fc58 100644 --- a/migrators/dbmatemigrator/go.mod +++ b/migrators/dbmatemigrator/go.mod @@ -7,7 +7,7 @@ replace github.com/peterldowns/pgtestdb => ../../ require ( github.com/amacneil/dbmate/v2 v2.4.0 github.com/jackc/pgx/v5 v5.5.1 - github.com/peterldowns/pgtestdb v0.0.12 + github.com/peterldowns/pgtestdb v0.0.13 github.com/peterldowns/testy v0.0.1 ) diff --git a/migrators/golangmigrator/go.mod b/migrators/golangmigrator/go.mod index bae08cc..56691e5 100644 --- a/migrators/golangmigrator/go.mod +++ b/migrators/golangmigrator/go.mod @@ -7,7 +7,7 @@ replace github.com/peterldowns/pgtestdb => ../../ require ( github.com/golang-migrate/migrate/v4 v4.16.0 github.com/jackc/pgx/v5 v5.5.1 - github.com/peterldowns/pgtestdb v0.0.12 + github.com/peterldowns/pgtestdb v0.0.13 github.com/peterldowns/testy v0.0.1 ) diff --git a/migrators/goosemigrator/go.mod b/migrators/goosemigrator/go.mod index 876f948..e5f2ec2 100644 --- a/migrators/goosemigrator/go.mod +++ b/migrators/goosemigrator/go.mod @@ -6,7 +6,7 @@ replace github.com/peterldowns/pgtestdb => ../../ require ( github.com/jackc/pgx/v5 v5.5.1 - github.com/peterldowns/pgtestdb v0.0.12 + github.com/peterldowns/pgtestdb v0.0.13 github.com/peterldowns/testy v0.0.1 github.com/pressly/goose/v3 v3.11.2 ) diff --git a/migrators/pgmigrator/go.mod b/migrators/pgmigrator/go.mod index 9cac693..83f97e9 100644 --- a/migrators/pgmigrator/go.mod +++ b/migrators/pgmigrator/go.mod @@ -7,7 +7,7 @@ replace github.com/peterldowns/pgtestdb => ../../ require ( github.com/jackc/pgx/v5 v5.5.1 github.com/peterldowns/pgmigrate v0.0.5 - github.com/peterldowns/pgtestdb v0.0.12 + github.com/peterldowns/pgtestdb v0.0.13 github.com/peterldowns/testy v0.0.1 ) diff --git a/migrators/sqlmigrator/go.mod b/migrators/sqlmigrator/go.mod index b2f1585..8bf6d43 100644 --- a/migrators/sqlmigrator/go.mod +++ b/migrators/sqlmigrator/go.mod @@ -6,7 +6,7 @@ replace github.com/peterldowns/pgtestdb => ../../ require ( github.com/jackc/pgx/v5 v5.5.1 - github.com/peterldowns/pgtestdb v0.0.12 + github.com/peterldowns/pgtestdb v0.0.13 github.com/peterldowns/testy v0.0.1 github.com/rubenv/sql-migrate v1.4.0 )