package service import ( "bytes" "context" "testing" "baron-sso-backend/internal/domain" "baron-sso-backend/internal/repository" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/xuri/excelize/v2" ) type mockTenantRepo struct { mock.Mock repository.TenantRepository } func (m *mockTenantRepo) FindBySlug(ctx context.Context, slug string) (*domain.Tenant, error) { args := m.Called(ctx, slug) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*domain.Tenant), args.Error(1) } func (m *mockTenantRepo) Create(ctx context.Context, tenant *domain.Tenant) error { args := m.Called(ctx, tenant) return args.Error(0) } func (m *mockTenantRepo) AddDomain(ctx context.Context, tenantID, domainName string, isPrimary bool) error { args := m.Called(ctx, tenantID, domainName, isPrimary) return args.Error(0) } type mockUserGroupRepo struct { mock.Mock repository.UserGroupRepository } func (m *mockUserGroupRepo) ListByTenantID(ctx context.Context, tenantID string) ([]domain.UserGroup, error) { args := m.Called(ctx, tenantID) return args.Get(0).([]domain.UserGroup), args.Error(1) } func (m *mockUserGroupRepo) Create(ctx context.Context, ug *domain.UserGroup) error { args := m.Called(ctx, ug) return args.Error(0) } type mockUserRepo struct { mock.Mock repository.UserRepository } func (m *mockUserRepo) Update(ctx context.Context, user *domain.User) error { args := m.Called(ctx, user) return args.Error(0) } type mockKetoOutboxRepo struct { mock.Mock repository.KetoOutboxRepository } func (m *mockKetoOutboxRepo) Create(ctx context.Context, outbox *domain.KetoOutbox) error { args := m.Called(ctx, outbox) return args.Error(0) } type mockKratosService struct { mock.Mock } func (m *mockKratosService) ListIdentities(ctx context.Context) ([]KratosIdentity, error) { return nil, nil } func (m *mockKratosService) FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) { args := m.Called(ctx, identifier) return args.String(0), args.Error(1) } func (m *mockKratosService) GetIdentity(ctx context.Context, identityID string) (*KratosIdentity, error) { return nil, nil } func (m *mockKratosService) UpdateIdentity(ctx context.Context, id string, traits map[string]interface{}, state string) (*KratosIdentity, error) { args := m.Called(ctx, id, traits, state) return nil, args.Error(1) } func (m *mockKratosService) UpdateIdentityPassword(ctx context.Context, id, pw string) error { return nil } func (m *mockKratosService) DeleteIdentity(ctx context.Context, id string) error { return nil } func (m *mockKratosService) CreateUser(ctx context.Context, user *domain.BrokerUser, password string) (string, error) { args := m.Called(ctx, user, password) return args.String(0), args.Error(1) } func setupMocksForImport(tenantRepo *mockTenantRepo, ugRepo *mockUserGroupRepo, userRepo *mockUserRepo, ketoRepo *mockKetoOutboxRepo, kratos *mockKratosService, ctx context.Context, companySlug, email string) { tenantRepo.On("FindBySlug", ctx, companySlug).Return(&domain.Tenant{ID: companySlug + "-id", Slug: companySlug}, nil).Maybe() tenantRepo.On("FindBySlug", ctx, mock.Anything).Return(&domain.Tenant{ID: "fallback-id", Slug: "fallback"}, nil).Maybe() tenantRepo.On("AddDomain", ctx, mock.Anything, mock.Anything, true).Return(nil).Maybe() tenantRepo.On("Create", ctx, mock.Anything).Return(nil).Maybe() ugRepo.On("ListByTenantID", ctx, mock.Anything).Return([]domain.UserGroup{}, nil).Maybe() ugRepo.On("Create", ctx, mock.Anything).Return(nil).Maybe() kratos.On("FindIdentityIDByIdentifier", ctx, email).Return("user-id", nil).Maybe() kratos.On("UpdateIdentity", ctx, "user-id", mock.Anything, "active").Return(nil, nil).Maybe() userRepo.On("Update", ctx, mock.Anything).Return(nil).Maybe() ketoRepo.On("Create", ctx, mock.Anything).Return(nil).Maybe() } func TestImportOrgChart_CSV_BOM(t *testing.T) { tenantRepo := new(mockTenantRepo) ugRepo := new(mockUserGroupRepo) userRepo := new(mockUserRepo) ketoRepo := new(mockKetoOutboxRepo) kratos := new(mockKratosService) svc := NewOrgChartService(tenantRepo, ugRepo, userRepo, ketoRepo, kratos) csvData := "\xef\xbb\xbf이메일,이름,소속,직급,그룹,디비젼,팀,구분\n" + "test@example.com,홍길동,한맥,사원,엔지니어링,구조,계획,팀원" ctx := context.Background() setupMocksForImport(tenantRepo, ugRepo, userRepo, ketoRepo, kratos, ctx, "hanmac", "test@example.com") res, err := svc.ImportOrgChart(ctx, "root-id", bytes.NewReader([]byte(csvData)), "test.csv", "") assert.NoError(t, err) assert.NotNil(t, res) } func TestImportOrgChart_XLSX(t *testing.T) { tenantRepo := new(mockTenantRepo) ugRepo := new(mockUserGroupRepo) userRepo := new(mockUserRepo) ketoRepo := new(mockKetoOutboxRepo) kratos := new(mockKratosService) svc := NewOrgChartService(tenantRepo, ugRepo, userRepo, ketoRepo, kratos) xlsx := excelize.NewFile() xlsx.SetCellValue("Sheet1", "A1", "이메일") xlsx.SetCellValue("Sheet1", "B1", "이름") xlsx.SetCellValue("Sheet1", "C1", "소속") xlsx.SetCellValue("Sheet1", "A2", "xlsx@example.com") xlsx.SetCellValue("Sheet1", "B2", "엑셀맨") xlsx.SetCellValue("Sheet1", "C2", "삼안") var buf bytes.Buffer xlsx.Write(&buf) ctx := context.Background() setupMocksForImport(tenantRepo, ugRepo, userRepo, ketoRepo, kratos, ctx, "saman", "xlsx@example.com") res, err := svc.ImportOrgChart(ctx, "root-id", &buf, "test.xlsx", "") assert.NoError(t, err) assert.NotNil(t, res) } func TestImportOrgChart_MissingColumns(t *testing.T) { svc := NewOrgChartService(nil, nil, nil, nil, nil) ctx := context.Background() csvData := "소속,직급\n한맥,부장" res, err := svc.ImportOrgChart(ctx, "root", bytes.NewReader([]byte(csvData)), "test.csv", "") assert.Error(t, err) assert.Nil(t, res) } func TestImportOrgChart_RobustHeader(t *testing.T) { tenantRepo := new(mockTenantRepo) ugRepo := new(mockUserGroupRepo) userRepo := new(mockUserRepo) ketoRepo := new(mockKetoOutboxRepo) kratos := new(mockKratosService) svc := NewOrgChartService(tenantRepo, ugRepo, userRepo, ketoRepo, kratos) csvData := "\n\n 이 메 일 , 이 름 , 소 속 \n" + "robust@example.com,로버스트,바론" ctx := context.Background() setupMocksForImport(tenantRepo, ugRepo, userRepo, ketoRepo, kratos, ctx, "baron", "robust@example.com") res, err := svc.ImportOrgChart(ctx, "root-id", bytes.NewReader([]byte(csvData)), "test.csv", "") assert.NoError(t, err) assert.NotNil(t, res) } func TestImportOrgChart_MultiSheet_ComplexHeaders(t *testing.T) { tenantRepo := new(mockTenantRepo) ugRepo := new(mockUserGroupRepo) userRepo := new(mockUserRepo) ketoRepo := new(mockKetoOutboxRepo) kratos := new(mockKratosService) svc := NewOrgChartService(tenantRepo, ugRepo, userRepo, ketoRepo, kratos) xlsx := excelize.NewFile() xlsx.NewSheet("Sheet2") xlsx.SetCellValue("Sheet2", "A3", " 이 메 일 ") xlsx.SetCellValue("Sheet2", "B3", " 성 함 / 이 름 ") xlsx.SetCellValue("Sheet2", "C3", " 소 속 회 사 ") xlsx.SetCellValue("Sheet2", "A4", "sheet2@example.com") xlsx.SetCellValue("Sheet2", "B4", "시트투") xlsx.SetCellValue("Sheet2", "C4", "한맥") var buf bytes.Buffer xlsx.Write(&buf) ctx := context.Background() setupMocksForImport(tenantRepo, ugRepo, userRepo, ketoRepo, kratos, ctx, "hanmac", "sheet2@example.com") res, err := svc.ImportOrgChart(ctx, "root-id", &buf, "test.xlsx", "") assert.NoError(t, err) assert.NotNil(t, res) } func TestImportOrgChart_MessyHeader(t *testing.T) { tenantRepo := new(mockTenantRepo) ugRepo := new(mockUserGroupRepo) userRepo := new(mockUserRepo) ketoRepo := new(mockKetoOutboxRepo) kratos := new(mockKratosService) svc := NewOrgChartService(tenantRepo, ugRepo, userRepo, ketoRepo, kratos) csvData := " 이메일(ID)* , 성 명 , [소속] \n" + "messy@example.com,메시,바론" ctx := context.Background() setupMocksForImport(tenantRepo, ugRepo, userRepo, ketoRepo, kratos, ctx, "baron", "messy@example.com") res, err := svc.ImportOrgChart(ctx, "root-id", bytes.NewReader([]byte(csvData)), "test.csv", "") assert.NoError(t, err) assert.NotNil(t, res) }