package service import ( "baron-sso-backend/internal/domain" "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) // --- Mocks for Repositories --- type MockUserGroupRepository struct { mock.Mock } func (m *MockUserGroupRepository) Create(ctx context.Context, group *domain.UserGroup) error { return m.Called(ctx, group).Error(0) } func (m *MockUserGroupRepository) Update(ctx context.Context, group *domain.UserGroup) error { return m.Called(ctx, group).Error(0) } func (m *MockUserGroupRepository) Delete(ctx context.Context, id string) error { return m.Called(ctx, id).Error(0) } func (m *MockUserGroupRepository) FindByID(ctx context.Context, id string) (*domain.UserGroup, error) { args := m.Called(ctx, id) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*domain.UserGroup), args.Error(1) } func (m *MockUserGroupRepository) ListByTenantID(ctx context.Context, tenantID string) ([]domain.UserGroup, error) { args := m.Called(ctx, tenantID) return args.Get(0).([]domain.UserGroup), args.Error(1) } type MockUserRepository struct { mock.Mock } func (m *MockUserRepository) Create(ctx context.Context, user *domain.User) error { return nil } func (m *MockUserRepository) Update(ctx context.Context, user *domain.User) error { return nil } func (m *MockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) { return nil, nil } func (m *MockUserRepository) FindByID(ctx context.Context, id string) (*domain.User, error) { return nil, nil } func (m *MockUserRepository) FindByIDs(ctx context.Context, ids []string) ([]domain.User, error) { args := m.Called(ctx, ids) return args.Get(0).([]domain.User), args.Error(1) } func (m *MockUserRepository) ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error) { return nil, nil } func (m *MockUserRepository) List(ctx context.Context, offset, limit int, search string) ([]domain.User, int64, error) { return nil, 0, nil } type MockTenantRepository struct { mock.Mock } func (m *MockTenantRepository) Create(ctx context.Context, tenant *domain.Tenant) error { return nil } func (m *MockTenantRepository) Update(ctx context.Context, tenant *domain.Tenant) error { return nil } func (m *MockTenantRepository) FindByID(ctx context.Context, id string) (*domain.Tenant, error) { return nil, nil } func (m *MockTenantRepository) FindByIDs(ctx context.Context, ids []string) ([]domain.Tenant, error) { args := m.Called(ctx, ids) return args.Get(0).([]domain.Tenant), args.Error(1) } func (m *MockTenantRepository) FindBySlug(ctx context.Context, slug string) (*domain.Tenant, error) { return nil, nil } func (m *MockTenantRepository) FindByName(ctx context.Context, name string) (*domain.Tenant, error) { return nil, nil } func (m *MockTenantRepository) FindByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) { return nil, nil } func (m *MockTenantRepository) AddDomain(ctx context.Context, tenantID string, domainName string, verified bool) error { return nil } // --- Tests --- func TestUserGroupService_Create(t *testing.T) { mockRepo := new(MockUserGroupRepository) mockKeto := new(MockKetoService) // We don't need userRepo or tenantRepo for Create svc := NewUserGroupService(mockRepo, nil, nil, mockKeto, nil) group := &domain.UserGroup{ ID: "group-1", TenantID: "tenant-1", Name: "Test Group", } mockRepo.On("Create", mock.Anything, group).Return(nil) mockKeto.On("CreateRelation", mock.Anything, "UserGroup", group.ID, "parent_tenant", "Tenant:"+group.TenantID).Return(nil) err := svc.Create(context.Background(), group) assert.NoError(t, err) mockRepo.AssertExpectations(t) mockKeto.AssertExpectations(t) } func TestUserGroupService_AddMember(t *testing.T) { mockKeto := new(MockKetoService) svc := NewUserGroupService(nil, nil, nil, mockKeto, nil) groupID := "group-1" userID := "user-1" mockKeto.On("CreateRelation", mock.Anything, "UserGroup", groupID, "members", "User:"+userID).Return(nil) err := svc.AddMember(context.Background(), groupID, userID) assert.NoError(t, err) mockKeto.AssertExpectations(t) } func TestUserGroupService_AssignRoleToTenant(t *testing.T) { mockKeto := new(MockKetoService) svc := NewUserGroupService(nil, nil, nil, mockKeto, nil) groupID := "group-1" tenantID := "tenant-alpha" relation := "manage" expectedSubject := "UserGroup:" + groupID + "#members" mockKeto.On("CreateRelation", mock.Anything, "Tenant", tenantID, relation, expectedSubject).Return(nil) err := svc.AssignRoleToTenant(context.Background(), groupID, tenantID, relation) assert.NoError(t, err) mockKeto.AssertExpectations(t) } func TestUserGroupService_ListRoles(t *testing.T) { mockKeto := new(MockKetoService) mockTenantRepo := new(MockTenantRepository) svc := NewUserGroupService(nil, nil, mockTenantRepo, mockKeto, nil) groupID := "group-1" subject := "UserGroup:" + groupID + "#members" // Mock Keto relations tuples := []RelationTuple{ {Object: "t1", Relation: "manage", SubjectID: subject}, {Object: "t2", Relation: "view", SubjectID: subject}, } mockKeto.On("ListRelations", mock.Anything, "Tenant", "", "", subject).Return(tuples, nil) // Mock Tenant fetching tenants := []domain.Tenant{ {ID: "t1", Name: "Tenant One"}, {ID: "t2", Name: "Tenant Two"}, } mockTenantRepo.On("FindByIDs", mock.Anything, []string{"t1", "t2"}).Return(tenants, nil) roles, err := svc.ListRoles(context.Background(), groupID) assert.NoError(t, err) assert.Len(t, roles, 2) assert.Equal(t, "Tenant One", roles[0].TenantName) assert.Equal(t, "manage", roles[0].Relation) assert.Equal(t, "Tenant Two", roles[1].TenantName) assert.Equal(t, "view", roles[1].Relation) mockKeto.AssertExpectations(t) mockTenantRepo.AssertExpectations(t) } func TestUserGroupService_Get_WithKratosFallback(t *testing.T) { // This tests the logic where a user is in Keto but not in local DB mockRepo := new(MockUserGroupRepository) mockKeto := new(MockKetoService) mockUserRepo := new(MockUserRepository) // We need a way to mock KratosAdminService but it's a struct, not an interface. // For this POC test, we'll focus on the Keto and UserRepo parts. // If needed, we can refactor KratosAdminService to an interface. svc := NewUserGroupService(mockRepo, mockUserRepo, nil, mockKeto, nil) groupID := "group-1" mockRepo.On("FindByID", mock.Anything, groupID).Return(&domain.UserGroup{ID: groupID, Name: "Test"}, nil) tuples := []RelationTuple{ {Object: groupID, Relation: "members", SubjectID: "User:u1"}, } mockKeto.On("ListRelations", mock.Anything, "UserGroup", groupID, "members", "").Return(tuples, nil) // User u1 not in local DB mockUserRepo.On("FindByIDs", mock.Anything, []string{"u1"}).Return([]domain.User{}, nil) group, err := svc.Get(context.Background(), groupID) assert.NoError(t, err) assert.NotNil(t, group) // Members should be empty since Kratos is nil in this test setup assert.Len(t, group.Members, 0) }