diff --git a/src/app/detail/[task_id]/[user_type]/loaders.test.ts b/src/app/detail/[task_id]/[user_type]/loaders.test.ts new file mode 100644 index 000000000..9fd064c0e --- /dev/null +++ b/src/app/detail/[task_id]/[user_type]/loaders.test.ts @@ -0,0 +1,75 @@ +import type User from '@api/core/models/User.model' +import { loadSubtaskStatus, loadTask, loadTaskPath } from '@/app/detail/[task_id]/[user_type]/loaders' +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library' +import { z } from 'zod' + +const mockGetOneTask = jest.fn() +const mockGetTraversalPath = jest.fn() +const mockGetSubtaskStatus = jest.fn() + +jest.mock('@api/tasks/tasks.service', () => ({ + TasksService: jest.fn().mockImplementation(() => ({ + getOneTask: mockGetOneTask, + getTraversalPath: mockGetTraversalPath, + })), +})) + +jest.mock('@api/tasks/subtasks.service', () => ({ + SubtaskService: jest.fn().mockImplementation(() => ({ + getSubtaskStatus: mockGetSubtaskStatus, + })), +})) + +jest.mock('@api/view-settings/viewSettings.service', () => ({ + ViewSettingsService: jest.fn().mockImplementation(() => ({ + getViewSettingsForUser: jest.fn(), + })), +})) + +const user = { workspaceId: 'workspace-id' } as User + +const createPrismaError = ({ + code, + meta, +}: { + code: string + meta?: Record +}): PrismaClientKnownRequestError => + new PrismaClientKnownRequestError('Prisma known request error', { + code, + clientVersion: '5.19.0', + meta, + }) + +describe('detail loaders', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('treats invalid UUID Prisma raw-query errors as a missing task', async () => { + mockGetOneTask.mockRejectedValue(createPrismaError({ code: 'P2010', meta: { code: '22P02' } })) + + await expect(loadTask(user, 'not-a-uuid')).resolves.toBeNull() + }) + + it('treats invalid UUID Prisma column errors as an empty traversal path', async () => { + mockGetTraversalPath.mockRejectedValue( + createPrismaError({ code: 'P2023', meta: { message: 'Error creating UUID, invalid length: expected length 32' } }), + ) + + await expect(loadTaskPath(user, 'not-a-uuid')).resolves.toEqual([]) + }) + + it('treats Zod UUID validation errors as no renderable subtask status', async () => { + mockGetSubtaskStatus.mockRejectedValue(z.string().uuid().safeParse('not-a-uuid').error) + + await expect(loadSubtaskStatus(user, 'not-a-uuid')).resolves.toEqual({ count: 0, canCreateSubtask: false }) + }) + + it('still rethrows unrelated Prisma errors', async () => { + const error = createPrismaError({ code: 'P2002', meta: { target: ['id'] } }) + mockGetOneTask.mockRejectedValue(error) + + await expect(loadTask(user, 'task-id')).rejects.toBe(error) + }) +}) diff --git a/src/app/detail/[task_id]/[user_type]/loaders.ts b/src/app/detail/[task_id]/[user_type]/loaders.ts index 6617d937d..59dabc68b 100644 --- a/src/app/detail/[task_id]/[user_type]/loaders.ts +++ b/src/app/detail/[task_id]/[user_type]/loaders.ts @@ -6,10 +6,31 @@ import { ViewSettingsService } from '@api/view-settings/viewSettings.service' import httpStatus from 'http-status' import type { AncestorTaskResponse, SubTaskStatusResponse, TaskResponse } from '@/types/dto/tasks.dto' import type { CreateViewSettingsDTO } from '@/types/dto/viewSettings.dto' +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library' +import { ZodError } from 'zod' // this is needed since we are no longer making api round trip our dates are actual dates when we need it as string. const toJsonSafe = (value: T): T => JSON.parse(JSON.stringify(value)) as T +const isInvalidUuidPrismaError = (err: unknown) => { + if (!(err instanceof PrismaClientKnownRequestError)) { + return false + } + + if (err.code === 'P2010' && err.meta?.code === '22P02') { + return true + } + + const metaMessage = err.meta?.message + return err.code === 'P2023' && typeof metaMessage === 'string' && metaMessage.toLowerCase().includes('uuid') +} + +const isInvalidUuidZodError = (err: unknown) => + err instanceof ZodError && + err.issues.some((issue) => issue.code === 'invalid_string' && issue.message.toLowerCase().includes('uuid')) + +const isInvalidTaskIdError = (err: unknown) => isInvalidUuidPrismaError(err) || isInvalidUuidZodError(err) + export const loadTask = async (user: User, taskId: string): Promise => { try { const task = await new TasksService(user).getOneTask(taskId) @@ -17,6 +38,7 @@ export const loadTask = async (user: User, taskId: string): Promise