diff --git a/src/services/index.ts b/src/services/index.ts index a5d37f9..78c8c88 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -4,10 +4,12 @@ import { usersService } from './users/index.js' import { guildsService } from './guilds/index.js' import { uploadsService } from './uploads/index.js' import { channelsService } from './channels/index.js' +import { messagesService } from './messages/index.js' export const services: FastifyPluginAsync = async (fastify) => { await fastify.register(channelsService) await fastify.register(guildsService) + await fastify.register(messagesService) await fastify.register(uploadsService) await fastify.register(usersService) } diff --git a/src/services/messages/[messageId]/__test__/delete.test.ts b/src/services/messages/[messageId]/__test__/delete.test.ts new file mode 100644 index 0000000..d730a72 --- /dev/null +++ b/src/services/messages/[messageId]/__test__/delete.test.ts @@ -0,0 +1,90 @@ +import { application } from '../../../../application.js' +import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js' +import { prismaMock } from '../../../../__test__/setup.js' +import { messageExample } from '../../../../models/Message.js' +import { memberExample } from '../../../../models/Member.js' +import { userExample } from '../../../../models/User.js' +import { channelExample } from '../../../../models/Channel.js' + +describe('DELETE /messsages/[messageId]', () => { + it('succeeds', async () => { + prismaMock.message.findFirst.mockResolvedValue({ + ...messageExample, + channel: channelExample + } as any) + prismaMock.member.findFirst.mockResolvedValue({ + ...memberExample, + user: userExample + } as any) + prismaMock.message.delete.mockResolvedValue(messageExample) + const { accessToken } = await authenticateUserTest() + const response = await application.inject({ + method: 'DELETE', + url: `/messages/${messageExample.id}`, + headers: { + authorization: `Bearer ${accessToken}` + } + }) + const responseJson = response.json() + expect(response.statusCode).toEqual(200) + expect(responseJson.id).toEqual(messageExample.id) + expect(responseJson.value).toEqual(messageExample.value) + expect(responseJson.type).toEqual(messageExample.type) + expect(responseJson.mimetype).toEqual(messageExample.mimetype) + expect(responseJson.member.id).toEqual(memberExample.id) + expect(responseJson.member.isOwner).toEqual(memberExample.isOwner) + expect(responseJson.member.user.id).toEqual(userExample.id) + expect(responseJson.member.user.name).toEqual(userExample.name) + }) + + it('fails if the message is not found', async () => { + prismaMock.message.findFirst.mockResolvedValue(null) + const { accessToken } = await authenticateUserTest() + const response = await application.inject({ + method: 'DELETE', + url: `/messages/${messageExample.id}`, + headers: { + authorization: `Bearer ${accessToken}` + } + }) + expect(response.statusCode).toEqual(404) + }) + + it('fails if the member is not found', async () => { + prismaMock.message.findFirst.mockResolvedValue({ + ...messageExample, + channel: channelExample + } as any) + prismaMock.member.findFirst.mockResolvedValue(null) + const { accessToken } = await authenticateUserTest() + const response = await application.inject({ + method: 'DELETE', + url: `/messages/${messageExample.id}`, + headers: { + authorization: `Bearer ${accessToken}` + } + }) + expect(response.statusCode).toEqual(404) + }) + + it('fails if the member is not owner of the message', async () => { + const randomUserIdOwnerOfMessage = 14 + prismaMock.message.findFirst.mockResolvedValue({ + ...messageExample, + channel: channelExample + } as any) + prismaMock.member.findFirst.mockResolvedValue({ + ...memberExample, + userId: randomUserIdOwnerOfMessage + }) + const { accessToken } = await authenticateUserTest() + const response = await application.inject({ + method: 'DELETE', + url: `/messages/${messageExample.id}`, + headers: { + authorization: `Bearer ${accessToken}` + } + }) + expect(response.statusCode).toEqual(400) + }) +}) diff --git a/src/services/messages/[messageId]/__test__/put.test.ts b/src/services/messages/[messageId]/__test__/put.test.ts new file mode 100644 index 0000000..c82afc4 --- /dev/null +++ b/src/services/messages/[messageId]/__test__/put.test.ts @@ -0,0 +1,109 @@ +import { application } from '../../../../application.js' +import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js' +import { prismaMock } from '../../../../__test__/setup.js' +import { messageExample } from '../../../../models/Message.js' +import { memberExample } from '../../../../models/Member.js' +import { userExample } from '../../../../models/User.js' +import { channelExample } from '../../../../models/Channel.js' + +describe('PUT /messsages/[messageId]', () => { + it('succeeds', async () => { + const newValue = 'some message' + prismaMock.message.findFirst.mockResolvedValue({ + ...messageExample, + channel: channelExample + } as any) + prismaMock.member.findFirst.mockResolvedValue({ + ...memberExample, + user: userExample + } as any) + prismaMock.message.update.mockResolvedValue({ + ...messageExample, + value: newValue + }) + const { accessToken } = await authenticateUserTest() + const response = await application.inject({ + method: 'PUT', + url: `/messages/${messageExample.id}`, + headers: { + authorization: `Bearer ${accessToken}` + }, + payload: { + value: newValue + } + }) + const responseJson = response.json() + expect(response.statusCode).toEqual(200) + expect(responseJson.id).toEqual(messageExample.id) + expect(responseJson.value).toEqual(newValue) + expect(responseJson.type).toEqual(messageExample.type) + expect(responseJson.mimetype).toEqual(messageExample.mimetype) + expect(responseJson.member.id).toEqual(memberExample.id) + expect(responseJson.member.isOwner).toEqual(memberExample.isOwner) + expect(responseJson.member.user.id).toEqual(userExample.id) + expect(responseJson.member.user.name).toEqual(userExample.name) + }) + + it('fails if the message is not found', async () => { + const newValue = 'some message' + prismaMock.message.findFirst.mockResolvedValue(null) + const { accessToken } = await authenticateUserTest() + const response = await application.inject({ + method: 'PUT', + url: `/messages/${messageExample.id}`, + headers: { + authorization: `Bearer ${accessToken}` + }, + payload: { + value: newValue + } + }) + expect(response.statusCode).toEqual(404) + }) + + it('fails if the member is not found', async () => { + const newValue = 'some message' + prismaMock.message.findFirst.mockResolvedValue({ + ...messageExample, + channel: channelExample + } as any) + prismaMock.member.findFirst.mockResolvedValue(null) + const { accessToken } = await authenticateUserTest() + const response = await application.inject({ + method: 'PUT', + url: `/messages/${messageExample.id}`, + headers: { + authorization: `Bearer ${accessToken}` + }, + payload: { + value: newValue + } + }) + expect(response.statusCode).toEqual(404) + }) + + it('fails if the member is not owner of the message', async () => { + const newValue = 'some message' + const randomUserIdOwnerOfMessage = 14 + prismaMock.message.findFirst.mockResolvedValue({ + ...messageExample, + channel: channelExample + } as any) + prismaMock.member.findFirst.mockResolvedValue({ + ...memberExample, + userId: randomUserIdOwnerOfMessage + }) + const { accessToken } = await authenticateUserTest() + const response = await application.inject({ + method: 'PUT', + url: `/messages/${messageExample.id}`, + headers: { + authorization: `Bearer ${accessToken}` + }, + payload: { + value: newValue + } + }) + expect(response.statusCode).toEqual(400) + }) +}) diff --git a/src/services/messages/[messageId]/delete.ts b/src/services/messages/[messageId]/delete.ts new file mode 100644 index 0000000..7af3fd5 --- /dev/null +++ b/src/services/messages/[messageId]/delete.ts @@ -0,0 +1,118 @@ +import { Static, Type } from '@sinclair/typebox' +import { FastifyPluginAsync, FastifySchema } from 'fastify' + +import prisma from '../../../tools/database/prisma.js' +import { fastifyErrors } from '../../../models/utils.js' +import authenticateUser from '../../../tools/plugins/authenticateUser.js' +import { messageSchema } from '../../../models/Message.js' +import { memberSchema } from '../../../models/Member.js' +import { userPublicWithoutSettingsSchema } from '../../../models/User.js' + +const parametersSchema = Type.Object({ + messageId: messageSchema.id +}) + +type Parameters = Static + +const putServiceSchema: FastifySchema = { + description: 'UPDATE a message with its id.', + tags: ['messages'] as string[], + security: [ + { + bearerAuth: [] + } + ] as Array<{ [key: string]: [] }>, + params: parametersSchema, + response: { + 200: Type.Object({ + ...messageSchema, + member: Type.Object({ + ...memberSchema, + user: Type.Object(userPublicWithoutSettingsSchema) + }) + }), + 400: fastifyErrors[400], + 401: fastifyErrors[401], + 403: fastifyErrors[403], + 404: fastifyErrors[404], + 500: fastifyErrors[500] + } +} as const + +export const deleteMessageService: FastifyPluginAsync = async (fastify) => { + await fastify.register(authenticateUser) + + fastify.route<{ + Params: Parameters + }>({ + method: 'DELETE', + url: '/messages/:messageId', + schema: putServiceSchema, + handler: async (request, reply) => { + if (request.user == null) { + throw fastify.httpErrors.forbidden() + } + const { user } = request + const { messageId } = request.params + const messageCheck = await prisma.message.findFirst({ + where: { id: messageId }, + include: { + channel: true + } + }) + if (messageCheck == null || messageCheck.channel == null) { + throw fastify.httpErrors.notFound('Message not found') + } + const member = await prisma.member.findFirst({ + where: { + guildId: messageCheck.channel.guildId, + userId: user.current.id + }, + include: { + user: { + select: { + id: true, + name: true, + logo: true, + status: true, + biography: true, + website: true, + createdAt: true, + updatedAt: true + } + } + } + }) + if (member == null) { + throw fastify.httpErrors.notFound('Member not found') + } + if (member.userId !== user.current.id) { + throw fastify.httpErrors.badRequest( + 'You should be the owner of the message' + ) + } + const message = await prisma.message.delete({ + where: { + id: messageCheck.id + } + }) + const item = { + ...message, + member: { + ...member, + user: { + ...member.user, + email: null + } + } + } + await fastify.io.emitToMembers({ + event: 'messages', + guildId: item.member.guildId, + payload: { action: 'delete', item } + }) + reply.statusCode = 200 + return item + } + }) +} diff --git a/src/services/messages/[messageId]/put.ts b/src/services/messages/[messageId]/put.ts new file mode 100644 index 0000000..c5f00e8 --- /dev/null +++ b/src/services/messages/[messageId]/put.ts @@ -0,0 +1,130 @@ +import { Static, Type } from '@sinclair/typebox' +import { FastifyPluginAsync, FastifySchema } from 'fastify' + +import prisma from '../../../tools/database/prisma.js' +import { fastifyErrors } from '../../../models/utils.js' +import authenticateUser from '../../../tools/plugins/authenticateUser.js' +import { messageSchema } from '../../../models/Message.js' +import { memberSchema } from '../../../models/Member.js' +import { userPublicWithoutSettingsSchema } from '../../../models/User.js' + +const bodyPutServiceSchema = Type.Object({ + value: messageSchema.value +}) + +type BodyPutServiceSchemaType = Static + +const parametersSchema = Type.Object({ + messageId: messageSchema.id +}) + +type Parameters = Static + +const putServiceSchema: FastifySchema = { + description: 'UPDATE a message with its id.', + tags: ['messages'] as string[], + security: [ + { + bearerAuth: [] + } + ] as Array<{ [key: string]: [] }>, + body: bodyPutServiceSchema, + params: parametersSchema, + response: { + 200: Type.Object({ + ...messageSchema, + member: Type.Object({ + ...memberSchema, + user: Type.Object(userPublicWithoutSettingsSchema) + }) + }), + 400: fastifyErrors[400], + 401: fastifyErrors[401], + 403: fastifyErrors[403], + 404: fastifyErrors[404], + 500: fastifyErrors[500] + } +} as const + +export const putMessageService: FastifyPluginAsync = async (fastify) => { + await fastify.register(authenticateUser) + + fastify.route<{ + Body: BodyPutServiceSchemaType + Params: Parameters + }>({ + method: 'PUT', + url: '/messages/:messageId', + schema: putServiceSchema, + handler: async (request, reply) => { + if (request.user == null) { + throw fastify.httpErrors.forbidden() + } + const { user } = request + const { messageId } = request.params + const { value } = request.body + const messageCheck = await prisma.message.findFirst({ + where: { id: messageId, type: 'text' }, + include: { + channel: true + } + }) + if (messageCheck == null || messageCheck.channel == null) { + throw fastify.httpErrors.notFound('Message not found') + } + const member = await prisma.member.findFirst({ + where: { + guildId: messageCheck.channel.guildId, + userId: user.current.id + }, + include: { + user: { + select: { + id: true, + name: true, + logo: true, + status: true, + biography: true, + website: true, + createdAt: true, + updatedAt: true + } + } + } + }) + if (member == null) { + throw fastify.httpErrors.notFound('Member not found') + } + if (member.userId !== user.current.id) { + throw fastify.httpErrors.badRequest( + 'You should be the owner of the message' + ) + } + const message = await prisma.message.update({ + where: { + id: messageCheck.id + }, + data: { + value + } + }) + const item = { + ...message, + member: { + ...member, + user: { + ...member.user, + email: null + } + } + } + await fastify.io.emitToMembers({ + event: 'messages', + guildId: item.member.guildId, + payload: { action: 'update', item } + }) + reply.statusCode = 200 + return item + } + }) +} diff --git a/src/services/messages/index.ts b/src/services/messages/index.ts new file mode 100644 index 0000000..b53cb2b --- /dev/null +++ b/src/services/messages/index.ts @@ -0,0 +1,9 @@ +import { FastifyPluginAsync } from 'fastify' + +import { deleteMessageService } from './[messageId]/delete.js' +import { putMessageService } from './[messageId]/put.js' + +export const messagesService: FastifyPluginAsync = async (fastify) => { + await fastify.register(putMessageService) + await fastify.register(deleteMessageService) +}