forked from baron/baron-sso
440 lines
14 KiB
Go
440 lines
14 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",
|
|
domains: []string{"brsw.kr"},
|
|
},
|
|
{
|
|
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: "halla",
|
|
tenantType: domain.TenantTypeCompany,
|
|
parentSlug: "hanmac-family",
|
|
domains: []string{"hallasanup.com"},
|
|
},
|
|
{
|
|
name: "(주)피티씨",
|
|
slug: "ptc",
|
|
tenantType: domain.TenantTypeCompany,
|
|
parentSlug: "baron-group",
|
|
domains: []string{"pre-cast.co.kr"},
|
|
},
|
|
}
|
|
|
|
if len(configs) < len(expected) {
|
|
t.Fatalf("expected at least %d seed tenants, got %d", len(expected), len(configs))
|
|
}
|
|
|
|
wantFamilyChildOrder := []string{
|
|
"gpdtdc",
|
|
"saman",
|
|
"hanmac",
|
|
"baron-group",
|
|
"halla",
|
|
}
|
|
policyFamilyChildSlugs := map[string]bool{}
|
|
for _, slug := range wantFamilyChildOrder {
|
|
policyFamilyChildSlugs[slug] = true
|
|
}
|
|
gotFamilyChildOrder := make([]string, 0, len(wantFamilyChildOrder))
|
|
for _, config := range configs {
|
|
if config.ParentSlug == "hanmac-family" && policyFamilyChildSlugs[config.Slug] {
|
|
gotFamilyChildOrder = append(gotFamilyChildOrder, config.Slug)
|
|
}
|
|
}
|
|
if len(gotFamilyChildOrder) != len(wantFamilyChildOrder) {
|
|
t.Fatalf("hanmac-family child order = %#v, want %#v", gotFamilyChildOrder, wantFamilyChildOrder)
|
|
}
|
|
for i, wantSlug := range wantFamilyChildOrder {
|
|
if gotFamilyChildOrder[i] != wantSlug {
|
|
t.Fatalf("hanmac-family child order[%d] = %q, want %q", i, gotFamilyChildOrder[i], wantSlug)
|
|
}
|
|
}
|
|
|
|
configBySlug := make(map[string]InitialTenantConfig, len(configs))
|
|
for _, config := range configs {
|
|
configBySlug[config.Slug] = config
|
|
}
|
|
|
|
for _, want := range expected {
|
|
got, ok := configBySlug[want.slug]
|
|
if !ok {
|
|
t.Fatalf("tenant slug %q not found in seed configs", want.slug)
|
|
}
|
|
if got.Name != want.name {
|
|
t.Fatalf("tenant[%s] name = %q, want %q", want.slug, got.Name, want.name)
|
|
}
|
|
if got.Slug != want.slug {
|
|
t.Fatalf("tenant[%s] slug = %q, want %q", want.slug, got.Slug, want.slug)
|
|
}
|
|
if got.Type != want.tenantType {
|
|
t.Fatalf("tenant[%s] type = %q, want %q", want.slug, got.Type, want.tenantType)
|
|
}
|
|
if got.ParentSlug != want.parentSlug {
|
|
t.Fatalf("tenant[%s] parent slug = %q, want %q", want.slug, got.ParentSlug, want.parentSlug)
|
|
}
|
|
if len(got.Domains) != len(want.domains) {
|
|
t.Fatalf("tenant[%s] domains = %#v, want %#v", want.slug, got.Domains, want.domains)
|
|
}
|
|
for j, wantDomain := range want.domains {
|
|
if got.Domains[j] != wantDomain {
|
|
t.Fatalf("tenant[%s] domain[%d] = %q, want %q", want.slug, j, got.Domains[j], wantDomain)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSeedTenantCSVDefinesTopLevelSeedTenantStructure(t *testing.T) {
|
|
configs, err := loadSeedTenantConfigs()
|
|
if err != nil {
|
|
t.Fatalf("loadSeedTenantConfigs returned error: %v", err)
|
|
}
|
|
|
|
configBySlug := make(map[string]InitialTenantConfig, len(configs))
|
|
for _, config := range configs {
|
|
configBySlug[config.Slug] = config
|
|
}
|
|
|
|
expectedRoots := []struct {
|
|
slug string
|
|
tenantType string
|
|
}{
|
|
{slug: "hanmac-family", tenantType: domain.TenantTypeCompanyGroup},
|
|
{slug: "commercial", tenantType: domain.TenantTypeCompanyGroup},
|
|
{slug: "public-org", tenantType: domain.TenantTypeCompanyGroup},
|
|
{slug: "edu", tenantType: domain.TenantTypeCompanyGroup},
|
|
{slug: "personal", tenantType: domain.TenantTypePersonal},
|
|
}
|
|
|
|
for _, want := range expectedRoots {
|
|
got, ok := configBySlug[want.slug]
|
|
if !ok {
|
|
t.Fatalf("top-level seed tenant slug %q not found", want.slug)
|
|
}
|
|
if got.Type != want.tenantType {
|
|
t.Fatalf("top-level seed tenant[%s] type = %q, want %q", want.slug, got.Type, want.tenantType)
|
|
}
|
|
if got.ParentSlug != "" {
|
|
t.Fatalf("top-level seed tenant[%s] parent slug = %q, want empty", want.slug, got.ParentSlug)
|
|
}
|
|
}
|
|
}
|
|
|
|
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,visibility,org_unit_type,worksmobile_sync\n" +
|
|
"Root,COMPANY_GROUP,,root,Root memo,,,,\n" +
|
|
"Child,USER_GROUP,root,child,Child memo,child.example.com,private,팀,no\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)
|
|
}
|
|
if configs[1].Config["visibility"] != "private" {
|
|
t.Fatalf("child visibility = %#v, want private", configs[1].Config["visibility"])
|
|
}
|
|
if configs[1].Config["orgUnitType"] != "팀" {
|
|
t.Fatalf("child orgUnitType = %#v, want 팀", configs[1].Config["orgUnitType"])
|
|
}
|
|
if configs[1].Config["worksmobileExcluded"] != true {
|
|
t.Fatalf("child worksmobileExcluded = %#v, want true", configs[1].Config["worksmobileExcluded"])
|
|
}
|
|
}
|
|
|
|
func TestSeedTenantCSVDefinesMHDAsPrivateUserGroup(t *testing.T) {
|
|
configs, err := loadSeedTenantConfigs()
|
|
if err != nil {
|
|
t.Fatalf("loadSeedTenantConfigs returned error: %v", err)
|
|
}
|
|
|
|
configBySlug := make(map[string]InitialTenantConfig, len(configs))
|
|
for _, config := range configs {
|
|
configBySlug[config.Slug] = config
|
|
}
|
|
|
|
mhd, ok := configBySlug["mhd"]
|
|
if !ok {
|
|
t.Fatal("mhd seed tenant not found")
|
|
}
|
|
if mhd.Type != domain.TenantTypeUserGroup {
|
|
t.Fatalf("mhd type = %q, want %q", mhd.Type, domain.TenantTypeUserGroup)
|
|
}
|
|
if mhd.Config["visibility"] != "private" {
|
|
t.Fatalf("mhd visibility = %#v, want private", mhd.Config["visibility"])
|
|
}
|
|
if mhd.Config["worksmobileExcluded"] != true {
|
|
t.Fatalf("mhd worksmobileExcluded = %#v, want true", mhd.Config["worksmobileExcluded"])
|
|
}
|
|
}
|
|
|
|
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 TestSeedTenantsCreatesMissingSeedRowsAndRepairsExistingSeedSlug(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,
|
|
}
|
|
existingSeedTenantWithTypoSlug := domain.Tenant{
|
|
ID: "5a03efd2-e62f-4243-800d-58334bf48b2f",
|
|
Name: "한라산업개발",
|
|
Slug: "hanlla",
|
|
Type: domain.TenantTypeCompany,
|
|
Description: "seed tenant with a typo slug must be repaired by UUID",
|
|
Status: domain.TenantStatusActive,
|
|
ParentID: &existingRoot.ID,
|
|
}
|
|
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)
|
|
}
|
|
if err := db.Create(&existingSeedTenantWithTypoSlug).Error; err != nil {
|
|
t.Fatalf("failed to create existing seed tenant with typo slug: %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" +
|
|
"5a03efd2-e62f-4243-800d-58334bf48b2f,한라산업개발,COMPANY,existing-root,halla,seed typo slug must be repaired,hallasanup.com\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 manual domain.Tenant
|
|
if err := db.First(&manual, "id = ?", nonSeedTenant.ID).Error; err != nil {
|
|
t.Fatalf("failed to load non-seed tenant after seed: %v", err)
|
|
}
|
|
if manual.Slug != nonSeedTenant.Slug {
|
|
t.Fatalf("non-seed tenant slug = %q, want untouched %q", manual.Slug, nonSeedTenant.Slug)
|
|
}
|
|
|
|
var repairedSeed domain.Tenant
|
|
if err := db.First(&repairedSeed, "id = ?", existingSeedTenantWithTypoSlug.ID).Error; err != nil {
|
|
t.Fatalf("failed to load existing seed tenant after seed: %v", err)
|
|
}
|
|
if repairedSeed.Slug != "halla" {
|
|
t.Fatalf("existing seed tenant slug = %q, want halla", repairedSeed.Slug)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|