Files
repomix-mirror/website/server/tests/validation.test.ts
Kazuki Yamada 65b6f115eb test(server): Anchor leading-prefix check on the exact defect shape
decision(assertion-precision): switch the "no stray leading \`: : \`" guard from \`not.toContain(': : ')\` to an anchored regex \`/^Invalid request: : /\` — both catch the defect today, but the anchored form documents exactly which prefix shape we're guarding against and rules out any legitimate \`: : \` that might appear later in the message

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:31:15 +09:00

84 lines
3.3 KiB
TypeScript

import * as v from 'valibot';
import { describe, expect, test } from 'vitest';
import { AppError } from '../src/utils/errorHandler.js';
import { validateRequest } from '../src/utils/validation.js';
// Covers the three distinct paths through validateRequest:
// 1. successful parse → returns typed output
// 2. ValiError → wrapped as AppError(400) with the original issues preserved
// on `.cause` (the exact contract classifyRejectReason relies on)
// 3. anything else → re-thrown unchanged
//
// Tiny self-contained schemas keep the test focused — packRequestSchema's own
// behavior is covered indirectly through classifyRejectReason's drift tests.
describe('validateRequest', () => {
const schema = v.pipe(
v.strictObject({
name: v.pipe(v.string(), v.minLength(1, 'Name is required')),
count: v.optional(v.number()),
}),
v.check((data) => data.count === undefined || data.count >= 0, 'Count must be non-negative'),
);
test('returns parsed output on valid input', () => {
const result = validateRequest(schema, { name: 'pack', count: 3 });
expect(result).toEqual({ name: 'pack', count: 3 });
});
test('wraps ValiError as AppError(400) and joins issue messages', () => {
expect.assertions(4);
try {
validateRequest(schema, { name: '' });
} catch (error) {
expect(error).toBeInstanceOf(AppError);
const appError = error as AppError;
expect(appError.statusCode).toBe(400);
expect(appError.message).toContain('Name is required');
expect(appError.message.startsWith('Invalid request: ')).toBe(true);
}
});
test('preserves original ValiError on `.cause` so classifyRejectReason can read `.issues`', () => {
// Load-bearing contract: dropping `cause` here would silently break
// pack_completed.rejectReason labeling in production.
expect.assertions(2);
try {
validateRequest(schema, { name: '' });
} catch (error) {
const cause = (error as AppError).cause;
expect(cause).toBeInstanceOf(v.ValiError);
expect((cause as v.ValiError<typeof schema>).issues.length).toBeGreaterThan(0);
}
});
test('top-level check issue (no path) renders message without a stray leading `": "`', () => {
// `count: -1` fails the top-level v.check — valibot emits an issue with no
// path. The rendered message must not start with `": "`.
expect.assertions(2);
try {
validateRequest(schema, { name: 'pack', count: -1 });
} catch (error) {
const message = (error as AppError).message;
expect(message).toContain('Count must be non-negative');
// Anchor on the specific defect shape: `Invalid request: : <rest>` with
// an empty path between the two colons. A plain `not.toContain(': : ')`
// also works today, but the anchored regex documents the exact failure
// mode we're guarding against.
expect(message).not.toMatch(/^Invalid request: : /);
}
});
test('re-throws non-ValiError unchanged', () => {
// v.parse itself never throws non-ValiError, but a refine()-like callback
// could raise. Simulate with a schema whose v.check callback throws.
const exploding = v.pipe(
v.string(),
v.check(() => {
throw new RangeError('unexpected boom');
}, 'never reached'),
);
expect(() => validateRequest(exploding, 'anything')).toThrowError(RangeError);
});
});