From 566169687a753bdd4130bc999efcc2b0dcf84a40 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 20 Apr 2026 16:11:26 +0800 Subject: [PATCH] fix(iam): allow wildcard scope in schema seed --- sql/postgresql/iam_schema_v1.sql | 5 +- .../iam/repository/schema_integration_test.go | 138 ++++++++++++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 supply-api/internal/iam/repository/schema_integration_test.go diff --git a/sql/postgresql/iam_schema_v1.sql b/sql/postgresql/iam_schema_v1.sql index 535ffce9..8b3172a5 100644 --- a/sql/postgresql/iam_schema_v1.sql +++ b/sql/postgresql/iam_schema_v1.sql @@ -57,7 +57,10 @@ CREATE TABLE IF NOT EXISTS iam_scopes ( updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 约束 - CONSTRAINT chk_scope_code_format CHECK (code ~ '^[a-z][a-z0-9._]{0,63}$') + CONSTRAINT chk_scope_code_format CHECK ( + code = '*' + OR code ~ '^[a-z][a-z0-9._]{0,63}$' + ) ); CREATE INDEX IF NOT EXISTS idx_iam_scopes_code ON iam_scopes (code); diff --git a/supply-api/internal/iam/repository/schema_integration_test.go b/supply-api/internal/iam/repository/schema_integration_test.go new file mode 100644 index 00000000..43a7ca11 --- /dev/null +++ b/supply-api/internal/iam/repository/schema_integration_test.go @@ -0,0 +1,138 @@ +//go:build integration +// +build integration + +package repository + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +func getIAMTestDB(t *testing.T) *pgxpool.Pool { + t.Helper() + + host := os.Getenv("SUPPLY_API_DB_HOST") + if host == "" { + host = "/var/run/postgresql" + } + port := os.Getenv("SUPPLY_API_DB_PORT") + if port == "" { + port = "5432" + } + user := os.Getenv("SUPPLY_API_DB_USER") + if user == "" { + user = "long" + } + password := os.Getenv("SUPPLY_API_DB_PASSWORD") + dbName := os.Getenv("SUPPLY_API_DB_NAME") + if dbName == "" { + dbName = "supply_test" + } + + var dsn string + if host[0] == '/' { + dsn = "postgres://" + user + ":" + password + "@/" + dbName + "?host=" + host + "&sslmode=disable" + } else { + dsn = "postgres://" + user + ":" + password + "@" + host + ":" + port + "/" + dbName + "?sslmode=disable" + } + + pool, err := pgxpool.New(context.Background(), dsn) + if err != nil { + t.Skipf("跳过 IAM 集成测试:无法连接数据库: %v", err) + return nil + } + if err := pool.Ping(context.Background()); err != nil { + pool.Close() + t.Skipf("跳过 IAM 集成测试:无法 ping 数据库: %v", err) + return nil + } + + t.Cleanup(func() { + pool.Close() + }) + return pool +} + +func TestIAMSchemaV1_AppliesOnCleanSchema(t *testing.T) { + if testing.Short() { + t.Skip("integration only") + } + + pool := getIAMTestDB(t) + if pool == nil { + return + } + + prereqSQL := mustReadIAMSQLFile(t, filepath.Join("..", "..", "..", "..", "scripts", "devtest", "sql", "supply_iam_prereqs.sql")) + iamSchemaSQL := mustReadIAMSQLFile(t, filepath.Join("..", "..", "..", "..", "sql", "postgresql", "iam_schema_v1.sql")) + + ctx := context.Background() + conn, err := pool.Acquire(ctx) + if err != nil { + t.Fatalf("acquire connection failed: %v", err) + } + defer conn.Release() + + schemaName := fmt.Sprintf("iam_schema_contract_%d", time.Now().UnixNano()) + quotedSchema := quoteIAMIdentifier(schemaName) + + if _, err := conn.Exec(ctx, "CREATE SCHEMA "+quotedSchema); err != nil { + t.Fatalf("create schema failed: %v", err) + } + t.Cleanup(func() { + _, _ = pool.Exec(context.Background(), "DROP SCHEMA IF EXISTS "+quotedSchema+" CASCADE") + }) + + if _, err := conn.Exec(ctx, "SET search_path TO "+quotedSchema); err != nil { + t.Fatalf("set search_path failed: %v", err) + } + if _, err := conn.Exec(ctx, prereqSQL); err != nil { + t.Fatalf("apply IAM prereqs failed: %v", err) + } + if _, err := conn.Exec(ctx, iamSchemaSQL); err != nil { + t.Fatalf("apply IAM schema failed: %v", err) + } + + var allScopeCount int + if err := conn.QueryRow(ctx, "SELECT COUNT(*) FROM iam_scopes WHERE code = '*'").Scan(&allScopeCount); err != nil { + t.Fatalf("query all scope failed: %v", err) + } + if allScopeCount != 1 { + t.Fatalf("unexpected all-scope seed count: got=%d want=1", allScopeCount) + } + + var superAdminWildcardCount int + if err := conn.QueryRow(ctx, ` + SELECT COUNT(*) + FROM iam_role_scopes rs + JOIN iam_roles r ON r.id = rs.role_id + JOIN iam_scopes s ON s.id = rs.scope_id + WHERE r.code = 'super_admin' AND s.code = '*' + `).Scan(&superAdminWildcardCount); err != nil { + t.Fatalf("query super_admin wildcard scope mapping failed: %v", err) + } + if superAdminWildcardCount != 1 { + t.Fatalf("unexpected super_admin wildcard mapping count: got=%d want=1", superAdminWildcardCount) + } +} + +func mustReadIAMSQLFile(t *testing.T, path string) string { + t.Helper() + + payload, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read sql file failed: %s: %v", path, err) + } + return string(payload) +} + +func quoteIAMIdentifier(name string) string { + return `"` + strings.ReplaceAll(name, `"`, `""`) + `"` +}