From f23c4dc1562773f83dbcb78743e5881ea04041c2 Mon Sep 17 00:00:00 2001 From: iamanishx Date: Mon, 17 Feb 2025 09:18:45 +0000 Subject: [PATCH 01/11] added test --- src/graphql/types/Mutation/deleteComment.ts | 216 +++++---- .../graphql/Mutation/deletecomment.test.ts | 418 ++++++++++++++++++ test/routes/graphql/gql.tada-cache.d.ts | 6 + 3 files changed, 529 insertions(+), 111 deletions(-) create mode 100644 test/routes/graphql/Mutation/deletecomment.test.ts diff --git a/src/graphql/types/Mutation/deleteComment.ts b/src/graphql/types/Mutation/deleteComment.ts index 537fed85f05..4a66490ed8a 100644 --- a/src/graphql/types/Mutation/deleteComment.ts +++ b/src/graphql/types/Mutation/deleteComment.ts @@ -8,147 +8,141 @@ import { } from "~/src/graphql/inputs/MutationDeleteCommentInput"; import { Comment } from "~/src/graphql/types/Comment/Comment"; import { TalawaGraphQLError } from "~/src/utilities/TalawaGraphQLError"; +import type { GraphQLContext } from "../../context"; const mutationDeleteCommentArgumentsSchema = z.object({ input: mutationDeleteCommentInputSchema, }); -builder.mutationField("deleteComment", (t) => - t.field({ - args: { - input: t.arg({ - description: "", - required: true, - type: MutationDeleteCommentInput, - }), - }, - description: "Mutation field to delete a comment.", - resolve: async (_parent, args, ctx) => { - if (!ctx.currentClient.isAuthenticated) { - throw new TalawaGraphQLError({ - extensions: { - code: "unauthenticated", - }, - }); - } +export async function deleteCommentResolver( + _parent: unknown, + args: z.infer, + ctx: GraphQLContext, +) { + if (!ctx.currentClient.isAuthenticated) { + throw new TalawaGraphQLError({ + extensions: { code: "unauthenticated" }, + }); + } - const { - data: parsedArgs, - error, - success, - } = mutationDeleteCommentArgumentsSchema.safeParse(args); + const { + success, + data: parsedArgs, + error, + } = mutationDeleteCommentArgumentsSchema.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, existingComment] = await Promise.all([ - ctx.drizzleClient.query.usersTable.findFirst({ + const [currentUser, existingComment] = await Promise.all([ + ctx.drizzleClient.query.usersTable.findFirst({ + columns: { role: true }, + where: (fields, operators) => operators.eq(fields.id, currentUserId), + }), + ctx.drizzleClient.query.commentsTable.findFirst({ + columns: { creatorId: true }, + with: { + post: { columns: { - role: true, - }, - where: (fields, operators) => operators.eq(fields.id, currentUserId), - }), - ctx.drizzleClient.query.commentsTable.findFirst({ - columns: { - creatorId: true, + pinnedAt: true, }, with: { - post: { + organization: { columns: { - pinnedAt: 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 (existingComment === undefined) { - throw new TalawaGraphQLError({ - extensions: { - code: "arguments_associated_resources_not_found", - issues: [ - { - argumentPath: ["input", "id"], - }, - ], + if (existingComment === undefined) { + throw new TalawaGraphQLError({ + extensions: { + code: "arguments_associated_resources_not_found", + issues: [ + { + argumentPath: ["input", "id"], }, - }); - } + ], + }, + }); + } - const currentUserOrganizationMembership = - existingComment.post.organization.membershipsWhereOrganization[0]; + const currentUserOrganizationMembership = + existingComment.post.organization.membershipsWhereOrganization[0]; - if ( - currentUser.role !== "administrator" && - (currentUserOrganizationMembership === undefined || - (currentUserOrganizationMembership.role !== "administrator" && - existingComment.creatorId !== currentUserId)) - ) { - throw new TalawaGraphQLError({ - extensions: { - code: "unauthorized_action_on_arguments_associated_resources", - issues: [ - { - argumentPath: ["input", "id"], - }, - ], - }, - }); - } + if ( + currentUser.role !== "administrator" && + (currentUserOrganizationMembership === undefined || + (currentUserOrganizationMembership.role !== "administrator" && + existingComment.creatorId !== currentUserId)) + ) { + throw new TalawaGraphQLError({ + extensions: { + code: "unauthorized_action_on_arguments_associated_resources", + issues: [{ argumentPath: ["input", "id"] }], + }, + }); + } - const [deletedComment] = await ctx.drizzleClient - .delete(commentsTable) - .where(eq(commentsTable.id, parsedArgs.input.id)) - .returning(); + const [deletedComment] = await ctx.drizzleClient + .delete(commentsTable) + .where(eq(commentsTable.id, parsedArgs.input.id)) + .returning(); - // Deleted comment 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 (deletedComment === undefined) { - throw new TalawaGraphQLError({ - extensions: { - code: "unexpected", - }, - }); - } + if (deletedComment === undefined) { + throw new TalawaGraphQLError({ + extensions: { code: "unexpected" }, + }); + } + + return deletedComment; +} - return deletedComment; +builder.mutationField("deleteComment", (t) => + t.field({ + args: { + input: t.arg({ + description: "", + required: true, + type: MutationDeleteCommentInput, + }), }, + description: "Mutation field to delete a comment.", + resolve: deleteCommentResolver, type: Comment, }), ); diff --git a/test/routes/graphql/Mutation/deletecomment.test.ts b/test/routes/graphql/Mutation/deletecomment.test.ts new file mode 100644 index 00000000000..7d8f737849a --- /dev/null +++ b/test/routes/graphql/Mutation/deletecomment.test.ts @@ -0,0 +1,418 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { GraphQLContext } from "~/src/graphql/context"; +import { deleteCommentResolver } from "~/src/graphql/types/Mutation/deleteComment"; + +const validUuid = "11111111-1111-1111-1111-111111111111"; +const validArgs = { input: { id: validUuid } }; + +// --- Helper: Create a full mock GraphQLContext --- +function createMockContext( + overrides: Partial = {}, +): GraphQLContext { + return { + currentClient: { + isAuthenticated: true, + user: { id: "current-user-id" }, + }, + drizzleClient: { + query: { + usersTable: { findFirst: vi.fn() }, + commentsTable: { findFirst: vi.fn() }, + }, + delete: vi.fn(), + update: vi.fn(), + transaction: vi.fn(), + }, + minio: { + client: { removeObjects: vi.fn(() => Promise.resolve()) }, + bucketName: "talawa", + }, + envConfig: { API_BASE_URL: "http://localhost" }, + jwt: { sign: vi.fn() }, + log: { error: vi.fn(), info: vi.fn(), warn: vi.fn(), debug: vi.fn() }, + pubsub: { publish: vi.fn() }, + ...overrides, + } as GraphQLContext; +} + +// --- Helper: Mock usersTable.findFirst --- +interface User { + id: string; + role: string; +} + +function mockUsersTableFindFirst( + ctx: GraphQLContext, + returnValue: Partial = {}, +) { + ctx.drizzleClient.query.usersTable.findFirst = vi.fn().mockResolvedValue({ + id: "current-user-id", + role: "user", + ...returnValue, + }); +} + +interface CommentsFindFirstConfig { + columns?: { + creatorId?: boolean; + }; + where?: ( + fields: { id: string }, + operators: { eq: (a: string, b: string) => boolean }, + ) => boolean; + with?: { + post?: { + columns?: { + pinnedAt?: boolean; + }; + with?: { + organization?: { + columns?: { + countryCode?: boolean; + }; + with?: { + membershipsWhereOrganization?: { + columns?: { + role?: boolean; + }; + where?: ( + fields: { memberId: string }, + operators: { eq: (a: string, b: string) => boolean }, + ) => boolean; + }; + }; + }; + }; + }; + }; +} + +// --- Helper: Mock commentsTable.findFirst --- +interface Comment { + id: string; + creatorId: string; + post: { + organization: { + membershipsWhereOrganization: { role: string }[]; + }; + pinnedAt?: Date | null; + }; +} + +type ExtendedComment = { + id: string; + body: string; + creatorId: string | null; + createdAt: Date; + updatedAt: Date | null; + postId: string; + post?: { + pinnedAt: Date | null; + organization: { + membershipsWhereOrganization: { role: string }[]; + }; + }; +}; + +function mockCommentsTableFindFirst( + ctx: GraphQLContext, + returnValue: Partial = {}, +) { + ctx.drizzleClient.query.commentsTable.findFirst = vi.fn().mockResolvedValue({ + creatorId: "other-user-id", + post: { + organization: { membershipsWhereOrganization: [] }, + pinnedAt: null, + }, + ...returnValue, + }); +} + +// --- Helper: Create a fake delete chain for delete operations --- +function createFakeDeleteChain(returningData: unknown[]): { + where: () => { returning: () => Promise }; +} { + return { + where: vi.fn().mockReturnValue({ + returning: vi.fn().mockResolvedValue(returningData), + }), + }; +} + +describe("deleteCommentResolver", () => { + let ctx: GraphQLContext; + beforeEach(() => { + ctx = createMockContext(); + }); + + it("throws unauthenticated error when client is not authenticated", async () => { + ctx.currentClient.isAuthenticated = false; + await expect( + deleteCommentResolver({}, validArgs, ctx), + ).rejects.toThrowError( + expect.objectContaining({ + extensions: expect.objectContaining({ code: "unauthenticated" }), + }), + ); + }); + + it("throws invalid_arguments error when input is invalid", async () => { + await expect( + deleteCommentResolver( + {}, + { + input: { + id: "", + }, + }, + ctx, + ), + ).rejects.toThrowError( + expect.objectContaining({ + extensions: expect.objectContaining({ code: "invalid_arguments" }), + }), + ); + }); + + it("throws unauthenticated error when currentUser is undefined", async () => { + // Return undefined for the user. + ctx.drizzleClient.query.usersTable.findFirst = vi + .fn() + .mockResolvedValue(undefined); + mockCommentsTableFindFirst(ctx, { + creatorId: "other-user-id", + post: { + organization: { + membershipsWhereOrganization: [{ role: "administrator" }], + }, + pinnedAt: null, + }, + }); + await expect( + deleteCommentResolver({}, validArgs, ctx), + ).rejects.toThrowError( + expect.objectContaining({ + extensions: expect.objectContaining({ code: "unauthenticated" }), + }), + ); + }); + + it("throws arguments_associated_resources_not_found error when comment is not found", async () => { + mockUsersTableFindFirst(ctx, { role: "administrator" }); + ctx.drizzleClient.query.commentsTable.findFirst = vi + .fn() + .mockResolvedValue(undefined); + await expect( + deleteCommentResolver({}, validArgs, ctx), + ).rejects.toThrowError( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "arguments_associated_resources_not_found", + }), + }), + ); + }); + + it("throws unauthorized error when current user is not admin and not authorized", async () => { + // Set current user as non-admin. + mockUsersTableFindFirst(ctx, { role: "user" }); + // Simulate a comment not created by the current user and org membership is non-admin. + mockCommentsTableFindFirst(ctx, { + creatorId: "other-user-id", + post: { + organization: { membershipsWhereOrganization: [{ role: "member" }] }, + pinnedAt: null, + }, + }); + await expect( + deleteCommentResolver({}, validArgs, ctx), + ).rejects.toThrowError( + expect.objectContaining({ + extensions: expect.objectContaining({ + code: "unauthorized_action_on_arguments_associated_resources", + }), + }), + ); + }); + + it("throws unexpected error when deletion returns undefined", async () => { + mockUsersTableFindFirst(ctx, { role: "administrator" }); + mockCommentsTableFindFirst(ctx, { + creatorId: "current-user-id", + post: { + organization: { + membershipsWhereOrganization: [{ role: "administrator" }], + }, + pinnedAt: null, + }, + }); + ctx.drizzleClient.delete = vi + .fn() + .mockReturnValue(createFakeDeleteChain([])); + await expect( + deleteCommentResolver({}, validArgs, ctx), + ).rejects.toThrowError( + expect.objectContaining({ + extensions: expect.objectContaining({ code: "unexpected" }), + }), + ); + }); + + it("calls commentsTable.findFirst with correct configuration (covering lines 62 & 70)", async () => { + mockUsersTableFindFirst(ctx, { role: "administrator" }); + + const commentFindFirstSpy = vi + .spyOn(ctx.drizzleClient.query.commentsTable, "findFirst") + .mockResolvedValue({ + id: "comment-id-123", + body: "some body", + createdAt: new Date(), + updatedAt: null, + creatorId: "current-user-id", + postId: "post-id-123", + post: { + pinnedAt: null, + organization: { + membershipsWhereOrganization: [{ role: "administrator" }], + }, + }, + } as ExtendedComment); + + ctx.drizzleClient.delete = vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + returning: vi.fn().mockResolvedValue([ + { + id: validUuid, + creatorId: "current-user-id", + post: { + organization: { + membershipsWhereOrganization: [{ role: "administrator" }], + }, + pinnedAt: null, + }, + }, + ]), + }), + }); + + await deleteCommentResolver({}, validArgs, ctx); + expect(commentFindFirstSpy).toHaveBeenCalledTimes(1); + if (!commentFindFirstSpy.mock.calls[0]) { + throw new Error("Expected mock to have been called"); + } + const config = commentFindFirstSpy.mock + .calls[0][0] as CommentsFindFirstConfig; + + expect(config.columns).toEqual({ creatorId: true }); + expect(typeof config.where).toBe("function"); + if (config.where) { + const eqFn = (a: string, b: string) => a === b; + const whereResult = config.where({ id: validUuid }, { eq: eqFn }); + expect(whereResult).toBe(true); + } + + expect(config.with?.post?.columns).toEqual({ pinnedAt: true }); + expect(config.with?.post?.with?.organization?.columns).toEqual({ + countryCode: true, + }); + expect( + config.with?.post?.with?.organization?.with?.membershipsWhereOrganization + ?.columns, + ).toEqual({ role: true }); + expect( + typeof config.with?.post?.with?.organization?.with + ?.membershipsWhereOrganization?.where, + ).toBe("function"); + + const membershipWhereFn = + config.with?.post?.with?.organization?.with?.membershipsWhereOrganization + ?.where; + if (membershipWhereFn) { + const eqFn = (a: string, b: string) => a === b; + const membershipResult = membershipWhereFn( + { memberId: "current-user-id" }, + { eq: eqFn }, + ); + expect(membershipResult).toBe(true); + } + }); + + it("successfully deletes and returns the comment", async () => { + const deletedComment: Comment & { id: string } = { + id: validUuid, + creatorId: "current-user-id", + post: { + organization: { + membershipsWhereOrganization: [{ role: "administrator" }], + }, + pinnedAt: null, + }, + }; + mockUsersTableFindFirst(ctx, { role: "administrator" }); + mockCommentsTableFindFirst(ctx, { + creatorId: "current-user-id", + post: { + organization: { + membershipsWhereOrganization: [{ role: "administrator" }], + }, + pinnedAt: null, + }, + }); + ctx.drizzleClient.delete = vi + .fn() + .mockReturnValue(createFakeDeleteChain([deletedComment])); + const result = await deleteCommentResolver({}, validArgs, ctx); + expect(result).toEqual(deletedComment); + }); +}); + +describe("GraphQL schema wiring", () => { + let mutationFieldSpy: ReturnType; + let localBuilder: typeof import("~/src/graphql/builder").builder; + let localDeleteResolver: typeof deleteCommentResolver; + + beforeEach(async () => { + vi.resetModules(); + const builderModule = await import("~/src/graphql/builder"); + localBuilder = builderModule.builder; + mutationFieldSpy = vi.spyOn(localBuilder, "mutationField"); + + const mod = await import("~/src/graphql/types/Mutation/deleteComment"); + localDeleteResolver = mod.deleteCommentResolver; + }); + + afterEach(() => { + mutationFieldSpy.mockRestore(); + }); + + it("should register deleteComment mutation correctly", () => { + expect(mutationFieldSpy).toHaveBeenCalledWith( + "deleteComment", + expect.any(Function), + ); + const calls = mutationFieldSpy.mock.calls.filter( + (call) => call[0] === "deleteComment", + ); + expect(calls.length).toBeGreaterThan(0); + if (calls[0] === undefined) + throw new Error("No calls found for deleteComment mutation"); + const callback = calls[0][1] as (t: { + field: (config: unknown) => unknown; + arg: (config: unknown) => unknown; + }) => unknown; + const dummyT = { + field: vi.fn().mockImplementation((config) => config), + arg: vi.fn().mockImplementation((config) => config), + }; + const fieldConfig = callback(dummyT) as { args: Record }; + expect(dummyT.field).toHaveBeenCalledWith( + expect.objectContaining({ + type: expect.anything(), + description: "Mutation field to delete a comment.", + args: expect.any(Object), + resolve: localDeleteResolver, + }), + ); + expect(fieldConfig.args).toHaveProperty("input"); + }); +}); diff --git a/test/routes/graphql/gql.tada-cache.d.ts b/test/routes/graphql/gql.tada-cache.d.ts index a1463432fff..9350c22239a 100644 --- a/test/routes/graphql/gql.tada-cache.d.ts +++ b/test/routes/graphql/gql.tada-cache.d.ts @@ -44,5 +44,11 @@ declare module 'gql.tada' { TadaDocumentNode<{ deleteOrganization: { id: string; name: string | null; countryCode: "at" | "pg" | "ad" | "ae" | "af" | "ag" | "ai" | "al" | "am" | "ao" | "aq" | "ar" | "as" | "au" | "aw" | "ax" | "az" | "ba" | "bb" | "bd" | "be" | "bf" | "bg" | "bh" | "bi" | "bj" | "bl" | "bm" | "bn" | "bo" | "bq" | "br" | "bs" | "bt" | "bv" | "bw" | "by" | "bz" | "ca" | "cc" | "cd" | "cf" | "cg" | "ch" | "ci" | "ck" | "cl" | "cm" | "cn" | "co" | "cr" | "cu" | "cv" | "cw" | "cx" | "cy" | "cz" | "de" | "dj" | "dk" | "dm" | "do" | "dz" | "ec" | "ee" | "eg" | "eh" | "er" | "es" | "et" | "fi" | "fj" | "fk" | "fm" | "fo" | "fr" | "ga" | "gb" | "gd" | "ge" | "gf" | "gg" | "gh" | "gi" | "gl" | "gm" | "gn" | "gp" | "gq" | "gr" | "gs" | "gt" | "gu" | "gw" | "gy" | "hk" | "hm" | "hn" | "hr" | "ht" | "hu" | "id" | "ie" | "il" | "im" | "in" | "io" | "iq" | "ir" | "is" | "it" | "je" | "jm" | "jo" | "jp" | "ke" | "kg" | "kh" | "ki" | "km" | "kn" | "kp" | "kr" | "kw" | "ky" | "kz" | "la" | "lb" | "lc" | "li" | "lk" | "lr" | "ls" | "lt" | "lu" | "lv" | "ly" | "ma" | "mc" | "md" | "me" | "mf" | "mg" | "mh" | "mk" | "ml" | "mm" | "mn" | "mo" | "mp" | "mq" | "mr" | "ms" | "mt" | "mu" | "mv" | "mw" | "mx" | "my" | "mz" | "na" | "nc" | "ne" | "nf" | "ng" | "ni" | "nl" | "no" | "np" | "nr" | "nu" | "nz" | "om" | "pa" | "pe" | "pf" | "ph" | "pk" | "pl" | "pm" | "pn" | "pr" | "ps" | "pt" | "pw" | "py" | "qa" | "re" | "ro" | "rs" | "ru" | "rw" | "sa" | "sb" | "sc" | "sd" | "se" | "sg" | "sh" | "si" | "sj" | "sk" | "sl" | "sm" | "sn" | "so" | "sr" | "ss" | "st" | "sv" | "sx" | "sy" | "sz" | "tc" | "td" | "tf" | "tg" | "th" | "tj" | "tk" | "tl" | "tm" | "tn" | "to" | "tr" | "tt" | "tv" | "tw" | "tz" | "ua" | "ug" | "um" | "us" | "uy" | "uz" | "va" | "vc" | "ve" | "vg" | "vi" | "vn" | "vu" | "wf" | "ws" | "ye" | "yt" | "za" | "zm" | "zw" | null; } | null; }, { input: { id: string; }; }, void>; "mutation Mutation_deleteOrganizationMembership($input: MutationDeleteOrganizationMembershipInput!) {\n deleteOrganizationMembership(input: $input) {\n id\n name\n countryCode\n }\n}": TadaDocumentNode<{ deleteOrganizationMembership: { id: string; name: string | null; countryCode: "at" | "pg" | "ad" | "ae" | "af" | "ag" | "ai" | "al" | "am" | "ao" | "aq" | "ar" | "as" | "au" | "aw" | "ax" | "az" | "ba" | "bb" | "bd" | "be" | "bf" | "bg" | "bh" | "bi" | "bj" | "bl" | "bm" | "bn" | "bo" | "bq" | "br" | "bs" | "bt" | "bv" | "bw" | "by" | "bz" | "ca" | "cc" | "cd" | "cf" | "cg" | "ch" | "ci" | "ck" | "cl" | "cm" | "cn" | "co" | "cr" | "cu" | "cv" | "cw" | "cx" | "cy" | "cz" | "de" | "dj" | "dk" | "dm" | "do" | "dz" | "ec" | "ee" | "eg" | "eh" | "er" | "es" | "et" | "fi" | "fj" | "fk" | "fm" | "fo" | "fr" | "ga" | "gb" | "gd" | "ge" | "gf" | "gg" | "gh" | "gi" | "gl" | "gm" | "gn" | "gp" | "gq" | "gr" | "gs" | "gt" | "gu" | "gw" | "gy" | "hk" | "hm" | "hn" | "hr" | "ht" | "hu" | "id" | "ie" | "il" | "im" | "in" | "io" | "iq" | "ir" | "is" | "it" | "je" | "jm" | "jo" | "jp" | "ke" | "kg" | "kh" | "ki" | "km" | "kn" | "kp" | "kr" | "kw" | "ky" | "kz" | "la" | "lb" | "lc" | "li" | "lk" | "lr" | "ls" | "lt" | "lu" | "lv" | "ly" | "ma" | "mc" | "md" | "me" | "mf" | "mg" | "mh" | "mk" | "ml" | "mm" | "mn" | "mo" | "mp" | "mq" | "mr" | "ms" | "mt" | "mu" | "mv" | "mw" | "mx" | "my" | "mz" | "na" | "nc" | "ne" | "nf" | "ng" | "ni" | "nl" | "no" | "np" | "nr" | "nu" | "nz" | "om" | "pa" | "pe" | "pf" | "ph" | "pk" | "pl" | "pm" | "pn" | "pr" | "ps" | "pt" | "pw" | "py" | "qa" | "re" | "ro" | "rs" | "ru" | "rw" | "sa" | "sb" | "sc" | "sd" | "se" | "sg" | "sh" | "si" | "sj" | "sk" | "sl" | "sm" | "sn" | "so" | "sr" | "ss" | "st" | "sv" | "sx" | "sy" | "sz" | "tc" | "td" | "tf" | "tg" | "th" | "tj" | "tk" | "tl" | "tm" | "tn" | "to" | "tr" | "tt" | "tv" | "tw" | "tz" | "ua" | "ug" | "um" | "us" | "uy" | "uz" | "va" | "vc" | "ve" | "vg" | "vi" | "vn" | "vu" | "wf" | "ws" | "ye" | "yt" | "za" | "zm" | "zw" | null; } | null; }, { input: { organizationId: string; memberId: string; }; }, void>; + "\n query tag($input:QueryTagInput!) {\n tag(input: $input) {\n id\n name\n organization {\n id\n }\n createdAt\n }\n}": + TadaDocumentNode<{ tag: { id: string; name: string | null; organization: { id: string; } | null; createdAt: string | null; } | null; }, { input: { id: string; }; }, void>; + "\n mutation CreateTag($input:MutationCreateTagInput!) {\n createTag(input: $input) {\n id\n name\n createdAt\n organization{\n id\n name\n createdAt\n\n }\n }\n }": + TadaDocumentNode<{ createTag: { id: string; name: string | null; createdAt: string | null; organization: { id: string; name: string | null; createdAt: string | null; } | null; } | null; }, { input: { organizationId: string; name: string; folderId?: string | null | undefined; }; }, void>; + "\n query Organization($input: QueryOrganizationInput!, $first: Int!) {\n organization(input: $input) {\n id\n name\n members(first: $first) {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n ": + TadaDocumentNode<{ organization: { id: string; name: string | null; members: { edges: ({ node: { id: string; name: string | null; } | null; } | null)[] | null; } | null; } | null; }, { first: number; input: { id: string; }; }, void>; } } From 2d3abe746b04a1c526b8c17e4aa209464a811acf Mon Sep 17 00:00:00 2001 From: iamanishx Date: Mon, 17 Feb 2025 09:40:22 +0000 Subject: [PATCH 02/11] retest --- test/routes/graphql/Mutation/deletecomment.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/routes/graphql/Mutation/deletecomment.test.ts b/test/routes/graphql/Mutation/deletecomment.test.ts index 7d8f737849a..5c0b96b2cba 100644 --- a/test/routes/graphql/Mutation/deletecomment.test.ts +++ b/test/routes/graphql/Mutation/deletecomment.test.ts @@ -175,7 +175,6 @@ describe("deleteCommentResolver", () => { }); it("throws unauthenticated error when currentUser is undefined", async () => { - // Return undefined for the user. ctx.drizzleClient.query.usersTable.findFirst = vi .fn() .mockResolvedValue(undefined); From 474b0e34e883aa9b569612b6c2e746f28005193b Mon Sep 17 00:00:00 2001 From: iamanishx Date: Mon, 17 Feb 2025 11:27:14 +0000 Subject: [PATCH 03/11] rabbit-ai fixes --- test/routes/graphql/Mutation/deletecomment.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/routes/graphql/Mutation/deletecomment.test.ts b/test/routes/graphql/Mutation/deletecomment.test.ts index 5c0b96b2cba..4ab27cd1f6b 100644 --- a/test/routes/graphql/Mutation/deletecomment.test.ts +++ b/test/routes/graphql/Mutation/deletecomment.test.ts @@ -7,7 +7,7 @@ const validArgs = { input: { id: validUuid } }; // --- Helper: Create a full mock GraphQLContext --- function createMockContext( - overrides: Partial = {}, + overrides: Partial> = {}, ): GraphQLContext { return { currentClient: { From 5b6771e857fa7c64febdcb8818037a2f77b2d0a6 Mon Sep 17 00:00:00 2001 From: iamanishx Date: Mon, 17 Feb 2025 11:37:02 +0000 Subject: [PATCH 04/11] ai fix --- test/routes/graphql/Mutation/deletecomment.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/routes/graphql/Mutation/deletecomment.test.ts b/test/routes/graphql/Mutation/deletecomment.test.ts index 4ab27cd1f6b..c44ac249a56 100644 --- a/test/routes/graphql/Mutation/deletecomment.test.ts +++ b/test/routes/graphql/Mutation/deletecomment.test.ts @@ -296,11 +296,9 @@ describe("deleteCommentResolver", () => { await deleteCommentResolver({}, validArgs, ctx); expect(commentFindFirstSpy).toHaveBeenCalledTimes(1); - if (!commentFindFirstSpy.mock.calls[0]) { - throw new Error("Expected mock to have been called"); - } + expect(commentFindFirstSpy.mock.calls.length).toBeGreaterThan(0); const config = commentFindFirstSpy.mock - .calls[0][0] as CommentsFindFirstConfig; + .calls[0]?.[0] as CommentsFindFirstConfig; expect(config.columns).toEqual({ creatorId: true }); expect(typeof config.where).toBe("function"); From 2bb44083e694126a0235b79f5ab8f2e168f76bab Mon Sep 17 00:00:00 2001 From: iamanishx Date: Mon, 17 Feb 2025 11:51:47 +0000 Subject: [PATCH 05/11] ai fix --- test/routes/graphql/Mutation/deletecomment.test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/routes/graphql/Mutation/deletecomment.test.ts b/test/routes/graphql/Mutation/deletecomment.test.ts index c44ac249a56..3d1a6673e0d 100644 --- a/test/routes/graphql/Mutation/deletecomment.test.ts +++ b/test/routes/graphql/Mutation/deletecomment.test.ts @@ -5,7 +5,12 @@ import { deleteCommentResolver } from "~/src/graphql/types/Mutation/deleteCommen const validUuid = "11111111-1111-1111-1111-111111111111"; const validArgs = { input: { id: validUuid } }; -// --- Helper: Create a full mock GraphQLContext --- +/** + * Creates a mock GraphQLContext for testing. + * @param overrides - Partial context to override default values + * @returns GraphQLContext with mocked functionality + */ + function createMockContext( overrides: Partial> = {}, ): GraphQLContext { @@ -393,10 +398,11 @@ describe("GraphQL schema wiring", () => { expect(calls.length).toBeGreaterThan(0); if (calls[0] === undefined) throw new Error("No calls found for deleteComment mutation"); - const callback = calls[0][1] as (t: { + type BuilderCallback = (t: { field: (config: unknown) => unknown; arg: (config: unknown) => unknown; }) => unknown; + const callback = calls[0][1] as BuilderCallback; const dummyT = { field: vi.fn().mockImplementation((config) => config), arg: vi.fn().mockImplementation((config) => config), From dca4b497c6a963bf7e71ab610516b03d4516d548 Mon Sep 17 00:00:00 2001 From: iamanishx Date: Mon, 17 Feb 2025 11:59:18 +0000 Subject: [PATCH 06/11] retest --- test/routes/graphql/Mutation/deletecomment.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/routes/graphql/Mutation/deletecomment.test.ts b/test/routes/graphql/Mutation/deletecomment.test.ts index 3d1a6673e0d..895c227f7db 100644 --- a/test/routes/graphql/Mutation/deletecomment.test.ts +++ b/test/routes/graphql/Mutation/deletecomment.test.ts @@ -92,7 +92,6 @@ interface CommentsFindFirstConfig { }; } -// --- Helper: Mock commentsTable.findFirst --- interface Comment { id: string; creatorId: string; From 70d7bb3461e1c6d62394fdf5a2d1eb5334befb2c Mon Sep 17 00:00:00 2001 From: iamanishx Date: Mon, 17 Feb 2025 12:05:47 +0000 Subject: [PATCH 07/11] retest --- test/routes/graphql/Mutation/deletecomment.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/routes/graphql/Mutation/deletecomment.test.ts b/test/routes/graphql/Mutation/deletecomment.test.ts index 895c227f7db..a699022c4a4 100644 --- a/test/routes/graphql/Mutation/deletecomment.test.ts +++ b/test/routes/graphql/Mutation/deletecomment.test.ts @@ -238,7 +238,7 @@ describe("deleteCommentResolver", () => { ); }); - it("throws unexpected error when deletion returns undefined", async () => { + it("throws unexpected error when comment deletion operation returns empty result", async () => { mockUsersTableFindFirst(ctx, { role: "administrator" }); mockCommentsTableFindFirst(ctx, { creatorId: "current-user-id", From 2d52b7c9acc825cfdafb9b26c27d1c663bbfd803 Mon Sep 17 00:00:00 2001 From: iamanishx Date: Mon, 17 Feb 2025 15:11:45 +0000 Subject: [PATCH 08/11] retest --- test/routes/graphql/Mutation/deletecomment.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/routes/graphql/Mutation/deletecomment.test.ts b/test/routes/graphql/Mutation/deletecomment.test.ts index a699022c4a4..15dc1f75ac7 100644 --- a/test/routes/graphql/Mutation/deletecomment.test.ts +++ b/test/routes/graphql/Mutation/deletecomment.test.ts @@ -132,7 +132,6 @@ function mockCommentsTableFindFirst( }); } -// --- Helper: Create a fake delete chain for delete operations --- function createFakeDeleteChain(returningData: unknown[]): { where: () => { returning: () => Promise }; } { From 2f2831f3fb4bc54a60d911f0fe667ac2351a7ed0 Mon Sep 17 00:00:00 2001 From: iamanishx Date: Mon, 17 Feb 2025 15:22:33 +0000 Subject: [PATCH 09/11] retest --- test/routes/graphql/Mutation/deletecomment.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/routes/graphql/Mutation/deletecomment.test.ts b/test/routes/graphql/Mutation/deletecomment.test.ts index 15dc1f75ac7..a699022c4a4 100644 --- a/test/routes/graphql/Mutation/deletecomment.test.ts +++ b/test/routes/graphql/Mutation/deletecomment.test.ts @@ -132,6 +132,7 @@ function mockCommentsTableFindFirst( }); } +// --- Helper: Create a fake delete chain for delete operations --- function createFakeDeleteChain(returningData: unknown[]): { where: () => { returning: () => Promise }; } { From d03ffa6ace3c7fe48765807a5b8e5c0dae471f1e Mon Sep 17 00:00:00 2001 From: iamanishx Date: Mon, 17 Feb 2025 16:13:17 +0000 Subject: [PATCH 10/11] retest --- test/routes/graphql/Mutation/deletecomment.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/routes/graphql/Mutation/deletecomment.test.ts b/test/routes/graphql/Mutation/deletecomment.test.ts index a699022c4a4..15dc1f75ac7 100644 --- a/test/routes/graphql/Mutation/deletecomment.test.ts +++ b/test/routes/graphql/Mutation/deletecomment.test.ts @@ -132,7 +132,6 @@ function mockCommentsTableFindFirst( }); } -// --- Helper: Create a fake delete chain for delete operations --- function createFakeDeleteChain(returningData: unknown[]): { where: () => { returning: () => Promise }; } { From 590eee78ccea845b0b320f00a27a6125be7b915d Mon Sep 17 00:00:00 2001 From: iamanishx Date: Mon, 17 Feb 2025 16:55:53 +0000 Subject: [PATCH 11/11] fixes --- src/graphql/types/Mutation/deleteComment.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/graphql/types/Mutation/deleteComment.ts b/src/graphql/types/Mutation/deleteComment.ts index 4a66490ed8a..397b43beb61 100644 --- a/src/graphql/types/Mutation/deleteComment.ts +++ b/src/graphql/types/Mutation/deleteComment.ts @@ -51,7 +51,9 @@ export async function deleteCommentResolver( where: (fields, operators) => operators.eq(fields.id, currentUserId), }), ctx.drizzleClient.query.commentsTable.findFirst({ - columns: { creatorId: true }, + columns: { + creatorId: true, + }, with: { post: { columns: {