**Before** - any user with workpace_members permission was able to remove a user from their workspace. This triggered the deletion of workspaceMember + of userWorkspace, but did not delete the user (even if they had no workspace left) nor the roleTarget (acts as junction between role and userWorkspace) which was left with a userWorkspaceId pointing to nothing. This is because roleTarget points to userWorkspaceId but the foreign key constraint was not implemented - any user could delete their own account. This triggered the deletion of all their workspaceMembers, but not of their userWorkspace nor their user nor the roleTarget --> we have orphaned userWorkspace, not technically but product wise - a userWorkspace without a workspaceMember does not make sense So the problems are - we have some roleTargets pointing to non-existing userWorkspaceId (which caused https://github.com/twentyhq/twenty/issues/14608 ) - we have userWorkspaces that should not exist and that have no workspaceMember counterpart - it is not possible for a user to leave a workspace by themselves, they can only leave all workspaces at once, except if they are being removed from the workspace by another user **Now** - if a user has multiple workspaces, they are given the possibility to leave one workspace while remaining in the others (we show two buttons: Leave workspace and Delete account buttons). if a user has just one workspace, they only see Delete account - when a user leaves a workspace, we delete their workspaceMember, userWorkspace and roleTarget. If they don't belong to any other workspace we also soft-delete their user - soft-deleted users get hard deleted after 30 days thanks to a cron - we have two commands to clean the orphans roleTarget and userWorkspace (TODO: query db to see how many must be run) **Next** - once the commands have been run, we can implement and introduce the foreign key constraint on roleTarget Fixes https://github.com/twentyhq/twenty/issues/14608
77 lines
2.5 KiB
TypeScript
77 lines
2.5 KiB
TypeScript
import { APP_FILTER } from '@nestjs/core';
|
|
import { type NestExpressApplication } from '@nestjs/platform-express';
|
|
import {
|
|
Test,
|
|
type TestingModule,
|
|
type TestingModuleBuilder,
|
|
} from '@nestjs/testing';
|
|
|
|
import { AppModule } from 'src/app.module';
|
|
import { CommandModule } from 'src/command/command.module';
|
|
import { StripeSDKMockService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/mocks/stripe-sdk-mock.service';
|
|
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
|
import { CAPTCHA_DRIVER } from 'src/engine/core-modules/captcha/constants/captcha-driver.constants';
|
|
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
|
import { ExceptionHandlerMockService } from 'src/engine/core-modules/exception-handler/mocks/exception-handler-mock.service';
|
|
import { MockedUnhandledExceptionFilter } from 'src/engine/core-modules/exception-handler/mocks/mock-unhandled-exception.filter';
|
|
|
|
interface TestingModuleCreatePreHook {
|
|
(moduleBuilder: TestingModuleBuilder): TestingModuleBuilder;
|
|
}
|
|
|
|
/**
|
|
* Hook for adding items to nest application
|
|
*/
|
|
export type TestingAppCreatePreHook = (
|
|
app: NestExpressApplication,
|
|
) => Promise<void>;
|
|
|
|
/**
|
|
* Sets basic integration testing module of app
|
|
*/
|
|
export const createApp = async (
|
|
config: {
|
|
moduleBuilderHook?: TestingModuleCreatePreHook;
|
|
appInitHook?: TestingAppCreatePreHook;
|
|
} = {},
|
|
): Promise<NestExpressApplication> => {
|
|
const stripeSDKMockService = new StripeSDKMockService();
|
|
const mockExceptionHandlerService = new ExceptionHandlerMockService();
|
|
let moduleBuilder: TestingModuleBuilder = Test.createTestingModule({
|
|
imports: [AppModule, CommandModule],
|
|
providers: [
|
|
{
|
|
provide: APP_FILTER,
|
|
useClass: MockedUnhandledExceptionFilter,
|
|
},
|
|
],
|
|
})
|
|
.overrideProvider(StripeSDKService)
|
|
.useValue(stripeSDKMockService)
|
|
.overrideProvider(ExceptionHandlerService)
|
|
.useValue(mockExceptionHandlerService)
|
|
.overrideProvider(CAPTCHA_DRIVER)
|
|
.useValue({
|
|
validate: async () => ({ success: true }),
|
|
});
|
|
|
|
if (config.moduleBuilderHook) {
|
|
moduleBuilder = config.moduleBuilderHook(moduleBuilder);
|
|
}
|
|
|
|
const moduleFixture: TestingModule = await moduleBuilder.compile();
|
|
|
|
const app = moduleFixture.createNestApplication<NestExpressApplication>({
|
|
rawBody: true,
|
|
cors: true,
|
|
});
|
|
|
|
if (config.appInitHook) {
|
|
await config.appInitHook(app);
|
|
}
|
|
|
|
await app.init();
|
|
|
|
return app;
|
|
};
|