package test import ( "fmt" "strconv" "testing" "time" "queryorchestration/internal/client" "queryorchestration/internal/serviceconfig" db "queryorchestration/internal/database" "github.com/docker/go-connections/nat" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) type CreateDatabaseConfig struct { NoMigrations bool } const ( dbAlias = "postgres" dbPort = 5432 ) func CreateDB(t testing.TB, cfg serviceconfig.ConfigProvider) { CreateDBWithParams(t, cfg, &CreateDatabaseConfig{}) } func CreateDBWithParams(t testing.TB, cfg serviceconfig.ConfigProvider, dcfg *CreateDatabaseConfig) { port, err := nat.NewPort("tcp", strconv.Itoa(dbPort)) require.NoError(t, err) cfg.SetDBUser("pass") cfg.SetDBSecret("postgres") cfg.SetDBName(GetAlias(t, dbAlias)) cfg.SetDBNoSSL(true) network := GetNetwork(t) req := testcontainers.ContainerRequest{ Image: "postgres:17.2-alpine3.21", Name: "postgres_test_queryorchestration", Env: map[string]string{ "POSTGRES_USER": cfg.GetDBUser(), "POSTGRES_PASSWORD": cfg.GetDBSecret(), }, Cmd: []string{"postgres", "-c", "max_connections=1000"}, ExposedPorts: []string{port.Port()}, WaitingFor: wait.ForAll( wait.ForExposedPort(), wait.ForListeningPort(port), wait.ForLog("database system is ready to accept connections"), wait.ForSQL(port, "postgres", func(host string, port nat.Port) string { return fmt.Sprintf("postgres://%s:%s@%s:%d/postgres?sslmode=disable", cfg.GetDBUser(), cfg.GetDBSecret(), host, port.Int()) }), ), Networks: []string{network}, NetworkAliases: map[string][]string{ network: {dbAlias}, }, } container, err := testcontainers.GenericContainer(t.Context(), testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, Reuse: true, }) require.NoError(t, err) host, err := container.Host(t.Context()) require.NoError(t, err) mappedPort, err := container.MappedPort(t.Context(), port) require.NoError(t, err) cfg.SetDBHost(host) cfg.SetDBPort(mappedPort.Int()) if !dcfg.NoMigrations { err := db.RunMigrations(t.Context(), cfg) require.NoError(t, err) err = cfg.SetDBPool(t.Context()) require.NoError(t, err) // Truncate all transactional tables so each test run starts with a clean slate. // The postgres container is reused across runs; without this, seed data from a // previous run causes duplicate key violations. // Excluded: // schema_migrations — golang-migrate tracking table; truncating would force // migrations to re-run and break the NoChange guard. // labels — reference/lookup table seeded by migration 110; its rows // are never created/deleted by the application and must be // present for the documentlabels FK to resolve. _, err = cfg.GetDBPool().Exec(t.Context(), ` DO $$ DECLARE r RECORD; BEGIN FOR r IN ( SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename NOT IN ('schema_migrations', 'labels') ) LOOP EXECUTE 'TRUNCATE TABLE public.' || quote_ident(r.tablename) || ' RESTART IDENTITY CASCADE'; END LOOP; END $$; `) require.NoError(t, err) } } // MustParseDate parses a date string in YYYY-MM-DD format and panics on error. // This is useful for test data where dates are known to be valid. func MustParseDate(dateStr string) time.Time { t, err := time.Parse("2006-01-02", dateStr) if err != nil { panic(fmt.Sprintf("failed to parse date %q: %v", dateStr, err)) } return t } // CreateTestClient creates a client using the client service, which automatically creates the root folder. // This is the preferred way to create clients in tests to ensure proper initialization. func CreateTestClient(t testing.TB, cfg serviceconfig.ConfigProvider, clientID, clientName string) { t.Helper() clientSvc := client.New(cfg) _, err := clientSvc.Create(t.Context(), client.CreateParams{ ID: clientID, Name: clientName, }) require.NoError(t, err) }