feat(store): goose-миграции схемы + seed default tenant, тест на testcontainers
This commit is contained in:
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user