feat(05-03): add user-plugin organisations table (structure only)
- create_organisations folds PHP v3.2.0; no users-table ALTER this phase - Organisation model is an FK target; app.key placeholder in config
This commit is contained in:
36
updates/10_organisations.go
Normal file
36
updates/10_organisations.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Folds PHP updates/v3.2.0/create_organisations_table.php.
|
||||
//
|
||||
// Phase 5 does not ALTER the users table — deferred to Phase 7's AUTH-02 per
|
||||
// 05-CONTEXT.md user-confirmed decision; this migration only creates the FK
|
||||
// target golem15_user_organisations table that Fonoteka's org credential
|
||||
// tables reference.
|
||||
var organisationsMigrations = []*gormigrate.Migration{
|
||||
{
|
||||
ID: "202609180002_create_organisations",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`
|
||||
CREATE TABLE golem15_user_organisations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)`).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Exec("DROP TABLE IF EXISTS golem15_user_organisations").Error
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(organisationsMigrations...)
|
||||
}
|
||||
54
updates/organisations_test.go
Normal file
54
updates/organisations_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/summercms/lagoon"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
)
|
||||
|
||||
func TestCreateOrganisationsMigration(t *testing.T) {
|
||||
sqlDB := dedicatedDB(t, "user_organisations")
|
||||
gdb, err := lagoon.Use(t.Context(), sqlDB)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
opts := &gormigrate.Options{
|
||||
TableName: "summer_migrations_golem15_user",
|
||||
UseTransaction: true,
|
||||
}
|
||||
m := gormigrate.New(gdb, opts, All())
|
||||
if err := m.Migrate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !gdb.Migrator().HasTable("golem15_user_organisations") {
|
||||
t.Fatal("golem15_user_organisations missing after migrate")
|
||||
}
|
||||
if !gdb.Migrator().HasTable("users") {
|
||||
t.Fatal("users missing after migrate")
|
||||
}
|
||||
for _, col := range []string{"id", "slug", "name", "description", "created_at", "updated_at"} {
|
||||
if !gdb.Migrator().HasColumn("golem15_user_organisations", col) {
|
||||
t.Fatalf("missing column %s", col)
|
||||
}
|
||||
}
|
||||
|
||||
var ids []string
|
||||
if err := gdb.Raw(`SELECT id FROM summer_migrations_golem15_user ORDER BY id`).Scan(&ids).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(ids) != 2 || ids[1] != "202609180002_create_organisations" {
|
||||
t.Fatalf("history = %v", ids)
|
||||
}
|
||||
|
||||
if err := m.RollbackLast(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gdb.Migrator().HasTable("golem15_user_organisations") {
|
||||
t.Fatal("rollback must drop golem15_user_organisations")
|
||||
}
|
||||
if !gdb.Migrator().HasTable("users") {
|
||||
t.Fatal("users must survive organisations rollback")
|
||||
}
|
||||
}
|
||||
139
updates/postgres_test.go
Normal file
139
updates/postgres_test.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/postgres"
|
||||
)
|
||||
|
||||
var (
|
||||
userPG *postgres.PostgresContainer
|
||||
userSQL *sql.DB
|
||||
userDSN string
|
||||
userPGErr error
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
code := 1
|
||||
if !testShort() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
userPGErr = startUserPostgres(ctx)
|
||||
cancel()
|
||||
if userPGErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "golem15.user: testcontainers postgres: %v\n", userPGErr)
|
||||
stopUserPostgres()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
code = m.Run()
|
||||
stopUserPostgres()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func testShort() bool {
|
||||
for _, a := range os.Args {
|
||||
if a == "-test.short" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func startUserPostgres(ctx context.Context) error {
|
||||
ctr, err := postgres.Run(ctx,
|
||||
"postgres:16-alpine",
|
||||
postgres.WithDatabase("golem15_user"),
|
||||
postgres.WithUsername("golem15_user"),
|
||||
postgres.WithPassword("golem15_user"),
|
||||
postgres.BasicWaitStrategies(),
|
||||
testcontainers.WithEnv(map[string]string{
|
||||
"POSTGRES_INITDB_ARGS": "--locale-provider=icu --icu-locale=pl-PL --encoding=UTF8",
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userPG = ctr
|
||||
dsn, err := ctr.ConnectionString(ctx, "sslmode=disable")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
_ = db.Close()
|
||||
return err
|
||||
}
|
||||
userSQL = db
|
||||
userDSN = dsn
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopUserPostgres() {
|
||||
if userSQL != nil {
|
||||
_ = userSQL.Close()
|
||||
}
|
||||
if userPG != nil {
|
||||
_ = testcontainers.TerminateContainer(userPG)
|
||||
}
|
||||
}
|
||||
|
||||
func userAdminDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
if testing.Short() {
|
||||
t.Skip("requires testcontainers postgres")
|
||||
}
|
||||
if userPGErr != nil {
|
||||
t.Fatalf("postgres unavailable: %v", userPGErr)
|
||||
}
|
||||
if userSQL == nil {
|
||||
t.Fatal("postgres unavailable: container was not started")
|
||||
}
|
||||
return userSQL
|
||||
}
|
||||
|
||||
func dedicatedDB(t *testing.T, name string) *sql.DB {
|
||||
t.Helper()
|
||||
admin := userAdminDB(t)
|
||||
ctx := t.Context()
|
||||
quoted := `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
|
||||
if _, err := admin.ExecContext(ctx, `CREATE DATABASE `+quoted+` TEMPLATE template0 ENCODING 'UTF8' LOCALE_PROVIDER icu ICU_LOCALE 'pl-PL'`); err != nil && !strings.Contains(err.Error(), "already exists") {
|
||||
t.Fatalf("create %s: %v", name, err)
|
||||
}
|
||||
dsn, err := dsnWithDB(userDSN, name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = db.Close()
|
||||
_, _ = admin.ExecContext(context.Background(), `DROP DATABASE IF EXISTS `+quoted+` WITH (FORCE)`)
|
||||
})
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func dsnWithDB(dsn, name string) (string, error) {
|
||||
u, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
u.Path = "/" + name
|
||||
return u.String(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user