forked from baron/baron-sso
삭제된 사용자 RP 관계 정리
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// --- Mocks ---
|
||||
@@ -98,6 +99,75 @@ func (m *MockOryProvider) GetPasswordPolicy() (*domain.PasswordPolicy, error) {
|
||||
return args.Get(0).(*domain.PasswordPolicy), args.Error(1)
|
||||
}
|
||||
|
||||
type userHandlerMockKetoService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *userHandlerMockKetoService) CheckPermission(ctx context.Context, subject, namespace, object, relation string) (bool, error) {
|
||||
args := m.Called(ctx, subject, namespace, object, relation)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *userHandlerMockKetoService) CreateRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
||||
return m.Called(ctx, namespace, object, relation, subject).Error(0)
|
||||
}
|
||||
|
||||
func (m *userHandlerMockKetoService) DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
||||
return m.Called(ctx, namespace, object, relation, subject).Error(0)
|
||||
}
|
||||
|
||||
func (m *userHandlerMockKetoService) ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]service.RelationTuple, error) {
|
||||
args := m.Called(ctx, namespace, object, relation, subject)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]service.RelationTuple), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *userHandlerMockKetoService) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
|
||||
args := m.Called(ctx, namespace, relation, subject)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]string), args.Error(1)
|
||||
}
|
||||
|
||||
type userHandlerMockKetoOutboxRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *userHandlerMockKetoOutboxRepository) Create(ctx context.Context, entry *domain.KetoOutbox) error {
|
||||
return m.Called(ctx, entry).Error(0)
|
||||
}
|
||||
|
||||
func (m *userHandlerMockKetoOutboxRepository) CreateWithTx(tx *gorm.DB, entry *domain.KetoOutbox) error {
|
||||
return m.Called(tx, entry).Error(0)
|
||||
}
|
||||
|
||||
func (m *userHandlerMockKetoOutboxRepository) FindPending(ctx context.Context, limit int) ([]domain.KetoOutbox, error) {
|
||||
args := m.Called(ctx, limit)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.KetoOutbox), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *userHandlerMockKetoOutboxRepository) ListCurrentBySubject(ctx context.Context, namespace, subject string) ([]domain.KetoOutbox, error) {
|
||||
args := m.Called(ctx, namespace, subject)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.KetoOutbox), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *userHandlerMockKetoOutboxRepository) 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 *userHandlerMockKetoOutboxRepository) MarkProcessed(ctx context.Context, id string) error {
|
||||
return m.Called(ctx, id).Error(0)
|
||||
}
|
||||
|
||||
type fakeUserHandlerWorksmobileSyncer struct {
|
||||
upserts []domain.User
|
||||
}
|
||||
@@ -1083,13 +1153,35 @@ func TestUserHandler_DeleteUserDeletesLocalReadModel(t *testing.T) {
|
||||
app := fiber.New()
|
||||
mockKratos := new(MockKratosAdmin)
|
||||
userRepo := new(MockUserRepoForHandler)
|
||||
h := &UserHandler{KratosAdmin: mockKratos, UserRepo: userRepo}
|
||||
mockKeto := new(userHandlerMockKetoService)
|
||||
mockOutbox := new(userHandlerMockKetoOutboxRepository)
|
||||
h := &UserHandler{
|
||||
KratosAdmin: mockKratos,
|
||||
UserRepo: userRepo,
|
||||
KetoService: mockKeto,
|
||||
KetoOutboxRepo: mockOutbox,
|
||||
}
|
||||
|
||||
app.Delete("/users/:id", func(c *fiber.Ctx) error {
|
||||
c.Locals("user_profile", &domain.UserProfileResponse{ID: "admin-1", Role: domain.RoleSuperAdmin})
|
||||
return h.DeleteUser(c)
|
||||
})
|
||||
|
||||
mockKeto.On("ListRelations", mock.Anything, "RelyingParty", "", "", "User:u-1").Return([]service.RelationTuple{
|
||||
{Namespace: "RelyingParty", Object: "client-1", Relation: "admins", SubjectID: "User:u-1"},
|
||||
{Namespace: "RelyingParty", Object: "client-2", Relation: "audit_viewer", SubjectID: "User:u-1"},
|
||||
}, nil).Once()
|
||||
mockKeto.On("DeleteRelation", mock.Anything, "RelyingParty", "client-1", "admins", "User:u-1").Return(nil).Once()
|
||||
mockKeto.On("DeleteRelation", mock.Anything, "RelyingParty", "client-2", "audit_viewer", "User:u-1").Return(nil).Once()
|
||||
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(entry *domain.KetoOutbox) bool {
|
||||
return entry.Namespace == "RelyingParty" && entry.Object == "client-1" && entry.Relation == "admins" && entry.Subject == "User:u-1" && entry.Action == domain.KetoOutboxActionDelete
|
||||
})).Return(nil).Once()
|
||||
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(entry *domain.KetoOutbox) bool {
|
||||
return entry.Namespace == "RelyingParty" && entry.Object == "client-2" && entry.Relation == "audit_viewer" && entry.Subject == "User:u-1" && entry.Action == domain.KetoOutboxActionDelete
|
||||
})).Return(nil).Once()
|
||||
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(entry *domain.KetoOutbox) bool {
|
||||
return entry.Namespace == "System" && entry.Object == "global" && entry.Relation == "super_admins" && entry.Subject == "User:u-1" && entry.Action == domain.KetoOutboxActionDelete
|
||||
})).Return(nil).Once()
|
||||
mockKratos.On("DeleteIdentity", mock.Anything, "u-1").Return(nil).Once()
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/users/u-1", nil)
|
||||
@@ -1098,6 +1190,107 @@ func TestUserHandler_DeleteUserDeletesLocalReadModel(t *testing.T) {
|
||||
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
assert.Equal(t, []string{"u-1"}, userRepo.deletedIDs)
|
||||
mockKratos.AssertExpectations(t)
|
||||
mockKeto.AssertExpectations(t)
|
||||
mockOutbox.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUserHandler_BulkDeleteUsers_CleansUpRelyingPartyRelations(t *testing.T) {
|
||||
app := fiber.New()
|
||||
mockKratos := new(MockKratosAdmin)
|
||||
mockKeto := new(userHandlerMockKetoService)
|
||||
mockOutbox := new(userHandlerMockKetoOutboxRepository)
|
||||
h := &UserHandler{
|
||||
KratosAdmin: mockKratos,
|
||||
KetoService: mockKeto,
|
||||
KetoOutboxRepo: mockOutbox,
|
||||
}
|
||||
|
||||
app.Delete("/users/bulk", func(c *fiber.Ctx) error {
|
||||
c.Locals("user_profile", &domain.UserProfileResponse{ID: "admin-1", Role: domain.RoleSuperAdmin})
|
||||
return h.BulkDeleteUsers(c)
|
||||
})
|
||||
|
||||
mockKratos.On("GetIdentity", mock.Anything, "u-1").Return(&service.KratosIdentity{ID: "u-1"}, nil).Once()
|
||||
mockKeto.On("ListRelations", mock.Anything, "RelyingParty", "", "", "User:u-1").Return([]service.RelationTuple{
|
||||
{Namespace: "RelyingParty", Object: "client-1", Relation: "admins", SubjectID: "User:u-1"},
|
||||
}, nil).Once()
|
||||
mockKeto.On("DeleteRelation", mock.Anything, "RelyingParty", "client-1", "admins", "User:u-1").Return(nil).Once()
|
||||
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(entry *domain.KetoOutbox) bool {
|
||||
return entry.Namespace == "RelyingParty" && entry.Object == "client-1" && entry.Relation == "admins" && entry.Subject == "User:u-1" && entry.Action == domain.KetoOutboxActionDelete
|
||||
})).Return(nil).Once()
|
||||
mockKratos.On("DeleteIdentity", mock.Anything, "u-1").Return(nil).Once()
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"userIds": []string{"u-1"},
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req := httptest.NewRequest(http.MethodDelete, "/users/bulk", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
mockKratos.AssertExpectations(t)
|
||||
mockKeto.AssertExpectations(t)
|
||||
mockOutbox.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUserHandler_DeleteUserFallsBackToKetoOutboxWhenLiveRelationsAreEmpty(t *testing.T) {
|
||||
app := fiber.New()
|
||||
mockKratos := new(MockKratosAdmin)
|
||||
userRepo := new(MockUserRepoForHandler)
|
||||
mockKeto := new(userHandlerMockKetoService)
|
||||
mockOutbox := new(userHandlerMockKetoOutboxRepository)
|
||||
h := &UserHandler{
|
||||
KratosAdmin: mockKratos,
|
||||
UserRepo: userRepo,
|
||||
KetoService: mockKeto,
|
||||
KetoOutboxRepo: mockOutbox,
|
||||
}
|
||||
|
||||
app.Delete("/users/:id", func(c *fiber.Ctx) error {
|
||||
c.Locals("user_profile", &domain.UserProfileResponse{ID: "admin-1", Role: domain.RoleSuperAdmin})
|
||||
return h.DeleteUser(c)
|
||||
})
|
||||
|
||||
mockKeto.On("ListRelations", mock.Anything, "RelyingParty", "", "", "User:u-1").Return([]service.RelationTuple{}, nil).Times(3)
|
||||
mockOutbox.On("ListCurrentBySubject", mock.Anything, "RelyingParty", "User:u-1").Return([]domain.KetoOutbox{
|
||||
{
|
||||
Namespace: "RelyingParty",
|
||||
Object: "client-1",
|
||||
Relation: "admins",
|
||||
Subject: "User:u-1",
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
},
|
||||
{
|
||||
Namespace: "RelyingParty",
|
||||
Object: "client-2",
|
||||
Relation: "config_editor",
|
||||
Subject: "User:u-1",
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
},
|
||||
}, nil).Once()
|
||||
mockKeto.On("DeleteRelation", mock.Anything, "RelyingParty", "client-1", "admins", "User:u-1").Return(nil).Once()
|
||||
mockKeto.On("DeleteRelation", mock.Anything, "RelyingParty", "client-2", "config_editor", "User:u-1").Return(nil).Once()
|
||||
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(entry *domain.KetoOutbox) bool {
|
||||
return entry.Namespace == "RelyingParty" && entry.Object == "client-1" && entry.Relation == "admins" && entry.Subject == "User:u-1" && entry.Action == domain.KetoOutboxActionDelete
|
||||
})).Return(nil).Once()
|
||||
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(entry *domain.KetoOutbox) bool {
|
||||
return entry.Namespace == "RelyingParty" && entry.Object == "client-2" && entry.Relation == "config_editor" && entry.Subject == "User:u-1" && entry.Action == domain.KetoOutboxActionDelete
|
||||
})).Return(nil).Once()
|
||||
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(entry *domain.KetoOutbox) bool {
|
||||
return entry.Namespace == "System" && entry.Object == "global" && entry.Relation == "super_admins" && entry.Subject == "User:u-1" && entry.Action == domain.KetoOutboxActionDelete
|
||||
})).Return(nil).Once()
|
||||
mockKratos.On("DeleteIdentity", mock.Anything, "u-1").Return(nil).Once()
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/users/u-1", nil)
|
||||
resp, err := app.Test(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
assert.Equal(t, []string{"u-1"}, userRepo.deletedIDs)
|
||||
mockKratos.AssertExpectations(t)
|
||||
mockKeto.AssertExpectations(t)
|
||||
mockOutbox.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUserHandler_UpdateUser_AdminOnlyField(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user