39948-vm/backend/tests/request-validation.test.ts
2026-07-02 14:22:33 +02:00

257 lines
7.2 KiB
TypeScript

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<RouteError | null> {
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<string, unknown> {
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<Request<{ id: string }, unknown, UserUpdateTestBody>>({
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);
});