1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/bootstrap/tenant_seed_test.go

319 lines
10 KiB
Go

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) {
configs, err := loadSeedTenantConfigs()
if err != nil {
t.Fatalf("loadSeedTenantConfigs returned error: %v", err)
}
expected := []struct {
name string
slug string
tenantType string
parentSlug string
domains []string
}{
{
name: "한맥가족",
slug: "hanmac-family",
tenantType: domain.TenantTypeCompanyGroup,
},
{
name: "삼안",
slug: "saman",
tenantType: domain.TenantTypeCompany,
parentSlug: "hanmac-family",
domains: []string{"samaneng.com"},
},
{
name: "한맥기술",
slug: "hanmac",
tenantType: domain.TenantTypeCompany,
parentSlug: "hanmac-family",
domains: []string{"hanmaceng.co.kr"},
},
{
name: "총괄기획&기술개발센터",
slug: "gpdtdc",
tenantType: domain.TenantTypeCompany,
parentSlug: "hanmac-family",
domains: []string{"baroncs.co.kr"},
},
{
name: "바론그룹",
slug: "baron-group",
tenantType: domain.TenantTypeCompanyGroup,
parentSlug: "hanmac-family",
},
{
name: "(주)장헌",
slug: "jangheon",
tenantType: domain.TenantTypeCompany,
parentSlug: "baron-group",
domains: []string{"jangheon.com"},
},
{
name: "장헌산업",
slug: "jangheon-sanup",
tenantType: domain.TenantTypeCompany,
parentSlug: "baron-group",
domains: []string{"jangheon.co.kr"},
},
{
name: "한라산업개발",
slug: "hanlla",
tenantType: domain.TenantTypeCompany,
parentSlug: "baron-group",
domains: []string{"hanllasanup.co.kr"},
},
{
name: "(주)피티씨",
slug: "ptc",
tenantType: domain.TenantTypeCompany,
parentSlug: "baron-group",
domains: []string{"pre-cast.co.kr"},
},
{
name: "Personal",
slug: "personal",
tenantType: domain.TenantTypePersonal,
},
}
if len(configs) != len(expected) {
t.Fatalf("expected %d seed tenants, got %d", len(expected), len(configs))
}
for i, want := range expected {
got := configs[i]
if got.Name != want.name {
t.Fatalf("tenant[%d] name = %q, want %q", i, got.Name, want.name)
}
if got.Slug != want.slug {
t.Fatalf("tenant[%d] slug = %q, want %q", i, got.Slug, want.slug)
}
if got.Type != want.tenantType {
t.Fatalf("tenant[%d] type = %q, want %q", i, got.Type, want.tenantType)
}
if got.ParentSlug != want.parentSlug {
t.Fatalf("tenant[%d] parent slug = %q, want %q", i, got.ParentSlug, want.parentSlug)
}
if len(got.Domains) != len(want.domains) {
t.Fatalf("tenant[%d] domains = %#v, want %#v", i, got.Domains, want.domains)
}
for j, wantDomain := range want.domains {
if got.Domains[j] != wantDomain {
t.Fatalf("tenant[%d] domain[%d] = %q, want %q", i, j, got.Domains[j], wantDomain)
}
}
}
}
func TestNormalizeSeedTenantTypeAllowsOrganization(t *testing.T) {
if got := normalizeSeedTenantType("organization"); got != domain.TenantTypeOrganization {
t.Fatalf("normalizeSeedTenantType(organization) = %q, want %q", got, domain.TenantTypeOrganization)
}
}
func TestLoadSeedTenantConfigsUsesConfiguredCSVPath(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "seed-tenant.csv")
csv := "name,type,parent_tenant_slug,slug,memo,email_domain\n" +
"Root,COMPANY_GROUP,,root,Root memo,\n" +
"Child,COMPANY,root,child,Child memo,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)
configs, err := loadSeedTenantConfigs()
if err != nil {
t.Fatalf("loadSeedTenantConfigs returned error: %v", err)
}
if len(configs) != 2 {
t.Fatalf("expected 2 configs, got %d", len(configs))
}
if configs[1].ParentSlug != "root" {
t.Fatalf("child parent slug = %q, want root", configs[1].ParentSlug)
}
if len(configs[1].Domains) != 1 || configs[1].Domains[0] != "child.example.com" {
t.Fatalf("child domains = %#v, want child.example.com", configs[1].Domains)
}
}
func TestIsSeedTenantSlugUsesConfiguredCSVPath(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "seed-tenant.csv")
csv := "name,type,parent_tenant_slug,slug,memo,email_domain\n" +
"Root,COMPANY_GROUP,,protected-root,Root memo,\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 !IsSeedTenantSlug("protected-root") {
t.Fatal("protected-root must be detected as seed tenant")
}
if IsSeedTenantSlug("normal-tenant") {
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)
}
}