forked from baron/baron-sso
callback 검증 보강. seed-tenant 추가보강
This commit is contained in:
@@ -42,15 +42,6 @@ type InitialTenantConfig struct {
|
||||
func SeedTenants(db *gorm.DB) error {
|
||||
slog.Info("[Bootstrap] Checking initial tenant seed...")
|
||||
|
||||
var tenantCount int64
|
||||
if err := db.Model(&domain.Tenant{}).Count(&tenantCount).Error; err != nil {
|
||||
return fmt.Errorf("count tenants before seed: %w", err)
|
||||
}
|
||||
if tenantCount > 0 {
|
||||
slog.Info("[Bootstrap] Tenant seed skipped because tenants already exist", "count", tenantCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
configs, err := loadSeedTenantConfigs()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -59,7 +50,62 @@ func SeedTenants(db *gorm.DB) error {
|
||||
return errors.New("seed tenant csv has no tenant rows")
|
||||
}
|
||||
|
||||
return seedTenantConfigs(db, configs)
|
||||
existingSlugs, existingIDs, err := loadExistingTenantIdentitySet(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
missingConfigs := filterMissingSeedTenantConfigs(configs, existingSlugs, existingIDs)
|
||||
if len(missingConfigs) == 0 {
|
||||
slog.Info("[Bootstrap] Tenant seed skipped because all seed slugs already exist", "count", len(configs))
|
||||
return nil
|
||||
}
|
||||
|
||||
slog.Info(
|
||||
"[Bootstrap] Tenant seed will create missing seed tenants",
|
||||
"total", len(configs),
|
||||
"missing", len(missingConfigs),
|
||||
"existing", len(configs)-len(missingConfigs),
|
||||
)
|
||||
return seedTenantConfigs(db, missingConfigs)
|
||||
}
|
||||
|
||||
func loadExistingTenantIdentitySet(db *gorm.DB) (map[string]bool, map[string]bool, error) {
|
||||
var tenants []domain.Tenant
|
||||
if err := db.Select("id", "slug").Find(&tenants).Error; err != nil {
|
||||
return nil, nil, fmt.Errorf("load existing tenants before seed: %w", err)
|
||||
}
|
||||
|
||||
slugs := make(map[string]bool, len(tenants))
|
||||
ids := make(map[string]bool, len(tenants))
|
||||
for _, tenant := range tenants {
|
||||
slug := strings.TrimSpace(strings.ToLower(tenant.Slug))
|
||||
if slug != "" {
|
||||
slugs[slug] = true
|
||||
}
|
||||
id := strings.TrimSpace(strings.ToLower(tenant.ID))
|
||||
if id != "" {
|
||||
ids[id] = true
|
||||
}
|
||||
}
|
||||
return slugs, ids, nil
|
||||
}
|
||||
|
||||
func filterMissingSeedTenantConfigs(configs []InitialTenantConfig, existingSlugs map[string]bool, existingIDs map[string]bool) []InitialTenantConfig {
|
||||
filtered := make([]InitialTenantConfig, 0, len(configs))
|
||||
for _, config := range configs {
|
||||
slug := strings.TrimSpace(strings.ToLower(config.Slug))
|
||||
id := strings.TrimSpace(strings.ToLower(config.TenantID))
|
||||
if slug == "" || existingSlugs[slug] || (id != "" && existingIDs[id]) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, config)
|
||||
existingSlugs[slug] = true
|
||||
if id != "" {
|
||||
existingIDs[id] = true
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func seedTenantConfigs(db *gorm.DB, configs []InitialTenantConfig) error {
|
||||
|
||||
@@ -2,9 +2,19 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/testsupport"
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
postgres_module "github.com/testcontainers/testcontainers-go/modules/postgres"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
gorm_postgres "gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestSeedTenantCSVDefinesWorksmobileDomainClassTenants(t *testing.T) {
|
||||
@@ -165,3 +175,144 @@ func TestIsSeedTenantSlugUsesConfiguredCSVPath(t *testing.T) {
|
||||
t.Fatal("normal-tenant must not be detected as seed tenant")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterMissingSeedTenantConfigsSkipsExistingSlugs(t *testing.T) {
|
||||
configs := []InitialTenantConfig{
|
||||
{TenantID: "existing-root-id", Name: "Existing Root", Slug: "existing-root"},
|
||||
{Name: "Missing Child", Slug: "missing-child", ParentSlug: "existing-root"},
|
||||
{TenantID: "existing-child-id", Name: "Existing Child", Slug: "existing-child", ParentSlug: "existing-root"},
|
||||
{TenantID: "existing-other-id", Name: "Conflicting ID", Slug: "new-slug"},
|
||||
}
|
||||
existingSlugs := map[string]bool{
|
||||
"existing-root": true,
|
||||
"existing-child": true,
|
||||
}
|
||||
existingIDs := map[string]bool{
|
||||
"existing-root-id": true,
|
||||
"existing-child-id": true,
|
||||
"existing-other-id": true,
|
||||
}
|
||||
|
||||
filtered := filterMissingSeedTenantConfigs(configs, existingSlugs, existingIDs)
|
||||
|
||||
if len(filtered) != 1 {
|
||||
t.Fatalf("filtered count = %d, want 1: %#v", len(filtered), filtered)
|
||||
}
|
||||
if filtered[0].Slug != "missing-child" {
|
||||
t.Fatalf("filtered slug = %q, want missing-child", filtered[0].Slug)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeedTenantsCreatesMissingSeedRowsWithoutTouchingExistingSlugs(t *testing.T) {
|
||||
if !testsupport.DockerAvailable() {
|
||||
t.Skip("Docker provider is unavailable in this environment")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
postgresContainer, err := postgres_module.Run(ctx,
|
||||
"postgres:16-alpine",
|
||||
postgres_module.WithDatabase("testdb"),
|
||||
postgres_module.WithUsername("user"),
|
||||
postgres_module.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(
|
||||
wait.ForLog("database system is ready to accept connections").
|
||||
WithOccurrence(2).
|
||||
WithStartupTimeout(30*time.Second),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to start postgres container: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := postgresContainer.Terminate(ctx); err != nil {
|
||||
log.Printf("failed to terminate postgres container: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
connStr, err := postgresContainer.ConnectionString(ctx, "sslmode=disable")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get postgres connection string: %v", err)
|
||||
}
|
||||
db, err := gorm.Open(gorm_postgres.Open(connStr), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open postgres connection: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&domain.Tenant{}, &domain.TenantDomain{}, &domain.KetoOutbox{}); err != nil {
|
||||
t.Fatalf("failed to migrate seed test tables: %v", err)
|
||||
}
|
||||
|
||||
existingRoot := domain.Tenant{
|
||||
ID: "00000000-0000-0000-0000-000000000001",
|
||||
Name: "Existing Root Name",
|
||||
Slug: "existing-root",
|
||||
Type: domain.TenantTypeCompanyGroup,
|
||||
Description: "manual tenant must not be overwritten",
|
||||
Status: domain.TenantStatusActive,
|
||||
}
|
||||
nonSeedTenant := domain.Tenant{
|
||||
ID: "00000000-0000-0000-0000-000000000002",
|
||||
Name: "Manual Tenant",
|
||||
Slug: "manual-tenant",
|
||||
Type: domain.TenantTypeCompany,
|
||||
Status: domain.TenantStatusActive,
|
||||
}
|
||||
if err := db.Create(&existingRoot).Error; err != nil {
|
||||
t.Fatalf("failed to create existing root tenant: %v", err)
|
||||
}
|
||||
if err := db.Create(&nonSeedTenant).Error; err != nil {
|
||||
t.Fatalf("failed to create non-seed tenant: %v", err)
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "seed-tenant.csv")
|
||||
csv := "id,name,type,parent_tenant_slug,slug,memo,email_domain\n" +
|
||||
"10000000-0000-0000-0000-000000000001,Seed Root Name,COMPANY_GROUP,,existing-root,seed must be skipped,\n" +
|
||||
"00000000-0000-0000-0000-000000000002,Conflicting ID,COMPANY,existing-root,conflicting-id,seed id must be skipped,\n" +
|
||||
"10000000-0000-0000-0000-000000000002,Missing Child,COMPANY,existing-root,missing-child,created from seed,child.example.com\n"
|
||||
if err := os.WriteFile(path, []byte(csv), 0o600); err != nil {
|
||||
t.Fatalf("failed to write seed csv: %v", err)
|
||||
}
|
||||
t.Setenv(seedTenantCSVPathEnv, path)
|
||||
|
||||
if err := SeedTenants(db); err != nil {
|
||||
t.Fatalf("SeedTenants returned error: %v", err)
|
||||
}
|
||||
|
||||
var root domain.Tenant
|
||||
if err := db.First(&root, "slug = ?", "existing-root").Error; err != nil {
|
||||
t.Fatalf("failed to load existing root after seed: %v", err)
|
||||
}
|
||||
if root.ID != existingRoot.ID {
|
||||
t.Fatalf("existing root ID = %q, want %q", root.ID, existingRoot.ID)
|
||||
}
|
||||
if root.Name != existingRoot.Name {
|
||||
t.Fatalf("existing root name = %q, want untouched %q", root.Name, existingRoot.Name)
|
||||
}
|
||||
|
||||
var child domain.Tenant
|
||||
if err := db.Preload("Domains").First(&child, "slug = ?", "missing-child").Error; err != nil {
|
||||
t.Fatalf("missing seed child was not created: %v", err)
|
||||
}
|
||||
if child.ParentID == nil || *child.ParentID != existingRoot.ID {
|
||||
t.Fatalf("child parent ID = %v, want %q", child.ParentID, existingRoot.ID)
|
||||
}
|
||||
if len(child.Domains) != 1 || child.Domains[0].Domain != "child.example.com" {
|
||||
t.Fatalf("child domains = %#v, want child.example.com", child.Domains)
|
||||
}
|
||||
|
||||
var rootCount int64
|
||||
if err := db.Model(&domain.Tenant{}).Where("slug = ?", "existing-root").Count(&rootCount).Error; err != nil {
|
||||
t.Fatalf("failed to count existing root rows: %v", err)
|
||||
}
|
||||
if rootCount != 1 {
|
||||
t.Fatalf("existing-root row count = %d, want 1", rootCount)
|
||||
}
|
||||
|
||||
var conflictingIDCount int64
|
||||
if err := db.Model(&domain.Tenant{}).Where("slug = ?", "conflicting-id").Count(&conflictingIDCount).Error; err != nil {
|
||||
t.Fatalf("failed to count conflicting-id rows: %v", err)
|
||||
}
|
||||
if conflictingIDCount != 0 {
|
||||
t.Fatalf("conflicting-id row count = %d, want 0", conflictingIDCount)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user