From dee12a8127e5f3ff30f233328c2adb626c8a0d03 Mon Sep 17 00:00:00 2001 From: Vedant Gupta <115912707+im-vedant@users.noreply.github.com> Date: Wed, 19 Feb 2025 23:07:28 +0530 Subject: [PATCH] Test to src/graphql/types/Mutation/deleteAgendaItem.ts (#3256) * Add test file for deleteAgendaItem mutation * fix code quality --- .../inputs/MutationDeleteAgendaItemInput.ts | 4 +- .../types/Mutation/deleteAgendaItem.ts | 239 ++++++------ .../types/Mutation/deleteAgendaItem.test.ts | 355 ++++++++++++++++++ 3 files changed, 482 insertions(+), 116 deletions(-) create mode 100644 test/graphql/types/Mutation/deleteAgendaItem.test.ts diff --git a/src/graphql/inputs/MutationDeleteAgendaItemInput.ts b/src/graphql/inputs/MutationDeleteAgendaItemInput.ts index fed0693433a..30eff346c84 100644 --- a/src/graphql/inputs/MutationDeleteAgendaItemInput.ts +++ b/src/graphql/inputs/MutationDeleteAgendaItemInput.ts @@ -2,12 +2,12 @@ import { z } from "zod"; import { agendaItemsTableInsertSchema } from "~/src/drizzle/tables/agendaItems"; import { builder } from "~/src/graphql/builder"; -export const mutationDeleteAgendaItemInputSchema = z.object({ +export const MutationDeleteAgendaItemInputSchema = z.object({ id: agendaItemsTableInsertSchema.shape.id.unwrap(), }); export const MutationDeleteAgendaItemInput = builder - .inputRef>( + .inputRef>( "MutationDeleteAgendaItemInput", ) .implement({ diff --git a/src/graphql/types/Mutation/deleteAgendaItem.ts b/src/graphql/types/Mutation/deleteAgendaItem.ts index 002562d7d26..2cce13a45f8 100644 --- a/src/graphql/types/Mutation/deleteAgendaItem.ts +++ b/src/graphql/types/Mutation/deleteAgendaItem.ts @@ -2,160 +2,171 @@ import { eq } from "drizzle-orm"; import { z } from "zod"; import { agendaItemsTable } from "~/src/drizzle/tables/agendaItems"; import { builder } from "~/src/graphql/builder"; +import type { GraphQLContext } from "~/src/graphql/context"; import { MutationDeleteAgendaItemInput, - mutationDeleteAgendaItemInputSchema, + MutationDeleteAgendaItemInputSchema, } from "~/src/graphql/inputs/MutationDeleteAgendaItemInput"; import { AgendaItem } from "~/src/graphql/types/AgendaItem/AgendaItem"; import { TalawaGraphQLError } from "~/src/utilities/TalawaGraphQLError"; const mutationDeleteAgendaItemArgumentsSchema = z.object({ - input: mutationDeleteAgendaItemInputSchema, + input: MutationDeleteAgendaItemInputSchema, }); -builder.mutationField("deleteAgendaItem", (t) => - t.field({ - args: { - input: t.arg({ - description: "", - required: true, - type: MutationDeleteAgendaItemInput, - }), - }, - description: "Mutation field to delete an agenda item.", - resolve: async (_parent, args, ctx) => { - if (!ctx.currentClient.isAuthenticated) { - throw new TalawaGraphQLError({ - extensions: { - code: "unauthenticated", - }, - }); - } +export async function deleteAgendaItemResolver( + _parent: unknown, + args: { + input: { + id: string; + }; + }, + ctx: GraphQLContext, +) { + if (!ctx.currentClient.isAuthenticated) { + throw new TalawaGraphQLError({ + extensions: { + code: "unauthenticated", + }, + }); + } - const { - data: parsedArgs, - error, - success, - } = mutationDeleteAgendaItemArgumentsSchema.safeParse(args); + const { + data: parsedArgs, + error, + success, + } = mutationDeleteAgendaItemArgumentsSchema.safeParse(args); - if (!success) { - throw new TalawaGraphQLError({ - extensions: { - code: "invalid_arguments", - issues: error.issues.map((issue) => ({ - argumentPath: issue.path, - message: issue.message, - })), - }, - }); - } + if (!success) { + throw new TalawaGraphQLError({ + extensions: { + code: "invalid_arguments", + issues: error.issues.map((issue) => ({ + argumentPath: issue.path, + message: issue.message, + })), + }, + }); + } - const currentUserId = ctx.currentClient.user.id; + const currentUserId = ctx.currentClient.user.id; - const [currentUser, existingAgendaItem] = await Promise.all([ - ctx.drizzleClient.query.usersTable.findFirst({ + const [currentUser, existingAgendaItem] = await Promise.all([ + ctx.drizzleClient.query.usersTable.findFirst({ + columns: { + role: true, + }, + where: (fields, operators) => operators.eq(fields.id, currentUserId), + }), + ctx.drizzleClient.query.agendaItemsTable.findFirst({ + columns: { + type: true, + }, + with: { + folder: { columns: { - role: true, - }, - where: (fields, operators) => operators.eq(fields.id, currentUserId), - }), - ctx.drizzleClient.query.agendaItemsTable.findFirst({ - columns: { - type: true, + isAgendaItemFolder: true, }, with: { - folder: { + event: { columns: { - isAgendaItemFolder: true, + startAt: true, }, with: { - event: { + organization: { columns: { - startAt: true, + countryCode: true, }, with: { - organization: { + membershipsWhereOrganization: { columns: { - countryCode: true, - }, - with: { - membershipsWhereOrganization: { - columns: { - role: true, - }, - where: (fields, operators) => - operators.eq(fields.memberId, currentUserId), - }, + role: true, }, + where: (fields, operators) => + operators.eq(fields.memberId, currentUserId), }, }, }, }, }, }, - where: (fields, operators) => - operators.eq(fields.id, parsedArgs.input.id), - }), - ]); + }, + }, + where: (fields, operators) => + operators.eq(fields.id, parsedArgs.input.id), + }), + ]); - if (currentUser === undefined) { - throw new TalawaGraphQLError({ - extensions: { - code: "unauthenticated", - }, - }); - } + if (currentUser === undefined) { + throw new TalawaGraphQLError({ + extensions: { + code: "unauthenticated", + }, + }); + } - if (existingAgendaItem === undefined) { - throw new TalawaGraphQLError({ - extensions: { - code: "arguments_associated_resources_not_found", - issues: [ - { - argumentPath: ["input", "id"], - }, - ], + if (existingAgendaItem === undefined) { + throw new TalawaGraphQLError({ + extensions: { + code: "arguments_associated_resources_not_found", + issues: [ + { + argumentPath: ["input", "id"], }, - }); - } + ], + }, + }); + } - const currentUserOrganizationMembership = - existingAgendaItem.folder.event.organization - .membershipsWhereOrganization[0]; + const currentUserOrganizationMembership = + existingAgendaItem.folder.event.organization + .membershipsWhereOrganization[0]; - if ( - currentUser.role !== "administrator" && - (currentUserOrganizationMembership === undefined || - currentUserOrganizationMembership.role !== "administrator") - ) { - throw new TalawaGraphQLError({ - extensions: { - code: "unauthorized_action_on_arguments_associated_resources", - issues: [ - { - argumentPath: ["input", "id"], - }, - ], + if ( + currentUser.role !== "administrator" && + (currentUserOrganizationMembership === undefined || + currentUserOrganizationMembership.role !== "administrator") + ) { + throw new TalawaGraphQLError({ + extensions: { + code: "unauthorized_action_on_arguments_associated_resources", + issues: [ + { + argumentPath: ["input", "id"], }, - }); - } + ], + }, + }); + } - const [deletedAgendaItem] = await ctx.drizzleClient - .delete(agendaItemsTable) - .where(eq(agendaItemsTable.id, parsedArgs.input.id)) - .returning(); + const [deletedAgendaItem] = await ctx.drizzleClient + .delete(agendaItemsTable) + .where(eq(agendaItemsTable.id, parsedArgs.input.id)) + .returning(); - // Deleted agenda item not being returned means that either it was deleted or its `id` column was changed by external entities before this delete operation could take place. - if (deletedAgendaItem === undefined) { - throw new TalawaGraphQLError({ - extensions: { - code: "unexpected", - }, - }); - } + // Deleted agenda item not being returned means that either it was deleted or its `id` column was changed by external entities before this delete operation could take place. + if (deletedAgendaItem === undefined) { + throw new TalawaGraphQLError({ + extensions: { + code: "unexpected", + }, + }); + } - return deletedAgendaItem; + return deletedAgendaItem; +} + +builder.mutationField("deleteAgendaItem", (t) => + t.field({ + args: { + input: t.arg({ + description: "", + required: true, + type: MutationDeleteAgendaItemInput, + }), }, + description: "Mutation field to delete an agenda item.", + resolve: deleteAgendaItemResolver, type: AgendaItem, }), ); diff --git a/test/graphql/types/Mutation/deleteAgendaItem.test.ts b/test/graphql/types/Mutation/deleteAgendaItem.test.ts new file mode 100644 index 00000000000..61b90bf9f67 --- /dev/null +++ b/test/graphql/types/Mutation/deleteAgendaItem.test.ts @@ -0,0 +1,355 @@ +import type { FastifyBaseLogger } from "fastify"; +import type { Client as MinioClient } from "minio"; +import { createMockLogger } from "test/utilities/mockLogger"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { GraphQLContext } from "~/src/graphql/context"; +import { deleteAgendaItemResolver } from "~/src/graphql/types/Mutation/deleteAgendaItem"; + +interface MockDrizzleClient { + query: { + usersTable: { + findFirst: ReturnType; + }; + agendaItemsTable: { + findFirst: ReturnType; + }; + }; + delete: ReturnType; + where: ReturnType; + returning: ReturnType; +} + +// Simplified TestContext that uses the mock client +interface TestContext extends Omit { + drizzleClient: MockDrizzleClient & GraphQLContext["drizzleClient"]; + log: FastifyBaseLogger; +} + +// Mock the Drizzle client +const drizzleClientMock = { + query: { + usersTable: { + findFirst: vi.fn(), + }, + agendaItemsTable: { + findFirst: vi.fn(), + }, + }, + delete: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + returning: vi.fn(), +} as TestContext["drizzleClient"]; + +const mockLogger = createMockLogger(); + +const authenticatedContext: TestContext = { + currentClient: { + isAuthenticated: true, + user: { + id: "user_1", + }, + }, + drizzleClient: drizzleClientMock, + log: mockLogger, + envConfig: { + API_BASE_URL: "http://localhost:3000", + }, + jwt: { + sign: vi.fn().mockReturnValue("mock-token"), + }, + minio: { + bucketName: "talawa", + client: {} as MinioClient, // minimal mock that satisfies the type + }, + pubsub: { + publish: vi.fn(), + subscribe: vi.fn(), + }, +}; + +const unauthenticatedContext: TestContext = { + ...authenticatedContext, + currentClient: { + isAuthenticated: false, + }, +}; + +describe("deleteAgendaItem", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + // user is unauthenticated + it("should throw an error if the user is not authenticated", async () => { + await expect( + deleteAgendaItemResolver( + {}, + { + input: { + id: "1", + }, + }, + unauthenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + message: expect.any(String), + extensions: { code: "unauthenticated" }, + }), + ); + }); + it("should throw invalid_arguments error when deleting agenda item with invalid UUID format", async () => { + await expect( + deleteAgendaItemResolver( + {}, + { + input: { + id: "invalid-id", + }, + }, + authenticatedContext, + ), + ).rejects.toMatchObject({ + extensions: { code: "invalid_arguments" }, + }); + }); + + it("should throw unauthenticated error when user ID from token is not found in database", async () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue(undefined); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue( + undefined, + ); + + await expect( + deleteAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + message: expect.any(String), + extensions: { + code: "unauthenticated", + }, + }), + ); + }); + it("should throw arguments_associated_resources_not_found error when agenda item ID does not exist", async () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "regular", + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue( + undefined, + ); + + await expect( + deleteAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + message: expect.any(String), + extensions: { + code: "arguments_associated_resources_not_found", + issues: [ + { + argumentPath: ["input", "id"], + }, + ], + }, + }), + ); + }); + + it("should throw unauthorized_action error when non-admin user attempts to delete agenda item", async () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "regular", + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue({ + type: "general", + isAgendaItemFolder: true, + folder: { + startAt: "2025-02-19T10:00:00.000Z", + event: { + organization: { + countryCode: "us", + membershipsWhereOrganization: [ + { + role: "regular", + }, + ], + }, + }, + }, + }); + + await expect( + deleteAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + message: expect.any(String), + extensions: { + code: "unauthorized_action_on_arguments_associated_resources", + issues: [ + { + argumentPath: ["input", "id"], + }, + ], + }, + }), + ); + }); + + it("should throw unauthorized_action error when non admin user attempts to delete agenda item and current user organization membership is undefined", async () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "regular", + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue({ + type: "general", + isAgendaItemFolder: true, + folder: { + startAt: "2025-02-19T10:00:00.000Z", + event: { + organization: { + countryCode: "us", + membershipsWhereOrganization: [], + }, + }, + }, + }); + + await expect( + deleteAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + message: expect.any(String), + extensions: { + code: "unauthorized_action_on_arguments_associated_resources", + issues: [ + { + argumentPath: ["input", "id"], + }, + ], + }, + }), + ); + }); + + it("should delete the agenda item successfully", async () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "administrator", + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue({ + type: "general", + isAgendaItemFolder: true, + folder: { + startAt: "2025-02-19T10:00:00.000Z", + event: { + organization: { + countryCode: "us", + membershipsWhereOrganization: [ + { + role: "administrator", + }, + ], + }, + }, + }, + }); + drizzleClientMock.delete.mockReturnValue({ + where: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue([ + { + id: "123e4567-e89b-12d3-a456-426614174000", + }, + ]), + }); + + await expect( + deleteAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + }, + }, + authenticatedContext, + ), + ).resolves.toEqual({ + id: "123e4567-e89b-12d3-a456-426614174000", + }); + }); + + // it should fail to delete the agenda item + it("should fail to delete the agenda item", async () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "administrator", + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue({ + type: "general", + isAgendaItemFolder: true, + folder: { + startAt: "2025-02-19T10:00:00.000Z", + event: { + organization: { + countryCode: "us", + membershipsWhereOrganization: [ + { + role: "administrator", + }, + ], + }, + }, + }, + }); + drizzleClientMock.delete.mockReturnValue({ + where: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue([]), + }); + + await expect( + deleteAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + message: expect.any(String), + extensions: { + code: "unexpected", + }, + }), + ); + }); +});