feat(store): goose-миграции схемы + seed default tenant, тест на testcontainers

This commit is contained in:
2026-07-03 13:56:21 +07:00
parent fc10451340
commit 788f1db80e
6 changed files with 217 additions and 11 deletions
+28
View File
@@ -0,0 +1,28 @@
package store
import (
"context"
"database/sql"
"embed"
_ "github.com/jackc/pgx/v5/stdlib" // pgx database/sql driver
"github.com/pressly/goose/v3"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
// Migrate applies all pending goose migrations to the database at dsn.
func Migrate(ctx context.Context, dsn string) error {
db, err := sql.Open("pgx", dsn)
if err != nil {
return err
}
defer db.Close()
goose.SetBaseFS(migrationsFS)
if err := goose.SetDialect("postgres"); err != nil {
return err
}
return goose.UpContext(ctx, db, "migrations")
}
+34
View File
@@ -0,0 +1,34 @@
package store
import (
"context"
"testing"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestMigrateCreatesTablesAndSeed(t *testing.T) {
dsn := startPostgres(t)
ctx := context.Background()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
// таблицы существуют
for _, table := range []string{"users", "projects", "provider_accounts", "templates", "domains", "check_runs"} {
var exists bool
err := pool.QueryRow(ctx,
`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name=$1)`, table).Scan(&exists)
if err != nil || !exists {
t.Fatalf("table %s missing (err=%v)", table, err)
}
}
// seed default project присутствует
var name string
err = pool.QueryRow(ctx, `SELECT name FROM projects WHERE id='00000000-0000-0000-0000-000000000002'`).Scan(&name)
if err != nil || name != "default" {
t.Fatalf("seed project missing: name=%q err=%v", name, err)
}
}
+63
View File
@@ -0,0 +1,63 @@
-- +goose Up
CREATE TABLE users (
id uuid PRIMARY KEY,
email text NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE projects (
id uuid PRIMARY KEY,
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE provider_accounts (
id uuid PRIMARY KEY,
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
provider text NOT NULL,
secret_enc text NOT NULL,
comment text NOT NULL DEFAULT '',
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE templates (
id uuid PRIMARY KEY,
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name text NOT NULL,
doc jsonb NOT NULL,
version int NOT NULL DEFAULT 1,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE domains (
id uuid PRIMARY KEY,
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
provider_account_id uuid NOT NULL REFERENCES provider_accounts(id) ON DELETE CASCADE,
zone_name text NOT NULL,
zone_id text NOT NULL,
template_id uuid REFERENCES templates(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE check_runs (
id uuid PRIMARY KEY,
domain_id uuid NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
result jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
-- seed default tenant (Фаза 1B без логина)
INSERT INTO users (id, email)
VALUES ('00000000-0000-0000-0000-000000000001', 'default@local');
INSERT INTO projects (id, user_id, name)
VALUES ('00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000001', 'default');
-- +goose Down
DROP TABLE check_runs;
DROP TABLE domains;
DROP TABLE templates;
DROP TABLE provider_accounts;
DROP TABLE projects;
DROP TABLE users;
+39
View File
@@ -0,0 +1,39 @@
package store
import (
"context"
"testing"
"time"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/testcontainers/testcontainers-go/wait"
)
// startPostgres spins up an ephemeral PostgreSQL, applies migrations,
// and returns its DSN. Container is terminated on test cleanup.
func startPostgres(t *testing.T) string {
t.Helper()
ctx := context.Background()
container, err := postgres.Run(ctx, "postgres:16-alpine",
postgres.WithDatabase("dns_ar_test"),
postgres.WithUsername("test"),
postgres.WithPassword("test"),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).WithStartupTimeout(60*time.Second)),
)
if err != nil {
t.Fatalf("start postgres: %v", err)
}
t.Cleanup(func() { _ = testcontainers.TerminateContainer(container) })
dsn, err := container.ConnectionString(ctx, "sslmode=disable")
if err != nil {
t.Fatalf("dsn: %v", err)
}
if err := Migrate(ctx, dsn); err != nil {
t.Fatalf("migrate: %v", err)
}
return dsn
}