diff --git a/backend/src/connectors/v2/authorisation/index.ts b/backend/src/connectors/v2/authorisation/index.ts index 6277397cb..6b985dad9 100644 --- a/backend/src/connectors/v2/authorisation/index.ts +++ b/backend/src/connectors/v2/authorisation/index.ts @@ -1,4 +1,5 @@ import { ModelDoc } from '../../../models/v2/Model.js' +import { ReleaseDoc } from '../../../models/v2/Release.js' import { UserDoc } from '../../../models/v2/User.js' import config from '../../../utils/v2/config.js' import { SillyAuthorisationConnector } from './silly.js' @@ -7,11 +8,23 @@ export const ModelAction = { Create: 'create', View: 'view', } as const - export type ModelActionKeys = (typeof ModelAction)[keyof typeof ModelAction] +export const ReleaseAction = { + Create: 'create', + View: 'view', + Delete: 'delete', +} +export type ReleaseActionKeys = (typeof ReleaseAction)[keyof typeof ReleaseAction] + export abstract class BaseAuthorisationConnector { abstract userModelAction(user: UserDoc, model: ModelDoc, action: ModelActionKeys): Promise + abstract userReleaseAction( + user: UserDoc, + model: ModelDoc, + release: ReleaseDoc, + action: ReleaseActionKeys + ): Promise abstract getEntities(user: UserDoc): Promise> } diff --git a/backend/src/connectors/v2/authorisation/silly.ts b/backend/src/connectors/v2/authorisation/silly.ts index 4e6775ad6..89e6294fc 100644 --- a/backend/src/connectors/v2/authorisation/silly.ts +++ b/backend/src/connectors/v2/authorisation/silly.ts @@ -1,4 +1,5 @@ import { ModelDoc } from '../../../models/v2/Model.js' +import { ReleaseDoc } from '../../../models/v2/Release.js' import { UserDoc } from '../../../models/v2/User.js' import { toEntity } from '../../../utils/v2/entity.js' import { BaseAuthorisationConnector, ModelActionKeys } from './index.js' @@ -13,6 +14,11 @@ export class SillyAuthorisationConnector implements BaseAuthorisationConnector { return true } + async userReleaseAction(_user: UserDoc, _model: ModelDoc, _release: ReleaseDoc, _action: string): Promise { + // With silly authorisation, every user can complete every action. + return true + } + async getEntities(user: UserDoc) { return [toEntity('user', user.dn)] } diff --git a/backend/src/models/v2/Release.ts b/backend/src/models/v2/Release.ts index ae5182857..80f5a1dfc 100644 --- a/backend/src/models/v2/Release.ts +++ b/backend/src/models/v2/Release.ts @@ -51,6 +51,7 @@ const ReleaseSchema = new Schema( ) ReleaseSchema.plugin(MongooseDelete, { overrideMethods: 'all', deletedBy: true, deletedByType: Schema.Types.ObjectId }) +ReleaseSchema.index({ modelId: 1, semver: 1 }, { unique: true }) const ReleaseModel = model('v2_Release', ReleaseSchema) diff --git a/backend/src/routes/v2/release/deleteRelease.ts b/backend/src/routes/v2/release/deleteRelease.ts index 27c2e7066..bdfa39d81 100644 --- a/backend/src/routes/v2/release/deleteRelease.ts +++ b/backend/src/routes/v2/release/deleteRelease.ts @@ -2,6 +2,7 @@ import bodyParser from 'body-parser' import { Request, Response } from 'express' import { z } from 'zod' +import { deleteRelease as deleteReleaseService } from '../../../services/v2/release.js' import { parse } from '../../../utils/validate.js' export const deleteReleaseSchema = z.object({ @@ -18,7 +19,11 @@ interface DeleteReleaseResponse { export const deleteRelease = [ bodyParser.json(), async (req: Request, res: Response) => { - const _ = parse(req, deleteReleaseSchema) + const { + params: { modelId, semver }, + } = parse(req, deleteReleaseSchema) + + await deleteReleaseService(req.user, modelId, semver) return res.json({ message: 'Successfully removed release.', diff --git a/backend/src/routes/v2/release/getRelease.ts b/backend/src/routes/v2/release/getRelease.ts index 0a576da38..085d32510 100644 --- a/backend/src/routes/v2/release/getRelease.ts +++ b/backend/src/routes/v2/release/getRelease.ts @@ -3,6 +3,7 @@ import { Request, Response } from 'express' import { z } from 'zod' import { ReleaseInterface } from '../../../models/v2/Release.js' +import { getReleaseBySemver } from '../../../services/v2/release.js' import { parse } from '../../../utils/validate.js' export const getReleaseSchema = z.object({ @@ -19,28 +20,14 @@ interface getReleaseResponse { export const getRelease = [ bodyParser.json(), async (req: Request, res: Response) => { - const _ = parse(req, getReleaseSchema) + const { + params: { modelId, semver }, + } = parse(req, getReleaseSchema) - return res.json({ - release: { - modelId: 'example-model-1', - modelCardVersion: 14, - - name: 'Example Release 1', - semver: '1.2.3', - notes: 'This is an example release', - - minor: true, - draft: true, + const release = await getReleaseBySemver(req.user, modelId, semver) - files: ['example-file-id'], - images: ['example-image-id'], - - deleted: false, - - createdAt: new Date(), - updatedAt: new Date(), - }, + return res.json({ + release, }) }, ] diff --git a/backend/src/routes/v2/release/getReleases.ts b/backend/src/routes/v2/release/getReleases.ts index 3494149cd..a797eaf52 100644 --- a/backend/src/routes/v2/release/getReleases.ts +++ b/backend/src/routes/v2/release/getReleases.ts @@ -3,6 +3,7 @@ import { Request, Response } from 'express' import { z } from 'zod' import { ReleaseInterface } from '../../../models/v2/Release.js' +import { getModelReleases } from '../../../services/v2/release.js' import { parse } from '../../../utils/validate.js' export const getReleasesSchema = z.object({ @@ -20,49 +21,14 @@ interface getReleasesResponse { export const getReleases = [ bodyParser.json(), async (req: Request, res: Response) => { - const _ = parse(req, getReleasesSchema) + const { + params: { modelId }, + } = parse(req, getReleasesSchema) - return res.json({ - releases: [ - { - modelId: 'example-model-1', - modelCardVersion: 14, - - name: 'Example Release 1', - semver: '1.2.2', - notes: 'This is an example release', - - minor: true, - draft: true, - - files: ['example-file-id'], - images: ['example-image-id'], - - deleted: false, - - createdAt: new Date(), - updatedAt: new Date(), - }, - { - modelId: 'example-model-1', - modelCardVersion: 15, + const releases = await getModelReleases(req.user, modelId) - name: 'Example Release 2', - semver: '1.2.3', - notes: 'This is an example release', - - minor: true, - draft: true, - - files: ['example-file-id'], - images: ['example-image-id'], - - deleted: false, - - createdAt: new Date(), - updatedAt: new Date(), - }, - ], + return res.json({ + releases, }) }, ] diff --git a/backend/src/routes/v2/release/postRelease.ts b/backend/src/routes/v2/release/postRelease.ts index 301e9550f..bf02338eb 100644 --- a/backend/src/routes/v2/release/postRelease.ts +++ b/backend/src/routes/v2/release/postRelease.ts @@ -3,6 +3,7 @@ import { Request, Response } from 'express' import { z } from 'zod' import { ReleaseInterface } from '../../../models/v2/Release.js' +import { createRelease } from '../../../services/v2/release.js' import { parse } from '../../../utils/validate.js' export const postReleaseSchema = z.object({ @@ -33,28 +34,15 @@ interface PostReleaseResponse { export const postRelease = [ bodyParser.json(), async (req: Request, res: Response) => { - const _ = parse(req, postReleaseSchema) + const { + params: { modelId }, + body, + } = parse(req, postReleaseSchema) - return res.json({ - release: { - modelId: 'example-model-1', - modelCardVersion: 55, - - name: 'Example Release 1', - semver: '1.2.3', - notes: 'This is an example release', - - minor: true, - draft: true, + const release = await createRelease(req.user, { modelId, ...body }) - files: ['file-id'], - images: ['image-id'], - - deleted: false, - - createdAt: new Date(), - updatedAt: new Date(), - }, + return res.json({ + release, }) }, ] diff --git a/backend/src/services/v2/release.ts b/backend/src/services/v2/release.ts new file mode 100644 index 000000000..3f8c7dcab --- /dev/null +++ b/backend/src/services/v2/release.ts @@ -0,0 +1,74 @@ +import authorisation, { ModelAction, ReleaseAction } from '../../connectors/v2/authorisation/index.js' +import { ModelInterface } from '../../models/v2/Model.js' +import Release, { ReleaseDoc, ReleaseInterface } from '../../models/v2/Release.js' +import { UserDoc } from '../../models/v2/User.js' +import { asyncFilter } from '../../utils/v2/array.js' +import { Forbidden, NotFound } from '../../utils/v2/error.js' +import { getModelById } from './model.js' + +export type CreateReleaseParams = Pick< + ReleaseInterface, + 'modelId' | 'modelCardVersion' | 'name' | 'semver' | 'notes' | 'minor' | 'draft' | 'files' | 'images' +> +export async function createRelease(user: UserDoc, releaseParams: CreateReleaseParams) { + const model = await getModelById(user, releaseParams.modelId) + + const release = new Release({ + ...releaseParams, + }) + + if (!(await authorisation.userReleaseAction(user, model, release, ReleaseAction.Create))) { + throw Forbidden(`You do not have permission to create a release on this model.`, { + userDn: user.dn, + modelId: releaseParams.modelId, + }) + } + + await release.save() + + return release +} + +export async function getModelReleases( + user: UserDoc, + modelId: string +): Promise> { + const results = await Release.aggregate() + .match({ modelId }) + .sort({ updatedAt: -1 }) + .lookup({ from: 'v2_models', localField: 'modelId', foreignField: 'id', as: 'model' }) + .append({ $set: { model: { $arrayElemAt: ['$model', 0] } } }) + + return asyncFilter(results, (result) => authorisation.userReleaseAction(user, result, result.model, ModelAction.View)) +} + +export async function getReleaseBySemver(user: UserDoc, modelId: string, semver: string) { + const model = await getModelById(user, modelId) + const release = await Release.findOne({ + modelId, + semver, + }) + + if (!release) { + throw NotFound(`The requested release was not found.`, { modelId, semver }) + } + + if (!(await authorisation.userReleaseAction(user, model, release, ReleaseAction.View))) { + throw Forbidden(`You do not have permission to view this release.`, { userDn: user.dn }) + } + + return release +} + +export async function deleteRelease(user: UserDoc, modelId: string, semver: string) { + const model = await getModelById(user, modelId) + const release = await getReleaseBySemver(user, modelId, semver) + + if (!(await authorisation.userReleaseAction(user, model, release, ReleaseAction.Delete))) { + throw Forbidden(`You do not have permission to delete this release.`, { userDn: user.dn }) + } + + await release.delete() + + return { modelId, semver } +} diff --git a/backend/test/routes/model/release/__snapshots__/deleteRelease.spec.ts.snap b/backend/test/routes/model/release/__snapshots__/deleteRelease.spec.ts.snap new file mode 100644 index 000000000..8ad9febc6 --- /dev/null +++ b/backend/test/routes/model/release/__snapshots__/deleteRelease.spec.ts.snap @@ -0,0 +1,15 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`routes > release > deleteRelease > 200 > ok 1`] = ` +{ + "message": "Successfully removed release.", +} +`; + +exports[`routes > release > getRelease > 200 > ok 1`] = ` +{ + "release": { + "_id": "test", + }, +} +`; diff --git a/backend/test/routes/model/release/__snapshots__/getRelease.spec.ts.snap b/backend/test/routes/model/release/__snapshots__/getRelease.spec.ts.snap new file mode 100644 index 000000000..7840b71a0 --- /dev/null +++ b/backend/test/routes/model/release/__snapshots__/getRelease.spec.ts.snap @@ -0,0 +1,9 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`routes > release > getRelease > 200 > ok 1`] = ` +{ + "release": { + "_id": "test", + }, +} +`; diff --git a/backend/test/routes/model/release/__snapshots__/getReleases.spec.ts.snap b/backend/test/routes/model/release/__snapshots__/getReleases.spec.ts.snap new file mode 100644 index 000000000..475e06049 --- /dev/null +++ b/backend/test/routes/model/release/__snapshots__/getReleases.spec.ts.snap @@ -0,0 +1,11 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`routes > release > getReleases > 200 > ok 1`] = ` +{ + "releases": [ + { + "_id": "test", + }, + ], +} +`; diff --git a/backend/test/routes/model/release/__snapshots__/postRelease.spec.ts.snap b/backend/test/routes/model/release/__snapshots__/postRelease.spec.ts.snap new file mode 100644 index 000000000..90e04e139 --- /dev/null +++ b/backend/test/routes/model/release/__snapshots__/postRelease.spec.ts.snap @@ -0,0 +1,55 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`routes > release > postRelease > 200 > ok 1`] = ` +{ + "release": { + "_id": "test", + }, +} +`; + +exports[`routes > release > postRelease > 400 > no description 1`] = ` +{ + "error": { + "context": { + "errors": [ + { + "code": "invalid_type", + "expected": "string", + "message": "Required", + "path": [ + "body", + "name", + ], + "received": "undefined", + }, + ], + }, + "message": "Required", + "name": "Error", + }, +} +`; + +exports[`routes > release > postRelease > 400 > no name 1`] = ` +{ + "error": { + "context": { + "errors": [ + { + "code": "invalid_type", + "expected": "string", + "message": "Required", + "path": [ + "body", + "name", + ], + "received": "undefined", + }, + ], + }, + "message": "Required", + "name": "Error", + }, +} +`; diff --git a/backend/test/routes/model/release/deleteRelease.spec.ts b/backend/test/routes/model/release/deleteRelease.spec.ts new file mode 100644 index 000000000..2c0fe33dc --- /dev/null +++ b/backend/test/routes/model/release/deleteRelease.spec.ts @@ -0,0 +1,21 @@ +import { describe, expect, test, vi } from 'vitest' + +import { deleteReleaseSchema } from '../../../../src/routes/v2/release/deleteRelease.js' +import { createFixture, testDelete } from '../../../testUtils/routes.js' + +vi.mock('../../../../src/utils/config.js') +vi.mock('../../../../src/utils/user.js') + +describe('routes > release > deleteRelease', () => { + test('200 > ok', async () => { + vi.mock('../../../../src/services/v2/release.js', () => ({ + deleteRelease: vi.fn(() => ({ message: 'Successfully removed release.' })), + })) + + const fixture = createFixture(deleteReleaseSchema) + const res = await testDelete(`/api/v2/model/${fixture.params.modelId}/releases/${fixture.params.semver}`) + + expect(res.statusCode).toBe(200) + expect(res.body).matchSnapshot() + }) +}) diff --git a/backend/test/routes/model/release/getRelease.spec.ts b/backend/test/routes/model/release/getRelease.spec.ts new file mode 100644 index 000000000..db4721ea6 --- /dev/null +++ b/backend/test/routes/model/release/getRelease.spec.ts @@ -0,0 +1,21 @@ +import { describe, expect, test, vi } from 'vitest' + +import { getReleaseSchema } from '../../../../src/routes/v2/release/getRelease.js' +import { createFixture, testGet } from '../../../testUtils/routes.js' + +vi.mock('../../../../src/utils/config.js') +vi.mock('../../../../src/utils/user.js') + +vi.mock('../../../../src/services/v2/release.js', () => ({ + getReleaseBySemver: vi.fn(() => ({ _id: 'test' })), +})) + +describe('routes > release > getRelease', () => { + test('200 > ok', async () => { + const fixture = createFixture(getReleaseSchema) + const res = await testGet(`/api/v2/model/${fixture.params.modelId}/releases/${fixture.params.semver}`) + + expect(res.statusCode).toBe(200) + expect(res.body).matchSnapshot() + }) +}) diff --git a/backend/test/routes/model/release/getReleases.spec.ts b/backend/test/routes/model/release/getReleases.spec.ts new file mode 100644 index 000000000..b78a670dc --- /dev/null +++ b/backend/test/routes/model/release/getReleases.spec.ts @@ -0,0 +1,21 @@ +import { describe, expect, test, vi } from 'vitest' + +import { getReleasesSchema } from '../../../../src/routes/v2/release/getReleases.js' +import { createFixture, testGet } from '../../../testUtils/routes.js' + +vi.mock('../../../../src/utils/config.js') +vi.mock('../../../../src/utils/user.js') + +vi.mock('../../../../src/services/v2/release.js', () => ({ + getModelReleases: vi.fn(() => [{ _id: 'test' }]), +})) + +describe('routes > release > getReleases', () => { + test('200 > ok', async () => { + const fixture = createFixture(getReleasesSchema) + const res = await testGet(`/api/v2/model/${fixture.params.modelId}/releases`) + + expect(res.statusCode).toBe(200) + expect(res.body).matchSnapshot() + }) +}) diff --git a/backend/test/routes/model/release/postRelease.spec.ts b/backend/test/routes/model/release/postRelease.spec.ts new file mode 100644 index 000000000..82ee4595d --- /dev/null +++ b/backend/test/routes/model/release/postRelease.spec.ts @@ -0,0 +1,33 @@ +import { describe, expect, test, vi } from 'vitest' + +import { postReleaseSchema } from '../../../../src/routes/v2/release/postRelease.js' +import { createFixture, testPost } from '../../../testUtils/routes.js' + +vi.mock('../../../../src/utils/config.js') +vi.mock('../../../../src/utils/user.js') + +vi.mock('../../../../src/services/v2/release.js', () => ({ + createRelease: vi.fn(() => ({ _id: 'test' })), +})) + +describe('routes > release > postRelease', () => { + test('200 > ok', async () => { + const fixture = createFixture(postReleaseSchema) + const res = await testPost(`/api/v2/model/${fixture.params.modelId}/releases`, fixture) + + expect(res.statusCode).toBe(200) + expect(res.body).matchSnapshot() + }) + + test('400 > no name', async () => { + const fixture = createFixture(postReleaseSchema) as any + + // This release does not include a name. + delete fixture.body.name + + const res = await testPost(`/api/v2/model/${fixture.params.modelId}/releases`, fixture) + + expect(res.statusCode).toEqual(400) + expect(res.body).matchSnapshot() + }) +}) diff --git a/backend/test/services/__snapshots__/release.spec.ts.snap b/backend/test/services/__snapshots__/release.spec.ts.snap new file mode 100644 index 000000000..ea07e12b0 --- /dev/null +++ b/backend/test/services/__snapshots__/release.spec.ts.snap @@ -0,0 +1,43 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`services > release > getModelReleases > good 1`] = ` +[ + { + "modelId": "modelId", + }, +] +`; + +exports[`services > release > getModelReleases > good 2`] = ` +[ + { + "updatedAt": -1, + }, +] +`; + +exports[`services > release > getModelReleases > good 3`] = ` +[ + { + "as": "model", + "foreignField": "id", + "from": "v2_models", + "localField": "modelId", + }, +] +`; + +exports[`services > release > getModelReleases > good 4`] = ` +[ + { + "$set": { + "model": { + "$arrayElemAt": [ + "$model", + 0, + ], + }, + }, + }, +] +`; diff --git a/backend/test/services/release.spec.ts b/backend/test/services/release.spec.ts new file mode 100644 index 000000000..c66bc19a1 --- /dev/null +++ b/backend/test/services/release.spec.ts @@ -0,0 +1,121 @@ +import { describe, expect, test, vi } from 'vitest' + +import { createRelease, deleteRelease, getModelReleases, getReleaseBySemver } from '../../src/services/v2/release.js' + +const arrayAsyncFilter = vi.hoisted(() => ({ + asyncFilter: vi.fn(() => []), +})) +vi.mock('../../src/utils/v2/array.js', () => arrayAsyncFilter) + +const authorisationMocks = vi.hoisted(() => ({ + userReleaseAction: vi.fn(() => true), +})) +vi.mock('../../src/connectors/v2/authorisation/index.js', async () => ({ + ...((await vi.importActual('../../src/connectors/v2/authorisation/index.js')) as object), + default: authorisationMocks, +})) + +const modelMocks = vi.hoisted(() => ({ + getModelById: vi.fn(), +})) +vi.mock('../../src/services/v2/model.js', () => modelMocks) + +const releaseModelMocks = vi.hoisted(() => { + const obj: any = {} + + obj.aggregate = vi.fn(() => obj) + obj.match = vi.fn(() => obj) + obj.sort = vi.fn(() => obj) + obj.lookup = vi.fn(() => obj) + obj.append = vi.fn(() => obj) + obj.find = vi.fn(() => obj) + obj.findOne = vi.fn(() => obj) + obj.updateOne = vi.fn(() => obj) + obj.save = vi.fn(() => obj) + obj.delete = vi.fn(() => obj) + + const model: any = vi.fn(() => obj) + Object.assign(model, obj) + + return model +}) +vi.mock('../../src/models/v2/Release.js', () => ({ default: releaseModelMocks })) + +describe('services > release', () => { + test('createRelease > simple', async () => { + modelMocks.getModelById.mockResolvedValue(undefined) + + await createRelease({} as any, {} as any) + + expect(releaseModelMocks.save).toBeCalled() + expect(releaseModelMocks).toBeCalled() + }) + + test('createRelease > bad authorisation', async () => { + authorisationMocks.userReleaseAction.mockResolvedValueOnce(false) + expect(() => createRelease({} as any, {} as any)).rejects.toThrowError(/^You do not have permission/) + }) + + test('getModelReleases > good', async () => { + await getModelReleases({} as any, 'modelId') + + expect(releaseModelMocks.match.mock.calls.at(0)).toMatchSnapshot() + expect(releaseModelMocks.sort.mock.calls.at(0)).toMatchSnapshot() + expect(releaseModelMocks.lookup.mock.calls.at(0)).toMatchSnapshot() + expect(releaseModelMocks.append.mock.calls.at(0)).toMatchSnapshot() + }) + + test('getReleaseBySemver > good', async () => { + const mockRelease = { _id: 'release' } + + modelMocks.getModelById.mockResolvedValue(undefined) + releaseModelMocks.findOne.mockResolvedValue(mockRelease) + authorisationMocks.userReleaseAction.mockResolvedValueOnce(true) + + expect(await getReleaseBySemver({} as any, 'test', 'test')).toBe(mockRelease) + }) + + test('getReleaseBySemver > no release', async () => { + modelMocks.getModelById.mockResolvedValue(undefined) + releaseModelMocks.findOne.mockResolvedValue(undefined) + authorisationMocks.userReleaseAction.mockResolvedValueOnce(true) + + expect(() => getReleaseBySemver({} as any, 'test', 'test')).rejects.toThrowError( + /^The requested release was not found./ + ) + }) + + test('getReleaseBySemver > no permission', async () => { + const mockRelease = { _id: 'release' } + + modelMocks.getModelById.mockResolvedValue(undefined) + releaseModelMocks.findOne.mockResolvedValue(mockRelease) + authorisationMocks.userReleaseAction.mockResolvedValueOnce(false) + + expect(() => getReleaseBySemver({} as any, 'test', 'test')).rejects.toThrowError( + /^You do not have permission to view this release./ + ) + }) + + test('deleteRelease > success', async () => { + modelMocks.getModelById.mockResolvedValue(undefined) + authorisationMocks.userReleaseAction.mockResolvedValueOnce(true) + + expect(await deleteRelease({} as any, 'test', 'test')).toStrictEqual({ modelId: 'test', semver: 'test' }) + }) + + test('deleteRelease > no permission', async () => { + const mockRelease = { _id: 'release' } + + modelMocks.getModelById.mockResolvedValue(undefined) + releaseModelMocks.findOne.mockResolvedValue(mockRelease) + + authorisationMocks.userReleaseAction.mockResolvedValueOnce(true) + authorisationMocks.userReleaseAction.mockResolvedValueOnce(false) + + expect(() => deleteRelease({} as any, 'test', 'test')).rejects.toThrowError( + /^You do not have permission to delete this release./ + ) + expect(releaseModelMocks.save).not.toBeCalled() + }) +}) diff --git a/backend/test/testUtils/routes.ts b/backend/test/testUtils/routes.ts index c57104887..ae9a02511 100644 --- a/backend/test/testUtils/routes.ts +++ b/backend/test/testUtils/routes.ts @@ -17,6 +17,12 @@ interface Fixture { path?: unknown } +export function testDelete(path: string) { + const request = supertest(server) + + return request.delete(path) +} + export function testPost(path: string, fixture: Fixture) { const request = supertest(server)