forked from baron/baron-sso
make drop 초기화 추가. 한맥그룹 기본값 추가
This commit is contained in:
81
Makefile
81
Makefile
@@ -11,6 +11,12 @@ COMPOSE_INFRA := compose.infra.yaml
|
|||||||
COMPOSE_ORY := compose.ory.yaml
|
COMPOSE_ORY := compose.ory.yaml
|
||||||
COMPOSE_APP := docker-compose.yaml
|
COMPOSE_APP := docker-compose.yaml
|
||||||
AUTH_CONFIG_ENV := .generated/auth-config.env
|
AUTH_CONFIG_ENV := .generated/auth-config.env
|
||||||
|
DEV_SERVICES ?= backend adminfront devfront orgfront userfront
|
||||||
|
DEV_NETWORKS := baron_net ory-net hydranet kratosnet public_net
|
||||||
|
INFRA_CONTAINERS := baron_postgres baron_clickhouse baron_redis baron_gateway
|
||||||
|
ORY_CONTAINERS := ory_postgres ory_kratos ory_hydra ory_keto ory_oathkeeper ory_clickhouse ory_vector
|
||||||
|
APP_CONTAINERS := baron_backend baron_adminfront baron_devfront baron_orgfront baron_userfront
|
||||||
|
DROP_CONTAINERS := $(INFRA_CONTAINERS) $(ORY_CONTAINERS) $(APP_CONTAINERS) ory_stack_check
|
||||||
|
|
||||||
COMPOSE_CLI_ENV_ARGS :=
|
COMPOSE_CLI_ENV_ARGS :=
|
||||||
ifneq (,$(wildcard ./.env))
|
ifneq (,$(wildcard ./.env))
|
||||||
@@ -18,6 +24,13 @@ COMPOSE_CLI_ENV_ARGS += --env-file .env
|
|||||||
endif
|
endif
|
||||||
COMPOSE_CLI_ENV_ARGS += --env-file $(AUTH_CONFIG_ENV)
|
COMPOSE_CLI_ENV_ARGS += --env-file $(AUTH_CONFIG_ENV)
|
||||||
|
|
||||||
|
COMPOSE_DROP_ENV_ARGS :=
|
||||||
|
ifneq (,$(wildcard ./.env))
|
||||||
|
COMPOSE_DROP_ENV_ARGS += --env-file .env
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: build-auth-config validate-auth-config verify-auth-config up-all up-infra up-ory up-app up-backend ensure-networks ensure-infra ensure-ory up-dev up-front-dev dev down drop down-app down-backend down-infra down-ory check-infra ps logs-infra logs-ory logs-app
|
||||||
|
|
||||||
# --- 인증 설정 빌드/검증 ---
|
# --- 인증 설정 빌드/검증 ---
|
||||||
build-auth-config:
|
build-auth-config:
|
||||||
@echo "Building auth config..."
|
@echo "Building auth config..."
|
||||||
@@ -34,38 +47,94 @@ verify-auth-config: validate-auth-config
|
|||||||
|
|
||||||
# --- 기본 실행 ---
|
# --- 기본 실행 ---
|
||||||
# 주의: --remove-orphan 사용 금지 (다른 스택이 orphan으로 판단되어 종료될 수 있음)
|
# 주의: --remove-orphan 사용 금지 (다른 스택이 orphan으로 판단되어 종료될 수 있음)
|
||||||
up-all: validate-auth-config
|
up-all: ensure-networks validate-auth-config
|
||||||
@echo "Starting ALL stacks (infra + ory + app)..."
|
@echo "Starting ALL stacks (infra + ory + app)..."
|
||||||
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) up -d
|
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) up -d
|
||||||
|
|
||||||
# --- 개별 스택 실행 ---
|
# --- 개별 스택 실행 ---
|
||||||
up-infra:
|
up-infra: ensure-networks
|
||||||
@echo "Starting Infra stack (postgres/clickhouse/redis)..."
|
@echo "Starting Infra stack (postgres/clickhouse/redis)..."
|
||||||
docker compose -f $(COMPOSE_INFRA) up -d
|
docker compose -f $(COMPOSE_INFRA) up -d
|
||||||
|
|
||||||
up-ory: validate-auth-config
|
up-ory: ensure-networks validate-auth-config
|
||||||
@echo "Starting Ory stack (kratos/hydra/keto/oathkeeper)..."
|
@echo "Starting Ory stack (kratos/hydra/keto/oathkeeper)..."
|
||||||
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) up -d
|
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) up -d
|
||||||
|
|
||||||
up-app: validate-auth-config
|
up-app: ensure-networks validate-auth-config
|
||||||
@echo "Starting App stack (backend/userfront/adminfront/devfront)..."
|
@echo "Starting App stack (backend/userfront/adminfront/devfront)..."
|
||||||
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up -d
|
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up -d
|
||||||
|
|
||||||
up-backend: validate-auth-config
|
up-backend: ensure-networks validate-auth-config
|
||||||
@echo "Starting Backend only..."
|
@echo "Starting Backend only..."
|
||||||
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up -d backend
|
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up -d backend
|
||||||
|
|
||||||
up-dev: up-infra up-ory
|
ensure-networks:
|
||||||
|
@echo "Ensuring Docker networks..."
|
||||||
|
@for network in $(DEV_NETWORKS); do \
|
||||||
|
if ! docker network inspect "$$network" >/dev/null 2>&1; then \
|
||||||
|
echo "Creating Docker network $$network..."; \
|
||||||
|
docker network create "$$network"; \
|
||||||
|
else \
|
||||||
|
echo "Docker network $$network already exists."; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
|
||||||
|
ensure-infra: ensure-networks
|
||||||
|
@echo "Ensuring Infra stack..."
|
||||||
|
@missing=0; \
|
||||||
|
for container in $(INFRA_CONTAINERS); do \
|
||||||
|
if [ "$$(docker inspect -f '{{.State.Running}}' "$$container" 2>/dev/null)" != "true" ]; then \
|
||||||
|
missing=1; \
|
||||||
|
break; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
if [ "$$missing" -eq 1 ]; then \
|
||||||
|
echo "Starting missing Infra stack containers in daemon mode..."; \
|
||||||
|
docker compose -f $(COMPOSE_INFRA) up -d; \
|
||||||
|
else \
|
||||||
|
echo "Infra stack is already running."; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
ensure-ory: ensure-networks validate-auth-config
|
||||||
|
@echo "Ensuring Ory stack..."
|
||||||
|
@missing=0; \
|
||||||
|
for container in $(ORY_CONTAINERS); do \
|
||||||
|
if [ "$$(docker inspect -f '{{.State.Running}}' "$$container" 2>/dev/null)" != "true" ]; then \
|
||||||
|
missing=1; \
|
||||||
|
break; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
if [ "$$missing" -eq 1 ]; then \
|
||||||
|
echo "Starting missing Ory stack containers in daemon mode..."; \
|
||||||
|
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) up -d; \
|
||||||
|
else \
|
||||||
|
echo "Ory stack is already running."; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
up-dev: ensure-infra ensure-ory
|
||||||
@echo "Dev stack is up (infra + ory)."
|
@echo "Dev stack is up (infra + ory)."
|
||||||
|
|
||||||
up-front-dev: up-infra up-ory up-backend
|
up-front-dev: up-infra up-ory up-backend
|
||||||
@echo "Dev stack is up (infra + ory + backend)."
|
@echo "Dev stack is up (infra + ory + backend)."
|
||||||
|
|
||||||
|
dev: up-dev
|
||||||
|
@echo "Starting development app containers in foreground attach mode..."
|
||||||
|
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up $(DEV_SERVICES)
|
||||||
|
|
||||||
# --- 종료 (Down) ---
|
# --- 종료 (Down) ---
|
||||||
down:
|
down:
|
||||||
@echo "Stopping ALL stacks (infra + ory + app)..."
|
@echo "Stopping ALL stacks (infra + ory + app)..."
|
||||||
docker compose -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) down
|
docker compose -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) down
|
||||||
|
|
||||||
|
drop:
|
||||||
|
@echo "Dropping Baron SSO local Docker stack containers, volumes, and local images..."
|
||||||
|
-docker compose $(COMPOSE_DROP_ENV_ARGS) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) down -v --rmi local
|
||||||
|
@echo "Removing any remaining fixed-name Baron SSO containers..."
|
||||||
|
@for container in $(DROP_CONTAINERS); do \
|
||||||
|
docker rm -f "$$container" >/dev/null 2>&1 || true; \
|
||||||
|
done
|
||||||
|
@echo "Drop complete. External Docker networks are preserved."
|
||||||
|
|
||||||
down-app:
|
down-app:
|
||||||
@echo "Stopping App stack..."
|
@echo "Stopping App stack..."
|
||||||
docker compose -f $(COMPOSE_APP) down
|
docker compose -f $(COMPOSE_APP) down
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ test2@example.com,이몽룡,삼안,선임,개발,팀원,기술본부,개발실,
|
|||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = "org_chart_template.csv";
|
a.download = "org_user_import_template.csv";
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
@@ -130,18 +130,18 @@ test2@example.com,이몽룡,삼안,선임,개발,팀원,기술본부,개발실,
|
|||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" size="sm" className="gap-2">
|
<Button variant="outline" size="sm" className="gap-2">
|
||||||
<Upload size={14} />
|
<Upload size={14} />
|
||||||
{t("ui.admin.org.import_btn", "조직도 임포트 (CSV/XLSX)")}
|
{t("ui.admin.org.import_btn", "조직/사용자 통합 임포트")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{t("ui.admin.org.import_title", "조직도 일괄 등록")}
|
{t("ui.admin.org.import_title", "조직/사용자 통합 일괄 등록")}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t(
|
{t(
|
||||||
"msg.admin.org.import_description",
|
"msg.admin.org.import_description",
|
||||||
"CSV 또는 XLSX 파일을 업로드하여 조직 계층과 멤버를 한 번에 구성합니다.",
|
"CSV 또는 XLSX 파일을 업로드하여 조직 테넌트와 사용자를 함께 생성/업데이트하고 멤버십을 매핑합니다.",
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ scope = "Scope"
|
|||||||
|
|
||||||
[msg.admin.org]
|
[msg.admin.org]
|
||||||
hover_member_info = "Hover to see member details."
|
hover_member_info = "Hover to see member details."
|
||||||
import_description = "Upload a CSV file to bulk register the organization chart."
|
import_description = "Upload a CSV or XLSX file to create or update organization tenants and users together, then map memberships."
|
||||||
import_error = "An error occurred during organization chart import."
|
import_error = "An error occurred during organization chart import."
|
||||||
import_success = "Organization chart imported successfully."
|
import_success = "Organization chart imported successfully."
|
||||||
|
|
||||||
@@ -841,8 +841,8 @@ users = "Users"
|
|||||||
|
|
||||||
[ui.admin.org]
|
[ui.admin.org]
|
||||||
download_template = "Download Template"
|
download_template = "Download Template"
|
||||||
import_btn = "Import"
|
import_btn = "Org/User Import"
|
||||||
import_title = "Bulk Organization Import"
|
import_title = "Bulk Org/User Import"
|
||||||
start_import = "Start Import"
|
start_import = "Start Import"
|
||||||
|
|
||||||
[ui.admin.overview]
|
[ui.admin.overview]
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ scope = "관리 기능은 /admin 네임스페이스에서만 노출합니다."
|
|||||||
|
|
||||||
[msg.admin.org]
|
[msg.admin.org]
|
||||||
hover_member_info = "마우스를 올리면 상세 정보를 확인할 수 있습니다."
|
hover_member_info = "마우스를 올리면 상세 정보를 확인할 수 있습니다."
|
||||||
import_description = "CSV 또는 XLSX 파일을 업로드하여 조직도를 일괄 등록합니다. (필수 컬럼: 이메일, 이름)"
|
import_description = "CSV 또는 XLSX 파일을 업로드하여 조직 테넌트와 사용자를 함께 생성/업데이트하고 멤버십을 매핑합니다. (필수 컬럼: 이메일, 이름)"
|
||||||
import_error = "조직도 임포트 중 오류가 발생했습니다."
|
import_error = "조직도 임포트 중 오류가 발생했습니다."
|
||||||
import_success = "조직도가 성공적으로 임포트되었습니다."
|
import_success = "조직도가 성공적으로 임포트되었습니다."
|
||||||
|
|
||||||
@@ -843,8 +843,8 @@ users = "사용자"
|
|||||||
|
|
||||||
[ui.admin.org]
|
[ui.admin.org]
|
||||||
download_template = "템플릿 다운로드"
|
download_template = "템플릿 다운로드"
|
||||||
import_btn = "임포트"
|
import_btn = "조직/사용자 통합 임포트"
|
||||||
import_title = "조직도 대량 등록"
|
import_title = "조직/사용자 통합 일괄 등록"
|
||||||
start_import = "임포트 시작"
|
start_import = "임포트 시작"
|
||||||
|
|
||||||
[ui.admin.overview]
|
[ui.admin.overview]
|
||||||
|
|||||||
@@ -114,6 +114,26 @@ test.describe("Tenants Management", () => {
|
|||||||
await expect(page).toHaveURL(/.*\/tenants$/, { timeout: 15000 });
|
await expect(page).toHaveURL(/.*\/tenants$/, { timeout: 15000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should clarify organization import creates org tenants and users together", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/tenants");
|
||||||
|
await expect(page.locator("h2").last()).toContainText(
|
||||||
|
/테넌트 목록|Tenants/i,
|
||||||
|
{ timeout: 20000 },
|
||||||
|
);
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator("button")
|
||||||
|
.filter({ hasText: /임포트|Import/i })
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(page.getByRole("dialog")).toContainText(
|
||||||
|
/조직 테넌트.*사용자|organization tenants.*users/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("should show validation error on empty name", async ({ page }) => {
|
test("should show validation error on empty name", async ({ page }) => {
|
||||||
await page.goto("/tenants/new");
|
await page.goto("/tenants/new");
|
||||||
await expect(page.locator("h2").last()).toContainText(/추가|Create/i, {
|
await expect(page.locator("h2").last()).toContainText(/추가|Create/i, {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"baron-sso-backend/internal/repository"
|
"baron-sso-backend/internal/repository"
|
||||||
"baron-sso-backend/internal/service"
|
"baron-sso-backend/internal/service"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -13,6 +15,8 @@ import (
|
|||||||
type InitialTenantConfig struct {
|
type InitialTenantConfig struct {
|
||||||
Name string
|
Name string
|
||||||
Slug string
|
Slug string
|
||||||
|
Type string
|
||||||
|
ParentSlug string
|
||||||
Description string
|
Description string
|
||||||
Domains []string
|
Domains []string
|
||||||
}
|
}
|
||||||
@@ -20,11 +24,25 @@ type InitialTenantConfig struct {
|
|||||||
// Hardcoded for now, can be moved to config file or env later
|
// Hardcoded for now, can be moved to config file or env later
|
||||||
var defaultTenants = []InitialTenantConfig{
|
var defaultTenants = []InitialTenantConfig{
|
||||||
{
|
{
|
||||||
Name: "Hanmac Engineering",
|
Name: "한맥가족",
|
||||||
|
Slug: "hanmac-family",
|
||||||
|
Type: domain.TenantTypeCompanyGroup,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "한맥기술",
|
||||||
Slug: "hanmac",
|
Slug: "hanmac",
|
||||||
|
Type: domain.TenantTypeCompany,
|
||||||
|
ParentSlug: "hanmac-family",
|
||||||
Description: "Primary Family Company",
|
Description: "Primary Family Company",
|
||||||
Domains: []string{"hanmaceng.co.kr", "hmac.kr"},
|
Domains: []string{"hanmaceng.co.kr", "hmac.kr"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "삼안",
|
||||||
|
Slug: "saman",
|
||||||
|
Type: domain.TenantTypeCompany,
|
||||||
|
ParentSlug: "hanmac-family",
|
||||||
|
Domains: []string{"samaneng.com"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func SeedTenants(db *gorm.DB) error {
|
func SeedTenants(db *gorm.DB) error {
|
||||||
@@ -37,9 +55,65 @@ func SeedTenants(db *gorm.DB) error {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
for _, config := range defaultTenants {
|
for _, config := range defaultTenants {
|
||||||
|
tenantType := config.Type
|
||||||
|
if tenantType == "" {
|
||||||
|
tenantType = domain.TenantTypeCompany
|
||||||
|
}
|
||||||
|
|
||||||
|
var parentID *string
|
||||||
|
if config.ParentSlug != "" {
|
||||||
|
parent, err := repo.FindBySlug(ctx, config.ParentSlug)
|
||||||
|
if err != nil || parent == nil {
|
||||||
|
if err == nil {
|
||||||
|
err = errors.New("parent tenant not found")
|
||||||
|
}
|
||||||
|
slog.Error("Failed to resolve parent tenant for seed", "slug", config.Slug, "parentSlug", config.ParentSlug, "error", err)
|
||||||
|
return fmt.Errorf("resolve parent tenant %q for seed %q: %w", config.ParentSlug, config.Slug, err)
|
||||||
|
}
|
||||||
|
parentID = &parent.ID
|
||||||
|
}
|
||||||
|
|
||||||
existing, err := repo.FindBySlug(ctx, config.Slug)
|
existing, err := repo.FindBySlug(ctx, config.Slug)
|
||||||
if err == nil && existing != nil {
|
if err == nil && existing != nil {
|
||||||
slog.Info("[Bootstrap] Tenant already exists, checking domains...", "slug", config.Slug)
|
slog.Info("[Bootstrap] Tenant already exists, checking domains...", "slug", config.Slug)
|
||||||
|
changed := false
|
||||||
|
if existing.Name != config.Name {
|
||||||
|
existing.Name = config.Name
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if existing.Type != tenantType {
|
||||||
|
existing.Type = tenantType
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if existing.Status != domain.TenantStatusActive {
|
||||||
|
existing.Status = domain.TenantStatusActive
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if config.ParentSlug != "" {
|
||||||
|
if existing.ParentID == nil || *existing.ParentID != *parentID {
|
||||||
|
existing.ParentID = parentID
|
||||||
|
changed = true
|
||||||
|
if err := outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||||
|
Namespace: "Tenant",
|
||||||
|
Object: existing.ID,
|
||||||
|
Relation: "parents",
|
||||||
|
Subject: "Tenant:" + *parentID,
|
||||||
|
Action: domain.KetoOutboxActionCreate,
|
||||||
|
}); err != nil {
|
||||||
|
slog.Error("Failed to create outbox entry for seeded tenant hierarchy", "tenant", existing.ID, "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if existing.ParentID != nil {
|
||||||
|
existing.ParentID = nil
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
if err := repo.Update(ctx, existing); err != nil {
|
||||||
|
slog.Error("Failed to update seeded tenant", "slug", config.Slug, "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
// Optional: Check and add missing domains
|
// Optional: Check and add missing domains
|
||||||
for _, d := range config.Domains {
|
for _, d := range config.Domains {
|
||||||
found := false
|
found := false
|
||||||
@@ -60,7 +134,7 @@ func SeedTenants(db *gorm.DB) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("[Bootstrap] Creating default tenant", "name", config.Name, "slug", config.Slug)
|
slog.Info("[Bootstrap] Creating default tenant", "name", config.Name, "slug", config.Slug)
|
||||||
tenant, err := svc.RegisterTenant(ctx, config.Name, config.Slug, domain.TenantTypeCompany, config.Description, config.Domains, nil, "")
|
tenant, err := svc.RegisterTenant(ctx, config.Name, config.Slug, tenantType, config.Description, config.Domains, parentID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to seed tenant", "slug", config.Slug, "error", err)
|
slog.Error("Failed to seed tenant", "slug", config.Slug, "error", err)
|
||||||
return err
|
return err
|
||||||
|
|||||||
74
backend/internal/bootstrap/tenant_seed_test.go
Normal file
74
backend/internal/bootstrap/tenant_seed_test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"baron-sso-backend/internal/domain"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultTenantsSeedOrderAndHierarchy(t *testing.T) {
|
||||||
|
expected := []struct {
|
||||||
|
name string
|
||||||
|
slug string
|
||||||
|
tenantType string
|
||||||
|
parentSlug string
|
||||||
|
domains []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "한맥가족",
|
||||||
|
slug: "hanmac-family",
|
||||||
|
tenantType: domain.TenantTypeCompanyGroup,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "한맥기술",
|
||||||
|
slug: "hanmac",
|
||||||
|
tenantType: domain.TenantTypeCompany,
|
||||||
|
parentSlug: "hanmac-family",
|
||||||
|
domains: []string{"hanmaceng.co.kr", "hmac.kr"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "삼안",
|
||||||
|
slug: "saman",
|
||||||
|
tenantType: domain.TenantTypeCompany,
|
||||||
|
parentSlug: "hanmac-family",
|
||||||
|
domains: []string{"samaneng.com"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(defaultTenants) != len(expected) {
|
||||||
|
t.Fatalf("expected %d default tenants, got %d", len(expected), len(defaultTenants))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, want := range expected {
|
||||||
|
got := defaultTenants[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 tenantType := stringField(t, got, "Type"); tenantType != want.tenantType {
|
||||||
|
t.Fatalf("tenant[%d] type = %q, want %q", i, tenantType, want.tenantType)
|
||||||
|
}
|
||||||
|
if parentSlug := stringField(t, got, "ParentSlug"); parentSlug != want.parentSlug {
|
||||||
|
t.Fatalf("tenant[%d] parent slug = %q, want %q", i, parentSlug, want.parentSlug)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got.Domains, want.domains) {
|
||||||
|
t.Fatalf("tenant[%d] domains = %#v, want %#v", i, got.Domains, want.domains)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringField(t *testing.T, target InitialTenantConfig, name string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
value := reflect.ValueOf(target)
|
||||||
|
field := value.FieldByName(name)
|
||||||
|
if !field.IsValid() {
|
||||||
|
t.Fatalf("InitialTenantConfig.%s is required", name)
|
||||||
|
}
|
||||||
|
if field.Kind() != reflect.String {
|
||||||
|
t.Fatalf("InitialTenantConfig.%s must be a string", name)
|
||||||
|
}
|
||||||
|
return field.String()
|
||||||
|
}
|
||||||
79
test/make_dev_targets_test.sh
Normal file
79
test/make_dev_targets_test.sh
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
dry_run_dev="$(
|
||||||
|
make --dry-run --always-make -C "$repo_root" dev DEV_SERVICES="backend adminfront" 2>&1
|
||||||
|
)"
|
||||||
|
|
||||||
|
if ! grep -q "Ensuring Infra stack" <<<"$dry_run_dev"; then
|
||||||
|
echo "make dev must ensure the infra stack first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q "Ensuring Ory stack" <<<"$dry_run_dev"; then
|
||||||
|
echo "make dev must ensure the Ory stack first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
app_up_line="$(
|
||||||
|
grep -E "docker compose .* -f docker-compose.yaml up .*backend.*adminfront" <<<"$dry_run_dev" | tail -1
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -z "$app_up_line" ]]; then
|
||||||
|
echo "make dev must run docker compose up for development app services." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q -- " -d" <<<"$app_up_line"; then
|
||||||
|
echo "make dev must run app services in foreground attach mode without -d." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
dry_run_up_dev="$(
|
||||||
|
make --dry-run --always-make -C "$repo_root" up-dev 2>&1
|
||||||
|
)"
|
||||||
|
|
||||||
|
if ! grep -q "Ensuring Infra stack" <<<"$dry_run_up_dev"; then
|
||||||
|
echo "make up-dev must ensure the infra stack." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q "Ensuring Ory stack" <<<"$dry_run_up_dev"; then
|
||||||
|
echo "make up-dev must ensure the Ory stack." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
dry_run_up_all="$(
|
||||||
|
make --dry-run --always-make -C "$repo_root" up-all 2>&1
|
||||||
|
)"
|
||||||
|
|
||||||
|
if ! grep -q "Ensuring Docker networks" <<<"$dry_run_up_all"; then
|
||||||
|
echo "make up-all must ensure external Docker networks before compose up." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q 'docker network create "$network"' <<<"$dry_run_up_all"; then
|
||||||
|
echo "make up-all must create missing external Docker networks." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
dry_run_drop="$(
|
||||||
|
make --dry-run --always-make -C "$repo_root" drop 2>&1
|
||||||
|
)"
|
||||||
|
|
||||||
|
if ! grep -q "Dropping Baron SSO local Docker stack" <<<"$dry_run_drop"; then
|
||||||
|
echo "make drop must announce that it is dropping the local stack." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q -- "down -v --rmi local" <<<"$dry_run_drop"; then
|
||||||
|
echo "make drop must remove containers, volumes, and local compose images." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q "docker rm -f" <<<"$dry_run_drop"; then
|
||||||
|
echo "make drop must force-remove known fixed-name stack containers." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user