(Feat): Initial Commit
Some checks failed
Build & Test / build-test (push) Has been cancelled
Build & Test / swagger-codegen-cli (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled

This commit is contained in:
2026-07-03 19:41:31 +05:30
commit 7e940c83a7
461 changed files with 45002 additions and 0 deletions

View File

@@ -0,0 +1,382 @@
package test
import (
"context"
"crypto/md5" //nolint:gosec
"database/sql"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
"allaboutapps.dev/aw/go-starter/internal/config"
"allaboutapps.dev/aw/go-starter/internal/test/fixtures"
pUtil "allaboutapps.dev/aw/go-starter/internal/util"
dbutil "allaboutapps.dev/aw/go-starter/internal/util/db"
"github.com/aarondl/sqlboiler/v4/boil"
"github.com/allaboutapps/integresql-client-go"
"github.com/allaboutapps/integresql-client-go/pkg/util"
"github.com/pkg/errors"
migrate "github.com/rubenv/sql-migrate"
)
var (
client *integresql.Client
// tracks IntegreSQL template initialization and hash relookup (to reidentify the pool from a precomputed poolID)
poolInitMap = &sync.Map{} // "poolID" -> *sync.Once
poolHashMap = &sync.Map{} // "poolID" -> "poolHash"
// we will compute a db template hash over the following dirs/files
migDir = config.DatabaseMigrationFolder
fixFile = filepath.Join(pUtil.GetProjectRootDir(), "/internal/test/fixtures/fixtures.go")
selfFile = filepath.Join(pUtil.GetProjectRootDir(), "/internal/test/test_database.go")
defaultPoolPaths = []string{migDir, fixFile, selfFile}
)
//nolint:gochecknoinits
func init() {
// autoinitialize IntegreSQL client
c, err := integresql.DefaultClientFromEnv()
if err != nil {
panic(errors.Wrap(err, "Failed to create new integresql-client"))
}
client = c
// pin migrate to use the globally defined `migrations` table identifier
migrate.SetTable(config.DatabaseMigrationTable)
}
// WithTestDatabase returns an isolated test database based on the current migrations and fixtures.
func WithTestDatabase(t *testing.T, closure func(db *sql.DB)) {
t.Helper()
ctx := t.Context()
WithTestDatabaseContext(ctx, t, closure)
}
// WithTestDatabaseContext returns an isolated test database based on the current migrations and fixtures.
// The provided context will be used during setup (instead of the default background context).
func WithTestDatabaseContext(ctx context.Context, t *testing.T, closure func(db *sql.DB)) {
t.Helper()
poolID := strings.Join(defaultPoolPaths, ",")
// Get a hold of the &sync.Once{} for this integresql pool in this pkg scope...
initOnce, _ := poolInitMap.LoadOrStore(poolID, &sync.Once{})
doOnce, ok := initOnce.(*sync.Once)
if !ok {
t.Fatalf("Expected doOnce to be of type *sync.Once, but got %T", initOnce)
}
doOnce.Do(func() {
t.Helper()
// compute and store poolID -> poolHash map (computes hash of all files/dirs specified)
poolHash := storePoolHash(t, poolID, defaultPoolPaths)
// properly build up the template database once
execClosureNewIntegresTemplate(ctx, t, poolHash, func(db *sql.DB) error {
t.Helper()
countMigrations, err := ApplyMigrations(t, db)
if err != nil {
t.Fatalf("Failed to apply migrations for %q: %v\n", poolHash, err)
return err
}
t.Logf("Applied %d migrations for hash %q", countMigrations, poolHash)
countFixtures, err := ApplyTestFixtures(ctx, t, db)
if err != nil {
t.Fatalf("Failed to apply test fixtures for %q: %v\n", poolHash, err)
return err
}
t.Logf("Applied %d test fixtures for hash %q", countFixtures, poolHash)
return nil
})
})
// execute closure in a new IntegreSQL database build from above template
execClosureNewIntegresDatabase(ctx, t, getPoolHash(t, poolID), "WithTestDatabase", closure)
}
type DatabaseDumpConfig struct {
DumpFile string // required, absolute path to dump file
ApplyMigrations bool // optional, default false
ApplyTestFixtures bool // optional, default false
}
// WithTestDatabaseFromDump returns an isolated test database based on a dump file.
func WithTestDatabaseFromDump(t *testing.T, config DatabaseDumpConfig, closure func(db *sql.DB)) {
t.Helper()
ctx := t.Context()
WithTestDatabaseFromDumpContext(ctx, t, config, closure)
}
// WithTestDatabaseFromDumpContext returns an isolated test database based on a dump file.
// The provided context will be used during setup (instead of the default background context).
func WithTestDatabaseFromDumpContext(ctx context.Context, t *testing.T, config DatabaseDumpConfig, closure func(db *sql.DB)) {
t.Helper()
// DumpFile is mandadory.
if config.DumpFile == "" {
t.Fatal("DatabaseDumpConfig.DumpFile is mandadory and cannot be ''")
}
// poolID must incorporate additional config args in the final hash
fragments := fmt.Sprintf("?migrations=%v&fixtures=%v", config.ApplyMigrations, config.ApplyTestFixtures)
poolID := strings.Join([]string{config.DumpFile, selfFile}, ",") + fragments
// Get a hold of the &sync.Once{} for this integresql pool in this pkg scope...
initOnce, _ := poolInitMap.LoadOrStore(poolID, &sync.Once{})
doOnce, ok := initOnce.(*sync.Once)
if !ok {
t.Fatalf("Expected doOnce to be of type *sync.Once, but got %T", initOnce)
}
doOnce.Do(func() {
t.Helper()
// compute and store poolID -> poolHash map (computes hash of all files/dirs specified)
poolHash := storePoolHash(t, poolID, []string{config.DumpFile, selfFile}, fragments)
// properly build up the template database once
execClosureNewIntegresTemplate(ctx, t, poolHash, func(db *sql.DB) error {
t.Helper()
if err := ApplyDump(ctx, t, db, config.DumpFile); err != nil {
t.Fatalf("Failed to apply dumps for %q: %v\n", poolHash, err)
return err
}
t.Logf("Applied dump for hash %q", poolHash)
if config.ApplyMigrations {
countMigrations, err := ApplyMigrations(t, db)
if err != nil {
t.Fatalf("Failed to apply migrations for %q: %v\n", poolHash, err)
return err
}
t.Logf("Applied %d migrations for hash %q", countMigrations, poolHash)
}
if config.ApplyTestFixtures {
countFixtures, err := ApplyTestFixtures(ctx, t, db)
if err != nil {
t.Fatalf("Failed to apply test fixtures for %q: %v\n", poolHash, err)
return err
}
t.Logf("Applied %d test fixtures for hash %q", countFixtures, poolHash)
}
return nil
})
})
// execute closure in a new IntegreSQL database build from above template
execClosureNewIntegresDatabase(ctx, t, getPoolHash(t, poolID), "WithTestDatabaseFromDump", closure)
}
// WithTestDatabaseEmpty returns an isolated test database with no migrations applied or fixtures inserted.
func WithTestDatabaseEmpty(t *testing.T, closure func(db *sql.DB)) {
t.Helper()
ctx := t.Context()
WithTestDatabaseEmptyContext(ctx, t, closure)
}
// WithTestDatabaseEmptyContext returns an isolated test database with no migrations applied or fixtures inserted.
// The provided context will be used during setup (instead of the default background context).
func WithTestDatabaseEmptyContext(ctx context.Context, t *testing.T, closure func(db *sql.DB)) {
t.Helper()
poolID := selfFile
// Get a hold of the &sync.Once{} for this integresql pool in this pkg scope...
initOnce, _ := poolInitMap.LoadOrStore(poolID, &sync.Once{})
doOnce, ok := initOnce.(*sync.Once)
if !ok {
t.Fatalf("Expected doOnce to be of type *sync.Once, but got %T", initOnce)
}
doOnce.Do(func() {
t.Helper()
// compute and store poolID -> poolHash map (computes hash of all files/dirs specified)
poolHash := storePoolHash(t, poolID, []string{selfFile})
// properly build up the template database once (noop)
execClosureNewIntegresTemplate(ctx, t, poolHash, func(_ *sql.DB) error {
t.Helper()
return nil
})
})
// execute closure in a new IntegreSQL database build from above template
execClosureNewIntegresDatabase(ctx, t, getPoolHash(t, poolID), "WithTestDatabaseEmpty", closure)
}
// Adds poolID to poolHashMap pointing to the final integresql hash
// Expects hashPaths to be absolute paths to actual files or directories (its contents will be md5 hashed)
// Optional fragments can be used to further enhance the computed md5
func storePoolHash(t *testing.T, poolID string, hashPaths []string, fragments ...string) string {
t.Helper()
// compute a new integreSQL pool hash
poolHash, err := util.GetTemplateHash(hashPaths...)
if err != nil {
t.Fatalf("Failed to create template hash for %v: %#v", poolID, err)
}
// update the hash with optional provided fragments
if len(fragments) > 0 {
poolHash = fmt.Sprintf("%x", md5.Sum([]byte(poolHash+strings.Join(fragments, ",")))) //nolint:gosec
}
// and point poolID to it (sideffect synchronized store!)
poolHashMap.Store(poolID, poolHash) // save it for all runners
return poolHash
}
// Gets precomputed integresql hash via poolID identifier from our synchronized map (see storePoolHash)
func getPoolHash(t *testing.T, poolID string) string {
t.Helper()
poolHash, ok := poolHashMap.Load(poolID)
if !ok {
t.Fatalf("Failed to get poolHash from poolID '%v'. Is poolHashMap initialized yet?", poolID)
return ""
}
hash, ok := poolHash.(string)
if !ok {
t.Fatalf("Expected poolHash to be of type string, but got %T", poolHash)
return ""
}
return hash
}
// Executes closure on an integresql **template** database
func execClosureNewIntegresTemplate(ctx context.Context, t *testing.T, poolHash string, closure func(db *sql.DB) error) {
t.Helper()
if err := client.SetupTemplateWithDBClient(ctx, poolHash, closure); err != nil {
// This error is exceptionally fatal as it hinders ANY future other
// test execution with this hash as the template was *never* properly
// setuped successfully. All GetTestDatabase will wait unti timeout
// unless we interrupt them by discarding the base template...
discardError := client.DiscardTemplate(ctx, poolHash)
if discardError != nil {
t.Fatalf("Failed to setup template database, also discarding failed for poolHash %q: %v, %v", poolHash, err, discardError)
}
t.Fatalf("Failed to setup template database (discarded) for poolHash %q: %v", poolHash, err)
}
}
// Executes closure on an integresql **test** database (scaffolded from a template)
func execClosureNewIntegresDatabase(ctx context.Context, t *testing.T, poolHash string, callee string, closure func(db *sql.DB)) {
t.Helper()
testDatabase, err := client.GetTestDatabase(ctx, poolHash)
if err != nil {
t.Fatalf("Failed to obtain test database: %v", err)
}
connectionString := testDatabase.Config.ConnectionString()
t.Logf("%v: %q", callee, testDatabase.Config.Database)
db, err := sql.Open("postgres", connectionString)
if err != nil {
t.Fatalf("Failed to setup test database for connectionString %q: %v", connectionString, err)
}
if err := db.PingContext(ctx); err != nil {
t.Fatalf("Failed to ping test database for connectionString %q: %v", connectionString, err)
}
closure(db)
// this database object is managed and should close automatically after running the test
if err := db.Close(); err != nil {
t.Fatalf("Failed to close db %q: %v", connectionString, err)
}
// disallow any further refs to managed object after running the test
//nolint: wastedassign
db = nil
}
// ApplyMigrations applies all current database migrations to db
func ApplyMigrations(t *testing.T, db *sql.DB) (int, error) {
t.Helper()
// In case an old default sql-migrate migration table (named "gorp_migrations") still exists we rename it to the new name equivalent
// in sync with the settings in dbconfig.yml and config.DatabaseMigrationTable.
if _, err := db.ExecContext(t.Context(), fmt.Sprintf("ALTER TABLE IF EXISTS gorp_migrations RENAME TO %s;", config.DatabaseMigrationTable)); err != nil {
return 0, fmt.Errorf("failed to rename migrations table: %w", err)
}
migrations := &migrate.FileMigrationSource{Dir: migDir}
countMigrations, err := migrate.Exec(db, "postgres", migrations, migrate.Up)
if err != nil {
return 0, fmt.Errorf("failed to apply migrations: %w", err)
}
return countMigrations, nil
}
// ApplyTestFixtures applies all current test fixtures (insert) to db
func ApplyTestFixtures(ctx context.Context, t *testing.T, db *sql.DB) (int, error) {
t.Helper()
inserts := fixtures.Inserts()
// insert test fixtures in an auto-managed db transaction
err := dbutil.WithTransaction(ctx, db, func(tx boil.ContextExecutor) error {
t.Helper()
for _, fixture := range inserts {
if err := fixture.Insert(ctx, tx, boil.Infer()); err != nil {
return fmt.Errorf("failed to insert fixture: %w", err)
}
}
return nil
})
if err != nil {
return 0, fmt.Errorf("failed to apply test fixtures: %w", err)
}
return len(inserts), nil
}
// ApplyDump applies dumpFile (absolute path to .sql file) to db
func ApplyDump(ctx context.Context, t *testing.T, db *sql.DB, dumpFile string) error {
t.Helper()
// ensure file exists
if _, err := os.Stat(dumpFile); err != nil {
return fmt.Errorf("failed to stat dump file: %w", err)
}
// we need to get the db name before being able to do anything.
var targetDB string
if err := db.QueryRowContext(ctx, "SELECT current_database();").Scan(&targetDB); err != nil {
return fmt.Errorf("failed to get current database: %w", err)
}
cmd := exec.CommandContext(ctx, "bash", "-c", fmt.Sprintf("cat %q | psql %q", dumpFile, targetDB)) //nolint:gosec
combinedOutput, err := cmd.CombinedOutput()
if err != nil {
return errors.Wrap(err, string(combinedOutput))
}
return nil
}