mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-02 02:29:51 +00:00
928 lines
23 KiB
TypeScript
928 lines
23 KiB
TypeScript
import { codeBlock } from 'common-tags';
|
|
import { z } from 'zod/v4';
|
|
import { logger } from '~test/util.ts';
|
|
import {
|
|
DeepNullish,
|
|
Ini,
|
|
Json,
|
|
Json5,
|
|
Jsonc,
|
|
LooseArray,
|
|
LooseRecord,
|
|
MultidocYaml,
|
|
NotCircular,
|
|
Nullish,
|
|
Toml,
|
|
UtcDate,
|
|
Yaml,
|
|
isEmailAdress,
|
|
multidocYaml,
|
|
withDebugMessage,
|
|
withDepType,
|
|
withTraceMessage,
|
|
} from './index.ts';
|
|
|
|
describe('util/schema-utils/index', () => {
|
|
describe('LooseArray', () => {
|
|
it('parses array', () => {
|
|
const s = LooseArray(z.string());
|
|
expect(s.parse(['foo', 'bar'])).toEqual(['foo', 'bar']);
|
|
});
|
|
|
|
it('drops wrong items', () => {
|
|
const s = LooseArray(z.string());
|
|
expect(s.parse(['foo', 123, null, undefined, []])).toEqual(['foo']);
|
|
});
|
|
|
|
it('runs callback for wrong elements', () => {
|
|
let err: z.ZodError | undefined = undefined;
|
|
const Schema = LooseArray(z.string(), {
|
|
onError: ({ error }) => {
|
|
err = error;
|
|
},
|
|
});
|
|
|
|
const res = Schema.parse(['foo', 123, 'bar']);
|
|
|
|
expect(res).toEqual(['foo', 'bar']);
|
|
expect(err).toMatchObject({
|
|
issues: [
|
|
{
|
|
message: 'Invalid input: expected string, received number',
|
|
code: 'invalid_type',
|
|
expected: 'string',
|
|
path: [1],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('LooseRecord', () => {
|
|
it('parses record', () => {
|
|
const s = LooseRecord(z.string());
|
|
expect(s.parse({ foo: 'bar' })).toEqual({ foo: 'bar' });
|
|
});
|
|
|
|
it('drops wrong items', () => {
|
|
const s = LooseRecord(z.string());
|
|
expect(s.parse({ foo: 'foo', bar: 123 })).toEqual({ foo: 'foo' });
|
|
});
|
|
|
|
it('supports key schema', () => {
|
|
const s = LooseRecord(
|
|
z
|
|
.string()
|
|
.refine((x) => x === 'bar')
|
|
.transform((x) => x.toUpperCase()),
|
|
z.string().transform((x) => x.toUpperCase()),
|
|
);
|
|
expect(s.parse({ foo: 'foo', bar: 'bar' })).toEqual({ BAR: 'BAR' });
|
|
});
|
|
|
|
it('reports key schema errors', () => {
|
|
let errorData: unknown = null;
|
|
const s = LooseRecord(
|
|
z.string().refine((x) => x === 'bar'),
|
|
z.string(),
|
|
{
|
|
onError: (x) => {
|
|
errorData = x;
|
|
},
|
|
},
|
|
);
|
|
|
|
s.parse({ foo: 'foo', bar: 'bar' });
|
|
|
|
expect(errorData).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
code: 'custom',
|
|
message: 'Invalid input',
|
|
path: ['foo'],
|
|
},
|
|
],
|
|
},
|
|
input: { bar: 'bar', foo: 'foo' },
|
|
});
|
|
});
|
|
|
|
it('runs callback for wrong elements', () => {
|
|
let err: z.ZodError | undefined = undefined;
|
|
const Schema = LooseRecord(
|
|
z.object({ foo: z.object({ bar: z.string() }) }),
|
|
{
|
|
onError: ({ error }) => {
|
|
err = error;
|
|
},
|
|
},
|
|
);
|
|
|
|
const res = Schema.parse({
|
|
aaa: { foo: { bar: 42 } },
|
|
bbb: { foo: { baz: 'asdf' } },
|
|
ccc: { foo: { bar: 'baz' } },
|
|
});
|
|
|
|
expect(res).toEqual({ ccc: { foo: { bar: 'baz' } } });
|
|
expect(err).toMatchObject({
|
|
issues: [
|
|
{
|
|
message: 'Invalid input: expected string, received number',
|
|
code: 'invalid_type',
|
|
expected: 'string',
|
|
path: ['aaa', 'foo', 'bar'],
|
|
},
|
|
{
|
|
message: 'Invalid input: expected string, received undefined',
|
|
code: 'invalid_type',
|
|
expected: 'string',
|
|
path: ['bbb', 'foo', 'bar'],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Nullish', () => {
|
|
it('converts null to undefined', () => {
|
|
const s = z.object({ a: Nullish(z.string()) });
|
|
expect(s.parse({ a: null })).toEqual({});
|
|
});
|
|
|
|
it('converts undefined to undefined (key absent)', () => {
|
|
const s = z.object({ a: Nullish(z.string()) });
|
|
expect(s.parse({})).toEqual({});
|
|
});
|
|
|
|
it('preserves valid string values', () => {
|
|
const s = z.object({ a: Nullish(z.string()) });
|
|
expect(s.parse({ a: 'hello' })).toEqual({ a: 'hello' });
|
|
});
|
|
|
|
it('rejects wrong types', () => {
|
|
const s = Nullish(z.string());
|
|
expect(() => s.parse(42)).toThrow(
|
|
'Invalid input: expected string, received number',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('DeepNullish', () => {
|
|
describe('optional leaf', () => {
|
|
const s = DeepNullish(z.object({ a: z.string().optional() }));
|
|
|
|
it.each`
|
|
input | expected
|
|
${{ a: null }} | ${{}}
|
|
${{}} | ${{}}
|
|
${{ a: 'hello' }} | ${{ a: 'hello' }}
|
|
`('parses $input', ({ input, expected }) => {
|
|
expect(s.parse(input)).toEqual(expected);
|
|
});
|
|
|
|
it('rejects wrong types', () => {
|
|
expect(() => s.parse({ a: 42 })).toThrow(
|
|
'Invalid input: expected string, received number',
|
|
);
|
|
});
|
|
});
|
|
|
|
it.each`
|
|
input | expected
|
|
${{ outer: { inner: null } }} | ${{ outer: {} }}
|
|
${{ outer: null }} | ${{}}
|
|
`('recurses into nested objects: $input', ({ input, expected }) => {
|
|
const s = DeepNullish(
|
|
z.object({
|
|
outer: z.object({ inner: z.string().optional() }).optional(),
|
|
}),
|
|
);
|
|
expect(s.parse(input)).toEqual(expected);
|
|
});
|
|
|
|
it('recurses into record valueType', () => {
|
|
const s = DeepNullish(z.record(z.string(), z.string().optional()));
|
|
const result = s.parse({ key: null });
|
|
expect(result.key).toBeUndefined();
|
|
});
|
|
|
|
it('recurses into array element', () => {
|
|
const s = DeepNullish(z.array(z.string().optional()));
|
|
expect(s.parse([null, 'hello'])).toEqual([undefined, 'hello']);
|
|
});
|
|
|
|
it.each`
|
|
input | expected
|
|
${{ val: { a: null } }} | ${{ val: {} }}
|
|
${{ val: { a: 'x' } }} | ${{ val: { a: 'x' } }}
|
|
${{ val: 42 }} | ${{ val: 42 }}
|
|
`('recurses into union options: $input', ({ input, expected }) => {
|
|
const s = DeepNullish(
|
|
z.object({
|
|
val: z.union([z.object({ a: z.string().optional() }), z.number()]),
|
|
}),
|
|
);
|
|
expect(s.parse(input)).toEqual(expected);
|
|
});
|
|
|
|
it.each`
|
|
input | expected
|
|
${{}} | ${{ y: false }}
|
|
${{ y: null }} | ${{ y: false }}
|
|
${{ y: true }} | ${{ y: true }}
|
|
`('handles default over optional: $input', ({ input, expected }) => {
|
|
const s = DeepNullish(
|
|
z.object({ y: z.boolean().optional().default(false) }),
|
|
);
|
|
expect(s.parse(input)).toEqual(expected);
|
|
});
|
|
|
|
it.each`
|
|
input | expected
|
|
${{ a: null }} | ${{ a: null }}
|
|
${{ a: 'hello' }} | ${{ a: 'hello' }}
|
|
`(
|
|
'preserves nullable fields (null not stripped): $input',
|
|
({ input, expected }) => {
|
|
const s = DeepNullish(z.object({ a: z.nullable(z.string()) }));
|
|
expect(s.parse(input)).toEqual(expected);
|
|
},
|
|
);
|
|
|
|
describe('recurses into pipe output side', () => {
|
|
const s = DeepNullish(Json.pipe(z.object({ a: z.string().optional() })));
|
|
|
|
it('parses valid input', () => {
|
|
expect(s.parse('{"a":"x"}')).toEqual({ a: 'x' });
|
|
});
|
|
|
|
it('strips optional inside pipe', () => {
|
|
expect(s.parse('{"a":null}')).toEqual({});
|
|
expect(s.parse('{}')).toEqual({});
|
|
});
|
|
|
|
it('still rejects invalid JSON', () => {
|
|
expect(s.safeParse('{{{')).toMatchObject({ success: false });
|
|
});
|
|
});
|
|
|
|
describe('Loose* schemas remain opaque (closure boundary)', () => {
|
|
it('LooseArray element optionals are not rewritten, but cleaned up', () => {
|
|
const s = DeepNullish(
|
|
LooseArray(z.object({ b: z.string().optional() })),
|
|
);
|
|
expect(s.parse([{ b: null }])).toEqual([]);
|
|
expect(s.parse([{ b: 'x' }])).toEqual([{ b: 'x' }]);
|
|
});
|
|
|
|
it('LooseRecord value optionals are not rewritten, but cleaned up', () => {
|
|
const s = DeepNullish(
|
|
LooseRecord(z.object({ b: z.string().optional() })),
|
|
);
|
|
expect(s.parse({ k: { b: null } })).toEqual({});
|
|
expect(s.parse({ k: { b: null }, j: { b: '' } })).toEqual({
|
|
j: { b: '' },
|
|
});
|
|
expect(s.parse({ k: { b: 'x' } })).toEqual({ k: { b: 'x' } });
|
|
});
|
|
});
|
|
|
|
it('passes through leaf types unchanged', () => {
|
|
expect(DeepNullish(z.string()).parse('hello')).toBe('hello');
|
|
});
|
|
|
|
it('rejects wrong types on leaf', () => {
|
|
expect(() => DeepNullish(z.string()).parse(42)).toThrow(
|
|
'Invalid input: expected string, received number',
|
|
);
|
|
});
|
|
|
|
it.each`
|
|
input | expected
|
|
${{ a: null }} | ${{}}
|
|
${{ a: 'x' }} | ${{ a: 'x' }}
|
|
`(
|
|
'is idempotent when schema already uses Nullish: $input',
|
|
({ input, expected }) => {
|
|
const s = DeepNullish(z.object({ a: Nullish(z.string()) }));
|
|
expect(s.parse(input)).toEqual(expected);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe('Json', () => {
|
|
it('parses json', () => {
|
|
const Schema = Json.pipe(z.object({ foo: z.literal('bar') }));
|
|
|
|
expect(Schema.parse('{"foo": "bar"}')).toEqual({ foo: 'bar' });
|
|
|
|
expect(Schema.safeParse(42)).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid input: expected string, received number',
|
|
code: 'invalid_type',
|
|
expected: 'string',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
|
|
expect(Schema.safeParse('{"foo": "foo"}')).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid input: expected "bar"',
|
|
code: 'invalid_value',
|
|
values: ['bar'],
|
|
path: ['foo'],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
|
|
expect(Schema.safeParse('["foo", "bar"]')).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid input: expected object, received array',
|
|
code: 'invalid_type',
|
|
expected: 'object',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
|
|
expect(Schema.safeParse('{{{}}}')).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid JSON',
|
|
code: 'custom',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Json5', () => {
|
|
it('parses JSON5', () => {
|
|
const Schema = Json5.pipe(z.object({ foo: z.literal('bar') }));
|
|
|
|
expect(Schema.parse('{"foo": "bar"}')).toEqual({ foo: 'bar' });
|
|
|
|
expect(Schema.safeParse(42)).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid input: expected string, received number',
|
|
code: 'invalid_type',
|
|
expected: 'string',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
|
|
expect(Schema.safeParse('{"foo": "foo"}')).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid input: expected "bar"',
|
|
code: 'invalid_value',
|
|
values: ['bar'],
|
|
path: ['foo'],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
|
|
expect(Schema.safeParse('["foo", "bar"]')).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid input: expected object, received array',
|
|
code: 'invalid_type',
|
|
expected: 'object',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
|
|
expect(Schema.safeParse('{{{}}}')).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid JSON5',
|
|
code: 'custom',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Jsonc', () => {
|
|
it('parses JSONC', () => {
|
|
const Schema = Jsonc.pipe(z.object({ foo: z.literal('bar') }));
|
|
|
|
expect(Schema.parse('{"foo": "bar"}')).toEqual({ foo: 'bar' });
|
|
|
|
expect(Schema.safeParse(42)).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid input: expected string, received number',
|
|
code: 'invalid_type',
|
|
expected: 'string',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
|
|
expect(Schema.safeParse('{"foo": "foo"}')).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid input: expected "bar"',
|
|
code: 'invalid_value',
|
|
values: ['bar'],
|
|
path: ['foo'],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
|
|
expect(Schema.safeParse('["foo", "bar"]')).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid input: expected object, received array',
|
|
code: 'invalid_type',
|
|
expected: 'object',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
|
|
expect(Schema.safeParse('{')).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid JSONC',
|
|
code: 'custom',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('UtcDate', () => {
|
|
it('parses date', () => {
|
|
expect(UtcDate.parse('2020-04-04').toString()).toBe(
|
|
'2020-04-04T00:00:00.000Z',
|
|
);
|
|
});
|
|
|
|
it('rejects invalid date', () => {
|
|
expect(() => UtcDate.parse('foobar')).toThrow('Invalid date');
|
|
});
|
|
});
|
|
|
|
describe('Yaml', () => {
|
|
const Schema = Yaml.pipe(
|
|
z.object({ foo: z.array(z.object({ bar: z.literal('baz') })) }),
|
|
);
|
|
|
|
it('parses valid yaml', () => {
|
|
expect(Schema.parse('foo:\n- bar: baz')).toEqual({
|
|
foo: [{ bar: 'baz' }],
|
|
});
|
|
});
|
|
|
|
it('throws error for non-string', () => {
|
|
expect(Schema.safeParse(42)).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid input: expected string, received number',
|
|
code: 'invalid_type',
|
|
expected: 'string',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
});
|
|
|
|
it('throws error for invalid yaml', () => {
|
|
expect(Schema.safeParse('clearly: "invalid" "yaml"')).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid YAML',
|
|
code: 'custom',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('MultidocYaml', () => {
|
|
const Schema = MultidocYaml.pipe(
|
|
z.array(
|
|
z.object({
|
|
foo: z.number(),
|
|
}),
|
|
),
|
|
);
|
|
|
|
it('parses valid yaml', () => {
|
|
expect(
|
|
Schema.parse(codeBlock`
|
|
foo: 111
|
|
---
|
|
foo: 222
|
|
`),
|
|
).toEqual([{ foo: 111 }, { foo: 222 }]);
|
|
});
|
|
|
|
it('throws error for non-string', () => {
|
|
expect(Schema.safeParse(42)).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid input: expected string, received number',
|
|
code: 'invalid_type',
|
|
expected: 'string',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
});
|
|
|
|
it('throws error for invalid yaml', () => {
|
|
expect(Schema.safeParse('clearly: "invalid" "yaml"')).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid YAML',
|
|
code: 'custom',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('multidocYaml()', () => {
|
|
const Schema = multidocYaml().pipe(
|
|
z.array(
|
|
z.object({
|
|
foo: z.number(),
|
|
}),
|
|
),
|
|
);
|
|
|
|
it('parses valid yaml', () => {
|
|
expect(
|
|
Schema.parse(codeBlock`
|
|
foo: 111
|
|
---
|
|
foo: 222
|
|
`),
|
|
).toEqual([{ foo: 111 }, { foo: 222 }]);
|
|
});
|
|
|
|
it('rejects invalid yaml', () => {
|
|
expect(multidocYaml().safeParse(': invalid: yaml: {')).toMatchObject({
|
|
success: false,
|
|
error: { issues: [{ code: 'custom', message: 'Invalid YAML' }] },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('withDepType()', () => {
|
|
const Base = z.array(
|
|
z.object({ depName: z.string(), depType: z.string().optional() }),
|
|
);
|
|
|
|
it('sets depType on all deps when force=true (default)', () => {
|
|
const result = withDepType(Base, 'devDependencies').parse([
|
|
{ depName: 'foo' },
|
|
{ depName: 'bar', depType: 'existing' },
|
|
]);
|
|
expect(result).toEqual([
|
|
{ depName: 'foo', depType: 'devDependencies' },
|
|
{ depName: 'bar', depType: 'devDependencies' },
|
|
]);
|
|
});
|
|
|
|
it('preserves existing depType when force=false', () => {
|
|
const result = withDepType(Base, 'devDependencies', false).parse([
|
|
{ depName: 'foo' },
|
|
{ depName: 'bar', depType: 'existing' },
|
|
]);
|
|
expect(result).toEqual([
|
|
{ depName: 'foo', depType: 'devDependencies' },
|
|
{ depName: 'bar', depType: 'existing' },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('isEmailAdress()', () => {
|
|
it('returns true for valid email', () => {
|
|
expect(isEmailAdress('user@example.com')).toBe(true);
|
|
});
|
|
|
|
it('returns false for invalid email', () => {
|
|
expect(isEmailAdress('not-an-email')).toBe(false);
|
|
});
|
|
|
|
it('returns true for GitHub app style email', () => {
|
|
expect(isEmailAdress('211370388+foo[bot]@users.noreply.github.com')).toBe(
|
|
true,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Toml', () => {
|
|
const Schema = Toml.pipe(
|
|
z.object({ foo: z.object({ bar: z.literal('baz') }) }),
|
|
);
|
|
|
|
it('parses valid toml', () => {
|
|
const content = codeBlock`
|
|
[foo]
|
|
bar = "baz"
|
|
`;
|
|
expect(Schema.parse(content)).toEqual({
|
|
foo: { bar: 'baz' },
|
|
});
|
|
});
|
|
|
|
it('throws error for invalid schema', () => {
|
|
const content = codeBlock`
|
|
[foo]
|
|
bar = "brb"
|
|
`;
|
|
expect(Schema.safeParse(content)).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
code: 'invalid_value',
|
|
values: ['baz'],
|
|
path: ['foo', 'bar'],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
});
|
|
|
|
it('throws error for invalid toml', () => {
|
|
expect(Schema.safeParse('clearly_invalid')).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
message: 'Invalid TOML',
|
|
code: 'custom',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Ini', () => {
|
|
const Schema = Ini.pipe(
|
|
z.object({ foo: z.object({ bar: z.literal('baz') }) }),
|
|
);
|
|
|
|
it('parses valid ini', () => {
|
|
const content = codeBlock`
|
|
[foo]
|
|
bar = "baz"
|
|
`;
|
|
expect(Schema.parse(content)).toEqual({
|
|
foo: { bar: 'baz' },
|
|
});
|
|
});
|
|
|
|
it('throws error for invalid schema', () => {
|
|
expect(Schema.safeParse('clearly_invalid')).toMatchObject({
|
|
error: {
|
|
issues: [
|
|
{
|
|
code: 'invalid_type',
|
|
expected: 'object',
|
|
path: ['foo'],
|
|
},
|
|
],
|
|
},
|
|
success: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('logging utils', () => {
|
|
it('logs debug message and returns fallback value', () => {
|
|
const Str = z
|
|
.string()
|
|
.catch(withDebugMessage('default string', 'Debug message'));
|
|
|
|
const result = Str.parse(42);
|
|
|
|
expect(result).toBe('default string');
|
|
|
|
expect(logger.logger.debug).toHaveBeenCalledWith(
|
|
{ err: expect.objectContaining({ issues: expect.any(Array) }) },
|
|
'Debug message',
|
|
);
|
|
});
|
|
|
|
it('logs trace message and returns fallback value', () => {
|
|
const Str = z
|
|
.string()
|
|
.catch(withTraceMessage('default string', 'Trace message'));
|
|
|
|
const result = Str.parse(42);
|
|
|
|
expect(result).toBe('default string');
|
|
|
|
expect(logger.logger.trace).toHaveBeenCalledWith(
|
|
{ err: expect.objectContaining({ issues: expect.any(Array) }) },
|
|
'Trace message',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('NotCircular', () => {
|
|
const Schema = NotCircular.pipe(z.any());
|
|
|
|
it.each`
|
|
value
|
|
${undefined}
|
|
${null}
|
|
${123}
|
|
${'string'}
|
|
${true}
|
|
`('allows non-circular primitive $value', ({ value }) => {
|
|
expect(Schema.parse(value)).toBe(value);
|
|
});
|
|
|
|
it.each`
|
|
value
|
|
${[1, 2, 3]}
|
|
${[{ a: 1 }, { b: 2 }]}
|
|
${[[1, 2], [3, 4]]}
|
|
`('allows non-circular array $value', ({ value }) => {
|
|
expect(Schema.parse(value)).toEqual(value);
|
|
});
|
|
|
|
it.each`
|
|
value
|
|
${{ a: 1, b: 2 }}
|
|
${{ a: { b: 1 }, c: { d: 2 } }}
|
|
`('allows non-circular object $value', ({ value }) => {
|
|
expect(Schema.parse(value)).toEqual(value);
|
|
});
|
|
|
|
it('allows objects reuse', () => {
|
|
const reused = { value: 42 };
|
|
const obj = {
|
|
foo: reused,
|
|
bar: reused,
|
|
};
|
|
|
|
expect(Schema.parse(obj)).toEqual({
|
|
foo: { value: 42 },
|
|
bar: { value: 42 },
|
|
});
|
|
});
|
|
|
|
it('rejects circular objects', () => {
|
|
const obj: any = { a: 1 };
|
|
obj.self = obj;
|
|
|
|
expect(Schema.safeParse(obj)).toMatchObject({
|
|
success: false,
|
|
error: {
|
|
issues: [
|
|
{
|
|
code: 'custom',
|
|
message: 'values cannot be circular data structures',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it('rejects circular arrays', () => {
|
|
const arr: any[] = [1, 2, 3];
|
|
arr.push(arr);
|
|
|
|
expect(Schema.safeParse(arr)).toMatchObject({
|
|
success: false,
|
|
error: {
|
|
issues: [
|
|
{
|
|
code: 'custom',
|
|
message: 'values cannot be circular data structures',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it('rejects deeply nested circular references', () => {
|
|
const obj: any = {
|
|
a: {
|
|
b: {
|
|
c: {
|
|
d: {},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
obj.a.b.c.d.circular = obj.a;
|
|
|
|
expect(Schema.safeParse(obj)).toMatchObject({
|
|
success: false,
|
|
error: {
|
|
issues: [
|
|
{
|
|
code: 'custom',
|
|
message: 'values cannot be circular data structures',
|
|
path: [],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it('can be combined with other schema types', () => {
|
|
const Obj = z.object({
|
|
data: NotCircular.pipe(z.any()),
|
|
});
|
|
|
|
expect(Obj.parse({ data: { a: 1, b: 2 } })).toEqual({
|
|
data: { a: 1, b: 2 },
|
|
});
|
|
|
|
const obj: any = { a: 1 };
|
|
obj.self = obj;
|
|
|
|
expect(Obj.safeParse({ data: obj })).toMatchObject({
|
|
success: false,
|
|
error: {
|
|
issues: [
|
|
{
|
|
code: 'custom',
|
|
message: 'values cannot be circular data structures',
|
|
path: ['data'],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
});
|
|
});
|