Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions src/app/detail/[task_id]/[user_type]/loaders.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>
}): 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)
})
})
24 changes: 24 additions & 0 deletions src/app/detail/[task_id]/[user_type]/loaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,39 @@ 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 = <T>(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<TaskResponse | null> => {
try {
const task = await new TasksService(user).getOneTask(taskId)
return toJsonSafe(task) as unknown as TaskResponse
} catch (err) {
const nonRenderableStatuses: number[] = [httpStatus.NOT_FOUND, httpStatus.UNAUTHORIZED]
if (err instanceof APIError && nonRenderableStatuses.includes(err.status)) return null
if (isInvalidTaskIdError(err)) return null
throw err
}
}
Expand All @@ -26,6 +48,7 @@ export const loadTaskPath = async (user: User, taskId: string): Promise<Ancestor
return toJsonSafe(await new TasksService(user).getTraversalPath(taskId))
} catch (err) {
if (err instanceof APIError && err.status === httpStatus.NOT_FOUND) return []
if (isInvalidTaskIdError(err)) return []
throw err
}
}
Expand All @@ -35,6 +58,7 @@ export const loadSubtaskStatus = async (user: User, taskId: string): Promise<Sub
return await new SubtaskService(user).getSubtaskStatus(taskId)
} catch (err) {
if (err instanceof APIError && err.status === httpStatus.NOT_FOUND) return { count: 0, canCreateSubtask: false }
if (isInvalidTaskIdError(err)) return { count: 0, canCreateSubtask: false }
throw err
}
}
Expand Down
Loading