From 7e183372f37a1dde0bd0a897705b7cd06f9e815c Mon Sep 17 00:00:00 2001 From: aman Date: Thu, 30 Apr 2026 13:41:15 +0530 Subject: [PATCH] fix(deleter): cascade service user delete on organization delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Service users belonging to a deleted organization were left orphaned in the serviceusers table — their credentials remained valid and the auth path returned a successful Principal for them after the parent org was gone. Add a service user cleanup step in DeleteOrganization between groups and roles; reuses serviceUserService.Delete which already removes credentials, org-membership policies, and SpiceDB relations. Refs: #1585 Co-Authored-By: Claude Opus 4.7 (1M context) --- .mockery.yaml | 4 + cmd/serve.go | 4 +- core/deleter/mocks/service_user_service.go | 144 +++++++++++++++++++++ core/deleter/service.go | 64 +++++---- core/deleter/service_test.go | 84 +++++++++--- 5 files changed, 258 insertions(+), 42 deletions(-) create mode 100644 core/deleter/mocks/service_user_service.go diff --git a/.mockery.yaml b/.mockery.yaml index 906e75271..500cd3fa1 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -87,3 +87,7 @@ packages: config: dir: "core/event/mocks" all: true + github.com/raystack/frontier/core/deleter: + config: + dir: "core/deleter/mocks" + all: true diff --git a/cmd/serve.go b/cmd/serve.go index 404202e5e..3398a1bf9 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -561,8 +561,8 @@ func buildAPIDependencies( ) cascadeDeleter := deleter.NewCascadeDeleter(organizationService, projectService, resourceService, - groupService, policyService, roleService, invitationService, userService, customerService, - subscriptionService, invoiceService, + groupService, policyService, roleService, invitationService, userService, serviceUserService, + customerService, subscriptionService, invoiceService, ) // we should default it with a stdout logger repository as postgres can start to bloat really fast diff --git a/core/deleter/mocks/service_user_service.go b/core/deleter/mocks/service_user_service.go new file mode 100644 index 000000000..49a15fc1d --- /dev/null +++ b/core/deleter/mocks/service_user_service.go @@ -0,0 +1,144 @@ +// Code generated by mockery v2.53.5. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + serviceuser "github.com/raystack/frontier/core/serviceuser" +) + +// ServiceUserService is an autogenerated mock type for the ServiceUserService type +type ServiceUserService struct { + mock.Mock +} + +type ServiceUserService_Expecter struct { + mock *mock.Mock +} + +func (_m *ServiceUserService) EXPECT() *ServiceUserService_Expecter { + return &ServiceUserService_Expecter{mock: &_m.Mock} +} + +// Delete provides a mock function with given fields: ctx, id +func (_m *ServiceUserService) Delete(ctx context.Context, id string) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ServiceUserService_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type ServiceUserService_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *ServiceUserService_Expecter) Delete(ctx interface{}, id interface{}) *ServiceUserService_Delete_Call { + return &ServiceUserService_Delete_Call{Call: _e.mock.On("Delete", ctx, id)} +} + +func (_c *ServiceUserService_Delete_Call) Run(run func(ctx context.Context, id string)) *ServiceUserService_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *ServiceUserService_Delete_Call) Return(_a0 error) *ServiceUserService_Delete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ServiceUserService_Delete_Call) RunAndReturn(run func(context.Context, string) error) *ServiceUserService_Delete_Call { + _c.Call.Return(run) + return _c +} + +// List provides a mock function with given fields: ctx, flt +func (_m *ServiceUserService) List(ctx context.Context, flt serviceuser.Filter) ([]serviceuser.ServiceUser, error) { + ret := _m.Called(ctx, flt) + + if len(ret) == 0 { + panic("no return value specified for List") + } + + var r0 []serviceuser.ServiceUser + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, serviceuser.Filter) ([]serviceuser.ServiceUser, error)); ok { + return rf(ctx, flt) + } + if rf, ok := ret.Get(0).(func(context.Context, serviceuser.Filter) []serviceuser.ServiceUser); ok { + r0 = rf(ctx, flt) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]serviceuser.ServiceUser) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, serviceuser.Filter) error); ok { + r1 = rf(ctx, flt) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ServiceUserService_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List' +type ServiceUserService_List_Call struct { + *mock.Call +} + +// List is a helper method to define mock.On call +// - ctx context.Context +// - flt serviceuser.Filter +func (_e *ServiceUserService_Expecter) List(ctx interface{}, flt interface{}) *ServiceUserService_List_Call { + return &ServiceUserService_List_Call{Call: _e.mock.On("List", ctx, flt)} +} + +func (_c *ServiceUserService_List_Call) Run(run func(ctx context.Context, flt serviceuser.Filter)) *ServiceUserService_List_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(serviceuser.Filter)) + }) + return _c +} + +func (_c *ServiceUserService_List_Call) Return(_a0 []serviceuser.ServiceUser, _a1 error) *ServiceUserService_List_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ServiceUserService_List_Call) RunAndReturn(run func(context.Context, serviceuser.Filter) ([]serviceuser.ServiceUser, error)) *ServiceUserService_List_Call { + _c.Call.Return(run) + return _c +} + +// NewServiceUserService creates a new instance of ServiceUserService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewServiceUserService(t interface { + mock.TestingT + Cleanup(func()) +}) *ServiceUserService { + mock := &ServiceUserService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/deleter/service.go b/core/deleter/service.go index 7d40108eb..a96998524 100644 --- a/core/deleter/service.go +++ b/core/deleter/service.go @@ -28,6 +28,7 @@ import ( "github.com/raystack/frontier/core/project" "github.com/raystack/frontier/core/resource" + "github.com/raystack/frontier/core/serviceuser" ) const ( @@ -77,6 +78,11 @@ type UserService interface { Delete(ctx context.Context, id string) error } +type ServiceUserService interface { + List(ctx context.Context, flt serviceuser.Filter) ([]serviceuser.ServiceUser, error) + Delete(ctx context.Context, id string) error +} + type CustomerService interface { Delete(ctx context.Context, id string) error List(ctx context.Context, filter customer.Filter) ([]customer.Customer, error) @@ -92,37 +98,40 @@ type InvoiceService interface { } type Service struct { - projService ProjectService - orgService OrganizationService - resService ResourceService - groupService GroupService - policyService PolicyService - roleService RoleService - invitationService InvitationService - userService UserService - customerService CustomerService - subService SubscriptionService - invoiceService InvoiceService + projService ProjectService + orgService OrganizationService + resService ResourceService + groupService GroupService + policyService PolicyService + roleService RoleService + invitationService InvitationService + userService UserService + serviceUserService ServiceUserService + customerService CustomerService + subService SubscriptionService + invoiceService InvoiceService } func NewCascadeDeleter(orgService OrganizationService, projService ProjectService, resService ResourceService, groupService GroupService, policyService PolicyService, roleService RoleService, invitationService InvitationService, userService UserService, + serviceUserService ServiceUserService, customerService CustomerService, subService SubscriptionService, invoiceService InvoiceService) *Service { return &Service{ - projService: projService, - orgService: orgService, - resService: resService, - groupService: groupService, - policyService: policyService, - roleService: roleService, - invitationService: invitationService, - userService: userService, - customerService: customerService, - subService: subService, - invoiceService: invoiceService, + projService: projService, + orgService: orgService, + resService: resService, + groupService: groupService, + policyService: policyService, + roleService: roleService, + invitationService: invitationService, + userService: userService, + serviceUserService: serviceUserService, + customerService: customerService, + subService: subService, + invoiceService: invoiceService, } } @@ -199,6 +208,17 @@ func (d Service) DeleteOrganization(ctx context.Context, id string) error { } } + // delete all service users (clears credentials, org membership, and SpiceDB tuples) + serviceUsers, err := d.serviceUserService.List(ctx, serviceuser.Filter{OrgID: id}) + if err != nil { + return err + } + for _, su := range serviceUsers { + if err = d.serviceUserService.Delete(ctx, su.ID); err != nil { + return fmt.Errorf("failed to delete org while deleting a service user[%s]: %w", su.ID, err) + } + } + // delete all roles roles, err := d.roleService.List(ctx, role.Filter{ OrgID: id, diff --git a/core/deleter/service_test.go b/core/deleter/service_test.go index e206c7019..134953cc0 100644 --- a/core/deleter/service_test.go +++ b/core/deleter/service_test.go @@ -16,6 +16,7 @@ import ( "github.com/raystack/frontier/core/project" "github.com/raystack/frontier/core/resource" "github.com/raystack/frontier/core/role" + "github.com/raystack/frontier/core/serviceuser" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -29,6 +30,7 @@ func newMocks(t *testing.T) ( *mocks.RoleService, *mocks.InvitationService, *mocks.UserService, + *mocks.ServiceUserService, *mocks.CustomerService, *mocks.SubscriptionService, *mocks.InvoiceService, @@ -42,6 +44,7 @@ func newMocks(t *testing.T) ( mocks.NewRoleService(t), mocks.NewInvitationService(t), mocks.NewUserService(t), + mocks.NewServiceUserService(t), mocks.NewCustomerService(t), mocks.NewSubscriptionService(t), mocks.NewInvoiceService(t) @@ -49,7 +52,7 @@ func newMocks(t *testing.T) ( func TestDeleteProject(t *testing.T) { t.Run("deletes policies, resources, then project model", func(t *testing.T) { - orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc := newMocks(t) + orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc := newMocks(t) polSvc.EXPECT().List(mock.Anything, policy.Filter{ProjectID: "proj-1"}). Return([]policy.Policy{{ID: "pol-1"}, {ID: "pol-2"}}, nil) @@ -62,38 +65,38 @@ func TestDeleteProject(t *testing.T) { projSvc.EXPECT().DeleteModel(mock.Anything, "proj-1").Return(nil) - svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc) + svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc) err := svc.DeleteProject(context.Background(), "proj-1") assert.NoError(t, err) }) t.Run("returns error when policy list fails", func(t *testing.T) { - _, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc := newMocks(t) + _, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc := newMocks(t) orgSvc := mocks.NewOrganizationService(t) polSvc.EXPECT().List(mock.Anything, policy.Filter{ProjectID: "proj-1"}). Return(nil, errors.New("db error")) - svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc) + svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc) err := svc.DeleteProject(context.Background(), "proj-1") assert.ErrorContains(t, err, "db error") }) t.Run("returns error when policy delete fails", func(t *testing.T) { - _, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc := newMocks(t) + _, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc := newMocks(t) orgSvc := mocks.NewOrganizationService(t) polSvc.EXPECT().List(mock.Anything, policy.Filter{ProjectID: "proj-1"}). Return([]policy.Policy{{ID: "pol-fail"}}, nil) polSvc.EXPECT().Delete(mock.Anything, "pol-fail").Return(errors.New("delete error")) - svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc) + svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc) err := svc.DeleteProject(context.Background(), "proj-1") assert.ErrorContains(t, err, "pol-fail") }) t.Run("no policies — still deletes resources and project", func(t *testing.T) { - orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc := newMocks(t) + orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc := newMocks(t) polSvc.EXPECT().List(mock.Anything, policy.Filter{ProjectID: "proj-1"}). Return([]policy.Policy{}, nil) @@ -101,7 +104,7 @@ func TestDeleteProject(t *testing.T) { Return([]resource.Resource{}, nil) projSvc.EXPECT().DeleteModel(mock.Anything, "proj-1").Return(nil) - svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc) + svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc) err := svc.DeleteProject(context.Background(), "proj-1") assert.NoError(t, err) }) @@ -109,7 +112,7 @@ func TestDeleteProject(t *testing.T) { func TestDeleteOrganization(t *testing.T) { t.Run("full cascade delete", func(t *testing.T) { - orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc := newMocks(t) + orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc := newMocks(t) // canDelete: no customers custSvc.EXPECT().List(mock.Anything, customer.Filter{OrgID: "org-1"}). @@ -135,6 +138,11 @@ func TestDeleteOrganization(t *testing.T) { Return([]group.Group{{ID: "grp-1", Name: "g1"}}, nil) grpSvc.EXPECT().Delete(mock.Anything, "grp-1").Return(nil) + // service users + suSvc.EXPECT().List(mock.Anything, serviceuser.Filter{OrgID: "org-1"}). + Return([]serviceuser.ServiceUser{{ID: "su-1"}}, nil) + suSvc.EXPECT().Delete(mock.Anything, "su-1").Return(nil) + // roles roleSvc.EXPECT().List(mock.Anything, role.Filter{OrgID: "org-1"}). Return([]role.Role{{ID: "role-1", Name: "r1"}}, nil) @@ -153,13 +161,13 @@ func TestDeleteOrganization(t *testing.T) { // finally delete org model orgSvc.EXPECT().DeleteModel(mock.Anything, "org-1").Return(nil) - svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc) + svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc) err := svc.DeleteOrganization(context.Background(), "org-1") assert.NoError(t, err) }) t.Run("blocked when billed customer has invoices", func(t *testing.T) { - _, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc := newMocks(t) + _, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc := newMocks(t) orgSvc := mocks.NewOrganizationService(t) custSvc.EXPECT().List(mock.Anything, customer.Filter{OrgID: "org-1"}). @@ -167,15 +175,55 @@ func TestDeleteOrganization(t *testing.T) { invocSvc.EXPECT().List(mock.Anything, invoice.Filter{CustomerID: "cust-1"}). Return([]invoice.Invoice{{ID: "inv-1"}}, nil) - svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc) + svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc) err := svc.DeleteOrganization(context.Background(), "org-1") assert.ErrorIs(t, err, deleter.ErrDeleteNotAllowed) }) + + t.Run("propagates error when service user list fails", func(t *testing.T) { + orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc := newMocks(t) + + custSvc.EXPECT().List(mock.Anything, customer.Filter{OrgID: "org-1"}). + Return([]customer.Customer{}, nil) + polSvc.EXPECT().List(mock.Anything, policy.Filter{OrgID: "org-1"}). + Return([]policy.Policy{}, nil) + projSvc.EXPECT().List(mock.Anything, project.Filter{OrgID: "org-1"}). + Return([]project.Project{}, nil) + grpSvc.EXPECT().List(mock.Anything, group.Filter{OrganizationID: "org-1"}). + Return([]group.Group{}, nil) + suSvc.EXPECT().List(mock.Anything, serviceuser.Filter{OrgID: "org-1"}). + Return(nil, errors.New("su list failed")) + + svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc) + err := svc.DeleteOrganization(context.Background(), "org-1") + assert.ErrorContains(t, err, "su list failed") + }) + + t.Run("propagates error when service user delete fails", func(t *testing.T) { + orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc := newMocks(t) + + custSvc.EXPECT().List(mock.Anything, customer.Filter{OrgID: "org-1"}). + Return([]customer.Customer{}, nil) + polSvc.EXPECT().List(mock.Anything, policy.Filter{OrgID: "org-1"}). + Return([]policy.Policy{}, nil) + projSvc.EXPECT().List(mock.Anything, project.Filter{OrgID: "org-1"}). + Return([]project.Project{}, nil) + grpSvc.EXPECT().List(mock.Anything, group.Filter{OrganizationID: "org-1"}). + Return([]group.Group{}, nil) + suSvc.EXPECT().List(mock.Anything, serviceuser.Filter{OrgID: "org-1"}). + Return([]serviceuser.ServiceUser{{ID: "su-1"}}, nil) + suSvc.EXPECT().Delete(mock.Anything, "su-1").Return(errors.New("su delete failed")) + + svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc) + err := svc.DeleteOrganization(context.Background(), "org-1") + assert.ErrorContains(t, err, "su delete failed") + assert.ErrorContains(t, err, "su-1") + }) } func TestDeleteCustomers(t *testing.T) { t.Run("deletes subscriptions invoices and customer", func(t *testing.T) { - orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc := newMocks(t) + orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc := newMocks(t) c := customer.Customer{ID: "cust-1", ProviderID: "stripe-1"} custSvc.EXPECT().List(mock.Anything, customer.Filter{OrgID: "org-1"}). @@ -184,13 +232,13 @@ func TestDeleteCustomers(t *testing.T) { invocSvc.EXPECT().DeleteByCustomer(mock.Anything, c).Return(nil) custSvc.EXPECT().Delete(mock.Anything, "cust-1").Return(nil) - svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc) + svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc) err := svc.DeleteCustomers(context.Background(), "org-1") assert.NoError(t, err) }) t.Run("skips subscription and invoice delete when no provider", func(t *testing.T) { - orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc := newMocks(t) + orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc := newMocks(t) c := customer.Customer{ID: "cust-no-provider", ProviderID: ""} custSvc.EXPECT().List(mock.Anything, customer.Filter{OrgID: "org-1"}). @@ -198,7 +246,7 @@ func TestDeleteCustomers(t *testing.T) { // no sub or invoice delete expected custSvc.EXPECT().Delete(mock.Anything, "cust-no-provider").Return(nil) - svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc) + svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc) err := svc.DeleteCustomers(context.Background(), "org-1") assert.NoError(t, err) }) @@ -206,13 +254,13 @@ func TestDeleteCustomers(t *testing.T) { func TestDeleteUser(t *testing.T) { t.Run("removes user from all orgs then deletes", func(t *testing.T) { - orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc := newMocks(t) + orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc := newMocks(t) orgSvc.EXPECT().ListByUser(mock.Anything, mock.Anything, mock.Anything). Return(nil, nil) usrSvc.EXPECT().Delete(mock.Anything, "user-1").Return(nil) - svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, custSvc, subSvc, invocSvc) + svc := deleter.NewCascadeDeleter(orgSvc, projSvc, resSvc, grpSvc, polSvc, roleSvc, invSvc, usrSvc, suSvc, custSvc, subSvc, invocSvc) err := svc.DeleteUser(context.Background(), "user-1") assert.NoError(t, err) })