From a6de41a83ec44b152d12b15c45d574dd8a4d20c4 Mon Sep 17 00:00:00 2001 From: Anders Rasmussen Date: Tue, 3 Oct 2023 15:02:41 +0200 Subject: [PATCH 1/4] #9 - Added folder endpoints --- .../20231003122910_item_folder/migration.sql | 32 +++ prisma/schema.prisma | 9 + src/locales/da.json | 20 ++ src/locales/en.json | 20 ++ src/modules/item/blob/__test__/delete.test.ts | 6 + src/modules/item/folder/__test__/add.test.ts | 159 ++++++++++++ .../item/folder/__test__/delete.test.ts | 156 ++++++++++++ src/modules/item/folder/__test__/edit.test.ts | 208 +++++++++++++++ src/modules/item/folder/__test__/read.test.ts | 156 ++++++++++++ src/modules/item/folder/folder.controller.ts | 107 ++++++++ src/modules/item/folder/folder.route.ts | 76 ++++++ src/modules/item/folder/folder.schema.ts | 238 ++++++++++++++++++ src/modules/item/folder/folder.service.ts | 79 ++++++ src/modules/item/folder/index.ts | 13 + src/modules/item/index.ts | 2 + 15 files changed, 1281 insertions(+) create mode 100644 prisma/migrations/20231003122910_item_folder/migration.sql create mode 100644 src/modules/item/folder/__test__/add.test.ts create mode 100644 src/modules/item/folder/__test__/delete.test.ts create mode 100644 src/modules/item/folder/__test__/edit.test.ts create mode 100644 src/modules/item/folder/__test__/read.test.ts create mode 100644 src/modules/item/folder/folder.controller.ts create mode 100644 src/modules/item/folder/folder.route.ts create mode 100644 src/modules/item/folder/folder.schema.ts create mode 100644 src/modules/item/folder/folder.service.ts create mode 100644 src/modules/item/folder/index.ts diff --git a/prisma/migrations/20231003122910_item_folder/migration.sql b/prisma/migrations/20231003122910_item_folder/migration.sql new file mode 100644 index 0000000..f88e36b --- /dev/null +++ b/prisma/migrations/20231003122910_item_folder/migration.sql @@ -0,0 +1,32 @@ +-- DropForeignKey +ALTER TABLE "ItemBlob" DROP CONSTRAINT "ItemBlob_itemId_fkey"; + +-- DropForeignKey +ALTER TABLE "ItemSharing" DROP CONSTRAINT "ItemSharing_itemId_fkey"; + +-- DropForeignKey +ALTER TABLE "ItemSharing" DROP CONSTRAINT "ItemSharing_userId_fkey"; + +-- CreateTable +CREATE TABLE "ItemFolder" ( + "id" SERIAL NOT NULL, + "color" VARCHAR(255) NOT NULL, + "itemId" INTEGER NOT NULL, + + CONSTRAINT "ItemFolder_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "ItemFolder_itemId_key" ON "ItemFolder"("itemId"); + +-- AddForeignKey +ALTER TABLE "ItemFolder" ADD CONSTRAINT "ItemFolder_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ItemBlob" ADD CONSTRAINT "ItemBlob_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ItemSharing" ADD CONSTRAINT "ItemSharing_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ItemSharing" ADD CONSTRAINT "ItemSharing_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d59e10c..2db0ecd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -51,6 +51,7 @@ model Item { owner User @relation(fields: [ownerId], references: [id]) parentItem Item? @relation("ItemToItem", fields: [parentId], references: [id]) Items Item[] @relation("ItemToItem") + ItemFolder ItemFolder? ItemBlob ItemBlob? ItemSharing ItemSharing[] @@ -64,6 +65,14 @@ model Item { @@index([deletedAt]) } +model ItemFolder { + id Int @id @default(autoincrement()) + color String @db.VarChar(255) + itemId Int @unique + + item Item @relation(fields: [itemId], references: [id], onDelete: Cascade) +} + model ItemBlob { id Int @id @default(autoincrement()) blobUrl String @db.VarChar(1024) diff --git a/src/locales/da.json b/src/locales/da.json index 705b1b9..a680484 100644 --- a/src/locales/da.json +++ b/src/locales/da.json @@ -98,6 +98,26 @@ "notFound": "Delning ikke fundet", "alreadyExists": "Delning findes allerede" }, + "folder": { + "notFound": "Mappen blev ikke fundet" + }, "notFound": "Item ikke fundet" + }, + "folder": { + "id": { + "required": "id er påkrævet", + "type": "id skal være et tal" + }, + "name": { + "required": "Navn er påkrævet", + "type": "Navn skal være en tekst" + }, + "color": { + "required": "Farve er påkrævet", + "type": "Farve skal være en tekst" + }, + "itemid": { + "type": "Item id skal være et tal" + } } } diff --git a/src/locales/en.json b/src/locales/en.json index 787a4d4..2514120 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -98,6 +98,26 @@ "notFound": "Sharing not found", "alreadyExists": "Sharing already exists" }, + "folder": { + "notFound": "Folder not found" + }, "notFound": "Item not found" + }, + "folder": { + "id": { + "required": "id is required", + "type": "id must be a number" + }, + "name": { + "required": "Name is required", + "type": "Name must be a string" + }, + "color": { + "required": "Color is required", + "type": "Color must be a string" + }, + "itemid": { + "type": "Item id must be a number" + } } } diff --git a/src/modules/item/blob/__test__/delete.test.ts b/src/modules/item/blob/__test__/delete.test.ts index 22a9bd1..fe1bfac 100644 --- a/src/modules/item/blob/__test__/delete.test.ts +++ b/src/modules/item/blob/__test__/delete.test.ts @@ -2,11 +2,13 @@ import { User } from '@prisma/client'; import UserService from '../../../auth/user.service'; import AuthService from '../../../auth/auth.service'; import BlobService from '../blob.service'; +import ItemService from '../../item.service'; describe('DELETE /api/blob/:id', () => { let userService: UserService; let authService: AuthService; let blobService: BlobService; + let itemService: ItemService; let user: User; let otherUser: User; @@ -15,6 +17,7 @@ describe('DELETE /api/blob/:id', () => { authService = new AuthService(); userService = new UserService(); blobService = new BlobService(); + itemService = new ItemService(); user = await userService.createUser({ name: 'Joe Biden the 1st', @@ -49,6 +52,9 @@ describe('DELETE /api/blob/:id', () => { expect(response.statusCode).toBe(204); expect(response.body).toEqual(''); + + await expect(blobService.getByItemId(blob.id)).rejects.toThrowError(); + await expect(itemService.getById(blob.id)).rejects.toThrowError(); }); it('should return status 401, when unauthorized', async () => { diff --git a/src/modules/item/folder/__test__/add.test.ts b/src/modules/item/folder/__test__/add.test.ts new file mode 100644 index 0000000..3841e1b --- /dev/null +++ b/src/modules/item/folder/__test__/add.test.ts @@ -0,0 +1,159 @@ +import { User } from '@prisma/client'; +import UserService from '../../../auth/user.service'; +import AuthService from '../../../auth/auth.service'; +import FolderService from '../folder.service'; + +describe('POST /api/folder', () => { + let userService: UserService; + let authService: AuthService; + let folderService: FolderService; + + let user: User; + let otherUser: User; + + beforeAll(async () => { + authService = new AuthService(); + userService = new UserService(); + folderService = new FolderService(); + + user = await userService.createUser({ + name: 'Joe Biden the 1st', + email: 'joe@biden.com', + password: '1234', + }); + otherUser = await userService.createUser({ + name: 'Joe Biden the 2nd', + email: 'joe2@biden.com', + password: '4321', + }); + }); + + it('should return status 200 and folder', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/folder', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + name: 'Folder Name', + color: '#78BC61', + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ + id: expect.any(Number), + name: 'Folder Name', + color: '#78BC61', + parentId: null, + ownerId: user.id, + mimeType: 'application/vnd.cloudstore.folder', + createdAt: expect.any(String), + deletedAt: null, + updatedAt: expect.any(String), + }); + }); + + it('should return status 401, when unauthorized', async () => { + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/folder', + headers: { + authorization: 'invalid_access_token!!!', + }, + payload: { + name: 'Folder Name', + color: '#78BC61', + parentId: null, + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + + it('should return status 401, when folder name is not provided', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/folder', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + color: '#78BC61', + parentId: null, + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'ValidationError', + errors: { + _: ['Name is required'], + }, + statusCode: 400, + }); + }); + + it('should return status 401, when folder color is not provided', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/folder', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + name: 'Folder name', + parentId: null, + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'ValidationError', + errors: { + _: ['Color is required'], + }, + statusCode: 400, + }); + }); + + it("should return status 400, when parent id isn't a number", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/folder', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + name: 'Folder Name', + color: '#78BC61', + parentId: 'invalid_id', + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'ValidationError', + errors: { + parentId: ['Item id must be a number'], + }, + statusCode: 400, + }); + }); +}); diff --git a/src/modules/item/folder/__test__/delete.test.ts b/src/modules/item/folder/__test__/delete.test.ts new file mode 100644 index 0000000..1b8479b --- /dev/null +++ b/src/modules/item/folder/__test__/delete.test.ts @@ -0,0 +1,156 @@ +import { User } from '@prisma/client'; +import UserService from '../../../auth/user.service'; +import AuthService from '../../../auth/auth.service'; +import FolderService from '../folder.service'; +import ItemService from '../../item.service'; + +describe('DELETE /api/folder/:id', () => { + let userService: UserService; + let authService: AuthService; + let folderService: FolderService; + let itemService: ItemService; + + let user: User; + let otherUser: User; + + beforeAll(async () => { + authService = new AuthService(); + userService = new UserService(); + folderService = new FolderService(); + itemService = new ItemService(); + + user = await userService.createUser({ + name: 'Joe Biden the 1st', + email: 'joe@biden.com', + password: '1234', + }); + otherUser = await userService.createUser({ + name: 'Joe Biden the 2nd', + email: 'joe2@biden.com', + password: '4321', + }); + }); + + it('should delete folder AND item', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const folder = await folderService.createFolder({ + mimeType: 'application/vnd.cloudstore.folder', + name: 'Folder1', + ownerId: user.id, + parentId: null, + color: '#78BC61', + }); + + const response = await global.fastify.inject({ + method: 'DELETE', + url: '/api/folder/' + folder.id, + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.body).toEqual(''); + + await expect(folderService.getByItemId(folder.id)).rejects.toThrowError(); + await expect(itemService.getById(folder.id)).rejects.toThrowError(); + }); + + it('should return status 401, when unauthorized', async () => { + const folder = await folderService.createFolder({ + mimeType: 'application/vnd.cloudstore.folder', + name: 'Folder1', + ownerId: user.id, + parentId: null, + color: '#78BC61', + }); + + const response = await global.fastify.inject({ + method: 'DELETE', + url: '/api/folder/' + folder.id, + headers: { + authorization: 'invalid_access_token!!!', + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + + it('should return status 401, when folder id is provided but you do not own it', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const folder = await folderService.createFolder({ + mimeType: 'application/vnd.cloudstore.folder', + name: 'Folder1', + ownerId: otherUser.id, + parentId: null, + color: '#78BC61', + }); + + const response = await global.fastify.inject({ + method: 'DELETE', + url: '/api/folder/' + folder.id, + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + + it("should return status 400, when folder id isn't a number", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'DELETE', + url: '/api/folder/invalid_id', + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'ValidationError', + errors: { + id: ['id must be a number'], + }, + statusCode: 400, + }); + }); + + it("should return status 400, when folder with id doesn't exist", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'DELETE', + url: '/api/folder/1234', + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'BadRequestError', + errors: { + _: ['Folder not found'], + }, + statusCode: 400, + }); + }); +}); diff --git a/src/modules/item/folder/__test__/edit.test.ts b/src/modules/item/folder/__test__/edit.test.ts new file mode 100644 index 0000000..38738c9 --- /dev/null +++ b/src/modules/item/folder/__test__/edit.test.ts @@ -0,0 +1,208 @@ +import { User } from '@prisma/client'; +import UserService from '../../../auth/user.service'; +import AuthService from '../../../auth/auth.service'; +import FolderService from '../folder.service'; + +describe('PUT /api/folder', () => { + let userService: UserService; + let authService: AuthService; + let folderService: FolderService; + + let user: User; + let otherUser: User; + + beforeAll(async () => { + authService = new AuthService(); + userService = new UserService(); + folderService = new FolderService(); + + user = await userService.createUser({ + name: 'Joe Biden the 1st', + email: 'joe@biden.com', + password: '1234', + }); + otherUser = await userService.createUser({ + name: 'Joe Biden the 2nd', + email: 'joe2@biden.com', + password: '4321', + }); + }); + + it('should return status 200 and folder', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const folder = await folderService.createFolder({ + mimeType: 'application/vnd.cloudstore.folder', + name: 'Folder1', + ownerId: user.id, + parentId: null, + color: '#78BC61', + }); + + const response = await global.fastify.inject({ + method: 'PUT', + url: '/api/folder', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + id: folder.id, + name: folder.name + ' updated', + color: '#79BC61', + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ + ...folder, + name: folder.name + ' updated', + color: '#79BC61', + createdAt: folder.createdAt.toISOString(), + updatedAt: expect.any(String), + deletedAt: folder.deletedAt?.toISOString() ?? null, + }); + }); + + it('should return status 401, when unauthorized', async () => { + const folder = await folderService.createFolder({ + mimeType: 'application/vnd.cloudstore.folder', + name: 'Folder1', + ownerId: user.id, + parentId: null, + color: '#78BC61', + }); + + const response = await global.fastify.inject({ + method: 'PUT', + url: '/api/folder', + headers: { + authorization: 'invalid_access_token!!!', + }, + payload: { + id: folder.id, + name: folder.name + ' updated', + color: '#79BC61', + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + + it('should return status 401, when folder id is provided but you do not own it', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const folder = await folderService.createFolder({ + mimeType: 'application/vnd.cloudstore.folder', + name: 'Folder1', + ownerId: otherUser.id, + parentId: null, + color: '#78BC61', + }); + + const response = await global.fastify.inject({ + method: 'PUT', + url: '/api/folder', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + id: folder.id, + name: folder.name + ' updated', + color: '#79BC61', + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + + it("should return status 400, when folder id isn't a number", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'PUT', + url: '/api/folder', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + id: 'invalid_id', + name: 'updated', + color: '#79BC61', + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'ValidationError', + errors: { + id: ['id must be a number'], + }, + statusCode: 400, + }); + }); + + it("should return status 400, when folder id isn't given", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'PUT', + url: '/api/folder', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + name: 'updated', + color: '#79BC61', + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'ValidationError', + errors: { + _: ['id is required'], + }, + statusCode: 400, + }); + }); + + it("should return status 400, when folder with id doesn't exist", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'PUT', + url: '/api/folder', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + id: 1234, + name: 'updated', + color: '#79BC61', + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'BadRequestError', + errors: { + _: ['Folder not found'], + }, + statusCode: 400, + }); + }); +}); diff --git a/src/modules/item/folder/__test__/read.test.ts b/src/modules/item/folder/__test__/read.test.ts new file mode 100644 index 0000000..4cf904c --- /dev/null +++ b/src/modules/item/folder/__test__/read.test.ts @@ -0,0 +1,156 @@ +import { User } from '@prisma/client'; +import UserService from '../../../auth/user.service'; +import AuthService from '../../../auth/auth.service'; +import FolderService from '../folder.service'; + +describe('GET /api/folder/:id', () => { + let userService: UserService; + let authService: AuthService; + let folderService: FolderService; + + let user: User; + let otherUser: User; + + beforeAll(async () => { + authService = new AuthService(); + userService = new UserService(); + folderService = new FolderService(); + + user = await userService.createUser({ + name: 'Joe Biden the 1st', + email: 'joe@biden.com', + password: '1234', + }); + otherUser = await userService.createUser({ + name: 'Joe Biden the 2nd', + email: 'joe2@biden.com', + password: '4321', + }); + }); + + it('should return status 200 and folder', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const folder = await folderService.createFolder({ + mimeType: 'application/vnd.cloudstore.folder', + name: 'Folder1', + ownerId: user.id, + parentId: null, + color: '#78BC61', + }); + + const response = await global.fastify.inject({ + method: 'GET', + url: '/api/folder/' + folder.id, + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ + ...folder, + createdAt: folder.createdAt.toISOString(), + updatedAt: folder.updatedAt.toISOString(), + deletedAt: folder.deletedAt?.toISOString() ?? null, + }); + }); + + it('should return status 401, when unauthorized', async () => { + const folder = await folderService.createFolder({ + mimeType: 'application/vnd.cloudstore.folder', + name: 'Folder1', + ownerId: user.id, + parentId: null, + color: '#78BC61', + }); + + const response = await global.fastify.inject({ + method: 'GET', + url: '/api/folder/' + folder.id, + headers: { + authorization: 'invalid_access_token!!!', + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + + it('should return status 401, when folder id is provided but you do not own it', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const folder = await folderService.createFolder({ + mimeType: 'application/vnd.cloudstore.folder', + name: 'Folder1', + ownerId: otherUser.id, + parentId: null, + color: '#78BC61', + }); + + const response = await global.fastify.inject({ + method: 'GET', + url: '/api/folder/' + folder.id, + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + + it("should return status 400, when folder id isn't a number", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'GET', + url: '/api/folder/invalid_id', + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'ValidationError', + errors: { + id: ['id must be a number'], + }, + statusCode: 400, + }); + }); + + it("should return status 400, when folder with id doesn't exist", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'GET', + url: '/api/folder/1234', + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'BadRequestError', + errors: { + _: ['Folder not found'], + }, + statusCode: 400, + }); + }); +}); diff --git a/src/modules/item/folder/folder.controller.ts b/src/modules/item/folder/folder.controller.ts new file mode 100644 index 0000000..77ab3c0 --- /dev/null +++ b/src/modules/item/folder/folder.controller.ts @@ -0,0 +1,107 @@ +import { FastifyReply, FastifyRequest } from 'fastify'; +import { ReadInput, EditInput, AddInput, DeleteInput } from './folder.schema'; +import FolderService from './folder.service'; + +export default class ItemController { + private folderService: FolderService; + + constructor(folderService: FolderService) { + this.folderService = folderService; + } + + public async readHandler( + request: FastifyRequest<{ + Params: ReadInput; + }>, + reply: FastifyReply, + ) { + try { + const folder = await this.folderService.getByItemId(request.params.id); + + if (folder.ownerId !== request.user.sub) { + return reply.unauthorized(); + } + + return reply.code(200).send(folder); + } catch (e) { + if (e instanceof Error) { + return reply.badRequest(request.i18n.t(e.message)); + } + + /* istanbul ignore next */ + return reply.badRequest(); + } + } + + public async editHandler( + request: FastifyRequest<{ + Body: EditInput; + }>, + reply: FastifyReply, + ) { + try { + const folder = await this.folderService.getByItemId(request.body.id); + + if (folder.ownerId !== request.user.sub) { + return reply.unauthorized(); + } + + const updatedFolder = await this.folderService.updateFolder(request.body); + + return reply.code(200).send(updatedFolder); + } catch (e) { + if (e instanceof Error) { + return reply.badRequest(request.i18n.t(e.message)); + } + + /* istanbul ignore next */ + return reply.badRequest(); + } + } + + public async addHandler( + request: FastifyRequest<{ + Body: AddInput; + }>, + reply: FastifyReply, + ) { + try { + const folder = await this.folderService.createFolder({ + name: request.body.name, + mimeType: 'application/vnd.cloudstore.folder', + color: request.body.color, + ownerId: request.user.sub, + parentId: request.body.parentId ?? null, + }); + + return reply.code(200).send(folder); + } catch (e) { + /* istanbul ignore next */ + return reply.badRequest(); + } + } + + public async deleteHandler( + request: FastifyRequest<{ + Params: DeleteInput; + }>, + reply: FastifyReply, + ) { + try { + const folder = await this.folderService.getByItemId(request.params.id); + + if (folder.ownerId !== request.user.sub) { + return reply.unauthorized(); + } + + await this.folderService.deleteFolderByItemId(request.params.id); + } catch (e) { + if (e instanceof Error) { + return reply.badRequest(request.i18n.t(e.message)); + } + + /* istanbul ignore next */ + return reply.badRequest(); + } + } +} diff --git a/src/modules/item/folder/folder.route.ts b/src/modules/item/folder/folder.route.ts new file mode 100644 index 0000000..75cacf8 --- /dev/null +++ b/src/modules/item/folder/folder.route.ts @@ -0,0 +1,76 @@ +import { FastifyInstance } from 'fastify'; +import FolderController from './folder.controller'; +import FolderService from './folder.service'; + +export default async (fastify: FastifyInstance) => { + const folderController = new FolderController(new FolderService()); + + fastify.get( + '/:id', + { + schema: { + headers: { + Authorization: true, + }, + tags: ['Folder'], + params: { $ref: 'readFolderSchema' }, + response: { + 200: { $ref: 'readFolderResponseSchema' }, + }, + }, + onRequest: [fastify.authenticate], + }, + folderController.readHandler.bind(folderController), + ); + + fastify.put( + '/', + { + schema: { + headers: { + Authorization: true, + }, + tags: ['Folder'], + body: { $ref: 'editFolderSchema' }, + response: { + 200: { $ref: 'editFolderResponseSchema' }, + }, + }, + onRequest: [fastify.authenticate], + }, + folderController.editHandler.bind(folderController), + ); + + fastify.post( + '/', + { + schema: { + headers: { + Authorization: true, + }, + tags: ['Folder'], + body: { $ref: 'addFolderSchema' }, + response: { + 200: { $ref: 'addFolderResponseSchema' }, + }, + }, + onRequest: [fastify.authenticate], + }, + folderController.addHandler.bind(folderController), + ); + + fastify.delete( + '/:id', + { + schema: { + headers: { + Authorization: true, + }, + tags: ['Folder'], + params: { $ref: 'deleteFolderSchema' }, + }, + onRequest: [fastify.authenticate], + }, + folderController.deleteHandler.bind(folderController), + ); +}; diff --git a/src/modules/item/folder/folder.schema.ts b/src/modules/item/folder/folder.schema.ts new file mode 100644 index 0000000..ac9f760 --- /dev/null +++ b/src/modules/item/folder/folder.schema.ts @@ -0,0 +1,238 @@ +import { FromSchema } from 'json-schema-to-ts'; +import { Item, UpdateItem } from '../item.schema'; +import { ItemFolder as prismaItemFolderType } from '@prisma/client'; + +const readFolderSchema = { + $id: 'readFolderSchema', + type: 'object', + properties: { + id: { + type: 'number', + errorMessage: { + type: 'folder.id.type', + }, + }, + }, + required: ['id'], + errorMessage: { + required: { + id: 'folder.id.required', + }, + }, +} as const; + +const readFolderResponseSchema = { + $id: 'readFolderResponseSchema', + type: 'object', + properties: { + id: { + type: 'number', + }, + color: { + type: 'string', + }, + parentId: { + type: ['number', 'null'], + }, + name: { + type: 'string', + }, + mimeType: { + type: 'string', + }, + ownerId: { + type: 'number', + }, + deletedAt: { + type: ['string', 'null'], + }, + createdAt: { + type: 'string', + }, + updatedAt: { + type: 'string', + }, + }, +} as const; + +const editFolderSchema = { + $id: 'editFolderSchema', + type: 'object', + properties: { + id: { + type: 'number', + errorMessage: { + type: 'folder.id.type', + }, + }, + name: { + type: 'string', + errorMessage: { + type: 'folder.name.type', + }, + }, + color: { + type: 'string', + errorMessage: { + type: 'folder.color.type', + }, + }, + parentId: { + type: ['number', 'null'], + errorMessage: { + type: 'folder.itemid.type', + }, + }, + }, + required: ['id'], + errorMessage: { + required: { + id: 'folder.id.required', + }, + }, +} as const; + +const editFolderResponseSchema = { + $id: 'editFolderResponseSchema', + type: 'object', + properties: { + id: { + type: 'number', + }, + color: { + type: 'string', + }, + parentId: { + type: ['number', 'null'], + }, + name: { + type: 'string', + }, + mimeType: { + type: 'string', + }, + ownerId: { + type: 'number', + }, + deletedAt: { + type: ['string', 'null'], + }, + createdAt: { + type: 'string', + }, + updatedAt: { + type: 'string', + }, + }, +} as const; +const addFolderSchema = { + $id: 'addFolderSchema', + type: 'object', + properties: { + name: { + type: 'string', + errorMessage: { + type: 'folder.name.type', + }, + }, + color: { + type: 'string', + errorMessage: { + type: 'folder.color.type', + }, + }, + parentId: { + type: ['number', 'null'], + errorMessage: { + type: 'folder.itemid.type', + }, + }, + }, + required: ['name', 'color'], + errorMessage: { + required: { + name: 'folder.name.required', + color: 'folder.color.required', + }, + }, +} as const; + +const addFolderResponseSchema = { + $id: 'addFolderResponseSchema', + type: 'object', + properties: { + id: { + type: 'number', + }, + name: { + type: 'string', + }, + color: { + type: 'string', + }, + parentId: { + type: ['number', 'null'], + }, + mimeType: { + type: 'string', + }, + ownerId: { + type: 'number', + }, + deletedAt: { + type: ['string', 'null'], + }, + createdAt: { + type: 'string', + }, + updatedAt: { + type: 'string', + }, + }, +} as const; + +const deleteFolderSchema = { + $id: 'deleteFolderSchema', + type: 'object', + properties: { + id: { + type: 'number', + errorMessage: { + type: 'folder.id.type', + }, + }, + }, + required: ['id'], + errorMessage: { + required: { + itemId: 'folder.id.required', + }, + }, +} as const; + +export type AddInput = FromSchema; +export type ReadInput = FromSchema; +export type EditInput = FromSchema; +export type DeleteInput = FromSchema; + +export type AddFolder = { + name: string; + color: string; + mimeType: string; + ownerId: number; + parentId: number | null; +}; + +export type ItemFolder = prismaItemFolderType & { item: Item }; +export type Folder = Omit & Item; +export type UpdateFolder = { id: number } & Partial & UpdateItem; + +export const folderSchemas = [ + addFolderSchema, + addFolderResponseSchema, + readFolderSchema, + readFolderResponseSchema, + editFolderSchema, + editFolderResponseSchema, + deleteFolderSchema, +]; diff --git a/src/modules/item/folder/folder.service.ts b/src/modules/item/folder/folder.service.ts new file mode 100644 index 0000000..f79a364 --- /dev/null +++ b/src/modules/item/folder/folder.service.ts @@ -0,0 +1,79 @@ +import { prisma } from '../../../plugins/prisma'; +import { Folder, AddFolder, UpdateFolder, ItemFolder } from './folder.schema'; + +export default class FolderService { + public async createFolder(input: AddFolder): Promise { + const itemFolder = await prisma.itemFolder.create({ + data: { + color: input.color, + item: { + create: { + name: input.name, + mimeType: input.mimeType, + ownerId: input.ownerId, + parentId: input.parentId, + }, + }, + }, + include: { + item: true, + }, + }); + + return this.formatItemFolder(itemFolder); + } + + public async getByItemId(itemId: number): Promise { + const itemFolder = await prisma.itemFolder.findUnique({ + where: { + itemId, + }, + include: { + item: true, + }, + }); + + if (!itemFolder) { + throw new Error('item.folder.notFound'); + } + + return this.formatItemFolder(itemFolder); + } + + public async updateFolder(input: UpdateFolder): Promise { + const itemFolder = await prisma.itemFolder.update({ + data: { + color: input.color, + item: { + update: { + name: input.name, + parentId: input.parentId, + }, + }, + }, + where: { + itemId: input.id, + }, + include: { + item: true, + }, + }); + + return this.formatItemFolder(itemFolder); + } + + public async deleteFolderByItemId(itemId: number): Promise { + await prisma.item.delete({ + where: { + id: itemId, + }, + }); + } + + private formatItemFolder(itemFolder: ItemFolder): Folder { + return { + color: itemFolder.color, + ...itemFolder.item, + }; + } +} diff --git a/src/modules/item/folder/index.ts b/src/modules/item/folder/index.ts new file mode 100644 index 0000000..4a5613d --- /dev/null +++ b/src/modules/item/folder/index.ts @@ -0,0 +1,13 @@ +import { FastifyInstance, FastifyPluginOptions } from 'fastify'; + +import fastifyPlugin from 'fastify-plugin'; +import { folderSchemas } from './folder.schema'; +import folderRoute from './folder.route'; + +export default fastifyPlugin(async (fastify: FastifyInstance, options: FastifyPluginOptions) => { + for (const schema of folderSchemas) { + fastify.addSchema(schema); + } + + await fastify.register(folderRoute, options); +}); diff --git a/src/modules/item/index.ts b/src/modules/item/index.ts index 8d508f1..c09ee34 100644 --- a/src/modules/item/index.ts +++ b/src/modules/item/index.ts @@ -1,10 +1,12 @@ import { FastifyInstance, FastifyPluginOptions } from 'fastify'; import fastifyPlugin from 'fastify-plugin'; +import folder from './folder'; import { getOptionsWithPrefix } from '..'; import blob from './blob'; import sharing from './sharing'; export default fastifyPlugin(async (fastify: FastifyInstance, options: FastifyPluginOptions) => { await fastify.register(blob, getOptionsWithPrefix(options, '/blob')); + await fastify.register(folder, getOptionsWithPrefix(options, '/folder')); await fastify.register(sharing, getOptionsWithPrefix(options, '/sharing')); }); From 76672cc2da5143d288263c3d762c069dad18b6db Mon Sep 17 00:00:00 2001 From: Anders Rasmussen Date: Tue, 3 Oct 2023 15:10:10 +0200 Subject: [PATCH 2/4] #9 - Fixed tests --- src/modules/item/folder/__test__/add.test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/modules/item/folder/__test__/add.test.ts b/src/modules/item/folder/__test__/add.test.ts index 3841e1b..ddeb9be 100644 --- a/src/modules/item/folder/__test__/add.test.ts +++ b/src/modules/item/folder/__test__/add.test.ts @@ -1,31 +1,22 @@ import { User } from '@prisma/client'; import UserService from '../../../auth/user.service'; import AuthService from '../../../auth/auth.service'; -import FolderService from '../folder.service'; describe('POST /api/folder', () => { let userService: UserService; let authService: AuthService; - let folderService: FolderService; let user: User; - let otherUser: User; beforeAll(async () => { authService = new AuthService(); userService = new UserService(); - folderService = new FolderService(); user = await userService.createUser({ name: 'Joe Biden the 1st', email: 'joe@biden.com', password: '1234', }); - otherUser = await userService.createUser({ - name: 'Joe Biden the 2nd', - email: 'joe2@biden.com', - password: '4321', - }); }); it('should return status 200 and folder', async () => { From 31c9005158639978764fc394fcad94cbab2cc949 Mon Sep 17 00:00:00 2001 From: Anders Rasmussen Date: Wed, 4 Oct 2023 08:10:42 +0200 Subject: [PATCH 3/4] #9 - Added changes according to feedback --- src/locales/da.json | 4 +- src/locales/en.json | 4 +- src/modules/item/folder/__test__/add.test.ts | 44 ++++++++++++++++++- .../item/folder/__test__/delete.test.ts | 5 +-- src/modules/item/folder/__test__/edit.test.ts | 5 +-- src/modules/item/folder/__test__/read.test.ts | 5 +-- src/modules/item/folder/folder.controller.ts | 22 +++++++--- src/modules/item/folder/folder.route.ts | 9 +++- src/modules/item/folder/folder.schema.ts | 7 ++- src/modules/item/folder/folder.service.ts | 2 +- 10 files changed, 78 insertions(+), 29 deletions(-) diff --git a/src/locales/da.json b/src/locales/da.json index a680484..1f04e66 100644 --- a/src/locales/da.json +++ b/src/locales/da.json @@ -116,8 +116,8 @@ "required": "Farve er påkrævet", "type": "Farve skal være en tekst" }, - "itemid": { - "type": "Item id skal være et tal" + "parentId": { + "type": "Parent id skal være et tal" } } } diff --git a/src/locales/en.json b/src/locales/en.json index 2514120..6d94b41 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -116,8 +116,8 @@ "required": "Color is required", "type": "Color must be a string" }, - "itemid": { - "type": "Item id must be a number" + "parentId": { + "type": "Parent id must be a number" } } } diff --git a/src/modules/item/folder/__test__/add.test.ts b/src/modules/item/folder/__test__/add.test.ts index ddeb9be..1e78862 100644 --- a/src/modules/item/folder/__test__/add.test.ts +++ b/src/modules/item/folder/__test__/add.test.ts @@ -1,22 +1,31 @@ import { User } from '@prisma/client'; import UserService from '../../../auth/user.service'; import AuthService from '../../../auth/auth.service'; +import FolderService from '../folder.service'; describe('POST /api/folder', () => { let userService: UserService; let authService: AuthService; + let folderService: FolderService; let user: User; + let otherUser: User; beforeAll(async () => { authService = new AuthService(); userService = new UserService(); + folderService = new FolderService(); user = await userService.createUser({ name: 'Joe Biden the 1st', email: 'joe@biden.com', password: '1234', }); + otherUser = await userService.createUser({ + name: 'Joe Biden the 2nd', + email: 'joe2@biden.com', + password: '4321', + }); }); it('should return status 200 and folder', async () => { @@ -72,6 +81,39 @@ describe('POST /api/folder', () => { }); }); + it('should return status 401, when parent id is provided, but no access to parent', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const folder = await folderService.createFolder({ + name: 'Folder1', + ownerId: otherUser.id, + parentId: null, + color: '#78BC61', + }); + + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/folder', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + name: 'Folder Name', + color: '#78BC61', + parentId: folder.id, + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + it('should return status 401, when folder name is not provided', async () => { const { accessToken } = await authService.createTokens(user.id); @@ -142,7 +184,7 @@ describe('POST /api/folder', () => { expect(response.json()).toEqual({ error: 'ValidationError', errors: { - parentId: ['Item id must be a number'], + parentId: ['Parent id must be a number'], }, statusCode: 400, }); diff --git a/src/modules/item/folder/__test__/delete.test.ts b/src/modules/item/folder/__test__/delete.test.ts index 1b8479b..91d9785 100644 --- a/src/modules/item/folder/__test__/delete.test.ts +++ b/src/modules/item/folder/__test__/delete.test.ts @@ -35,7 +35,6 @@ describe('DELETE /api/folder/:id', () => { const { accessToken } = await authService.createTokens(user.id); const folder = await folderService.createFolder({ - mimeType: 'application/vnd.cloudstore.folder', name: 'Folder1', ownerId: user.id, parentId: null, @@ -58,7 +57,6 @@ describe('DELETE /api/folder/:id', () => { it('should return status 401, when unauthorized', async () => { const folder = await folderService.createFolder({ - mimeType: 'application/vnd.cloudstore.folder', name: 'Folder1', ownerId: user.id, parentId: null, @@ -83,11 +81,10 @@ describe('DELETE /api/folder/:id', () => { }); }); - it('should return status 401, when folder id is provided but you do not own it', async () => { + it('should return status 401, when folder id is not accessible to you', async () => { const { accessToken } = await authService.createTokens(user.id); const folder = await folderService.createFolder({ - mimeType: 'application/vnd.cloudstore.folder', name: 'Folder1', ownerId: otherUser.id, parentId: null, diff --git a/src/modules/item/folder/__test__/edit.test.ts b/src/modules/item/folder/__test__/edit.test.ts index 38738c9..85a908e 100644 --- a/src/modules/item/folder/__test__/edit.test.ts +++ b/src/modules/item/folder/__test__/edit.test.ts @@ -32,7 +32,6 @@ describe('PUT /api/folder', () => { const { accessToken } = await authService.createTokens(user.id); const folder = await folderService.createFolder({ - mimeType: 'application/vnd.cloudstore.folder', name: 'Folder1', ownerId: user.id, parentId: null, @@ -65,7 +64,6 @@ describe('PUT /api/folder', () => { it('should return status 401, when unauthorized', async () => { const folder = await folderService.createFolder({ - mimeType: 'application/vnd.cloudstore.folder', name: 'Folder1', ownerId: user.id, parentId: null, @@ -95,11 +93,10 @@ describe('PUT /api/folder', () => { }); }); - it('should return status 401, when folder id is provided but you do not own it', async () => { + it('should return status 401, when folder id is not accessible to you', async () => { const { accessToken } = await authService.createTokens(user.id); const folder = await folderService.createFolder({ - mimeType: 'application/vnd.cloudstore.folder', name: 'Folder1', ownerId: otherUser.id, parentId: null, diff --git a/src/modules/item/folder/__test__/read.test.ts b/src/modules/item/folder/__test__/read.test.ts index 4cf904c..c9dc3ca 100644 --- a/src/modules/item/folder/__test__/read.test.ts +++ b/src/modules/item/folder/__test__/read.test.ts @@ -32,7 +32,6 @@ describe('GET /api/folder/:id', () => { const { accessToken } = await authService.createTokens(user.id); const folder = await folderService.createFolder({ - mimeType: 'application/vnd.cloudstore.folder', name: 'Folder1', ownerId: user.id, parentId: null, @@ -58,7 +57,6 @@ describe('GET /api/folder/:id', () => { it('should return status 401, when unauthorized', async () => { const folder = await folderService.createFolder({ - mimeType: 'application/vnd.cloudstore.folder', name: 'Folder1', ownerId: user.id, parentId: null, @@ -83,11 +81,10 @@ describe('GET /api/folder/:id', () => { }); }); - it('should return status 401, when folder id is provided but you do not own it', async () => { + it('should return status 401, when folder id is not accessible to you', async () => { const { accessToken } = await authService.createTokens(user.id); const folder = await folderService.createFolder({ - mimeType: 'application/vnd.cloudstore.folder', name: 'Folder1', ownerId: otherUser.id, parentId: null, diff --git a/src/modules/item/folder/folder.controller.ts b/src/modules/item/folder/folder.controller.ts index 77ab3c0..2f61f2b 100644 --- a/src/modules/item/folder/folder.controller.ts +++ b/src/modules/item/folder/folder.controller.ts @@ -1,12 +1,15 @@ import { FastifyReply, FastifyRequest } from 'fastify'; import { ReadInput, EditInput, AddInput, DeleteInput } from './folder.schema'; import FolderService from './folder.service'; +import AccessService from '../sharing/access.service'; -export default class ItemController { +export default class FolderController { private folderService: FolderService; + private accessService: AccessService; - constructor(folderService: FolderService) { + constructor(folderService: FolderService, accessService: AccessService) { this.folderService = folderService; + this.accessService = accessService; } public async readHandler( @@ -18,7 +21,7 @@ export default class ItemController { try { const folder = await this.folderService.getByItemId(request.params.id); - if (folder.ownerId !== request.user.sub) { + if (!(await this.accessService.hasAccessToItem(folder.id, request.user.sub))) { return reply.unauthorized(); } @@ -42,7 +45,7 @@ export default class ItemController { try { const folder = await this.folderService.getByItemId(request.body.id); - if (folder.ownerId !== request.user.sub) { + if (!(await this.accessService.hasAccessToItem(folder.id, request.user.sub))) { return reply.unauthorized(); } @@ -66,9 +69,16 @@ export default class ItemController { reply: FastifyReply, ) { try { + if ( + request.body.parentId !== null && + request.body.parentId !== undefined && + !(await this.accessService.hasAccessToItem(request.body.parentId, request.user.sub)) + ) { + return reply.unauthorized(); + } + const folder = await this.folderService.createFolder({ name: request.body.name, - mimeType: 'application/vnd.cloudstore.folder', color: request.body.color, ownerId: request.user.sub, parentId: request.body.parentId ?? null, @@ -90,7 +100,7 @@ export default class ItemController { try { const folder = await this.folderService.getByItemId(request.params.id); - if (folder.ownerId !== request.user.sub) { + if (!(await this.accessService.hasAccessToItem(folder.id, request.user.sub))) { return reply.unauthorized(); } diff --git a/src/modules/item/folder/folder.route.ts b/src/modules/item/folder/folder.route.ts index 75cacf8..fdb0bb3 100644 --- a/src/modules/item/folder/folder.route.ts +++ b/src/modules/item/folder/folder.route.ts @@ -1,9 +1,16 @@ import { FastifyInstance } from 'fastify'; import FolderController from './folder.controller'; import FolderService from './folder.service'; +import AccessService from '../sharing/access.service'; +import ItemService from '../item.service'; +import SharingService from '../sharing/sharing.service'; export default async (fastify: FastifyInstance) => { - const folderController = new FolderController(new FolderService()); + const folderService = new FolderService(); + const folderController = new FolderController( + folderService, + new AccessService(new ItemService(), new SharingService()), + ); fastify.get( '/:id', diff --git a/src/modules/item/folder/folder.schema.ts b/src/modules/item/folder/folder.schema.ts index ac9f760..8711143 100644 --- a/src/modules/item/folder/folder.schema.ts +++ b/src/modules/item/folder/folder.schema.ts @@ -80,7 +80,7 @@ const editFolderSchema = { parentId: { type: ['number', 'null'], errorMessage: { - type: 'folder.itemid.type', + type: 'folder.parentId.type', }, }, }, @@ -144,7 +144,7 @@ const addFolderSchema = { parentId: { type: ['number', 'null'], errorMessage: { - type: 'folder.itemid.type', + type: 'folder.parentId.type', }, }, }, @@ -218,14 +218,13 @@ export type DeleteInput = FromSchema; export type AddFolder = { name: string; color: string; - mimeType: string; ownerId: number; parentId: number | null; }; export type ItemFolder = prismaItemFolderType & { item: Item }; export type Folder = Omit & Item; -export type UpdateFolder = { id: number } & Partial & UpdateItem; +export type UpdateFolder = { id: number } & Partial & Omit; export const folderSchemas = [ addFolderSchema, diff --git a/src/modules/item/folder/folder.service.ts b/src/modules/item/folder/folder.service.ts index f79a364..5dd672d 100644 --- a/src/modules/item/folder/folder.service.ts +++ b/src/modules/item/folder/folder.service.ts @@ -9,7 +9,7 @@ export default class FolderService { item: { create: { name: input.name, - mimeType: input.mimeType, + mimeType: 'application/vnd.cloudstore.folder', ownerId: input.ownerId, parentId: input.parentId, }, From 13f406edab1c4559fa35cc0ee1929ab87e8c00cb Mon Sep 17 00:00:00 2001 From: Anders Rasmussen Date: Wed, 4 Oct 2023 09:09:06 +0200 Subject: [PATCH 4/4] #9 - Added changes according to feedback --- src/modules/item/folder/__test__/delete.test.ts | 3 ++- src/modules/item/folder/folder.controller.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/item/folder/__test__/delete.test.ts b/src/modules/item/folder/__test__/delete.test.ts index 91d9785..927c5c9 100644 --- a/src/modules/item/folder/__test__/delete.test.ts +++ b/src/modules/item/folder/__test__/delete.test.ts @@ -49,7 +49,8 @@ describe('DELETE /api/folder/:id', () => { }, }); - expect(response.body).toEqual(''); + expect(response.statusCode).toBe(204); + expect(response.body).toBe(''); await expect(folderService.getByItemId(folder.id)).rejects.toThrowError(); await expect(itemService.getById(folder.id)).rejects.toThrowError(); diff --git a/src/modules/item/folder/folder.controller.ts b/src/modules/item/folder/folder.controller.ts index 2f61f2b..f2fa6da 100644 --- a/src/modules/item/folder/folder.controller.ts +++ b/src/modules/item/folder/folder.controller.ts @@ -105,6 +105,7 @@ export default class FolderController { } await this.folderService.deleteFolderByItemId(request.params.id); + return reply.code(204).send(); } catch (e) { if (e instanceof Error) { return reply.badRequest(request.i18n.t(e.message));