From f76fd0c6a2aad994db4689e6be5a67f3f2745ab6 Mon Sep 17 00:00:00 2001 From: Shiva <148421597+shivasankaran18@users.noreply.github.com> Date: Sat, 15 Feb 2025 21:31:05 +0530 Subject: [PATCH] Test to Mutation/updateAgendaItem.ts (#3216) * feat:added updateAgendaItem.test.ts * fix:code_quality * fix:coderabbit sugg * fix:improved codequality --- .../inputs/MutationUpdateAgendaItemInput.ts | 4 +- .../types/Mutation/updateAgendaItem.ts | 467 ++++++----- .../types/Mutation/updateAgendaItem.test.ts | 756 ++++++++++++++++++ 3 files changed, 986 insertions(+), 241 deletions(-) create mode 100644 test/graphql/types/Mutation/updateAgendaItem.test.ts diff --git a/src/graphql/inputs/MutationUpdateAgendaItemInput.ts b/src/graphql/inputs/MutationUpdateAgendaItemInput.ts index 5e532013faa..a3f5346ec26 100644 --- a/src/graphql/inputs/MutationUpdateAgendaItemInput.ts +++ b/src/graphql/inputs/MutationUpdateAgendaItemInput.ts @@ -2,7 +2,7 @@ import type { z } from "zod"; import { agendaItemsTableInsertSchema } from "~/src/drizzle/tables/agendaItems"; import { builder } from "~/src/graphql/builder"; -export const mutationUpdateAgendaItemInputSchema = agendaItemsTableInsertSchema +export const MutationUpdateAgendaItemInputSchema = agendaItemsTableInsertSchema .pick({ description: true, duration: true, @@ -22,7 +22,7 @@ export const mutationUpdateAgendaItemInputSchema = agendaItemsTableInsertSchema ); export const MutationUpdateAgendaItemInput = builder - .inputRef>( + .inputRef>( "MutationUpdateAgendaItemInput", ) .implement({ diff --git a/src/graphql/types/Mutation/updateAgendaItem.ts b/src/graphql/types/Mutation/updateAgendaItem.ts index 28978e5bad8..b58581cad0c 100644 --- a/src/graphql/types/Mutation/updateAgendaItem.ts +++ b/src/graphql/types/Mutation/updateAgendaItem.ts @@ -2,296 +2,285 @@ 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 { MutationUpdateAgendaItemInput, - mutationUpdateAgendaItemInputSchema, + MutationUpdateAgendaItemInputSchema, } from "~/src/graphql/inputs/MutationUpdateAgendaItemInput"; import { AgendaItem } from "~/src/graphql/types/AgendaItem/AgendaItem"; import { TalawaGraphQLError } from "~/src/utilities/TalawaGraphQLError"; import { isNotNullish } from "~/src/utilities/isNotNullish"; const mutationUpdateAgendaItemArgumentsSchema = z.object({ - input: mutationUpdateAgendaItemInputSchema, + input: MutationUpdateAgendaItemInputSchema, }); -builder.mutationField("updateAgendaItem", (t) => - t.field({ - args: { - input: t.arg({ - description: "", - required: true, - type: MutationUpdateAgendaItemInput, - }), - }, - description: "Mutation field to update an agenda item.", - resolve: async (_parent, args, ctx) => { - if (!ctx.currentClient.isAuthenticated) { - throw new TalawaGraphQLError({ - extensions: { - code: "unauthenticated", - }, - }); - } +export async function updateAgendaItemResolver( + _parent: unknown, + args: { + input: { + id: string; + name?: string | null | undefined; + description?: string | null | undefined; + duration?: string | null | undefined; + folderId?: string | null | undefined; + key?: string | null | undefined; + }; + }, + ctx: GraphQLContext, +) { + if (!ctx.currentClient.isAuthenticated) { + throw new TalawaGraphQLError({ + extensions: { + code: "unauthenticated", + }, + }); + } - const { - data: parsedArgs, - error, - success, - } = mutationUpdateAgendaItemArgumentsSchema.safeParse(args); + const { + data: parsedArgs, + error, + success, + } = mutationUpdateAgendaItemArgumentsSchema.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({ - columns: { - role: true, - }, - where: (fields, operators) => operators.eq(fields.id, currentUserId), - }), - ctx.drizzleClient.query.agendaItemsTable.findFirst({ + const currentUser = await ctx.drizzleClient.query.usersTable.findFirst({ + columns: { + role: true, + }, + where: (fields, operators) => operators.eq(fields.id, currentUserId), + }); + + if (currentUser === undefined) { + throw new TalawaGraphQLError({ + extensions: { + code: "unauthenticated", + }, + }); + } + + const existingAgendaItem = + await ctx.drizzleClient.query.agendaItemsTable.findFirst({ + columns: { + type: true, + }, + with: { + folder: { columns: { - type: true, + eventId: true, }, with: { - folder: { - columns: { - eventId: true, - }, + event: { + columns: {}, with: { - event: { - columns: { - startAt: true, - }, + organization: { + columns: {}, 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, { eq }) => eq(fields.id, parsedArgs.input.id), + }); - 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"], }, + ], + }, + }); + } + + function validateAgendaItemTypeConstraints( + type: string, + input: { + duration?: string | null | undefined; + key?: string | null | undefined; + }, + ) { + const issues: Array<{ argumentPath: string[]; message: string }> = []; + + if (type === "note") { + if (input.duration !== undefined) { + issues.push({ + argumentPath: ["input", "duration"], + message: `Cannot be provided for an agenda item of type "${type}"`, }); } - - if (existingAgendaItem === undefined) { - throw new TalawaGraphQLError({ - extensions: { - code: "arguments_associated_resources_not_found", - issues: [ - { - argumentPath: ["input", "id"], - }, - ], - }, + if (input.key !== undefined) { + issues.push({ + argumentPath: ["input", "key"], + message: `Cannot be provided for an agenda item of type "${type}"`, }); } + } - if (existingAgendaItem.type === "note") { - if ( - parsedArgs.input.duration !== undefined && - parsedArgs.input.key !== undefined - ) { - throw new TalawaGraphQLError({ - extensions: { - code: "forbidden_action_on_arguments_associated_resources", - issues: [ - { - argumentPath: ["input", "duration"], - message: `Cannot be provided for an agenda item of type "${existingAgendaItem.type}"`, - }, - { - argumentPath: ["input", "key"], - message: `Cannot be provided for an agenda item of type "${existingAgendaItem.type}"`, - }, - ], - }, - }); - } - - if (parsedArgs.input.duration !== undefined) { - throw new TalawaGraphQLError({ - extensions: { - code: "forbidden_action_on_arguments_associated_resources", - issues: [ - { - argumentPath: ["input", "duration"], - message: `Cannot be provided for an agenda item of type "${existingAgendaItem.type}"`, - }, - ], - }, - }); - } + if ( + (type === "general" || type === "scripture") && + input.key !== undefined + ) { + issues.push({ + argumentPath: ["input", "key"], + message: `Cannot be provided for an agenda item of type "${type}"`, + }); + } - if (parsedArgs.input.key !== undefined) { - throw new TalawaGraphQLError({ - extensions: { - code: "forbidden_action_on_arguments_associated_resources", - issues: [ - { - argumentPath: ["input", "key"], - message: `Cannot be provided for an agenda item of type "${existingAgendaItem.type}"`, - }, - ], - }, - }); - } - } + return issues; + } + const validationIssues = validateAgendaItemTypeConstraints( + existingAgendaItem.type, + parsedArgs.input, + ); - if ( - (existingAgendaItem.type === "general" || - existingAgendaItem.type === "scripture") && - parsedArgs.input.key !== undefined - ) { - throw new TalawaGraphQLError({ - extensions: { - code: "forbidden_action_on_arguments_associated_resources", - issues: [ - { - argumentPath: ["input", "key"], - message: `Cannot be provided for an agenda item of type "${existingAgendaItem.type}"`, - }, - ], - }, - }); - } + if (validationIssues.length > 0) { + throw new TalawaGraphQLError({ + extensions: { + code: "forbidden_action_on_arguments_associated_resources", + issues: validationIssues, + }, + }); + } - if (isNotNullish(parsedArgs.input.folderId)) { - const folderId = parsedArgs.input.folderId; + if (isNotNullish(parsedArgs.input.folderId)) { + const folderId = parsedArgs.input.folderId; - const existingAgendaFolder = - await ctx.drizzleClient.query.agendaFoldersTable.findFirst({ - columns: { - eventId: true, - isAgendaItemFolder: true, - }, - where: (fields, operators) => operators.eq(fields.id, folderId), - }); + const existingAgendaFolder = + await ctx.drizzleClient.query.agendaFoldersTable.findFirst({ + columns: { + eventId: true, + isAgendaItemFolder: true, + }, + where: (fields, operators) => operators.eq(fields.id, folderId), + }); - if (existingAgendaFolder === undefined) { - throw new TalawaGraphQLError({ - extensions: { - code: "arguments_associated_resources_not_found", - issues: [ - { - argumentPath: ["input", "folderId"], - }, - ], + if (existingAgendaFolder === undefined) { + throw new TalawaGraphQLError({ + extensions: { + code: "arguments_associated_resources_not_found", + issues: [ + { + argumentPath: ["input", "folderId"], }, - }); - } + ], + }, + }); + } - if ( - existingAgendaFolder.eventId !== existingAgendaItem.folder.eventId - ) { - throw new TalawaGraphQLError({ - extensions: { - code: "forbidden_action_on_arguments_associated_resources", - issues: [ - { - argumentPath: ["input", "folderId"], - message: - "This agenda folder does not belong to the event to the agenda item.", - }, - ], + if (existingAgendaFolder.eventId !== existingAgendaItem.folder.eventId) { + throw new TalawaGraphQLError({ + extensions: { + code: "forbidden_action_on_arguments_associated_resources", + issues: [ + { + argumentPath: ["input", "folderId"], + message: + "This agenda folder does not belong to the event to the agenda item.", }, - }); - } + ], + }, + }); + } - if (!existingAgendaFolder.isAgendaItemFolder) { - throw new TalawaGraphQLError({ - extensions: { - code: "forbidden_action_on_arguments_associated_resources", - issues: [ - { - argumentPath: ["input", "folderId"], - message: - "This agenda folder cannot be a folder to agenda items.", - }, - ], + if (!existingAgendaFolder.isAgendaItemFolder) { + throw new TalawaGraphQLError({ + extensions: { + code: "forbidden_action_on_arguments_associated_resources", + issues: [ + { + argumentPath: ["input", "folderId"], + message: "This agenda folder cannot be a folder to agenda items.", }, - }); - } - } + ], + }, + }); + } + } - 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 [updatedAgendaItem] = await ctx.drizzleClient - .update(agendaItemsTable) - .set({ - description: parsedArgs.input.description, - duration: parsedArgs.input.duration, - folderId: parsedArgs.input.folderId, - key: parsedArgs.input.key, - name: parsedArgs.input.name, - updaterId: currentUserId, - }) - .where(eq(agendaItemsTable.id, parsedArgs.input.id)) - .returning(); + const [updatedAgendaItem] = await ctx.drizzleClient + .update(agendaItemsTable) + .set({ + description: parsedArgs.input.description, + duration: parsedArgs.input.duration, + folderId: parsedArgs.input.folderId, + key: parsedArgs.input.key, + name: parsedArgs.input.name, + updaterId: currentUserId, + }) + .where(eq(agendaItemsTable.id, parsedArgs.input.id)) + .returning(); - // Updated agenda item not being returned means that either it was deleted or its `id` column was changed by external entities before this update operation could take place. - if (updatedAgendaItem === undefined) { - throw new TalawaGraphQLError({ - extensions: { - code: "unexpected", - }, - }); - } + // Updated agenda item not being returned means that either it was deleted or its `id` column was changed by external entities before this update operation could take place. + if (updatedAgendaItem === undefined) { + throw new TalawaGraphQLError({ + extensions: { + code: "unexpected", + }, + }); + } - return updatedAgendaItem; + return updatedAgendaItem; +} + +builder.mutationField("updateAgendaItem", (t) => + t.field({ + args: { + input: t.arg({ + description: "", + required: true, + type: MutationUpdateAgendaItemInput, + }), }, + description: "Mutation field to update an agenda item.", + resolve: updateAgendaItemResolver, type: AgendaItem, }), ); diff --git a/test/graphql/types/Mutation/updateAgendaItem.test.ts b/test/graphql/types/Mutation/updateAgendaItem.test.ts new file mode 100644 index 00000000000..908cf79b862 --- /dev/null +++ b/test/graphql/types/Mutation/updateAgendaItem.test.ts @@ -0,0 +1,756 @@ +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 { updateAgendaItemResolver } from "~/src/graphql/types/Mutation/updateAgendaItem"; + +interface MockDrizzleClient { + query: { + usersTable: { + findFirst: ReturnType; + }; + agendaItemsTable: { + findFirst: ReturnType; + }; + agendaFoldersTable: { + findFirst: ReturnType; + }; + }; + update: ReturnType; + set: 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(), + }, + agendaFoldersTable: { + findFirst: vi.fn(), + }, + }, + update: vi.fn().mockReturnThis(), + set: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + returning: vi.fn(), +} as unknown 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("updateAgendaItemResolver", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + /** + * Test authentication check + * Verifies that the resolver rejects unauthorized access attempts + * Expected: Returns unauthenticated error when client is not authenticated + */ + + it("should throw unauthenticated error when attempting to update agenda item without authentication", async () => { + await expect( + updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + name: "Updated Name", + }, + }, + unauthenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + message: expect.any(String), + extensions: { code: "unauthenticated" }, + }), + ); + }); + + it("should throw invalid_arguments error when updating agenda item with invalid UUID format", async () => { + await expect( + updateAgendaItemResolver( + {}, + { + 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); + + await expect( + updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + name: "Updated Name", + }, + }, + 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( + updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + name: "Updated Name", + }, + }, + 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 update agenda item", async () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "regular", + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue({ + type: "general", + folder: { + event: { + organization: { + membershipsWhereOrganization: [], + }, + }, + }, + }); + + await expect( + updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + name: "Updated Name", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + message: expect.any(String), + extensions: { + code: "unauthorized_action_on_arguments_associated_resources", + issues: [ + { + argumentPath: ["input", "id"], + }, + ], + }, + }), + ); + }); + + it.each([ + { + type: "note", + input: { duration: "10", key: "C" }, + expectedIssues: [ + { + argumentPath: ["input", "duration"], + message: 'Cannot be provided for an agenda item of type "note"', + }, + { + argumentPath: ["input", "key"], + message: 'Cannot be provided for an agenda item of type "note"', + }, + ], + }, + ])( + "should throw forbidden_action error for invalid fields on %s-type agenda item", + async ({ type, input, expectedIssues }) => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "administrator", + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue({ + type, + folder: { + event: { + organization: { + membershipsWhereOrganization: [{ role: "administrator" }], + }, + }, + }, + }); + + await expect( + updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + ...input, + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + message: expect.any(String), + extensions: { + code: "forbidden_action_on_arguments_associated_resources", + issues: expectedIssues, + }, + }), + ); + }, + ); + + it("should throw forbidden_action error when attempting to set key for general-type agenda item", async () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "administrator", + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue({ + type: "general", + folder: { + event: { + organization: { + membershipsWhereOrganization: [{ role: "administrator" }], + }, + }, + }, + }); + + await expect( + updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + key: "C", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + message: expect.any(String), + extensions: { + code: "forbidden_action_on_arguments_associated_resources", + issues: [ + { + argumentPath: ["input", "key"], + message: + 'Cannot be provided for an agenda item of type "general"', + }, + ], + }, + }), + ); + }); + + it("should throw arguments_associated_resources_not_found error when specified folder ID does not exist", async () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "administrator", + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue({ + type: "general", + folder: { + eventId: "event_1", + event: { + organization: { + membershipsWhereOrganization: [{ role: "administrator" }], + }, + }, + }, + }); + drizzleClientMock.query.agendaFoldersTable.findFirst.mockResolvedValue( + undefined, + ); + + await expect( + updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + folderId: "123e4567-e89b-12d3-a456-426614174001", + name: "Updated Name", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + message: expect.any(String), + extensions: { + code: "arguments_associated_resources_not_found", + issues: [ + { + argumentPath: ["input", "folderId"], + }, + ], + }, + }), + ); + }); + + it("should throw forbidden_action error when target folder belongs to different event", async () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "administrator", + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue({ + type: "general", + folder: { + eventId: "event_1", + event: { + organization: { + membershipsWhereOrganization: [{ role: "administrator" }], + }, + }, + }, + }); + drizzleClientMock.query.agendaFoldersTable.findFirst.mockResolvedValue({ + eventId: "event_2", + isAgendaItemFolder: true, + }); + + await expect( + updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + folderId: "123e4567-e89b-12d3-a456-426614174001", + name: "Updated Name", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + message: expect.any(String), + extensions: { + code: "forbidden_action_on_arguments_associated_resources", + issues: [ + { + argumentPath: ["input", "folderId"], + message: + "This agenda folder does not belong to the event to the agenda item.", + }, + ], + }, + }), + ); + }); + + it("should throw forbidden_action error when target folder is not marked as agenda item folder", async () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "administrator", + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue({ + type: "general", + folder: { + eventId: "event_1", + event: { + organization: { + membershipsWhereOrganization: [{ role: "administrator" }], + }, + }, + }, + }); + drizzleClientMock.query.agendaFoldersTable.findFirst.mockResolvedValue({ + eventId: "event_1", + isAgendaItemFolder: false, + }); + + await expect( + updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + folderId: "123e4567-e89b-12d3-a456-426614174001", + name: "Updated Name", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + message: expect.any(String), + extensions: { + code: "forbidden_action_on_arguments_associated_resources", + issues: [ + { + argumentPath: ["input", "folderId"], + message: "This agenda folder cannot be a folder to agenda items.", + }, + ], + }, + }), + ); + }); + + it("should successfully update agenda item when admin user provides valid input", async () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "administrator", + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue({ + type: "general", + folder: { + eventId: "event_1", + event: { + organization: { + membershipsWhereOrganization: [{ role: "administrator" }], + }, + }, + }, + }); + drizzleClientMock.query.agendaFoldersTable.findFirst.mockResolvedValue({ + eventId: "event_1", + isAgendaItemFolder: true, + }); + drizzleClientMock.update.mockReturnValue({ + set: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + returning: vi + .fn() + .mockResolvedValue([ + { id: "123e4567-e89b-12d3-a456-426614174000", name: "Updated Name" }, + ]), + }); + + const result = await updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + name: "Updated Name", + }, + }, + authenticatedContext, + ); + + expect(result).toEqual({ + id: "123e4567-e89b-12d3-a456-426614174000", + name: "Updated Name", + }); + }); + + it("should throw unexpected error when database update operation returns empty result", async () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "administrator", + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue({ + type: "general", + folder: { + eventId: "event_1", + event: { + organization: { + membershipsWhereOrganization: [{ role: "administrator" }], + }, + }, + }, + }); + drizzleClientMock.update.mockReturnValue({ + set: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue([]), + }); + + await expect( + updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + name: "Updated Name", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + message: expect.any(String), + extensions: { + code: "unexpected", + }, + }), + ); + }); + + // Test for scripture-type agenda item with key + it("should throw forbidden_action error when attempting to set key for scripture-type agenda item", async () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "administrator", + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue({ + type: "scripture", + folder: { + event: { + organization: { + membershipsWhereOrganization: [{ role: "administrator" }], + }, + }, + }, + }); + + await expect( + updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + key: "C", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + message: expect.any(String), + extensions: { + code: "forbidden_action_on_arguments_associated_resources", + issues: [ + { + argumentPath: ["input", "key"], + message: + 'Cannot be provided for an agenda item of type "scripture"', + }, + ], + }, + }), + ); + }); + + // Helper function to reduce setup code duplication + const setupNoteTypeAgendaItemTest = () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "administrator", + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue({ + type: "note", + folder: { + event: { + organization: { + membershipsWhereOrganization: [{ role: "administrator" }], + }, + }, + }, + }); + }; + + describe("note-type agenda item validations", () => { + beforeEach(() => { + setupNoteTypeAgendaItemTest(); + }); + + it("should throw forbidden_action error when attempting to set both duration and key", async () => { + await expect( + updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + duration: "10", + key: "C", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + extensions: { + code: "forbidden_action_on_arguments_associated_resources", + issues: expect.arrayContaining([ + { + argumentPath: ["input", "duration"], + message: 'Cannot be provided for an agenda item of type "note"', + }, + { + argumentPath: ["input", "key"], + message: 'Cannot be provided for an agenda item of type "note"', + }, + ]), + }, + }), + ); + }); + + it("should throw forbidden_action error when attempting to set only duration", async () => { + await expect( + updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + duration: "10", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + extensions: { + code: "forbidden_action_on_arguments_associated_resources", + issues: [ + { + argumentPath: ["input", "duration"], + message: 'Cannot be provided for an agenda item of type "note"', + }, + ], + }, + }), + ); + }); + + it("should throw forbidden_action error when attempting to set only key", async () => { + await expect( + updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + key: "C", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + extensions: { + code: "forbidden_action_on_arguments_associated_resources", + issues: [ + { + argumentPath: ["input", "key"], + message: 'Cannot be provided for an agenda item of type "note"', + }, + ], + }, + }), + ); + }); + }); + + it("should handle folder validation and update authorization", async () => { + drizzleClientMock.query.usersTable.findFirst.mockResolvedValue({ + role: "regular", // Non-admin user + }); + drizzleClientMock.query.agendaItemsTable.findFirst.mockResolvedValue({ + type: "general", + folder: { + eventId: "event_1", + event: { + organization: { + membershipsWhereOrganization: [ + { role: "regular" }, // Non-admin role in organization (lines 242-243) + ], + }, + }, + }, + }); + + // Test unauthorized update attempt + await expect( + updateAgendaItemResolver( + {}, + { + input: { + id: "123e4567-e89b-12d3-a456-426614174000", + name: "Updated Name", + }, + }, + authenticatedContext, + ), + ).rejects.toThrowError( + expect.objectContaining({ + extensions: { + code: "unauthorized_action_on_arguments_associated_resources", + issues: [ + { + argumentPath: ["input", "id"], + }, + ], + }, + }), + ); + }); +});