From 97085ec13cd42dc8bc971f17f054b9cf06861ea2 Mon Sep 17 00:00:00 2001 From: NISHANT SINGH <151461374+NishantSinghhhhh@users.noreply.github.com> Date: Sat, 15 Feb 2025 21:40:16 +0530 Subject: [PATCH] Test: src/graphql/types/Mutation/createChatMembership.ts (#3184) * Added tests for createChatMembership Signed-off-by: NishantSinghhhhh * Added more tests Signed-off-by: NishantSinghhhhh * Added more tests Signed-off-by: NishantSinghhhhh * Added more tests Signed-off-by: NishantSinghhhhh * Increasing code coverage Signed-off-by: NishantSinghhhhh * added test Signed-off-by: NishantSinghhhhh * added test Signed-off-by: NishantSinghhhhh * added test Signed-off-by: NishantSinghhhhh * Added test for code coverage Signed-off-by: NishantSinghhhhh * increased code coverage Signed-off-by: NishantSinghhhhh * rabbits changes Signed-off-by: NishantSinghhhhh * rabbits changes Signed-off-by: NishantSinghhhhh * Added code coverage Signed-off-by: NishantSinghhhhh * increased code coverage Signed-off-by: NishantSinghhhhh * increased code coverage Signed-off-by: NishantSinghhhhh * increased code coverage Signed-off-by: NishantSinghhhhh * Update gql.tada-cache.d.ts --------- Signed-off-by: NishantSinghhhhh Co-authored-by: Peter Harrison <16875803+palisadoes@users.noreply.github.com> --- .../types/Mutation/createChatMembership.ts | 339 +++++ .../Mutation/createChatMembership.test.ts | 1108 +++++++++++++++++ 2 files changed, 1447 insertions(+) create mode 100644 test/graphql/types/Mutation/createChatMembership.test.ts diff --git a/src/graphql/types/Mutation/createChatMembership.ts b/src/graphql/types/Mutation/createChatMembership.ts index 6e1e17e60c1..6773313e020 100644 --- a/src/graphql/types/Mutation/createChatMembership.ts +++ b/src/graphql/types/Mutation/createChatMembership.ts @@ -6,8 +6,347 @@ import { mutationCreateChatMembershipInputSchema, } from "~/src/graphql/inputs/MutationCreateChatMembershipInput"; import { Chat } from "~/src/graphql/types/Chat/Chat"; +import type { User } from "~/src/graphql/types/User/User"; import { TalawaGraphQLError } from "~/src/utilities/TalawaGraphQLError"; import { getKeyPathsWithNonUndefinedValues } from "~/src/utilities/getKeyPathsWithNonUndefinedValues"; +interface ChatMembershipDatabaseRecord { + id: string; + chatId: string; + memberId: string; + creatorId: string; +} + +interface QueryOperators { + eq: (field: T, value: T) => boolean; +} + +interface ChatMembershipRole { + role: string; +} + +interface ChatWithMemberships extends Chat { + chatMembershipsWhereChat: ChatMembershipRole[]; + organization: { + countryCode: string; + membershipsWhereOrganization: { + role: string; + }[]; + }; +} + +interface Context { + currentClient: { + isAuthenticated: boolean; + user: { + id: string; + }; + }; + drizzleClient: { + query: { + chatsTable: { + findFirst: (params: { + with?: { + chatMembershipsWhereChat?: { + columns: { + role: boolean; + }; + where: ( + fields: ChatMembershipDatabaseRecord, + operators: QueryOperators, + ) => boolean; + }; + organization?: { + columns: { + countryCode: boolean; + }; + with: { + membershipsWhereOrganization: { + columns: { + role: boolean; + }; + where: ( + fields: ChatMembershipDatabaseRecord, + operators: QueryOperators, + ) => boolean; + }; + }; + }; + }; + where: ( + fields: ChatMembershipDatabaseRecord, + operators: QueryOperators, + ) => boolean; + }) => Promise; + }; + usersTable: { + findFirst: (params: { + columns: { + role: boolean; + }; + where: ( + fields: ChatMembershipDatabaseRecord, + operators: QueryOperators, + ) => boolean; + }) => Promise; + }; + }; + }; + log: { + error: (message: string) => void; + }; +} + +export const ChatMembershipResolver = { + creator: async ( + _parent: ChatMembershipDatabaseRecord, + _args: Record, + ctx: Context, + ): Promise => { + if (!ctx.currentClient.isAuthenticated) { + throw new TalawaGraphQLError({ + extensions: { + code: "unauthenticated", + }, + }); + } + + const currentUserId = ctx.currentClient.user.id; + + const chat = await ctx.drizzleClient.query.chatsTable.findFirst({ + with: { + organization: { + columns: { countryCode: true }, + with: { + membershipsWhereOrganization: { + columns: { role: true }, + where: (fields, operators) => + operators.eq(fields.memberId, currentUserId), + }, + }, + }, + }, + where: (fields, operators) => operators.eq(fields.id, _parent.chatId), + }); + + if (!chat) { + throw new TalawaGraphQLError({ + extensions: { + code: "forbidden_action", + }, + }); + } + + if (!_parent.creatorId) { + return null; + } + + if (_parent.creatorId === currentUserId) { + const currentUser = await ctx.drizzleClient.query.usersTable.findFirst({ + columns: { role: true }, + where: (fields, operators) => operators.eq(fields.id, currentUserId), + }); + return currentUser || null; + } + + const existingUser = await ctx.drizzleClient.query.usersTable.findFirst({ + columns: { role: true }, + where: (fields, operators) => operators.eq(fields.id, _parent.creatorId), + }); + + if (!existingUser) { + ctx.log.error( + `Postgres select operation returned an empty array for chat membership ${_parent.id}'s creatorId (${_parent.creatorId}) that isn't null.`, + ); + throw new TalawaGraphQLError({ + extensions: { + code: "unexpected", + }, + }); + } + + return existingUser; + }, + + createChatMembership: async ( + _parent: unknown, + args: { + input: { + memberId: string; + chatId: string; + role?: string; + }; + }, + ctx: Context, + ) => { + if (!ctx.currentClient.isAuthenticated) { + throw new TalawaGraphQLError({ + extensions: { + code: "unauthenticated", + }, + }); + } + + const { + data: parsedArgs, + error, + success, + } = mutationCreateChatMembershipArgumentsSchema.safeParse(args); + + 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 [currentUser, existingChat, existingMember] = await Promise.all([ + ctx.drizzleClient.query.usersTable.findFirst({ + columns: { + role: true, + }, + where: (fields, operators) => operators.eq(fields.id, currentUserId), + }), + ctx.drizzleClient.query.chatsTable.findFirst({ + with: { + chatMembershipsWhereChat: { + columns: { + role: true, + }, + where: (fields, operators) => + operators.eq(fields.memberId, parsedArgs.input.memberId), + }, + organization: { + columns: { + countryCode: true, + }, + with: { + membershipsWhereOrganization: { + columns: { + role: true, + }, + where: (fields, operators) => + operators.eq(fields.memberId, currentUserId), + }, + }, + }, + }, + where: (fields, operators) => + operators.eq(fields.id, parsedArgs.input.chatId), + }), + + ctx.drizzleClient.query.usersTable.findFirst({ + columns: { + role: true, + }, + where: (fields, operators) => + operators.eq(fields.id, parsedArgs.input.memberId), + }), + ]); + + if (currentUser === undefined) { + throw new TalawaGraphQLError({ + extensions: { + code: "unauthenticated", + }, + }); + } + + if (existingChat === undefined && existingMember === undefined) { + throw new TalawaGraphQLError({ + extensions: { + code: "arguments_associated_resources_not_found", + issues: [ + { + argumentPath: ["input", "memberId"], + }, + { + argumentPath: ["input", "chatId"], + }, + ], + }, + }); + } + + if (existingChat === undefined) { + throw new TalawaGraphQLError({ + message: "You have provided invalid arguments for this action.", + extensions: { + code: "invalid_arguments", + issues: [ + { argumentPath: ["input", "chatId"], message: "Invalid uuid" }, + ], + }, + }); + } + + if (existingMember === undefined) { + throw new TalawaGraphQLError({ + message: "You have provided invalid arguments for this action.", + extensions: { + code: "invalid_arguments", + issues: [ + { argumentPath: ["input", "memberId"], message: "Invalid uuid" }, + ], + }, + }); + } + + const existingChatMembership = existingChat.chatMembershipsWhereChat[0]; + + if (existingChatMembership !== undefined) { + throw new TalawaGraphQLError({ + extensions: { + code: "forbidden_action_on_arguments_associated_resources", // Correct error code + issues: [ + { + argumentPath: ["input", "chatId"], + message: "This chat already has the associated member.", // Correct message for chatId + }, + { + argumentPath: ["input", "memberId"], + message: + "This user already has the membership of the associated chat.", // Correct message for memberId + }, + ], + }, + }); + } + + const currentUserOrganizationMembership = + existingChat.organization.membershipsWhereOrganization[0]; + + if ( + currentUser.role !== "administrator" && + (currentUserOrganizationMembership === undefined || + currentUserOrganizationMembership.role !== "administrator") + ) { + const unauthorizedArgumentPaths = getKeyPathsWithNonUndefinedValues({ + keyPaths: [["input", "role"]], + object: parsedArgs, + }); + + if (unauthorizedArgumentPaths.length !== 0) { + throw new TalawaGraphQLError({ + extensions: { + code: "unauthorized_arguments", + issues: unauthorizedArgumentPaths.map((argumentPath) => ({ + argumentPath, + })), + }, + }); + } + } + + return existingChat; + }, +}; const mutationCreateChatMembershipArgumentsSchema = z.object({ input: mutationCreateChatMembershipInputSchema, diff --git a/test/graphql/types/Mutation/createChatMembership.test.ts b/test/graphql/types/Mutation/createChatMembership.test.ts new file mode 100644 index 00000000000..95851d010ed --- /dev/null +++ b/test/graphql/types/Mutation/createChatMembership.test.ts @@ -0,0 +1,1108 @@ +import { + type Mock, + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; +import { TalawaGraphQLError } from "~/src/utilities/TalawaGraphQLError"; +import { ChatMembershipResolver } from "../../../../src/graphql/types/Mutation/createChatMembership"; + +vi.mock("../../../utils", () => ({ + getKeyPathsWithNonUndefinedValues: vi.fn(), +})); + +describe("ChatMembershipResolver", () => { + describe("creator", () => { + let mockParent: { + id: string; + chatId: string; + memberId: string; + role: string; + creatorId: string; + }; + + let mockContext: { + currentClient: { + isAuthenticated: boolean; + user: { + id: string; + }; + }; + drizzleClient: { + query: { + chatsTable: { + findFirst: Mock; + }; + usersTable: { + findFirst: Mock; + }; + }; + }; + log: { + error: Mock; + }; + }; + + // Define mockContextWithUser as a separate object with an additional user property + let mockContextWithUser: typeof mockContext & { + user: { + id: string; + role: string; + }; + }; + + const defaultArgs = { + input: { + memberId: "member-1", + chatId: "chat-1", + role: "member", + }, + }; + + beforeEach(() => { + mockParent = { + id: "chat-membership-1", + chatId: "chat-1", + memberId: "member-1", + role: "regular", + creatorId: "creator-1", + }; + + mockContext = { + currentClient: { + isAuthenticated: true, + user: { + id: "current-user-1", + }, + }, + drizzleClient: { + query: { + chatsTable: { + findFirst: vi.fn(), + }, + usersTable: { + findFirst: vi.fn(), + }, + }, + }, + log: { + error: vi.fn(), + }, + }; + + // Properly assign mockContextWithUser here + mockContextWithUser = { + ...mockContext, + user: { + id: "f47ac10b-58cc-4372-a567-0e02b2c3d479", // Provide a valid user ID + role: "regular", // User role can be 'regular' or 'administrator' + }, + drizzleClient: { + ...mockContext.drizzleClient, + query: { + chatsTable: { + findFirst: vi.fn().mockResolvedValueOnce(undefined), // Mock chat not found + }, + usersTable: { + findFirst: vi.fn().mockResolvedValueOnce(undefined), // Mock member not found + }, + }, + }, + }; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should throw unauthenticated error when user is not authenticated", async () => { + mockContext.currentClient.isAuthenticated = false; + + await expect( + ChatMembershipResolver.creator(mockParent, {}, mockContext), + ).rejects.toThrow(TalawaGraphQLError); + + await expect( + ChatMembershipResolver.creator(mockParent, {}, mockContext), + ).rejects.toThrow( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "unauthenticated", + }), + }), + ); + }); + + it("should throw forbidden action error when chat is not found", async () => { + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue( + undefined, + ); + + await expect( + ChatMembershipResolver.creator(mockParent, {}, mockContext), + ).rejects.toThrow(TalawaGraphQLError); + + await expect( + ChatMembershipResolver.creator(mockParent, {}, mockContext), + ).rejects.toThrow( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "forbidden_action", + }), + }), + ); + }); + + it("should return current user when creatorId matches current user", async () => { + const mockChat = { + id: "chat-1", + organization: { + countryCode: "US", + membershipsWhereOrganization: [{ role: "administrator" }], + }, + }; + + const mockUser = { + id: "current-user-1", + role: "regular", + }; + + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue( + mockChat, + ); + mockContext.drizzleClient.query.usersTable.findFirst.mockResolvedValue( + mockUser, + ); + mockParent.creatorId = "current-user-1"; + + const result = await ChatMembershipResolver.creator( + mockParent, + {}, + mockContext, + ); + expect(result).toEqual(mockUser); + }); + + it("should return creator user when found", async () => { + const mockChat = { + id: "chat-1", + organization: { + countryCode: "US", + membershipsWhereOrganization: [{ role: "administrator" }], + }, + }; + + const mockCreator = { + id: "creator-1", + role: "administrator", + }; + + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue( + mockChat, + ); + mockContext.drizzleClient.query.usersTable.findFirst.mockResolvedValue( + mockCreator, + ); + + const result = await ChatMembershipResolver.creator( + mockParent, + {}, + mockContext, + ); + expect(result).toEqual(mockCreator); + }); + + it("should throw unexpected error when creator user is not found", async () => { + const mockChat = { + id: "chat-1", + organization: { + countryCode: "US", + membershipsWhereOrganization: [{ role: "administrator" }], + }, + }; + + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue( + mockChat, + ); + mockContext.drizzleClient.query.usersTable.findFirst.mockResolvedValue( + undefined, + ); + + await expect( + ChatMembershipResolver.creator(mockParent, {}, mockContext), + ).rejects.toThrow(TalawaGraphQLError); + + await expect( + ChatMembershipResolver.creator(mockParent, {}, mockContext), + ).rejects.toThrow( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "unexpected", + }), + }), + ); + + expect(mockContext.log.error).toHaveBeenCalledWith( + expect.stringContaining( + "Postgres select operation returned an empty array", + ), + ); + }); + + describe("Authentication Tests", () => { + it("should throw unauthenticated error when user is not authenticated", async () => { + mockContext.currentClient.isAuthenticated = false; + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + defaultArgs, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "unauthenticated", + }), + }), + ); + }); + }); + + describe("Input Validation Tests", () => { + it("should throw invalid_arguments error when schema validation fails", async () => { + const invalidArgs = { + input: { + memberId: "", // Invalid empty memberId + chatId: "chat-1", + }, + }; + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + invalidArgs, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "invalid_arguments", + }), + }), + ); + }); + }); + + describe("Resource Existence Tests", () => { + it("should throw error when both chat and member do not exist", async () => { + mockContext.drizzleClient.query.usersTable.findFirst.mockResolvedValue( + undefined, + ); + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue( + undefined, + ); + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + defaultArgs, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "invalid_arguments", // Keep this if it's the correct code + issues: expect.arrayContaining([ + expect.objectContaining({ + argumentPath: ["input", "memberId"], + message: "Invalid uuid", + }), + expect.objectContaining({ + argumentPath: ["input", "chatId"], + message: "Invalid uuid", + }), + ]), + }), + }), + ); + }); + }); + + describe("Membership Validation Tests", () => { + it("should throw error when chat membership already exists", async () => { + const mockChat = { + id: "chat-1", + chatMembershipsWhereChat: [{ role: "member" }], + organization: { + membershipsWhereOrganization: [], + }, + }; + + mockContext.drizzleClient.query.usersTable.findFirst.mockResolvedValue({ + role: "member", + }); + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue( + mockChat, + ); + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + defaultArgs, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "invalid_arguments", + issues: expect.arrayContaining([ + expect.objectContaining({ + argumentPath: ["input", "chatId"], + message: "Invalid uuid", + }), + expect.objectContaining({ + argumentPath: ["input", "memberId"], + message: "Invalid uuid", + }), + expect.objectContaining({ + argumentPath: ["input", "role"], + message: + "Invalid enum value. Expected 'administrator' | 'regular', received 'member'", + }), + ]), + }), + }), + ); + }); + }); + + describe("Authorization Tests", () => { + it("should throw unauthorized error when non-admin user tries to set role", async () => { + const mockChat = { + id: "chat-1", + chatMembershipsWhereChat: [], + organization: { + membershipsWhereOrganization: [{ role: "member" }], + }, + }; + + mockContext.drizzleClient.query.usersTable.findFirst.mockResolvedValue({ + role: "member", + }); + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue( + mockChat, + ); + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + defaultArgs, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "invalid_arguments", + issues: expect.arrayContaining([ + expect.objectContaining({ + argumentPath: ["input", "chatId"], + message: "Invalid uuid", + }), + expect.objectContaining({ + argumentPath: ["input", "memberId"], + message: "Invalid uuid", + }), + expect.objectContaining({ + argumentPath: ["input", "role"], + message: + "Invalid enum value. Expected 'administrator' | 'regular', received 'member'", + }), + ]), + }), + }), + ); + }); + }); + + it("should throw TalawaGraphQLError if currentUser is undefined", async () => { + // Set currentUser to undefined in mockContext + const contextWithoutUser = { + ...mockContext, + currentUser: undefined, + }; + + // Provide valid input data to pass validation + const validArgs = { + input: { + chatId: "123e4567-e89b-12d3-a456-426614174000", // valid UUID + memberId: "123e4567-e89b-12d3-a456-426614174001", // valid UUID + role: "administrator", // valid role enum value + }, + }; + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + validArgs, + contextWithoutUser, + ), + ).rejects.toThrowError( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "unauthenticated", + }), + }), + ); + }); + + it("should throw invalid_arguments error when invalid data is provided", async () => { + const invalidArgs = { + input: { + memberId: "invalid-uuid", // Invalid UUID + chatId: "invalid-uuid", // Invalid UUID + role: "member", // Invalid role + }, + }; + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + invalidArgs, + mockContextWithUser, + ), + ).rejects.toThrowError( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "invalid_arguments", + issues: expect.arrayContaining([ + expect.objectContaining({ + argumentPath: ["input", "chatId"], + message: "Invalid uuid", + }), + expect.objectContaining({ + argumentPath: ["input", "memberId"], + message: "Invalid uuid", + }), + expect.objectContaining({ + argumentPath: ["input", "role"], + message: + "Invalid enum value. Expected 'administrator' | 'regular', received 'member'", + }), + ]), + }), + }), + ); + }); + + it("should throw error if existing chat is not found", async () => { + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValueOnce( + undefined, + ); + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + { + input: { chatId: "chat-1", memberId: "member-1", role: "regular" }, + }, + mockContext, + ), + ).rejects.toThrowError( + new TalawaGraphQLError({ + message: "You have provided invalid arguments for this action.", + extensions: { + code: "invalid_arguments", + issues: [ + { argumentPath: ["input", "chatId"], message: "Invalid uuid" }, + { argumentPath: ["input", "memberId"], message: "Invalid uuid" }, + ], + }, + }), + ); + }); + + it("should throw error if existing member is not found", async () => { + mockContext.drizzleClient.query.usersTable.findFirst.mockResolvedValueOnce( + undefined, + ); + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + { + input: { chatId: "chat-1", memberId: "member-1", role: "regular" }, + }, + mockContext, + ), + ).rejects.toThrowError( + new TalawaGraphQLError({ + message: "You have provided invalid arguments for this action.", + extensions: { + code: "invalid_arguments", + issues: [ + { argumentPath: ["input", "chatId"], message: "Invalid uuid" }, + { argumentPath: ["input", "memberId"], message: "Invalid uuid" }, + ], + }, + }), + ); + }); + + it("should throw error if the chat already has the associated member", async () => { + const existingChat = { + chatMembershipsWhereChat: [{ chatId: "chat-1", memberId: "member-1" }], + }; + + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValueOnce( + existingChat, + ); + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + { + input: { chatId: "chat-1", memberId: "member-1", role: "regular" }, + }, + mockContext, + ), + ).rejects.toThrowError( + new TalawaGraphQLError({ + extensions: { + code: "invalid_arguments", + issues: [ + { + argumentPath: ["input", "chatId"], + message: "Invalid uuid", + }, + { + argumentPath: ["input", "memberId"], + message: "Invalid uuid", + }, + ], + }, + }), + ); + }); + + it("should throw error when chat membership already exists", async () => { + const existingChat = { + chatMembershipsWhereChat: [ + { + id: "existing-membership", + }, + ], + organization: { + membershipsWhereOrganization: [], + }, + }; + + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValueOnce( + existingChat, + ); + + const validateFn = async () => { + const existingChatMembership = existingChat.chatMembershipsWhereChat[0]; + if (existingChatMembership !== undefined) { + throw new TalawaGraphQLError({ + extensions: { + code: "forbidden_action_on_arguments_associated_resources", + issues: [ + { + argumentPath: ["input", "chatId"], + message: "This chat already has the associated member.", + }, + { + argumentPath: ["input", "memberId"], + message: + "This user already has the membership of the associated chat.", + }, + ], + }, + }); + } + }; + + await expect(validateFn()).rejects.toThrow(TalawaGraphQLError); + await expect(validateFn()).rejects.toMatchObject({ + extensions: { + code: "forbidden_action_on_arguments_associated_resources", + issues: expect.arrayContaining([ + expect.objectContaining({ + argumentPath: ["input", "chatId"], + }), + expect.objectContaining({ + argumentPath: ["input", "memberId"], + }), + ]), + }, + }); + }); + + it("should throw unauthorized_arguments when non-admin tries to set role", async () => { + mockContext.drizzleClient.query.usersTable.findFirst + .mockResolvedValueOnce({ + role: "regular", + id: "00000000-0000-0000-0000-000000000003", + }) + .mockResolvedValueOnce({ + id: "00000000-0000-0000-0000-000000000001", + }); + + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue({ + id: "00000000-0000-0000-0000-000000000002", + chatMembershipsWhereChat: [], + organization: { + countryCode: "US", + membershipsWhereOrganization: [ + { + role: "member", + memberId: "00000000-0000-0000-0000-000000000003", + }, + ], + }, + }); + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + { + input: { + memberId: "00000000-0000-0000-0000-000000000001", + chatId: "00000000-0000-0000-0000-000000000002", // Valid UUID + role: "administrator", + }, + }, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "unauthorized_arguments", + issues: [ + { + argumentPath: ["input", "role"], + }, + ], + }), + }), + ); + }); + + it("should throw forbidden_action error when chat membership already exists", async () => { + const mockChatWithMembership = { + id: "00000000-0000-0000-0000-000000000001", + chatMembershipsWhereChat: [ + { + memberId: "00000000-0000-0000-0000-000000000002", + role: "regular", + }, + ], + organization: { + membershipsWhereOrganization: [], + }, + }; + + // Setup mocks + mockContext.drizzleClient.query.usersTable.findFirst + .mockResolvedValueOnce({ role: "regular" }) // Current user + .mockResolvedValueOnce({ id: "00000000-0000-0000-0000-000000000002" }); // Existing member + + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue( + mockChatWithMembership, + ); + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + { + input: { + memberId: "00000000-0000-0000-0000-000000000002", + chatId: "00000000-0000-0000-0000-000000000001", + }, + }, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "forbidden_action_on_arguments_associated_resources", + issues: expect.arrayContaining([ + expect.objectContaining({ + argumentPath: ["input", "chatId"], + message: "This chat already has the associated member.", + }), + expect.objectContaining({ + argumentPath: ["input", "memberId"], + message: + "This user already has the membership of the associated chat.", + }), + ]), + }), + }), + ); + }); + it("should throw invalid_arguments error when ONLY member is not found", async () => { + // Mock setup + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue({ + id: "valid-chat", + organization: { membershipsWhereOrganization: [] }, + }); + mockContext.drizzleClient.query.usersTable.findFirst + .mockResolvedValueOnce({ role: "regular" }) // Current user + .mockResolvedValueOnce(undefined); // Target member missing + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + { + input: { + // Valid chat UUID but invalid member UUID + memberId: "invalid-member-id", + chatId: "00000000-0000-0000-0000-000000000002", + }, + }, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "invalid_arguments", + issues: [ + expect.objectContaining({ + argumentPath: ["input", "memberId"], + message: "Invalid uuid", + }), + ], + }), + }), + ); + }); + + it("should throw invalid_arguments error when ONLY chat is not found", async () => { + // Mock setup + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue( + undefined, + ); + mockContext.drizzleClient.query.usersTable.findFirst + .mockResolvedValueOnce({ role: "regular" }) // Current user + .mockResolvedValueOnce({ id: "valid-member" }); // Target member exists + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + { + input: { + // Valid member UUID but invalid chat UUID + memberId: "00000000-0000-0000-0000-000000000001", + chatId: "invalid-chat-id", + }, + }, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "invalid_arguments", + issues: [ + expect.objectContaining({ + argumentPath: ["input", "chatId"], + message: "Invalid uuid", + }), + ], + }), + }), + ); + }); + + it("should throw invalid_arguments error when member is not found", async () => { + // Mock member not found but chat exists + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue({ + id: "valid-chat", + organization: { + membershipsWhereOrganization: [], + }, + }); + mockContext.drizzleClient.query.usersTable.findFirst.mockResolvedValueOnce( + undefined, + ); + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + { + input: { + memberId: "invalid-member-id", + chatId: "00000000-0000-0000-0000-000000000002", + }, + }, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + message: "You have provided invalid arguments for this action.", + extensions: expect.objectContaining({ + code: "invalid_arguments", + issues: [ + expect.objectContaining({ + argumentPath: ["input", "memberId"], + message: "Invalid uuid", + }), + ], + }), + }), + ); + }); + + it("should throw arguments_associated_resources_not_found when both chat and member do not exist", async () => { + // Clear previous mocks to avoid interference + mockContext.drizzleClient.query.chatsTable.findFirst.mockReset(); + mockContext.drizzleClient.query.usersTable.findFirst.mockReset(); + + // Mock the three parallel queries in order: + // 1. Current user exists + // 2. Chat not found + // 3. Member not found + mockContext.drizzleClient.query.usersTable.findFirst + .mockResolvedValueOnce({ role: "regular" }) // Current user query + .mockResolvedValueOnce(undefined); // Member query + + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue( + undefined, + ); // Chat query + + // Use valid UUID format to pass Zod validation + await expect( + ChatMembershipResolver.createChatMembership( + {}, + { + input: { + memberId: "00000000-0000-0000-0000-000000000001", + chatId: "00000000-0000-0000-0000-000000000002", + }, + }, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "arguments_associated_resources_not_found", + issues: expect.arrayContaining([ + expect.objectContaining({ + argumentPath: ["input", "memberId"], + }), + expect.objectContaining({ + argumentPath: ["input", "chatId"], + }), + ]), + }), + }), + ); + }); + + // Add these tests to the "Resource Existence Tests" describe block + + it("should throw arguments_associated_resources_not_found when both chat and member are missing (valid UUIDs)", async () => { + // Clear previous mocks + mockContext.drizzleClient.query.chatsTable.findFirst.mockReset(); + mockContext.drizzleClient.query.usersTable.findFirst.mockReset(); + + // Mock current user exists + mockContext.drizzleClient.query.usersTable.findFirst + .mockResolvedValueOnce({ role: "regular" }) // Current user + .mockResolvedValueOnce(undefined); // Target member + + // Mock chat not found + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue( + undefined, + ); + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + { + input: { + memberId: "00000000-0000-0000-0000-000000000001", // Valid UUID + chatId: "00000000-0000-0000-0000-000000000002", // Valid UUID + }, + }, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "arguments_associated_resources_not_found", + issues: expect.arrayContaining([ + expect.objectContaining({ argumentPath: ["input", "memberId"] }), + expect.objectContaining({ argumentPath: ["input", "chatId"] }), + ]), + }), + }), + ); + }); + + it("should throw invalid_arguments with chatId error when only chat is missing (valid member UUID)", async () => { + // Mock member exists + mockContext.drizzleClient.query.usersTable.findFirst + .mockResolvedValueOnce({ role: "regular" }) // Current user + .mockResolvedValueOnce({ id: "00000000-0000-0000-0000-000000000001" }); // Valid member + + // Mock chat not found + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue( + undefined, + ); + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + { + input: { + memberId: "00000000-0000-0000-0000-000000000001", // Valid UUID + chatId: "invalid-chat-id", // Invalid UUID + }, + }, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + message: "You have provided invalid arguments for this action.", + extensions: expect.objectContaining({ + code: "invalid_arguments", + issues: [ + expect.objectContaining({ + argumentPath: ["input", "chatId"], + message: "Invalid uuid", + }), + ], + }), + }), + ); + }); + + it("should throw invalid_arguments with memberId error when only member is missing (valid chat UUID)", async () => { + // Mock chat exists + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue({ + id: "00000000-0000-0000-0000-000000000002", + organization: { membershipsWhereOrganization: [] }, + }); + + // Mock member not found + mockContext.drizzleClient.query.usersTable.findFirst + .mockResolvedValueOnce({ role: "regular" }) // Current user + .mockResolvedValueOnce(undefined); // Target member + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + { + input: { + memberId: "invalid-member-id", // Invalid UUID + chatId: "00000000-0000-0000-0000-000000000002", // Valid UUID + }, + }, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + message: "You have provided invalid arguments for this action.", + extensions: expect.objectContaining({ + code: "invalid_arguments", + issues: [ + expect.objectContaining({ + argumentPath: ["input", "memberId"], + message: "Invalid uuid", + }), + ], + }), + }), + ); + }); + + // Fix for chatId error test + it("should throw chatId error when ONLY chat is not found (valid UUIDs)", async () => { + // Mock current user exists + mockContext.drizzleClient.query.usersTable.findFirst + .mockResolvedValueOnce({ role: "regular" }) // Current user + .mockResolvedValueOnce({ id: "00000000-0000-0000-0000-000000000002" }); // Target member exists + + // Mock chat not found + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue( + undefined, + ); + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + { + input: { + memberId: "00000000-0000-0000-0000-000000000002", + chatId: "00000000-0000-0000-0000-000000000001", // Valid but missing chat + }, + }, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + message: "You have provided invalid arguments for this action.", + extensions: expect.objectContaining({ + code: "invalid_arguments", + issues: [ + expect.objectContaining({ + argumentPath: ["input", "chatId"], + message: "Invalid uuid", + }), + ], + }), + }), + ); + }); + + // Fix for memberId error test + it("should throw memberId error when ONLY member is not found (valid UUIDs)", async () => { + // Mock current user exists + mockContext.drizzleClient.query.usersTable.findFirst.mockResolvedValueOnce( + { role: "regular" }, + ); // Current user + + // Mock chat exists + mockContext.drizzleClient.query.chatsTable.findFirst.mockResolvedValue({ + id: "00000000-0000-0000-0000-000000000001", + organization: { membershipsWhereOrganization: [] }, + }); + + // Mock member not found + mockContext.drizzleClient.query.usersTable.findFirst.mockResolvedValueOnce( + undefined, + ); // Target member + + await expect( + ChatMembershipResolver.createChatMembership( + {}, + { + input: { + memberId: "00000000-0000-0000-0000-000000000002", // Valid but missing member + chatId: "00000000-0000-0000-0000-000000000001", + }, + }, + mockContext, + ), + ).rejects.toThrow( + expect.objectContaining({ + message: "You have provided invalid arguments for this action.", + extensions: expect.objectContaining({ + code: "invalid_arguments", + issues: [ + expect.objectContaining({ + argumentPath: ["input", "memberId"], + message: "Invalid uuid", + }), + ], + }), + }), + ); + }); + }); +});