From 481f2d1cd664035e6f1181022926cef7587251b4 Mon Sep 17 00:00:00 2001 From: shiva Date: Thu, 13 Feb 2025 21:16:19 +0530 Subject: [PATCH 1/3] feat:test to updatedAt.test.ts --- src/graphql/types/FundCampaign/updatedAt.ts | 158 ++++++------ .../types/FundCampaign/updatedAt.test.ts | 233 ++++++++++++++++++ 2 files changed, 320 insertions(+), 71 deletions(-) create mode 100644 test/graphql/types/FundCampaign/updatedAt.test.ts diff --git a/src/graphql/types/FundCampaign/updatedAt.ts b/src/graphql/types/FundCampaign/updatedAt.ts index 9026f946ed5..9dfce3f45ab 100644 --- a/src/graphql/types/FundCampaign/updatedAt.ts +++ b/src/graphql/types/FundCampaign/updatedAt.ts @@ -1,93 +1,109 @@ import { TalawaGraphQLError } from "~/src/utilities/TalawaGraphQLError"; +import type { GraphQLContext } from "../../context"; import { FundCampaign } from "./FundCampaign"; -FundCampaign.implement({ - fields: (t) => ({ - updatedAt: t.field({ - description: "Date time at the time the fund campaign was last updated.", - resolve: async (parent, _args, ctx) => { - if (!ctx.currentClient.isAuthenticated) { - throw new TalawaGraphQLError({ - extensions: { - code: "unauthenticated", - }, - }); - } +export const updatedAtResolver = async ( + parent: FundCampaign, + args: unknown, + ctx: GraphQLContext, +) => { + try { + if (!ctx.currentClient.isAuthenticated) { + throw new TalawaGraphQLError({ + extensions: { + code: "unauthenticated", + }, + }); + } - const currentUserId = ctx.currentClient.user.id; + const currentUserId = ctx.currentClient.user.id; - const [currentUser, existingFund] = await Promise.all([ - ctx.drizzleClient.query.usersTable.findFirst({ + const [currentUser, existingFund] = await Promise.all([ + ctx.drizzleClient.query.usersTable.findFirst({ + columns: { + role: true, + }, + where: (fields, operators) => operators.eq(fields.id, currentUserId), + }), + ctx.drizzleClient.query.fundsTable.findFirst({ + columns: { + isTaxDeductible: true, + }, + with: { + organization: { columns: { - role: true, - }, - - where: (fields, operators) => - operators.eq(fields.id, currentUserId), - }), - ctx.drizzleClient.query.fundsTable.findFirst({ - columns: { - isTaxDeductible: 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, currentUserId), - }), - ]); + }, + }, + where: (fields, operators) => operators.eq(fields.id, currentUserId), + }), + ]); - if (currentUser === undefined) { - throw new TalawaGraphQLError({ - extensions: { - code: "unauthenticated", - }, - }); - } + if (currentUser === undefined) { + throw new TalawaGraphQLError({ + extensions: { + code: "unauthenticated", + }, + }); + } - // Fund id existing but the associated fund not existing is a business logic error and probably means that the corresponding data in the database is in a corrupted state. It must be investigated and fixed as soon as possible to prevent additional data corruption. - if (existingFund === undefined) { - ctx.log.error( - "Postgres select operation returned an empty array for a fund campaign's fund id that isn't null.", - ); + if (existingFund === undefined) { + ctx.log.error( + "Postgres select operation returned an empty array for a fund campaign's fund id that isn't null.", + ); - throw new TalawaGraphQLError({ - extensions: { - code: "unexpected", - }, - }); - } + throw new TalawaGraphQLError({ + extensions: { + code: "unexpected", + }, + }); + } - const currentUserOrganizationMembership = - existingFund.organization.membershipsWhereOrganization[0]; + const currentUserOrganizationMembership = + existingFund.organization.membershipsWhereOrganization[0]; - if ( - currentUser.role !== "administrator" && - (currentUserOrganizationMembership === undefined || - currentUserOrganizationMembership.role !== "administrator") - ) { - throw new TalawaGraphQLError({ - extensions: { - code: "unauthorized_action", - }, - }); - } + if ( + currentUser.role !== "administrator" && + (currentUserOrganizationMembership === undefined || + currentUserOrganizationMembership.role !== "administrator") + ) { + throw new TalawaGraphQLError({ + extensions: { + code: "unauthorized_action", + }, + }); + } - return parent.updatedAt; + return parent.updatedAt; + } catch (error) { + if (error instanceof TalawaGraphQLError) { + throw error; + } + + ctx.log.error("Error in updatedAtResolver:", error); + throw new TalawaGraphQLError({ + extensions: { + code: "unexpected", }, + }); + } +}; + +FundCampaign.implement({ + fields: (t) => ({ + updatedAt: t.field({ + description: "Date time at the time the fund campaign was last updated.", + resolve: updatedAtResolver, type: "DateTime", }), }), diff --git a/test/graphql/types/FundCampaign/updatedAt.test.ts b/test/graphql/types/FundCampaign/updatedAt.test.ts new file mode 100644 index 00000000000..9169ec3d08a --- /dev/null +++ b/test/graphql/types/FundCampaign/updatedAt.test.ts @@ -0,0 +1,233 @@ +import { vi } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; +import type { CurrentClient, GraphQLContext } from "~/src/graphql/context"; +import type { FundCampaign } from "~/src/graphql/types/FundCampaign/FundCampaign"; +import { updatedAtResolver } from "~/src/graphql/types/FundCampaign/updatedAt"; +import { TalawaGraphQLError } from "~/src/utilities/TalawaGraphQLError"; + +const mockFundCampaign: FundCampaign = { + id: "fund-123", + createdAt: new Date("2024-02-01T09:00:00Z"), + name: "Test Campaign", + creatorId: "creator-123", + updatedAt: new Date("2024-02-01T10:00:00Z"), + updaterId: "updater-123", + currencyCode: "USD", + endAt: new Date("2024-12-31T23:59:59Z"), + fundId: "fund-456", + goalAmount: 10000, + startAt: new Date("2024-02-01T00:00:00Z"), +}; + +const createMockContext = () => { + const mockContext = { + currentClient: { + isAuthenticated: true, + user: { id: "user-123" }, + } as CurrentClient, + drizzleClient: { + query: { + usersTable: { findFirst: vi.fn() }, + fundsTable: { findFirst: vi.fn() }, + }, + }, + log: { error: vi.fn(), info: vi.fn(), warn: vi.fn(), debug: vi.fn() }, + envConfig: { API_BASE_URL: "mock url" }, + jwt: { sign: vi.fn() }, + minio: { presignedUrl: vi.fn(), putObject: vi.fn(), getObject: vi.fn() }, + }; + return mockContext as unknown as GraphQLContext; +}; + +describe("updatedAtResolver", () => { + let ctx: GraphQLContext; + + beforeEach(() => { + ctx = createMockContext(); + }); + + it("should throw unauthenticated error if user is not logged in", async () => { + ctx.currentClient.isAuthenticated = false; + await expect(updatedAtResolver(mockFundCampaign, {}, ctx)).rejects.toThrow( + new TalawaGraphQLError({ extensions: { code: "unauthenticated" } }), + ); + }); + + it("should throw unauthenticated error if current user does not exist", async () => { + ( + ctx.drizzleClient.query.usersTable.findFirst as ReturnType + ).mockResolvedValue(undefined); + await expect(updatedAtResolver(mockFundCampaign, {}, ctx)).rejects.toThrow( + new TalawaGraphQLError({ extensions: { code: "unauthenticated" } }), + ); + }); + + it("should throw unexpected error if fund does not exist", async () => { + ( + ctx.drizzleClient.query.usersTable.findFirst as ReturnType + ).mockResolvedValue({ role: "member" }); + ( + ctx.drizzleClient.query.fundsTable.findFirst as ReturnType + ).mockResolvedValue(undefined); + await expect(updatedAtResolver(mockFundCampaign, {}, ctx)).rejects.toThrow( + new TalawaGraphQLError({ extensions: { code: "unexpected" } }), + ); + expect(ctx.log.error).toHaveBeenCalled(); + }); + + it("should throw unauthorized_action error if user is not an admin and not an organization admin", async () => { + const mockUser = { role: "member" }; + const mockFund = { + isTaxDeductible: true, + organization: { + countryCode: "US", + membershipsWhereOrganization: [], + }, + }; + ( + ctx.drizzleClient.query.usersTable.findFirst as ReturnType + ).mockResolvedValue(mockUser); + ( + ctx.drizzleClient.query.fundsTable.findFirst as ReturnType + ).mockResolvedValue(mockFund); + await expect(updatedAtResolver(mockFundCampaign, {}, ctx)).rejects.toThrow( + new TalawaGraphQLError({ extensions: { code: "unauthorized_action" } }), + ); + }); + + it("should return updatedAt if user is an admin", async () => { + const mockUser = { role: "administrator" }; + const mockFund = { + isTaxDeductible: true, + organization: { + countryCode: "US", + membershipsWhereOrganization: [], + }, + }; + ( + ctx.drizzleClient.query.usersTable.findFirst as ReturnType + ).mockResolvedValue(mockUser); + ( + ctx.drizzleClient.query.fundsTable.findFirst as ReturnType + ).mockResolvedValue(mockFund); + const result = await updatedAtResolver(mockFundCampaign, {}, ctx); + expect(result).toEqual(mockFundCampaign.updatedAt); + }); + + it("should return updatedAt if user is an organization admin", async () => { + const mockUser = { role: "member" }; + const mockFund = { + isTaxDeductible: true, + organization: { + countryCode: "US", + membershipsWhereOrganization: [{ role: "administrator" }], + }, + }; + ( + ctx.drizzleClient.query.usersTable.findFirst as ReturnType + ).mockResolvedValue(mockUser); + ( + ctx.drizzleClient.query.fundsTable.findFirst as ReturnType + ).mockResolvedValue(mockFund); + const result = await updatedAtResolver(mockFundCampaign, {}, ctx); + expect(result).toEqual(mockFundCampaign.updatedAt); + }); + + it("should handle database connection error", async () => { + ( + ctx.drizzleClient.query.usersTable.findFirst as ReturnType + ).mockRejectedValue(new Error("ECONNREFUSED")); + await expect(updatedAtResolver(mockFundCampaign, {}, ctx)).rejects.toThrow( + new TalawaGraphQLError({ extensions: { code: "unexpected" } }), + ); + expect(ctx.log.error).toHaveBeenCalled(); + }); + + it("should handle database timeout error", async () => { + ( + ctx.drizzleClient.query.usersTable.findFirst as ReturnType + ).mockRejectedValue(new Error("Query timeout")); + await expect(updatedAtResolver(mockFundCampaign, {}, ctx)).rejects.toThrow( + new TalawaGraphQLError({ extensions: { code: "unexpected" } }), + ); + expect(ctx.log.error).toHaveBeenCalled(); + }); + + it("should handle database constraint violation", async () => { + ( + ctx.drizzleClient.query.usersTable.findFirst as ReturnType + ).mockRejectedValue(new Error("violates foreign key constraint")); + await expect(updatedAtResolver(mockFundCampaign, {}, ctx)).rejects.toThrow( + new TalawaGraphQLError({ extensions: { code: "unexpected" } }), + ); + expect(ctx.log.error).toHaveBeenCalled(); + }); + + it("should handle database query syntax error", async () => { + ( + ctx.drizzleClient.query.usersTable.findFirst as ReturnType + ).mockRejectedValue(new Error("syntax error in SQL statement")); + await expect(updatedAtResolver(mockFundCampaign, {}, ctx)).rejects.toThrow( + new TalawaGraphQLError({ extensions: { code: "unexpected" } }), + ); + expect(ctx.log.error).toHaveBeenCalled(); + }); + + it("should handle concurrent updates to the same fund", async () => { + ( + ctx.drizzleClient.query.usersTable.findFirst as ReturnType + ).mockRejectedValueOnce(new Error("Database lock timeout")); + + await expect(updatedAtResolver(mockFundCampaign, {}, ctx)).rejects.toThrow( + new TalawaGraphQLError({ extensions: { code: "unexpected" } }), + ); + expect(ctx.log.error).toHaveBeenCalled(); + }); + + it("should handle database error during concurrent access", async () => { + ( + ctx.drizzleClient.query.usersTable.findFirst as ReturnType + ).mockRejectedValueOnce( + new Error("Database error during concurrent access"), + ); + + await expect(updatedAtResolver(mockFundCampaign, {}, ctx)).rejects.toThrow( + new TalawaGraphQLError({ extensions: { code: "unexpected" } }), + ); + expect(ctx.log.error).toHaveBeenCalled(); + }); + + it("should pass through TalawaGraphQLError without wrapping", async () => { + const originalError = new TalawaGraphQLError({ + message: "Custom error", + extensions: { code: "unexpected" }, + }); + ( + ctx.drizzleClient.query.usersTable.findFirst as ReturnType + ).mockRejectedValue(originalError); + await expect(updatedAtResolver(mockFundCampaign, {}, ctx)).rejects.toThrow( + originalError, + ); + expect(ctx.log.error).not.toHaveBeenCalled(); + }); + + it("should handle missing organization membership gracefully", async () => { + const mockUser = { role: "member" }; + const mockFund = { + isTaxDeductible: true, + organization: { + countryCode: "US", + membershipsWhereOrganization: [], + }, + }; + ( + ctx.drizzleClient.query.usersTable.findFirst as ReturnType + ).mockResolvedValue(mockUser); + ( + ctx.drizzleClient.query.fundsTable.findFirst as ReturnType + ).mockResolvedValue(mockFund); + await expect(updatedAtResolver(mockFundCampaign, {}, ctx)).rejects.toThrow( + new TalawaGraphQLError({ extensions: { code: "unauthorized_action" } }), + ); + }); +}); From c32990203617e55be001139e6daab81bc02e655b Mon Sep 17 00:00:00 2001 From: shiva Date: Fri, 14 Feb 2025 14:45:19 +0530 Subject: [PATCH 2/3] fix:coderabbit sugg --- src/graphql/types/FundCampaign/updatedAt.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/graphql/types/FundCampaign/updatedAt.ts b/src/graphql/types/FundCampaign/updatedAt.ts index 9dfce3f45ab..c06f7e1c163 100644 --- a/src/graphql/types/FundCampaign/updatedAt.ts +++ b/src/graphql/types/FundCampaign/updatedAt.ts @@ -2,6 +2,20 @@ import { TalawaGraphQLError } from "~/src/utilities/TalawaGraphQLError"; import type { GraphQLContext } from "../../context"; import { FundCampaign } from "./FundCampaign"; +/** + * Resolver for the updatedAt field of FundCampaign type. + * Validates user authentication and authorization before returning the last update timestamp. + * Only administrators and organization admins have access to this field. + * + * @param parent - The parent FundCampaign object containing the updatedAt field + * @param args - GraphQL arguments (unused) + * @param ctx - GraphQL context containing authentication and database clients + * @returns {Promise} The timestamp when the fund campaign was last updated + * @throws {TalawaGraphQLError} With code 'unauthenticated' if user is not logged in + * @throws {TalawaGraphQLError} With code 'unauthorized_action' if user lacks required permissions + * @throws {TalawaGraphQLError} With code 'unexpected' for database or other runtime errors + */ + export const updatedAtResolver = async ( parent: FundCampaign, args: unknown, From 5607f5d8982ab40c31549a193bfcca93c9a2e2da Mon Sep 17 00:00:00 2001 From: shiva Date: Fri, 14 Feb 2025 15:01:09 +0530 Subject: [PATCH 3/3] fix:improved code quality --- src/graphql/types/FundCampaign/updatedAt.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/graphql/types/FundCampaign/updatedAt.ts b/src/graphql/types/FundCampaign/updatedAt.ts index c06f7e1c163..0e29f98e608 100644 --- a/src/graphql/types/FundCampaign/updatedAt.ts +++ b/src/graphql/types/FundCampaign/updatedAt.ts @@ -83,14 +83,12 @@ export const updatedAtResolver = async ( }); } - const currentUserOrganizationMembership = - existingFund.organization.membershipsWhereOrganization[0]; + const hasAdminRole = + existingFund.organization.membershipsWhereOrganization.some( + (membership) => membership.role === "administrator", + ); - if ( - currentUser.role !== "administrator" && - (currentUserOrganizationMembership === undefined || - currentUserOrganizationMembership.role !== "administrator") - ) { + if (currentUser.role !== "administrator" && !hasAdminRole) { throw new TalawaGraphQLError({ extensions: { code: "unauthorized_action",