/ sqldb / postgres_fixture.go
postgres_fixture.go
  1  //go:build !js && !(windows && (arm || 386)) && !(linux && (ppc64 || mips || mipsle || mips64)) && !(netbsd || openbsd)
  2  
  3  package sqldb
  4  
  5  import (
  6  	"context"
  7  	"crypto/rand"
  8  	"database/sql"
  9  	"encoding/hex"
 10  	"fmt"
 11  	"strconv"
 12  	"strings"
 13  	"testing"
 14  	"time"
 15  
 16  	_ "github.com/jackc/pgx/v5"
 17  	"github.com/ory/dockertest/v3"
 18  	"github.com/ory/dockertest/v3/docker"
 19  	"github.com/stretchr/testify/require"
 20  )
 21  
 22  const (
 23  	testPgUser   = "test"
 24  	testPgPass   = "test"
 25  	testPgDBName = "test"
 26  	PostgresTag  = "11"
 27  )
 28  
 29  // TestPgFixture is a test fixture that starts a Postgres 11 instance in a
 30  // docker container.
 31  type TestPgFixture struct {
 32  	db       *sql.DB
 33  	pool     *dockertest.Pool
 34  	resource *dockertest.Resource
 35  	host     string
 36  	port     int
 37  }
 38  
 39  // NewTestPgFixture constructs a new TestPgFixture starting up a docker
 40  // container running Postgres 11. The started container will expire in after
 41  // the passed duration.
 42  func NewTestPgFixture(t testing.TB, expiry time.Duration) *TestPgFixture {
 43  	// Use a sensible default on Windows (tcp/http) and linux/osx (socket)
 44  	// by specifying an empty endpoint.
 45  	pool, err := dockertest.NewPool("")
 46  	require.NoError(t, err, "Could not connect to docker")
 47  
 48  	// Pulls an image, creates a container based on it and runs it.
 49  	resource, err := pool.RunWithOptions(&dockertest.RunOptions{
 50  		Repository: "postgres",
 51  		Tag:        PostgresTag,
 52  		Env: []string{
 53  			fmt.Sprintf("POSTGRES_USER=%v", testPgUser),
 54  			fmt.Sprintf("POSTGRES_PASSWORD=%v", testPgPass),
 55  			fmt.Sprintf("POSTGRES_DB=%v", testPgDBName),
 56  			"listen_addresses='*'",
 57  		},
 58  		Cmd: []string{
 59  			"postgres",
 60  			"-c", "log_statement=all",
 61  			"-c", "log_destination=stderr",
 62  			"-c", "max_connections=5000",
 63  		},
 64  	}, func(config *docker.HostConfig) {
 65  		// Set AutoRemove to true so that stopped container goes away
 66  		// by itself.
 67  		config.AutoRemove = true
 68  		config.RestartPolicy = docker.RestartPolicy{Name: "no"}
 69  	})
 70  	require.NoError(t, err, "Could not start resource")
 71  
 72  	hostAndPort := resource.GetHostPort("5432/tcp")
 73  	parts := strings.Split(hostAndPort, ":")
 74  	host := parts[0]
 75  	port, err := strconv.ParseInt(parts[1], 10, 64)
 76  	require.NoError(t, err)
 77  
 78  	fixture := &TestPgFixture{
 79  		host: host,
 80  		port: int(port),
 81  	}
 82  	databaseURL := fixture.GetConfig(testPgDBName).Dsn
 83  	log.Infof("Connecting to Postgres fixture: %v\n", databaseURL)
 84  
 85  	// Tell docker to hard kill the container in "expiry" seconds.
 86  	require.NoError(t, resource.Expire(uint(expiry.Seconds())))
 87  
 88  	// Exponential backoff-retry, because the application in the container
 89  	// might not be ready to accept connections yet.
 90  	pool.MaxWait = 120 * time.Second
 91  
 92  	var testDB *sql.DB
 93  	err = pool.Retry(func() error {
 94  		testDB, err = sql.Open("pgx", databaseURL)
 95  		if err != nil {
 96  			return err
 97  		}
 98  
 99  		return testDB.Ping()
100  	})
101  	require.NoError(t, err, "Could not connect to docker")
102  
103  	// Now fill in the rest of the fixture.
104  	fixture.db = testDB
105  	fixture.pool = pool
106  	fixture.resource = resource
107  
108  	return fixture
109  }
110  
111  // GetConfig returns the full config of the Postgres node.
112  func (f *TestPgFixture) GetConfig(dbName string) *PostgresConfig {
113  	return &PostgresConfig{
114  		Dsn: fmt.Sprintf(
115  			"postgres://%v:%v@%v:%v/%v?sslmode=disable",
116  			testPgUser, testPgPass, f.host, f.port, dbName,
117  		),
118  	}
119  }
120  
121  // TearDown stops the underlying docker container.
122  func (f *TestPgFixture) TearDown(t testing.TB) {
123  	err := f.pool.Purge(f.resource)
124  	require.NoError(t, err, "Could not purge resource")
125  }
126  
127  // randomDBName generates a random database name.
128  func randomDBName(t testing.TB) string {
129  	randBytes := make([]byte, 8)
130  	_, err := rand.Read(randBytes)
131  	require.NoError(t, err)
132  
133  	return "test_" + hex.EncodeToString(randBytes)
134  }
135  
136  // NewTestPostgresDB is a helper function that creates a Postgres database for
137  // testing using the given fixture.
138  func NewTestPostgresDB(t testing.TB, fixture *TestPgFixture) *PostgresStore {
139  	t.Helper()
140  
141  	dbName := randomDBName(t)
142  
143  	t.Logf("Creating new Postgres DB '%s' for testing", dbName)
144  
145  	_, err := fixture.db.ExecContext(
146  		context.Background(), "CREATE DATABASE "+dbName,
147  	)
148  	require.NoError(t, err)
149  
150  	cfg := fixture.GetConfig(dbName)
151  	store, err := NewPostgresStore(cfg)
152  	require.NoError(t, err)
153  
154  	require.NoError(t, store.ApplyAllMigrations(
155  		context.Background(), GetMigrations()),
156  	)
157  
158  	t.Cleanup(func() {
159  		require.NoError(t, store.DB.Close())
160  	})
161  
162  	return store
163  }
164  
165  // NewTestPostgresDBWithVersion is a helper function that creates a Postgres
166  // database for testing and migrates it to the given version.
167  func NewTestPostgresDBWithVersion(t *testing.T, fixture *TestPgFixture,
168  	version uint) *PostgresStore {
169  
170  	t.Helper()
171  
172  	t.Logf("Creating new Postgres DB for testing, migrating to version %d",
173  		version)
174  
175  	dbName := randomDBName(t)
176  	_, err := fixture.db.ExecContext(
177  		context.Background(), "CREATE DATABASE "+dbName,
178  	)
179  	require.NoError(t, err)
180  
181  	storeCfg := fixture.GetConfig(dbName)
182  	storeCfg.SkipMigrations = true
183  	store, err := NewPostgresStore(storeCfg)
184  	require.NoError(t, err)
185  
186  	err = store.ExecuteMigrations(TargetVersion(version))
187  	require.NoError(t, err)
188  
189  	t.Cleanup(func() {
190  		require.NoError(t, store.DB.Close())
191  	})
192  
193  	return store
194  }