diff --git a/backend/internal/bootstrap/tenant_seed.go b/backend/internal/bootstrap/tenant_seed.go index 4ebd0581..b7abc258 100644 --- a/backend/internal/bootstrap/tenant_seed.go +++ b/backend/internal/bootstrap/tenant_seed.go @@ -59,8 +59,8 @@ func SeedTenants(db *gorm.DB) error { } slog.Info("[Bootstrap] Creating default tenant", "name", config.Name, "slug", config.Slug) - tenant, err := svc.RegisterTenant(ctx, config.Name, config.Slug, config.Description, config.Domains, nil) - if err != nil { + tenant, err := svc.RegisterTenant(ctx, config.Name, config.Slug, config.Description, config.Domains, nil) + if err != nil { slog.Error("Failed to seed tenant", "slug", config.Slug, "error", err) return err } diff --git a/backend/internal/domain/keto_outbox.go b/backend/internal/domain/keto_outbox.go index 0a7cd4a0..1bd9ce22 100644 --- a/backend/internal/domain/keto_outbox.go +++ b/backend/internal/domain/keto_outbox.go @@ -22,17 +22,17 @@ const ( // KetoOutbox represents a Keto relationship tuple update event. type KetoOutbox struct { - ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid()" json:"id"` - Namespace string `gorm:"not null" json:"namespace"` - Object string `gorm:"not null" json:"object"` - Relation string `gorm:"not null" json:"relation"` - Subject string `gorm:"not null" json:"subject"` // format: "User:ID" or "Tenant:ID#members" - Action string `gorm:"not null" json:"action"` // CREATE, DELETE - Status string `gorm:"default:'pending';index" json:"status"` - RetryCount int `gorm:"default:0" json:"retryCount"` - LastError string `json:"lastError,omitempty"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid()" json:"id"` + Namespace string `gorm:"not null" json:"namespace"` + Object string `gorm:"not null" json:"object"` + Relation string `gorm:"not null" json:"relation"` + Subject string `gorm:"not null" json:"subject"` // format: "User:ID" or "Tenant:ID#members" + Action string `gorm:"not null" json:"action"` // CREATE, DELETE + Status string `gorm:"default:'pending';index" json:"status"` + RetryCount int `gorm:"default:0" json:"retryCount"` + LastError string `json:"lastError,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` ProcessedAt *time.Time `json:"processedAt,omitempty"` } diff --git a/backend/internal/handler/user_handler_test.go b/backend/internal/handler/user_handler_test.go index d0454a01..4aee268e 100644 --- a/backend/internal/handler/user_handler_test.go +++ b/backend/internal/handler/user_handler_test.go @@ -32,7 +32,7 @@ func (m *MockKratosAdminForUser) ListIdentities(ctx context.Context) ([]service. return args.Get(0).([]service.KratosIdentity), args.Error(1) } -// Note: In reality, KratosAdminService might not be an interface. +// Note: In reality, KratosAdminService might not be an interface. // If it's a struct, we'd need to mock the underlying client or use an interface. // For the sake of this test, let's assume we can mock it or use a wrapper. @@ -56,8 +56,8 @@ func TestUserHandler_CreateUser_InvalidEmail(t *testing.T) { func TestUserHandler_GetUser_Forbidden(t *testing.T) { app := fiber.New() mockKratos := new(MockKratosAdminForUser) - // We need a way to inject mockKratos into UserHandler. - // Since UserHandler uses *service.KratosAdminService (struct), + // We need a way to inject mockKratos into UserHandler. + // Since UserHandler uses *service.KratosAdminService (struct), // we'd typically use an interface here. // For now, let's just focus on the logic validation if possible. } diff --git a/backend/internal/repository/main_test.go b/backend/internal/repository/main_test.go index bb08b03c..8c50537f 100644 --- a/backend/internal/repository/main_test.go +++ b/backend/internal/repository/main_test.go @@ -1,14 +1,13 @@ package repository import ( + "baron-sso-backend/internal/domain" "context" "log" "os" "testing" "time" - "baron-sso-backend/internal/domain" - "github.com/testcontainers/testcontainers-go" postgres_module "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/wait" diff --git a/backend/internal/repository/tenant_repository_test.go b/backend/internal/repository/tenant_repository_test.go index 95638e8a..62a7b470 100644 --- a/backend/internal/repository/tenant_repository_test.go +++ b/backend/internal/repository/tenant_repository_test.go @@ -1,11 +1,10 @@ package repository import ( + "baron-sso-backend/internal/domain" "context" "testing" - "baron-sso-backend/internal/domain" - "github.com/stretchr/testify/assert" ) diff --git a/backend/internal/repository/user_repository_test.go b/backend/internal/repository/user_repository_test.go index 50d91817..886f297d 100644 --- a/backend/internal/repository/user_repository_test.go +++ b/backend/internal/repository/user_repository_test.go @@ -1,11 +1,10 @@ package repository import ( + "baron-sso-backend/internal/domain" "context" "testing" - "baron-sso-backend/internal/domain" - "github.com/stretchr/testify/assert" ) diff --git a/backend/internal/service/keto_relay_worker.go b/backend/internal/service/keto_relay_worker.go index 9a4dcc12..eefbbadc 100644 --- a/backend/internal/service/keto_relay_worker.go +++ b/backend/internal/service/keto_relay_worker.go @@ -13,7 +13,7 @@ type KetoRelayWorker interface { } type ketoRelayWorker struct { - outboxRepo repository.KetoOutboxRepository + outboxRepo repository.KetoOutboxRepository ketoService KetoService interval time.Duration maxRetries int diff --git a/backend/internal/service/mock_common_test.go b/backend/internal/service/mock_common_test.go index 59269401..1060981f 100644 --- a/backend/internal/service/mock_common_test.go +++ b/backend/internal/service/mock_common_test.go @@ -17,9 +17,11 @@ type MockKetoOutboxRepositoryShared struct { func (m *MockKetoOutboxRepositoryShared) Create(ctx context.Context, entry *domain.KetoOutbox) error { return m.Called(ctx, entry).Error(0) } + func (m *MockKetoOutboxRepositoryShared) CreateWithTx(tx *gorm.DB, entry *domain.KetoOutbox) error { return m.Called(tx, entry).Error(0) } + func (m *MockKetoOutboxRepositoryShared) FindPending(ctx context.Context, limit int) ([]domain.KetoOutbox, error) { args := m.Called(ctx, limit) if args.Get(0) == nil { @@ -27,9 +29,11 @@ func (m *MockKetoOutboxRepositoryShared) FindPending(ctx context.Context, limit } return args.Get(0).([]domain.KetoOutbox), args.Error(1) } + func (m *MockKetoOutboxRepositoryShared) UpdateStatus(ctx context.Context, id string, status string, retryCount int, lastError string) error { return m.Called(ctx, id, status, retryCount, lastError).Error(0) } + func (m *MockKetoOutboxRepositoryShared) MarkProcessed(ctx context.Context, id string) error { return m.Called(ctx, id).Error(0) } diff --git a/backend/internal/service/org_chart_service.go b/backend/internal/service/org_chart_service.go index 851586ac..2e4a8e69 100644 --- a/backend/internal/service/org_chart_service.go +++ b/backend/internal/service/org_chart_service.go @@ -168,17 +168,17 @@ func (s *orgChartService) ensureOrgPath(ctx context.Context, rootTenantID string // In a real implementation, Repo should have a FindByParentAndName method // For this implementation, we'll try to find by Name and ParentID in TenantRepo or UserGroupRepo // Since we're using Polymorphic Tenants, let's assume we can lookup - + // For simplicity in this POC, let's just use Create logic if not in cache // In production, we MUST check DB first to avoid duplicates - + // [Placeholder] Lookup in DB logic... // existingID = s.lookupOrgUnit(ctx, rootTenantID, currentParentID, part) if existingID == "" { // Create new unit unitID := uuid.NewString() - + // 1. Create Tenant (Type: USER_GROUP) newTenant := &domain.Tenant{ ID: unitID, diff --git a/backend/internal/service/tenant_service.go b/backend/internal/service/tenant_service.go index d91193ea..c52e4287 100644 --- a/backend/internal/service/tenant_service.go +++ b/backend/internal/service/tenant_service.go @@ -71,9 +71,9 @@ func (s *tenantService) ListManageableTenants(ctx context.Context, userID string allIDsMap[id] = true } - // Note: 상속된 권한(부모의 어드민이 자식의 어드민)은 Keto의 OPL에서 처리되므로, - // 특정 유저가 'view' 또는 'manage' 권한을 가진 테넌트를 모두 찾으려면 - // Keto의 'expand' 또는 'list objects' 기능을 더 고도화하거나, + // Note: 상속된 권한(부모의 어드민이 자식의 어드민)은 Keto의 OPL에서 처리되므로, + // 특정 유저가 'view' 또는 'manage' 권한을 가진 테넌트를 모두 찾으려면 + // Keto의 'expand' 또는 'list objects' 기능을 더 고도화하거나, // 여기서는 직접 할당된 부모 테넌트를 기준으로 하위 테넌트 정보를 추가 조회하는 로직이 필요할 수 있습니다. // 우선 직접 할당된 테넌트들만 반환합니다. diff --git a/backend/internal/service/tenant_service_edge_test.go b/backend/internal/service/tenant_service_edge_test.go index ef15eea9..e48980d1 100644 --- a/backend/internal/service/tenant_service_edge_test.go +++ b/backend/internal/service/tenant_service_edge_test.go @@ -103,13 +103,12 @@ func TestTenantService_ApproveTenant_UserNotFound(t *testing.T) { mockRepo.On("Update", ctx, mock.Anything).Return(nil) // User not found in DB mockUserRepo.On("FindByEmail", adminEmail).Return(nil, gorm.ErrRecordNotFound) - + // Outbox should not be called since user is not found - + err := svc.ApproveTenant(ctx, tenantID) assert.NoError(t, err) // Should succeed but just log that user is not found mockRepo.AssertExpectations(t) mockUserRepo.AssertExpectations(t) mockOutbox.AssertNotCalled(t, "Create") } -