From a2722ae1f48cfbda76c971e4be75ff918af4030e Mon Sep 17 00:00:00 2001 From: mfrachet Date: Tue, 5 Jul 2022 06:48:59 +0200 Subject: [PATCH] feat(ab): delete an experiment and cascade --- packages/backend/prisma/ab.prisma | 6 +- .../20220705044258_init/migration.sql | 17 +++++ packages/backend/src/ab/ab.controller.ts | 14 ++++ packages/backend/src/ab/ab.service.ts | 8 +++ packages/backend/test/ab/ab.e2e-spec.ts | 72 +++++++++++++++++++ 5 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 packages/backend/prisma/migrations/20220705044258_init/migration.sql diff --git a/packages/backend/prisma/ab.prisma b/packages/backend/prisma/ab.prisma index b4717feed..5602d3432 100644 --- a/packages/backend/prisma/ab.prisma +++ b/packages/backend/prisma/ab.prisma @@ -19,19 +19,19 @@ model Variant { experimentUuid String? isControl Boolean @default(false) - Experiment Experiment? @relation(fields: [experimentUuid], references: [uuid]) + Experiment Experiment? @relation(fields: [experimentUuid], references: [uuid], onDelete: Cascade) VariantHit VariantHit[] } model VariantHit { id Int @id @default(autoincrement()) date DateTime @default(now()) - variant Variant @relation(fields: [variantUuid], references: [uuid]) + variant Variant @relation(fields: [variantUuid], references: [uuid], onDelete: Cascade) variantUuid String } model ExperimentEnvironment { - experiment Experiment @relation(fields: [experimentId], references: [uuid]) + experiment Experiment @relation(fields: [experimentId], references: [uuid], onDelete: Cascade) experimentId String environment Environment @relation(fields: [environmentId], references: [uuid]) environmentId String diff --git a/packages/backend/prisma/migrations/20220705044258_init/migration.sql b/packages/backend/prisma/migrations/20220705044258_init/migration.sql new file mode 100644 index 000000000..5c7245298 --- /dev/null +++ b/packages/backend/prisma/migrations/20220705044258_init/migration.sql @@ -0,0 +1,17 @@ +-- DropForeignKey +ALTER TABLE "ExperimentEnvironment" DROP CONSTRAINT "ExperimentEnvironment_experimentId_fkey"; + +-- DropForeignKey +ALTER TABLE "Variant" DROP CONSTRAINT "Variant_experimentUuid_fkey"; + +-- DropForeignKey +ALTER TABLE "VariantHit" DROP CONSTRAINT "VariantHit_variantUuid_fkey"; + +-- AddForeignKey +ALTER TABLE "Variant" ADD CONSTRAINT "Variant_experimentUuid_fkey" FOREIGN KEY ("experimentUuid") REFERENCES "Experiment"("uuid") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VariantHit" ADD CONSTRAINT "VariantHit_variantUuid_fkey" FOREIGN KEY ("variantUuid") REFERENCES "Variant"("uuid") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ExperimentEnvironment" ADD CONSTRAINT "ExperimentEnvironment_experimentId_fkey" FOREIGN KEY ("experimentId") REFERENCES "Experiment"("uuid") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/backend/src/ab/ab.controller.ts b/packages/backend/src/ab/ab.controller.ts index c83f75852..456f842f7 100644 --- a/packages/backend/src/ab/ab.controller.ts +++ b/packages/backend/src/ab/ab.controller.ts @@ -2,6 +2,7 @@ import { BadRequestException, Body, Controller, + Delete, Get, Param, Post, @@ -14,6 +15,8 @@ import { AbService } from './ab.service'; import { VariantAlreadyExists } from './errors'; import { VariantCreationSchema, VariantCreationDTO } from './experiment.dto'; import { HasExperimentAccess } from './guards/hasExperimentAccess'; +import { Roles } from '../shared/decorators/Roles'; +import { UserRoles } from '../users/roles'; @Controller() export class AbController { @@ -50,4 +53,15 @@ export class AbController { throw e; } } + + /** + * Delete an environment on a given project (by project id AND env id) + */ + @Delete('experiments/:experimentId') + @Roles(UserRoles.Admin) + @UseGuards(HasExperimentAccess) + @UseGuards(JwtAuthGuard) + deleteEnv(@Param('experimentId') experimentId: string) { + return this.abService.deleteExperiment(experimentId); + } } diff --git a/packages/backend/src/ab/ab.service.ts b/packages/backend/src/ab/ab.service.ts index 717e26dc4..728202b3d 100644 --- a/packages/backend/src/ab/ab.service.ts +++ b/packages/backend/src/ab/ab.service.ts @@ -131,4 +131,12 @@ export class AbService { return variant; } + + deleteExperiment(experimentId: string) { + return this.prisma.experiment.deleteMany({ + where: { + uuid: experimentId, + }, + }); + } } diff --git a/packages/backend/test/ab/ab.e2e-spec.ts b/packages/backend/test/ab/ab.e2e-spec.ts index 0314e2908..de134c883 100644 --- a/packages/backend/test/ab/ab.e2e-spec.ts +++ b/packages/backend/test/ab/ab.e2e-spec.ts @@ -191,4 +191,76 @@ describe('AbController (e2e)', () => { }); }); }); + + describe('/experiments/1 (DELETE)', () => { + it('gives a 401 when the user is not authenticated', () => + verifyAuthGuard(app, '/experiments/1', 'delete')); + + it('gives a 403 when trying to access a valid project but an invalid env', async () => { + const access_token = await authenticate(app); + + return request(app.getHttpServer()) + .delete('/experiments/2') + .set('Authorization', `Bearer ${access_token}`) + .expect(403) + .expect({ + statusCode: 403, + message: 'Forbidden resource', + error: 'Forbidden', + }); + }); + + it('gives a 403 when the user requests a forbidden project', async () => { + const access_token = await authenticate( + app, + 'jane.doe@gmail.com', + 'password', + ); + + return request(app.getHttpServer()) + .delete('/experiments/1') + .set('Authorization', `Bearer ${access_token}`) + .expect(403) + .expect({ + statusCode: 403, + message: 'Forbidden resource', + error: 'Forbidden', + }); + }); + + it('gives a 403 when the user is not allowed to perform the action', async () => { + const access_token = await authenticate( + app, + 'john.doe@gmail.com', + 'password', + ); + + return request(app.getHttpServer()) + .delete('/experiments/1') + .set('Authorization', `Bearer ${access_token}`) + .expect(403) + .expect({ + statusCode: 403, + message: 'Forbidden resource', + error: 'Forbidden', + }); + }); + + it('gives a 200 when the user is allowed to perform the action', async () => { + const access_token = await authenticate(app); + + const response = await request(app.getHttpServer()) + .delete('/experiments/1') + .set('Authorization', `Bearer ${access_token}`); + + expect(response.status).toBe(200); + + // Make sure the user can't access the project anymore + const getResponse = await request(app.getHttpServer()) + .get('/experiments/1') + .set('Authorization', `Bearer ${access_token}`); + + expect(getResponse.status).toBe(403); + }); + }); });