import assert from 'node:assert/strict'; import test from 'node:test'; import type { NextFunction, Request, RequestHandler } from 'express'; import Joi from 'joi'; import { createRequest, createResponse } from 'node-mocks-http'; import { commonErrorHandler } from '../src/helpers.ts'; import { validateRequest } from '../src/middlewares/validate-request.ts'; import { crud, file, publish, projects, tourPages, users, } from '../src/validators/request-schemas.ts'; import type { RouteError } from '../src/types/index.ts'; interface UserUpdateTestBody { data: { firstName: string; custom_permissions?: unknown; allowed_private_production_project_ids?: unknown; }; } function runMiddleware( middleware: RequestHandler, req: Request, ): Promise { const res = createResponse(); return new Promise((resolve) => { const next: NextFunction = (error?: unknown) => { resolve(error instanceof Error ? error : null); }; middleware(req, res, next); }); } function getBodyRecord(req: Request): Record { const body: unknown = req.body; if (body !== null && typeof body === 'object' && !Array.isArray(body)) { return Object.fromEntries(Object.entries(body)); } throw new TypeError('Expected request body to be an object.'); } void test('validateRequest applies converted sanitized values to request parts', async () => { const req = createRequest({ query: { limit: '25', page: '2', field: 'createdAt', sort: 'DESC', name: 'Lobby', }, }); const error = await runMiddleware(validateRequest(crud.list), req); assert.equal(error, null); assert.equal(req.query.limit, 25); assert.equal(req.query.page, 2); assert.equal(req.query.name, 'Lobby'); }); void test('validateRequest returns structured request validation error details', async () => { const req = createRequest({ body: { data: ['not-object'], }, }); const error = await runMiddleware(validateRequest(crud.create), req); assert.equal(error?.code, 400); assert.equal(error?.isRequestValidation, true); assert.equal(error?.details?.[0]?.path, 'body.data'); }); void test('commonErrorHandler sends request validation errors as JSON', () => { const error: RouteError = new Error('Invalid request'); error.code = 400; error.isRequestValidation = true; error.details = [{ path: 'body.email', message: 'email is required' }]; const req = createRequest(); const res = createResponse(); commonErrorHandler(error, req, res, () => {}); assert.equal(res.statusCode, 400); assert.deepEqual(res._getData(), { error: 'Invalid request', details: [{ path: 'body.email', message: 'email is required' }], }); }); void test('user update schema preserves omitted permissions fields', async () => { const req = createRequest>({ params: { id: '094121b9-c567-469a-b256-ba221b7fd5d6' }, body: { data: { firstName: 'Admin', }, }, }); const error = await runMiddleware(validateRequest(users.update), req); assert.equal(error, null); assert.equal( Object.prototype.hasOwnProperty.call(req.body.data, 'custom_permissions'), false, ); assert.equal( Object.prototype.hasOwnProperty.call( req.body.data, 'allowed_private_production_project_ids', ), false, ); }); void test('create schemas reject missing fields required before service layer', async () => { const userError = await runMiddleware( validateRequest(users.create), createRequest({ body: { data: { firstName: 'No Email' } } }), ); const projectError = await runMiddleware( validateRequest(projects.create), createRequest({ body: { data: { name: 'No Slug' } } }), ); const tourPageError = await runMiddleware( validateRequest(tourPages.create), createRequest({ body: { data: { name: 'No Project', slug: 'no-project' } } }), ); assert.equal(userError?.isRequestValidation, true); assert.equal(projectError?.isRequestValidation, true); assert.equal(tourPageError?.isRequestValidation, true); }); void test('validateRequest rejects invalid schema maps during route setup', () => { const invalidSchemas = { body: Joi.object() }; Object.defineProperty(invalidSchemas, 'cookies', { enumerable: true, value: Joi.object(), }); assert.throws( () => validateRequest(invalidSchemas), /Unsupported request validation part/, ); }); void test('publish schemas reject malformed project ids before service calls', async () => { const publishError = await runMiddleware( validateRequest(publish.publish), createRequest({ body: { projectId: 'not-a-uuid', title: 'Production', }, }), ); const saveToStageError = await runMiddleware( validateRequest(publish.saveToStage), createRequest({ body: { projectId: 'not-a-uuid', }, }), ); assert.equal(publishError?.isRequestValidation, true); assert.equal(saveToStageError?.isRequestValidation, true); }); void test('file presign schema enforces non-empty capped URL batches', async () => { const emptyError = await runMiddleware( validateRequest(file.presign), createRequest({ body: { urls: [] } }), ); const tooManyError = await runMiddleware( validateRequest(file.presign), createRequest({ body: { urls: Array.from({ length: 51 }, (_value, index) => `asset-${index}.jpg`), }, }), ); assert.equal(emptyError?.isRequestValidation, true); assert.equal(tooManyError?.isRequestValidation, true); }); void test('file upload schemas validate path params and convert chunk index', async () => { const uploadError = await runMiddleware( validateRequest(file.upload), createRequest({ params: { table: '../assets', field: 'image', }, }), ); const chunkReq = createRequest({ params: { sessionId: '094121b9-c567-469a-b256-ba221b7fd5d6', chunkIndex: '7', }, }); const chunkError = await runMiddleware(validateRequest(file.chunk), chunkReq); assert.equal(uploadError?.isRequestValidation, true); assert.equal(chunkError, null); assert.equal(chunkReq.params.chunkIndex, 7); }); void test('tour page reorder schema defaults dev environment and requires ordered ids', async () => { const validReq = createRequest({ body: { data: { projectId: '094121b9-c567-469a-b256-ba221b7fd5d6', orderedPageIds: ['6a373ead-7ff4-4f1c-9a69-29ff1e97420d'], }, }, }); const validError = await runMiddleware(validateRequest(tourPages.reorder), validReq); const missingOrderError = await runMiddleware( validateRequest(tourPages.reorder), createRequest({ body: { data: { projectId: '094121b9-c567-469a-b256-ba221b7fd5d6', }, }, }), ); assert.equal(validError, null); const validatedData = getBodyRecord(validReq).data; assert.equal( validatedData !== null && typeof validatedData === 'object' && !Array.isArray(validatedData) && 'environment' in validatedData ? validatedData.environment : undefined, 'dev', ); assert.equal(missingOrderError?.isRequestValidation, true); });