From 3c4b1dbd33f99df146767317ddc566aee4761da8 Mon Sep 17 00:00:00 2001 From: Nandgopal-R Date: Mon, 9 Feb 2026 04:36:50 +0530 Subject: [PATCH 1/3] fix: fix getFormById and form-responses for integration --- bruno/form-fields/createFields.bru | 57 ++++++-------- bruno/forms/publishForm.bru | 2 +- bruno/forms/unPublishForm.bru | 4 +- prisma/schema.prisma | 99 ++++++++++-------------- src/api/form-response/controller.ts | 103 ++++++++++++++++++++++++- src/api/form-response/routes.ts | 10 +++ src/api/forms/controller.ts | 114 +++++++++++++++++++++++++++- src/api/forms/routes.ts | 9 +++ src/index.ts | 3 +- src/test/form-response.test.ts | 106 +++++++++++++++++++++++++- src/test/forms.test.ts | 94 ++++++++++++++++++++++- src/types/form-response.ts | 2 + src/types/forms.ts | 10 +++ 13 files changed, 513 insertions(+), 100 deletions(-) diff --git a/bruno/form-fields/createFields.bru b/bruno/form-fields/createFields.bru index 0364c0b..08c2209 100644 --- a/bruno/form-fields/createFields.bru +++ b/bruno/form-fields/createFields.bru @@ -35,30 +35,30 @@ settings { docs { # Create Field - Adds a new field to a form. This endpoint handles the **linked-list ordering** logic automatically. You can insert a field at the very beginning (Head) or after a specific existing field. + Adds a new field to a form. This endpoint handles the linked-list ordering logic automatically. You can insert a field at the very beginning (Head) or after a specific existing field. - * **URL:** `/forms/:formId/fields` - * **Method:** `POST` - * **Auth Required:** Yes + - **URL:** `/forms/:formId/fields` + - **Method:** `POST` + - **Auth Required:** Yes - ### Path Parameters + ## Path Parameters | Parameter | Type | Description | - | :--- | :--- | :--- | + |---|---|---| | `formId` | `string` (UUID) | The ID of the form to add the field to. | - ### Request Body + ## Request Body | Field | Type | Required | Description | - | :--- | :--- | :--- | :--- | + |---|---|---|---| | `fieldName` | `string` | Yes | Internal unique name for the field (e.g., "user_email"). | | `label` | `string` | Yes | The question text shown to users. | | `fieldValueType` | `string` | Yes | Data type (e.g., "string", "number", "boolean"). | - | `fieldType` | `string` | Yes | UI Component type (e.g., "text", "select", "radio"). | - | `prevFieldId` | `string` | No | The ID of the field *after which* this new field should be inserted. Send `null` or omit to insert at the top (as the first question). | + | `fieldType` | `string` | Yes | UI component type (e.g., "text", "select", "radio"). | + | `prevFieldId` | `string \| null` | No | ID of the field after which the new field should be inserted. Omit or send `null` to insert at the beginning. | | `validation` | `object` | No | JSON object for validation rules (e.g., `{ "required": true }`). | - **Sample Input (Insert at Top/Head):** + ## Sample Input (Insert at Top) ```json { @@ -71,10 +71,11 @@ docs { "required": true } } + ``` - Sample Input (Insert after an existing field): - JSON + ## Sample Input (Insert After Another Field) + ```json { "fieldName": "age", "label": "How old are you?", @@ -82,13 +83,13 @@ docs { "fieldType": "number", "prevFieldId": "uuid-of-previous-field-123" } + ``` - Responses - ✅ 200 OK: Created + ## Responses - Returns the newly created field object. - JSON + ### 200 OK — Created + ```json { "success": true, "message": "Field created successfully", @@ -102,28 +103,18 @@ docs { "required": true }, "formId": "form_xyz_789", - "prevFieldId": null, - "createdAt": "2023-10-27T10:00:00.000Z" + "prevFieldId": null } } + ``` - ❌ 400 Bad Request: Invalid Position - - Occurs if prevFieldId is provided but that field does not exist in this form (meaning you are trying to insert after a field that doesn't exist). - JSON - - { - "success": false, - "message": "Previous field not found in the specified form" - } - - ❌ 404 Not Found: Form Missing - - Occurs if the form does not exist or the user does not have permission to edit it. - JSON + ### 404 Not Found — Form Missing + ```json { "success": false, "message": "Form not found" } + ``` + } diff --git a/bruno/forms/publishForm.bru b/bruno/forms/publishForm.bru index f17bb25..2a1a3ab 100644 --- a/bruno/forms/publishForm.bru +++ b/bruno/forms/publishForm.bru @@ -24,7 +24,7 @@ docs { Changes the status of a form to **Published**. Once published, the form becomes accessible to respondents for submission. - * **URL:** `/forms/:formId/publish` + * **URL:** `/forms/publish/:formId` * **Method:** `PATCH` * **Auth Required:** Yes diff --git a/bruno/forms/unPublishForm.bru b/bruno/forms/unPublishForm.bru index 62016f0..6f99f05 100644 --- a/bruno/forms/unPublishForm.bru +++ b/bruno/forms/unPublishForm.bru @@ -5,7 +5,7 @@ meta { } post { - url: http://localhost:8000/forms/publish/:formId + url: http://localhost:8000/forms/unpublish/:formId body: none auth: inherit } @@ -24,7 +24,7 @@ docs { Reverts the status of a form to **Unpublished**. This hides the form from the public, preventing any new submissions until it is published again. - * **URL:** `/forms/:formId/unpublish` + * **URL:** `/forms/unpublish/:formId` * **Method:** `PATCH` * **Auth Required:** Yes diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8c54fbf..a533dfc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,9 +1,3 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? -// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init - generator client { provider = "prisma-client-js" } @@ -13,18 +7,17 @@ datasource db { } model User { - id String @id @default(uuid()) - email String @unique + id String @id @default(uuid()) + email String @unique name String? - emailVerified Boolean @default(false) + emailVerified Boolean @default(false) image String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - sessions Session[] - accounts Account[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt forms Form[] + accounts Account[] formResponses FormResponse[] + sessions Session[] @@map("user") } @@ -38,8 +31,7 @@ model Session { userAgent String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("session") } @@ -51,25 +43,22 @@ model Account { providerId String accessToken String? refreshToken String? - idToken String? - scope String? - accessTokenExpiresAt DateTime? expiresAt DateTime? password String? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + accessTokenExpiresAt DateTime? + idToken String? + scope String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("account") } -// You will need this table for "Forgot Password" or "Verify Email" flows model Verification { id String @id @default(uuid()) identifier String - value String // The token itself + value String expiresAt DateTime createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -78,41 +67,31 @@ model Verification { } model Form { - id String @id @default(uuid()) - title String - description String? - isPublished Boolean @default(false) - formUrl String? - - ownerId String - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - + id String @id @default(uuid()) + title String + description String? + isPublished Boolean @default(false) + formUrl String? + ownerId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) formFields FormFields[] formResponses FormResponse[] } model FormFields { - id String @id @default(uuid()) + id String @id @default(uuid()) fieldName String label String? - fieldValueType String // e.g., "string", "number", "boolean" - fieldType String // e.g., "MCQ", "Radio", "Input" - validation Json? // e.g., { "required": true, "min": 5 } - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // 1. Link to the parent Form - formId String - form Form @relation(fields: [formId], references: [id], onDelete: Cascade) - - // 2. Doubly Linked List Logic (Self-Relations) - - // The "Previous" Field (Parent) - // This field stores the ID of the field passing control to this one - prevFieldId String? + fieldValueType String + fieldType String + validation Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + formId String + prevFieldId String? + form Form @relation(fields: [formId], references: [id], onDelete: Cascade) @@index([formId]) @@index([formId, prevFieldId]) @@ -120,18 +99,24 @@ model FormFields { } model FormResponse { - id String @id @default(uuid()) - - formId String - form Form @relation(fields: [formId], references: [id], onDelete: Cascade) - + id String @id @default(uuid()) + formId String respondentId String? +<<<<<<< HEAD respondent User? @relation(fields: [respondentId], references: [id], onDelete: SetNull) answers Json isSubmitted Boolean @default(false) submittedAt DateTime? updatedAt DateTime @updatedAt +======= + answers Json + submittedAt DateTime? + updatedAt DateTime @updatedAt + isSubmitted Boolean @default(false) + form Form @relation(fields: [formId], references: [id], onDelete: Cascade) + respondent User? @relation(fields: [respondentId], references: [id]) +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) @@unique([formId, respondentId]) @@index([formId]) diff --git a/src/api/form-response/controller.ts b/src/api/form-response/controller.ts index aad9bba..22578d7 100644 --- a/src/api/form-response/controller.ts +++ b/src/api/form-response/controller.ts @@ -69,12 +69,16 @@ export async function submitResponse({ formId: params.formId, respondentId: user.id, answers: body.answers, +<<<<<<< HEAD isSubmitted: true, submittedAt: new Date(), +======= + isSubmitted: body.isSubmitted ?? false, +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) }, }); logger.info( - `User ${user.id} submitted response ${response.id} for form ${params.formId}`, + `User ${user.id} ${body.isSubmitted ? "submitted" : "saved draft"} response ${response.id} for form ${params.formId}`, ); return { success: true, @@ -136,13 +140,33 @@ export async function saveDraftResponse({ formId: params.formId, respondentId: user.id, answers: body.answers, + isSubmitted: body.isSubmitted ?? false, }, }); +<<<<<<< HEAD logger.info(`User ${user.id} saved draft response for form ${params.formId}`); return { success: true, message: "Draft response saved successfully", +======= + if (response.count === 0) { + logger.warn(`No response found with ID ${params.responseId} to update`); + return { + success: false, + message: "No response found to update", + }; + } + + logger.info( + `Response ${params.responseId} ${body.isSubmitted ? "submitted" : "updated as draft"}`, + ); + return { + success: true, + message: body.isSubmitted + ? "Response submitted successfully" + : "Draft saved successfully", +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) data: response, }; } @@ -297,6 +321,7 @@ export async function getSubmittedResponse({ formId: r.formId, formTitle: r.form.title, answers: transformedAnswers, + rawAnswers: r.answers, // Include raw answers with field IDs for form loading }; }); @@ -310,6 +335,7 @@ export async function getSubmittedResponse({ }; } +<<<<<<< HEAD export async function getDraftResponse({ params, user, @@ -320,11 +346,19 @@ export async function getDraftResponse({ respondentId: user.id, formId: params.formId, isSubmitted: false, +======= +// Get all responses submitted by the current user across all forms +export async function getAllUserResponses({ user }: { user: { id: string } }) { + const responses = await prisma.formResponse.findMany({ + where: { + respondentId: user.id, +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) }, select: { id: true, formId: true, answers: true, +<<<<<<< HEAD form: { select: { title: true }, }, @@ -362,5 +396,72 @@ export async function getDraftResponse({ formTitle: draft.form.title, answers: transformed, }, +======= + isSubmitted: true, + submittedAt: true, + updatedAt: true, + form: { + select: { + id: true, + title: true, + description: true, + }, + }, + }, + orderBy: { + updatedAt: "desc", + }, + }); + + if (responses.length === 0) { + logger.info(`No responses found for user ${user.id}`); + return { + success: true, + message: "No responses found", + data: [], + }; + } + + // For each response, get the field names to transform answers + const formattedResponses = await Promise.all( + responses.map(async (r) => { + const fields = await prisma.formFields.findMany({ + where: { formId: r.formId }, + select: { id: true, fieldName: true }, + }); + + const fieldIdToNameMap = Object.fromEntries( + fields.map((f) => [f.id, f.fieldName]), + ); + + const transformedAnswers: Record = {}; + for (const [fieldId, value] of Object.entries( + r.answers as Record, + )) { + const fieldName = fieldIdToNameMap[fieldId] ?? fieldId; + transformedAnswers[fieldName] = value; + } + + return { + id: r.id, + formId: r.formId, + formTitle: r.form.title, + formDescription: r.form.description, + answers: transformedAnswers, + isSubmitted: r.isSubmitted, + submittedAt: r.submittedAt, + updatedAt: r.updatedAt, + }; + }), + ); + + logger.info( + `Retrieved ${formattedResponses.length} responses for user ${user.id}`, + ); + return { + success: true, + message: "Responses retrieved successfully", + data: formattedResponses, +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) }; } diff --git a/src/api/form-response/routes.ts b/src/api/form-response/routes.ts index eced51d..143b780 100644 --- a/src/api/form-response/routes.ts +++ b/src/api/form-response/routes.ts @@ -6,7 +6,11 @@ import { } from "../../types/form-response"; import { requireAuth } from "../auth/requireAuth"; import { +<<<<<<< HEAD getDraftResponse, +======= + getAllUserResponses, +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) getResponseForFormOwner, getSubmittedResponse, saveDraftResponse, @@ -15,8 +19,14 @@ import { export const formResponseRoutes = new Elysia({ prefix: "/responses" }) .use(requireAuth) +<<<<<<< HEAD .post("/submit/:formId", submitResponse, formResponseDTO) .post("/draft/:formId", saveDraftResponse, formResponseDTO) +======= + .get("/my", getAllUserResponses) // Get all responses for current user + .post("/:formId", submitResponse, formResponseDTO) + .put("/resume/:responseId", resumeResponse, resumeResponseDTO) +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) .get("/:formId", getResponseForFormOwner, formResponseForFormOwnerDTO) .get("/user/:formId", getSubmittedResponse, getSubmittedResponseDTO) .get("/draft/:formId", getDraftResponse, getSubmittedResponseDTO); diff --git a/src/api/forms/controller.ts b/src/api/forms/controller.ts index 1e8334b..1e4f58b 100644 --- a/src/api/forms/controller.ts +++ b/src/api/forms/controller.ts @@ -4,6 +4,7 @@ import type { Context, CreateFormContext, GetFormByIdContext, + GetPublicFormByIdContext, UpdateFormContext, } from "../../types/forms"; @@ -14,6 +15,13 @@ export async function getAllForms({ user }: Context) { title: true, isPublished: true, createdAt: true, + _count: { + select: { + formResponses: { + where: { isSubmitted: true }, + }, + }, + }, }, where: { ownerId: user.id }, }); @@ -27,6 +35,15 @@ export async function getAllForms({ user }: Context) { }; } + // Transform to include responseCount + const formsWithCount = forms.map((form) => ({ + id: form.id, + title: form.title, + isPublished: form.isPublished, + createdAt: form.createdAt, + responseCount: form._count.formResponses, + })); + logger.info("Fetched all forms for user", { userId: user.id, formCount: forms.length, @@ -34,7 +51,7 @@ export async function getAllForms({ user }: Context) { return { success: true, message: "All forms fetched successfully", - data: forms, + data: formsWithCount, }; } @@ -73,11 +90,45 @@ export async function getFormById({ user, params, set }: GetFormByIdContext) { }; } +<<<<<<< HEAD +======= + const fields = await prisma.formFields.findMany({ + where: { formId: params.formId }, + }); + + if (fields.length === 0) { + logger.info(`No fields found for formId: ${params.formId}`); + return { + success: true, + message: "Form fetched successfully (no fields)", + data: { ...form, fields: [] }, + }; + } + + const ordered: typeof fields = []; + + let current = fields.find( + (f): f is (typeof fields)[number] => f.prevFieldId === null, + ); + + while (current) { + ordered.push(current); + + current = fields.find( + (f): f is (typeof fields)[number] => f.prevFieldId === current!.id, + ); + } + +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) logger.info("Fetched form for user", { userId: user.id, formId: form.id }); return { success: true, message: "Form fetched successfully", +<<<<<<< HEAD data: form, +======= + data: { ...form, fields: ordered }, +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) }; } @@ -224,3 +275,64 @@ export async function unPublishForm({ user, params, set }: GetFormByIdContext) { data: form, }; } + +// Public endpoint - any authenticated user can access published forms +export async function getPublicFormById({ + params, + set, +}: GetPublicFormByIdContext) { + const form = await prisma.form.findFirst({ + where: { + id: params.formId, + isPublished: true, // Only allow access to published forms + }, + select: { + id: true, + title: true, + description: true, + isPublished: true, + createdAt: true, + }, + }); + + if (!form) { + set.status = 404; + return { + success: false, + message: "Form not found or not published", + }; + } + + const fields = await prisma.formFields.findMany({ + where: { formId: params.formId }, + }); + + if (fields.length === 0) { + logger.info(`No fields found for public formId: ${params.formId}`); + return { + success: true, + message: "Form fetched successfully (no fields)", + data: { ...form, fields: [] }, + }; + } + + // Order fields by linked list structure + const ordered: typeof fields = []; + let current = fields.find( + (f): f is (typeof fields)[number] => f.prevFieldId === null, + ); + + while (current) { + ordered.push(current); + current = fields.find( + (f): f is (typeof fields)[number] => f.prevFieldId === current!.id, + ); + } + + logger.info("Fetched public form", { formId: form.id }); + return { + success: true, + message: "Form fetched successfully", + data: { ...form, fields: ordered }, + }; +} diff --git a/src/api/forms/routes.ts b/src/api/forms/routes.ts index e6c8de1..bac39f6 100644 --- a/src/api/forms/routes.ts +++ b/src/api/forms/routes.ts @@ -10,11 +10,20 @@ import { deleteForm, getAllForms, getFormById, + getPublicFormById, publishForm, unPublishForm, updateForm, } from "./controller"; +// Public routes (no auth required for respondents) +export const publicFormRoutes = new Elysia({ prefix: "/forms" }).get( + "/public/:formId", + getPublicFormById, + getFormByIdDTO, +); + +// Protected routes (require auth) export const formRoutes = new Elysia({ prefix: "/forms" }) .use(requireAuth) .get("/", getAllForms) diff --git a/src/index.ts b/src/index.ts index d0d3e53..06697e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import { Elysia } from "elysia"; import { authRoutes } from "./api/auth/routes"; import { formFieldRoutes } from "./api/form-fields/routes"; import { formResponseRoutes } from "./api/form-response/routes"; -import { formRoutes } from "./api/forms/routes"; +import { formRoutes, publicFormRoutes } from "./api/forms/routes"; import { logger } from "./logger/index"; const app = new Elysia() @@ -45,6 +45,7 @@ const app = new Elysia() }) .get("/", () => "🦊 Elysia server started") .use(authRoutes) + .use(publicFormRoutes) // Public routes first (no auth) .use(formRoutes) .use(formFieldRoutes) .use(formResponseRoutes); diff --git a/src/test/form-response.test.ts b/src/test/form-response.test.ts index 2b4ea3e..fabda0b 100644 --- a/src/test/form-response.test.ts +++ b/src/test/form-response.test.ts @@ -45,7 +45,11 @@ const { saveDraftResponse, getResponseForFormOwner, getSubmittedResponse, +<<<<<<< HEAD getDraftResponse, +======= + getAllUserResponses, +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) } = await import("../api/form-response/controller"); describe("Form Response Controller Tests", () => { @@ -66,7 +70,7 @@ describe("Form Response Controller Tests", () => { // submitResponse // ===================================================== - it("submitResponse → success", async () => { + it("submitResponse → success (submitted)", async () => { formFindUniqueMock.mockResolvedValue({ id: "f1", isPublished: true, @@ -80,7 +84,7 @@ describe("Form Response Controller Tests", () => { const res = await submitResponse({ params: { formId: "f1" }, - body: { answers: {} }, + body: { answers: {}, isSubmitted: true }, user, set, } as any); @@ -129,6 +133,7 @@ describe("Form Response Controller Tests", () => { isPublished: true, }); +<<<<<<< HEAD formResponseFindUniqueMock.mockResolvedValue({ isSubmitted: true, }); @@ -138,6 +143,26 @@ describe("Form Response Controller Tests", () => { const res = await submitResponse({ params: { formId: "f1" }, body: { answers: {} }, +======= + it("resumeResponse → success (submit)", async () => { + formResponseUpdateManyMock.mockResolvedValue({ count: 1 }); + + const res = await resumeResponse({ + params: { responseId: "r1" }, + body: { answers: {}, isSubmitted: true }, + user, + } as any); + + expect(res.success).toBe(true); + }); + + it("resumeResponse → success (draft)", async () => { + formResponseUpdateManyMock.mockResolvedValue({ count: 1 }); + + const res = await resumeResponse({ + params: { responseId: "r1" }, + body: { answers: {}, isSubmitted: false }, +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) user, set, } as any); @@ -220,6 +245,43 @@ describe("Form Response Controller Tests", () => { expect(res.success).toBe(true); expect(res.data!.length).toBe(1); +<<<<<<< HEAD +======= + }); + + it("getResponseForFormOwner → form not found", async () => { + formFindUniqueMock.mockResolvedValue(null); + + const set: any = {}; + + const res = await getResponseForFormOwner({ + params: { formId: "f1" }, + user, + set, + } as any); + + expect(res.success).toBe(false); + expect(set.status).toBe(404); +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) + }); + + it("getResponseForFormOwner → no responses", async () => { + formFindUniqueMock.mockResolvedValue({ + id: "f1", + ownerId: user.id, + }); + + formResponseFindManyMock.mockResolvedValue([]); + + const set: any = {}; + + const res = await getResponseForFormOwner({ + params: { formId: "f1" }, + user, + set, + } as any); + + expect(res.success).toBe(false); }); // ===================================================== @@ -294,4 +356,44 @@ describe("Form Response Controller Tests", () => { expect(res.success).toBe(false); expect(set.status).toBe(404); }); + + // ===================================================== + // getAllUserResponses + // ===================================================== + + it("getAllUserResponses → success", async () => { + formResponseFindManyMock.mockResolvedValue([ + { + id: "r1", + formId: "f1", + answers: { field1: "A" }, + isSubmitted: true, + submittedAt: new Date(), + updatedAt: new Date(), + form: { + id: "f1", + title: "Form A", + description: "Desc", + }, + }, + ]); + + formFieldsFindManyMock.mockResolvedValue([ + { id: "field1", fieldName: "Name" }, + ]); + + const res = await getAllUserResponses({ user }); + + expect(res.success).toBe(true); + expect(res.data.length).toBe(1); + }); + + it("getAllUserResponses → empty", async () => { + formResponseFindManyMock.mockResolvedValue([]); + + const res = await getAllUserResponses({ user }); + + expect(res.success).toBe(true); + expect(res.data).toEqual([]); + }); }); diff --git a/src/test/forms.test.ts b/src/test/forms.test.ts index 606d4b0..7eb597b 100644 --- a/src/test/forms.test.ts +++ b/src/test/forms.test.ts @@ -60,28 +60,53 @@ describe("Forms Controller Tests", () => { // ============================== it("getAllForms → success", async () => { +<<<<<<< HEAD findManyMock.mockResolvedValue([ { id: "1", title: "A", isPublished: true, createdAt: new Date() }, +======= + formFindManyMock.mockResolvedValue([ + { + id: "1", + title: "A", + isPublished: true, + createdAt: new Date(), + _count: { formResponses: 3 }, + }, +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) ]); - const res = await getAllForms({ user } as any); + const res: any = await getAllForms({ user } as any); expect(res.success).toBe(true); +<<<<<<< HEAD expect(res.data!.length).toBe(1); +======= + expect(res.data.length).toBe(1); + expect(res.data[0].responseCount).toBe(3); +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) }); it("getAllForms → empty", async () => { findManyMock.mockResolvedValue([]); - const res = await getAllForms({ user } as any); + const res: any = await getAllForms({ user } as any); expect(res.message).toBe("No forms found"); expect(res.data).toEqual([]); }); +<<<<<<< HEAD // ============================== // createForm // ============================== +======= + it("getAllForms → DB error", async () => { + formFindManyMock.mockRejectedValue(new Error("DB fail")); + expect(getAllForms({ user } as any)).rejects.toThrow(); + }); + + // ===== createForm ===== +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) it("createForm → success", async () => { createMock.mockResolvedValue({ id: "1", title: "New" }); @@ -102,13 +127,49 @@ describe("Forms Controller Tests", () => { findFirstMock.mockResolvedValue({ id: "1" }); const set: any = {}; +<<<<<<< HEAD const res = await getFormById({ +======= + + const res: any = await getFormById({ + user, + params: { formId: "1" }, + set, + } as any); + + expect(res.success).toBe(true); + expect(res.data.id).toBe("1"); + expect(res.data.fields.length).toBe(2); + expect(res.data.fields[0].id).toBe("f1"); + expect(res.data.fields[1].id).toBe("f2"); + }); + + it("getFormById → no fields", async () => { + findFirstMock.mockResolvedValue({ + id: "1", + title: "Test Form", + description: "Desc", + isPublished: false, + createdAt: new Date(), + }); + + formFieldsFindManyMock.mockResolvedValue([]); + + const set: any = {}; + + const res: any = await getFormById({ +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) user, params: { formId: "1" }, set, } as any); expect(res.success).toBe(true); +<<<<<<< HEAD +======= + expect(res.message).toBe("Form fetched successfully (no fields)"); + expect(res.data.fields).toEqual([]); +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) }); it("getFormById → not found", async () => { @@ -161,9 +222,28 @@ describe("Forms Controller Tests", () => { expect(set.status).toBe(404); }); +<<<<<<< HEAD // ============================== // deleteForm // ============================== +======= + it("updateForm → DB error", async () => { + findFirstMock.mockRejectedValue(new Error("DB fail")); + + const set: any = {}; + + expect( + updateForm({ + user, + params: { formId: "1" }, + body: { title: "T", description: "D" }, + set, + } as any), + ).rejects.toThrow(); + }); + + // ===== deleteForm ===== +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) it("deleteForm → success", async () => { deleteManyMock.mockResolvedValue({ count: 1 }); @@ -204,6 +284,7 @@ describe("Forms Controller Tests", () => { const set: any = {}; +<<<<<<< HEAD const res = await publishForm({ user, params: { formId: "1" }, @@ -260,5 +341,14 @@ describe("Forms Controller Tests", () => { expect(res.success).toBe(false); expect(set.status).toBe(404); +======= + expect( + deleteForm({ + user, + params: { formId: "1" }, + set, + } as any), + ).rejects.toThrow(); +>>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) }); }); diff --git a/src/types/form-response.ts b/src/types/form-response.ts index db817c2..30c0a7d 100644 --- a/src/types/form-response.ts +++ b/src/types/form-response.ts @@ -23,6 +23,7 @@ export const formResponseDTO = { t.Null(), ]), ), + isSubmitted: t.Optional(t.Boolean()), // true = final submission, false/undefined = draft }), }; @@ -49,6 +50,7 @@ export const resumeResponseDTO = { t.Null(), ]), ), + isSubmitted: t.Optional(t.Boolean()), // true = final submission, false/undefined = draft }), }; diff --git a/src/types/forms.ts b/src/types/forms.ts index cdf9bb4..d2d3b77 100644 --- a/src/types/forms.ts +++ b/src/types/forms.ts @@ -5,6 +5,11 @@ export interface Context { set: { status?: number | string }; } +// Public context (no user required) +export interface PublicContext { + set: { status?: number | string }; +} + export const createFormDTO = { body: t.Object({ title: t.String(), @@ -28,6 +33,11 @@ export interface GetFormByIdContext extends Context { params: Static; } +// Public context for fetching published forms (no auth required) +export interface GetPublicFormByIdContext extends PublicContext { + params: Static; +} + export const updateFormDTO = { params: t.Object({ formId: t.String({ From 9a64ecf2f4d736d759be0ca84eb2d604fa6dc5a9 Mon Sep 17 00:00:00 2001 From: Nandgopal-R Date: Mon, 9 Feb 2026 05:05:11 +0530 Subject: [PATCH 2/3] feat: do integrations --- prisma/schema.prisma | 9 -- src/api/form-response/controller.ts | 153 ++-------------------- src/api/form-response/routes.ts | 18 +-- src/api/forms/controller.ts | 14 +- src/test/form-response.test.ts | 121 +++--------------- src/test/forms.test.ts | 190 +++++++++++----------------- 6 files changed, 109 insertions(+), 396 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a533dfc..68cb046 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -102,21 +102,12 @@ model FormResponse { id String @id @default(uuid()) formId String respondentId String? -<<<<<<< HEAD - respondent User? @relation(fields: [respondentId], references: [id], onDelete: SetNull) - - answers Json - isSubmitted Boolean @default(false) - submittedAt DateTime? - updatedAt DateTime @updatedAt -======= answers Json submittedAt DateTime? updatedAt DateTime @updatedAt isSubmitted Boolean @default(false) form Form @relation(fields: [formId], references: [id], onDelete: Cascade) respondent User? @relation(fields: [respondentId], references: [id]) ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) @@unique([formId, respondentId]) @@index([formId]) diff --git a/src/api/form-response/controller.ts b/src/api/form-response/controller.ts index 22578d7..368e696 100644 --- a/src/api/form-response/controller.ts +++ b/src/api/form-response/controller.ts @@ -4,6 +4,7 @@ import type { FormResponseContext, FormResponseForFormOwnerContext, GetSubmittedResponseContext, + ResumeResponseContext, } from "../../types/form-response"; export async function submitResponse({ @@ -36,45 +37,12 @@ export async function submitResponse({ }; } - const existing = await prisma.formResponse.findUnique({ - where: { - formId_respondentId: { - formId: params.formId, - respondentId: user.id, - }, - }, - }); - - if (existing?.isSubmitted) { - set.status = 400; - return { - success: false, - message: "You have already submitted this form", - }; - } - - const response = await prisma.formResponse.upsert({ - where: { - formId_respondentId: { - formId: params.formId, - respondentId: user.id, - }, - }, - update: { - answers: body.answers, - isSubmitted: true, - submittedAt: new Date(), - }, - create: { + const response = await prisma.formResponse.create({ + data: { formId: params.formId, respondentId: user.id, answers: body.answers, -<<<<<<< HEAD - isSubmitted: true, - submittedAt: new Date(), -======= isSubmitted: body.isSubmitted ?? false, ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) }, }); logger.info( @@ -87,69 +55,22 @@ export async function submitResponse({ }; } -export async function saveDraftResponse({ +export async function resumeResponse({ params, body, user, - set, -}: FormResponseContext) { - const form = await prisma.form.findUnique({ +}: ResumeResponseContext) { + const response = await prisma.formResponse.updateMany({ where: { - id: params.formId, - }, - }); - - if (!form) { - logger.warn(`Form with ID ${params.formId} not found`); - set.status = 404; - return { - success: false, - message: "Form not found", - }; - } - - const existing = await prisma.formResponse.findUnique({ - where: { - formId_respondentId: { - formId: params.formId, - respondentId: user.id, - }, - }, - }); - - if (existing?.isSubmitted) { - set.status = 400; - return { - success: false, - message: "Response already submitted and cannot be edited", - }; - } - - const response = await prisma.formResponse.upsert({ - where: { - formId_respondentId: { - formId: params.formId, - respondentId: user.id, - }, - }, - update: { - answers: body.answers, - isSubmitted: false, - }, - create: { - formId: params.formId, + id: params.responseId, respondentId: user.id, + }, + data: { answers: body.answers, isSubmitted: body.isSubmitted ?? false, }, }); -<<<<<<< HEAD - logger.info(`User ${user.id} saved draft response for form ${params.formId}`); - return { - success: true, - message: "Draft response saved successfully", -======= if (response.count === 0) { logger.warn(`No response found with ID ${params.responseId} to update`); return { @@ -166,7 +87,6 @@ export async function saveDraftResponse({ message: body.isSubmitted ? "Response submitted successfully" : "Draft saved successfully", ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) data: response, }; } @@ -197,7 +117,6 @@ export async function getResponseForFormOwner({ const responses = await prisma.formResponse.findMany({ where: { formId: params.formId, - isSubmitted: true, }, select: { id: true, @@ -267,7 +186,6 @@ export async function getSubmittedResponse({ where: { respondentId: user.id, formId: params.formId, - isSubmitted: true, }, select: { id: true, @@ -335,68 +253,16 @@ export async function getSubmittedResponse({ }; } -<<<<<<< HEAD -export async function getDraftResponse({ - params, - user, - set, -}: GetSubmittedResponseContext) { - const draft = await prisma.formResponse.findFirst({ - where: { - respondentId: user.id, - formId: params.formId, - isSubmitted: false, -======= // Get all responses submitted by the current user across all forms export async function getAllUserResponses({ user }: { user: { id: string } }) { const responses = await prisma.formResponse.findMany({ where: { respondentId: user.id, ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) }, select: { id: true, formId: true, answers: true, -<<<<<<< HEAD - form: { - select: { title: true }, - }, - }, - }); - - if (!draft) { - set.status = 404; - return { - success: false, - message: "No draft found", - }; - } - - const fields = await prisma.formFields.findMany({ - where: { formId: params.formId }, - select: { id: true, fieldName: true }, - }); - - const map = Object.fromEntries(fields.map((f) => [f.id, f.fieldName])); - - const transformed: Record = {}; - - for (const [fieldId, value] of Object.entries( - draft.answers as Record, - )) { - transformed[map[fieldId] ?? fieldId] = value; - } - - return { - success: true, - data: { - id: draft.id, - formId: draft.formId, - formTitle: draft.form.title, - answers: transformed, - }, -======= isSubmitted: true, submittedAt: true, updatedAt: true, @@ -462,6 +328,5 @@ export async function getAllUserResponses({ user }: { user: { id: string } }) { success: true, message: "Responses retrieved successfully", data: formattedResponses, ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) }; } diff --git a/src/api/form-response/routes.ts b/src/api/form-response/routes.ts index 143b780..8d7ac8e 100644 --- a/src/api/form-response/routes.ts +++ b/src/api/form-response/routes.ts @@ -3,30 +3,20 @@ import { formResponseDTO, formResponseForFormOwnerDTO, getSubmittedResponseDTO, + resumeResponseDTO, } from "../../types/form-response"; import { requireAuth } from "../auth/requireAuth"; import { -<<<<<<< HEAD - getDraftResponse, -======= - getAllUserResponses, ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) getResponseForFormOwner, getSubmittedResponse, - saveDraftResponse, + resumeResponse, submitResponse, } from "./controller"; export const formResponseRoutes = new Elysia({ prefix: "/responses" }) .use(requireAuth) -<<<<<<< HEAD .post("/submit/:formId", submitResponse, formResponseDTO) - .post("/draft/:formId", saveDraftResponse, formResponseDTO) -======= - .get("/my", getAllUserResponses) // Get all responses for current user - .post("/:formId", submitResponse, formResponseDTO) + .post("/draft/:formId", submitResponse, formResponseDTO) .put("/resume/:responseId", resumeResponse, resumeResponseDTO) ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) .get("/:formId", getResponseForFormOwner, formResponseForFormOwnerDTO) - .get("/user/:formId", getSubmittedResponse, getSubmittedResponseDTO) - .get("/draft/:formId", getDraftResponse, getSubmittedResponseDTO); + .get("/user/:formId", getSubmittedResponse, getSubmittedResponseDTO); diff --git a/src/api/forms/controller.ts b/src/api/forms/controller.ts index 1e4f58b..62cee04 100644 --- a/src/api/forms/controller.ts +++ b/src/api/forms/controller.ts @@ -80,6 +80,13 @@ export async function getFormById({ user, params, set }: GetFormByIdContext) { id: params.formId, ownerId: user.id, }, + select: { + id: true, + title: true, + description: true, + isPublished: true, + createdAt: true, + }, }); if (!form) { @@ -90,8 +97,6 @@ export async function getFormById({ user, params, set }: GetFormByIdContext) { }; } -<<<<<<< HEAD -======= const fields = await prisma.formFields.findMany({ where: { formId: params.formId }, }); @@ -119,16 +124,11 @@ export async function getFormById({ user, params, set }: GetFormByIdContext) { ); } ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) logger.info("Fetched form for user", { userId: user.id, formId: form.id }); return { success: true, message: "Form fetched successfully", -<<<<<<< HEAD - data: form, -======= data: { ...form, fields: ordered }, ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) }; } diff --git a/src/test/form-response.test.ts b/src/test/form-response.test.ts index fabda0b..683f2ab 100644 --- a/src/test/form-response.test.ts +++ b/src/test/form-response.test.ts @@ -3,10 +3,9 @@ import { beforeEach, describe, expect, it, mock } from "bun:test"; // ---------------- MOCK PRISMA ---------------- const formFindUniqueMock = mock(); -const formResponseFindUniqueMock = mock(); -const formResponseUpsertMock = mock(); +const formResponseCreateMock = mock(); +const formResponseUpdateManyMock = mock(); const formResponseFindManyMock = mock(); -const formResponseFindFirstMock = mock(); const formFieldsFindManyMock = mock(); mock.module("../db/prisma", () => ({ @@ -15,10 +14,9 @@ mock.module("../db/prisma", () => ({ findUnique: formFindUniqueMock, }, formResponse: { - findUnique: formResponseFindUniqueMock, - upsert: formResponseUpsertMock, + create: formResponseCreateMock, + updateMany: formResponseUpdateManyMock, findMany: formResponseFindManyMock, - findFirst: formResponseFindFirstMock, }, formFields: { findMany: formFieldsFindManyMock, @@ -42,23 +40,18 @@ mock.module("../logger", () => ({ // IMPORT AFTER MOCKS const { submitResponse, - saveDraftResponse, + resumeResponse, getResponseForFormOwner, getSubmittedResponse, -<<<<<<< HEAD - getDraftResponse, -======= getAllUserResponses, ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) } = await import("../api/form-response/controller"); describe("Form Response Controller Tests", () => { beforeEach(() => { formFindUniqueMock.mockReset(); - formResponseFindUniqueMock.mockReset(); - formResponseUpsertMock.mockReset(); + formResponseCreateMock.mockReset(); + formResponseUpdateManyMock.mockReset(); formResponseFindManyMock.mockReset(); - formResponseFindFirstMock.mockReset(); formFieldsFindManyMock.mockReset(); mockInfo.mockReset(); mockWarn.mockReset(); @@ -76,9 +69,7 @@ describe("Form Response Controller Tests", () => { isPublished: true, }); - formResponseFindUniqueMock.mockResolvedValue(null); - - formResponseUpsertMock.mockResolvedValue({ id: "r1" }); + formResponseCreateMock.mockResolvedValue({ id: "r1" }); const set: any = {}; @@ -127,23 +118,10 @@ describe("Form Response Controller Tests", () => { expect(set.status).toBe(403); }); - it("submitResponse → already submitted", async () => { - formFindUniqueMock.mockResolvedValue({ - id: "f1", - isPublished: true, - }); - -<<<<<<< HEAD - formResponseFindUniqueMock.mockResolvedValue({ - isSubmitted: true, - }); - - const set: any = {}; + // ===================================================== + // resumeResponse + // ===================================================== - const res = await submitResponse({ - params: { formId: "f1" }, - body: { answers: {} }, -======= it("resumeResponse → success (submit)", async () => { formResponseUpdateManyMock.mockResolvedValue({ count: 1 }); @@ -162,54 +140,22 @@ describe("Form Response Controller Tests", () => { const res = await resumeResponse({ params: { responseId: "r1" }, body: { answers: {}, isSubmitted: false }, ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) - user, - set, - } as any); - - expect(res.success).toBe(false); - expect(set.status).toBe(400); - }); - - // ===================================================== - // saveDraftResponse - // ===================================================== - - it("saveDraftResponse → success", async () => { - formFindUniqueMock.mockResolvedValue({ id: "f1" }); - formResponseFindUniqueMock.mockResolvedValue(null); - formResponseUpsertMock.mockResolvedValue({ id: "r1" }); - - const set: any = {}; - - const res = await saveDraftResponse({ - params: { formId: "f1" }, - body: { answers: {} }, user, - set, } as any); expect(res.success).toBe(true); }); - it("saveDraftResponse → already submitted", async () => { - formFindUniqueMock.mockResolvedValue({ id: "f1" }); + it("resumeResponse → not found", async () => { + formResponseUpdateManyMock.mockResolvedValue({ count: 0 }); - formResponseFindUniqueMock.mockResolvedValue({ - isSubmitted: true, - }); - - const set: any = {}; - - const res = await saveDraftResponse({ - params: { formId: "f1" }, + const res = await resumeResponse({ + params: { responseId: "r1" }, body: { answers: {} }, user, - set, } as any); expect(res.success).toBe(false); - expect(set.status).toBe(400); }); // ===================================================== @@ -245,8 +191,6 @@ describe("Form Response Controller Tests", () => { expect(res.success).toBe(true); expect(res.data!.length).toBe(1); -<<<<<<< HEAD -======= }); it("getResponseForFormOwner → form not found", async () => { @@ -262,7 +206,6 @@ describe("Form Response Controller Tests", () => { expect(res.success).toBe(false); expect(set.status).toBe(404); ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) }); it("getResponseForFormOwner → no responses", async () => { @@ -314,40 +257,12 @@ describe("Form Response Controller Tests", () => { expect(res.data!.length).toBe(1); }); - // ===================================================== - // getDraftResponse - // ===================================================== - - it("getDraftResponse → success", async () => { - formResponseFindFirstMock.mockResolvedValue({ - id: "r1", - formId: "f1", - answers: { field1: "A" }, - form: { title: "Form A" }, - }); - - formFieldsFindManyMock.mockResolvedValue([ - { id: "field1", fieldName: "Name" }, - ]); - - const set: any = {}; - - const res = await getDraftResponse({ - params: { formId: "f1" }, - user, - set, - } as any); - - expect(res.success).toBe(true); - expect(res.data!.id).toBe("r1"); - }); - - it("getDraftResponse → not found", async () => { - formResponseFindFirstMock.mockResolvedValue(null); + it("getSubmittedResponse → none found", async () => { + formResponseFindManyMock.mockResolvedValue([]); const set: any = {}; - const res = await getDraftResponse({ + const res = await getSubmittedResponse({ params: { formId: "f1" }, user, set, diff --git a/src/test/forms.test.ts b/src/test/forms.test.ts index 7eb597b..5b90f87 100644 --- a/src/test/forms.test.ts +++ b/src/test/forms.test.ts @@ -1,7 +1,8 @@ import { beforeEach, describe, expect, it, mock } from "bun:test"; // ---------- MOCK PRISMA ---------- -const findManyMock = mock(); +const formFindManyMock = mock(); +const formFieldsFindManyMock = mock(); const createMock = mock(); const findFirstMock = mock(); const updateMock = mock(); @@ -10,12 +11,15 @@ const deleteManyMock = mock(); mock.module("../db/prisma", () => ({ prisma: { form: { - findMany: findManyMock, + findMany: formFindManyMock, create: createMock, findFirst: findFirstMock, update: updateMock, deleteMany: deleteManyMock, }, + formFields: { + findMany: formFieldsFindManyMock, + }, }, })); @@ -32,19 +36,13 @@ mock.module("../logger", () => ({ })); // IMPORT AFTER MOCKS -const { - getAllForms, - createForm, - getFormById, - updateForm, - deleteForm, - publishForm, - unPublishForm, -} = await import("../api/forms/controller"); +const { getAllForms, createForm, getFormById, updateForm, deleteForm } = + await import("../api/forms/controller"); describe("Forms Controller Tests", () => { beforeEach(() => { - findManyMock.mockReset(); + formFindManyMock.mockReset(); + formFieldsFindManyMock.mockReset(); createMock.mockReset(); findFirstMock.mockReset(); updateMock.mockReset(); @@ -55,15 +53,9 @@ describe("Forms Controller Tests", () => { const user = { id: "user1" }; - // ============================== - // getAllForms - // ============================== + // ===== getAllForms ===== it("getAllForms → success", async () => { -<<<<<<< HEAD - findManyMock.mockResolvedValue([ - { id: "1", title: "A", isPublished: true, createdAt: new Date() }, -======= formFindManyMock.mockResolvedValue([ { id: "1", @@ -72,22 +64,17 @@ describe("Forms Controller Tests", () => { createdAt: new Date(), _count: { formResponses: 3 }, }, ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) ]); const res: any = await getAllForms({ user } as any); expect(res.success).toBe(true); -<<<<<<< HEAD - expect(res.data!.length).toBe(1); -======= expect(res.data.length).toBe(1); expect(res.data[0].responseCount).toBe(3); ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) }); it("getAllForms → empty", async () => { - findManyMock.mockResolvedValue([]); + formFindManyMock.mockResolvedValue([]); const res: any = await getAllForms({ user } as any); @@ -95,18 +82,12 @@ describe("Forms Controller Tests", () => { expect(res.data).toEqual([]); }); -<<<<<<< HEAD - // ============================== - // createForm - // ============================== -======= it("getAllForms → DB error", async () => { formFindManyMock.mockRejectedValue(new Error("DB fail")); expect(getAllForms({ user } as any)).rejects.toThrow(); }); // ===== createForm ===== ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) it("createForm → success", async () => { createMock.mockResolvedValue({ id: "1", title: "New" }); @@ -119,17 +100,45 @@ describe("Forms Controller Tests", () => { expect(res.success).toBe(true); }); - // ============================== - // getFormById - // ============================== + it("createForm → called", async () => { + createMock.mockResolvedValue({ id: "1" }); - it("getFormById → found", async () => { - findFirstMock.mockResolvedValue({ id: "1" }); + await createForm({ + user, + body: { title: "T", description: "D" }, + } as any); + + expect(createMock).toHaveBeenCalled(); + }); + + it("createForm → DB error", async () => { + createMock.mockRejectedValue(new Error("DB crash")); + + expect( + createForm({ + user, + body: { title: "X", description: "Y" }, + } as any), + ).rejects.toThrow(); + }); + + // ===== getFormById ===== + + it("getFormById → found with ordered fields", async () => { + findFirstMock.mockResolvedValue({ + id: "1", + title: "Test Form", + description: "Desc", + isPublished: false, + createdAt: new Date(), + }); + + formFieldsFindManyMock.mockResolvedValue([ + { id: "f1", formId: "1", prevFieldId: null }, + { id: "f2", formId: "1", prevFieldId: "f1" }, + ]); const set: any = {}; -<<<<<<< HEAD - const res = await getFormById({ -======= const res: any = await getFormById({ user, @@ -158,24 +167,21 @@ describe("Forms Controller Tests", () => { const set: any = {}; const res: any = await getFormById({ ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) user, params: { formId: "1" }, set, } as any); expect(res.success).toBe(true); -<<<<<<< HEAD -======= expect(res.message).toBe("Form fetched successfully (no fields)"); expect(res.data.fields).toEqual([]); ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) }); it("getFormById → not found", async () => { findFirstMock.mockResolvedValue(null); const set: any = {}; + const res = await getFormById({ user, params: { formId: "2" }, @@ -186,13 +192,29 @@ describe("Forms Controller Tests", () => { expect(set.status).toBe(404); }); - // ============================== - // updateForm - // ============================== + it("getFormById → DB error", async () => { + findFirstMock.mockRejectedValue(new Error("DB error")); + + const set: any = {}; + + await expect( + getFormById({ + user, + params: { formId: "1" }, + set, + } as any), + ).rejects.toThrow(); + }); + + // ===== updateForm ===== it("updateForm → success", async () => { findFirstMock.mockResolvedValue({ id: "1" }); - updateMock.mockResolvedValue({ id: "1" }); + + updateMock.mockResolvedValue({ + id: "1", + title: "Updated", + }); const set: any = {}; @@ -222,11 +244,6 @@ describe("Forms Controller Tests", () => { expect(set.status).toBe(404); }); -<<<<<<< HEAD - // ============================== - // deleteForm - // ============================== -======= it("updateForm → DB error", async () => { findFirstMock.mockRejectedValue(new Error("DB fail")); @@ -243,7 +260,6 @@ describe("Forms Controller Tests", () => { }); // ===== deleteForm ===== ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) it("deleteForm → success", async () => { deleteManyMock.mockResolvedValue({ count: 1 }); @@ -274,74 +290,11 @@ describe("Forms Controller Tests", () => { expect(set.status).toBe(404); }); - // ============================== - // publishForm - // ============================== - - it("publishForm → success", async () => { - findFirstMock.mockResolvedValue({ id: "1" }); - updateMock.mockResolvedValue({ id: "1", isPublished: true }); + it("deleteForm → DB error", async () => { + deleteManyMock.mockRejectedValue(new Error("DB crash")); const set: any = {}; -<<<<<<< HEAD - const res = await publishForm({ - user, - params: { formId: "1" }, - set, - } as any); - - expect(res.success).toBe(true); - }); - - it("publishForm → not found", async () => { - findFirstMock.mockResolvedValue(null); - - const set: any = {}; - - const res = await publishForm({ - user, - params: { formId: "1" }, - set, - } as any); - - expect(res.success).toBe(false); - expect(set.status).toBe(404); - }); - - // ============================== - // unPublishForm - // ============================== - - it("unPublishForm → success", async () => { - findFirstMock.mockResolvedValue({ id: "1" }); - updateMock.mockResolvedValue({ id: "1", isPublished: false }); - - const set: any = {}; - - const res = await unPublishForm({ - user, - params: { formId: "1" }, - set, - } as any); - - expect(res.success).toBe(true); - }); - - it("unPublishForm → not found", async () => { - findFirstMock.mockResolvedValue(null); - - const set: any = {}; - - const res = await unPublishForm({ - user, - params: { formId: "1" }, - set, - } as any); - - expect(res.success).toBe(false); - expect(set.status).toBe(404); -======= expect( deleteForm({ user, @@ -349,6 +302,5 @@ describe("Forms Controller Tests", () => { set, } as any), ).rejects.toThrow(); ->>>>>>> 1e9d4e8 (fix: fix getFormById and form-responses for integration) }); }); From 890e9988a40319cab8eeebcee8fb7a0469156949 Mon Sep 17 00:00:00 2001 From: Nandgopal-R Date: Mon, 9 Feb 2026 12:53:26 +0530 Subject: [PATCH 3/3] fix: add missing route --- src/api/form-response/routes.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/form-response/routes.ts b/src/api/form-response/routes.ts index 8d7ac8e..0e702a1 100644 --- a/src/api/form-response/routes.ts +++ b/src/api/form-response/routes.ts @@ -7,6 +7,7 @@ import { } from "../../types/form-response"; import { requireAuth } from "../auth/requireAuth"; import { + getAllUserResponses, getResponseForFormOwner, getSubmittedResponse, resumeResponse, @@ -18,5 +19,6 @@ export const formResponseRoutes = new Elysia({ prefix: "/responses" }) .post("/submit/:formId", submitResponse, formResponseDTO) .post("/draft/:formId", submitResponse, formResponseDTO) .put("/resume/:responseId", resumeResponse, resumeResponseDTO) + .get("/my", getAllUserResponses) .get("/:formId", getResponseForFormOwner, formResponseForFormOwnerDTO) .get("/user/:formId", getSubmittedResponse, getSubmittedResponseDTO);