github.com/quay/claircore@v1.5.28/test/integration/db.go (about)

     1  package integration
     2  
     3  import (
     4  	"context"
     5  	crand "crypto/rand"
     6  	"encoding/binary"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"math/rand"
    11  	"os"
    12  	"sync"
    13  	"testing"
    14  
    15  	"github.com/jackc/pgconn"
    16  	"github.com/jackc/pgx/v4"
    17  	"github.com/jackc/pgx/v4/log/testingadapter"
    18  	"github.com/jackc/pgx/v4/pgxpool"
    19  )
    20  
    21  var (
    22  	rngMu sync.Mutex
    23  	rng   *rand.Rand
    24  
    25  	up        sync.Once
    26  	pkgDB     *Engine
    27  	pkgConfig *pgxpool.Config
    28  )
    29  
    30  func mkIDs() (uint64, uint64) {
    31  	rngMu.Lock()
    32  	defer rngMu.Unlock()
    33  	return rng.Uint64(), rng.Uint64()
    34  }
    35  
    36  func init() {
    37  	// Seed our rng.
    38  	b := make([]byte, 8)
    39  	if _, err := io.ReadFull(crand.Reader, b); err != nil {
    40  		panic(err)
    41  	}
    42  	seed := rand.NewSource(int64(binary.BigEndian.Uint64(b)))
    43  	rng = rand.New(seed)
    44  }
    45  
    46  const (
    47  	// EnvPGConnString is the environment variable examined for a DSN for a
    48  	// pre-existing database engine. If unset, an appropriate database will
    49  	// attempt to be downloaded and run.
    50  	EnvPGConnString = "POSTGRES_CONNECTION_STRING"
    51  
    52  	// EnvPGVersion is the environment variable examined for the version of
    53  	// PostgreSQL used if an embedded binary would be used.
    54  	EnvPGVersion = `PGVERSION`
    55  
    56  	loadUUID        = `CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`
    57  	createRole      = `CREATE ROLE %s LOGIN PASSWORD '%[1]s';`
    58  	createDatabase  = `CREATE DATABASE %[2]s WITH OWNER %[1]s ENCODING 'UTF8';`
    59  	killConnections = `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1`
    60  	dropDatabase    = `DROP DATABASE %s;`
    61  	dropRole        = `DROP ROLE %s;`
    62  )
    63  
    64  // NewDB generates a unique database for use in integration tests.
    65  //
    66  // The returned database has a random name and a dedicated owner role
    67  // configured. The "uuid-ossp" extension is already loaded.
    68  //
    69  // DBSetup and NeedDB are expected to have been called correctly.
    70  func NewDB(ctx context.Context, t testing.TB) (*DB, error) {
    71  	dbid, roleid := mkIDs()
    72  	database := fmt.Sprintf("db%x", dbid)
    73  	role := fmt.Sprintf("role%x", roleid)
    74  	cfg := configureDatabase(ctx, t, pkgConfig, database, role)
    75  	if t.Failed() {
    76  		return nil, fmt.Errorf("failed to create database")
    77  	}
    78  	t.Logf("config: %+v", struct {
    79  		Host     string
    80  		Database string
    81  		User     string
    82  		Password string
    83  		Port     uint16
    84  	}{
    85  		Host:     cfg.ConnConfig.Host,
    86  		Port:     cfg.ConnConfig.Port,
    87  		Database: cfg.ConnConfig.Database,
    88  		User:     cfg.ConnConfig.User,
    89  		Password: cfg.ConnConfig.Password,
    90  	})
    91  
    92  	return &DB{
    93  		cfg: cfg,
    94  	}, nil
    95  }
    96  
    97  func configureDatabase(ctx context.Context, t testing.TB, root *pgxpool.Config, database, role string) *pgxpool.Config {
    98  	var cfg *pgxpool.Config
    99  	// First, connect as the superuser to create the new database and role.
   100  	cfg = root.Copy()
   101  	cfg.ConnConfig.Logger = testingadapter.NewLogger(t)
   102  	cfg.MaxConns = 10
   103  	conn, err := pgx.ConnectConfig(ctx, cfg.ConnConfig)
   104  	if err != nil {
   105  		t.Error(err)
   106  		return nil
   107  	}
   108  	// The creation commands don't have "IF NOT EXISTS" forms, so check for the
   109  	// specific error codes that mean they already exist.
   110  	var pgErr *pgconn.PgError
   111  	_, err = conn.Exec(ctx, fmt.Sprintf(createRole, role))
   112  	switch {
   113  	case errors.Is(err, nil):
   114  	case errors.As(err, &pgErr):
   115  		if pgErr.Code == "42710" {
   116  			t.Log("expected error:", pgErr.Message)
   117  			break
   118  		}
   119  		fallthrough
   120  	default:
   121  		t.Error(err)
   122  	}
   123  	_, err = conn.Exec(ctx, fmt.Sprintf(createDatabase, role, database))
   124  	switch {
   125  	case errors.Is(err, nil):
   126  	case errors.As(err, &pgErr):
   127  		if pgErr.Code == "42P04" {
   128  			t.Log("expected error:", pgErr.Message)
   129  			break
   130  		}
   131  		fallthrough
   132  	default:
   133  		t.Error(err)
   134  	}
   135  	if err := conn.Close(ctx); err != nil {
   136  		t.Error(err)
   137  	}
   138  	if t.Failed() {
   139  		return nil
   140  	}
   141  
   142  	// Next, connect to the newly created database as the superuser to load the
   143  	// uuid extension
   144  	cfg = cfg.Copy()
   145  	cfg.ConnConfig.Database = database
   146  	conn, err = pgx.ConnectConfig(ctx, cfg.ConnConfig)
   147  	if err != nil {
   148  		t.Error(err)
   149  		return nil
   150  	}
   151  	if _, err := conn.Exec(ctx, loadUUID); err != nil {
   152  		t.Error(err)
   153  	}
   154  	if err := conn.Close(ctx); err != nil {
   155  		t.Error(err)
   156  	}
   157  	if t.Failed() {
   158  		return nil
   159  	}
   160  
   161  	// Finally, return a config setup to connect as the new role to the new
   162  	// database.
   163  	cfg = root.Copy()
   164  	cfg.ConnConfig.User = role
   165  	cfg.ConnConfig.Password = role
   166  	cfg.ConnConfig.Database = database
   167  
   168  	return cfg
   169  }
   170  
   171  // DB is a handle for connecting to and cleaning up a test database.
   172  //
   173  // If [testing.Verbose] reports true, the database engine will be run with the
   174  // "auto_explain" module enabled. See the [auto_explain documentation] for more
   175  // information. Setting the environment variable "PGEXPLAIN_FORMAT" will control
   176  // the output format. This does not apply when the test harness is not
   177  // controlling the database.
   178  //
   179  // [auto_explain documentation]: https://www.postgresql.org/docs/current/auto-explain.html
   180  type DB struct {
   181  	cfg    *pgxpool.Config
   182  	noDrop bool
   183  }
   184  
   185  func (db *DB) String() string {
   186  	const dsnFmt = `host=%s port=%d database=%s user=%s password=%s sslmode=disable`
   187  	return fmt.Sprintf(dsnFmt,
   188  		db.cfg.ConnConfig.Host,
   189  		db.cfg.ConnConfig.Port,
   190  		db.cfg.ConnConfig.Database,
   191  		db.cfg.ConnConfig.User,
   192  		db.cfg.ConnConfig.Password)
   193  }
   194  
   195  // Config returns a pgxpool.Config for the test database.
   196  func (db *DB) Config() *pgxpool.Config {
   197  	return db.cfg.Copy()
   198  }
   199  
   200  // Close tears down the created database.
   201  func (db *DB) Close(ctx context.Context, t testing.TB) {
   202  	cfg := pkgConfig.Copy()
   203  	cfg.ConnConfig.Logger = testingadapter.NewLogger(t)
   204  	conn, err := pgx.ConnectConfig(ctx, cfg.ConnConfig)
   205  	if err != nil {
   206  		t.Fatal(err)
   207  	}
   208  	defer conn.Close(ctx)
   209  
   210  	if _, err := conn.Exec(ctx, killConnections, db.cfg.ConnConfig.Database); err != nil {
   211  		t.Error(err)
   212  	}
   213  
   214  	if !db.noDrop {
   215  		if _, err := conn.Exec(ctx, fmt.Sprintf(dropDatabase, db.cfg.ConnConfig.Database)); err != nil {
   216  			t.Error(err)
   217  		}
   218  		if _, err := conn.Exec(ctx, fmt.Sprintf(dropRole, db.cfg.ConnConfig.User)); err != nil {
   219  			t.Error(err)
   220  		}
   221  	}
   222  	db.cfg = nil
   223  }
   224  
   225  // NeedDB is like Skip, except that the test will run if the needed binaries
   226  // have been fetched.
   227  //
   228  // See the example for usage.
   229  func NeedDB(t testing.TB) {
   230  	t.Helper()
   231  	if testing.Short() {
   232  		t.Skip(`skipping integration test: short tests`)
   233  	}
   234  	if inGHA {
   235  		up.Do(startGithubActions(t))
   236  		return
   237  	}
   238  	if externalDB {
   239  		t.Log("using preconfigured external database")
   240  		up.Do(func() {
   241  			cfg, err := pgxpool.ParseConfig(os.Getenv(EnvPGConnString))
   242  			if err != nil {
   243  				t.Fatal(err)
   244  			}
   245  			pkgConfig = cfg
   246  			// Database was started externally, we don't have to arrange to have it
   247  			// torn down.
   248  		})
   249  		return
   250  	}
   251  
   252  	t.Log("using embedded database")
   253  	embedDB.DiscoverVersion(t)
   254  	up.Do(startEmbedded(t))
   255  }
   256  
   257  // DBSetup queues setup and teardown for a postgres engine instance. If the
   258  // "integration" build tag is not provided, then nothing is done. If the
   259  // environment variable at EnvPGConnString is populated and the "integration"
   260  // build tag is provided, then the value of that environment variable is used
   261  // instead of an embedded postgres binary.
   262  //
   263  // See the example for usage.
   264  func DBSetup() func() {
   265  	// This used to do a bunch of setup, but that got pretty gnarly.
   266  	//
   267  	// Most of the complexity was moved into [NeedDB] and helper functions run
   268  	// under the [up] sync.Once, because there's a [testing.T] that can actually
   269  	// log things.
   270  	return func() {
   271  		if pkgDB != nil {
   272  			pkgDB.Stop()
   273  		}
   274  	}
   275  }