385 lines
11 KiB
TypeScript
385 lines
11 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { z } from "zod";
|
|
import { sanitize } from "../../src/ts/utils/sanitize";
|
|
|
|
describe("sanitize function", () => {
|
|
describe("arrays", () => {
|
|
const numberArraySchema = z.array(z.number());
|
|
const numbersArrayMin2Schema = numberArraySchema.min(2);
|
|
|
|
const testCases: {
|
|
input: number[];
|
|
expected: {
|
|
numbers: number[] | boolean;
|
|
numbersMin: number[] | boolean;
|
|
};
|
|
}[] = [
|
|
{ input: [], expected: { numbers: true, numbersMin: false } },
|
|
{ input: [1, 2], expected: { numbers: true, numbersMin: true } },
|
|
{
|
|
input: [1, "2" as any],
|
|
expected: { numbers: [1], numbersMin: false },
|
|
},
|
|
{
|
|
input: ["one", "two"] as any,
|
|
expected: { numbers: [], numbersMin: false },
|
|
},
|
|
];
|
|
it.for(testCases)("number array with $input", ({ input, expected }) => {
|
|
const sanitized = expect(
|
|
expected.numbers === false
|
|
? () => sanitize(numberArraySchema, input)
|
|
: sanitize(numberArraySchema, input),
|
|
);
|
|
|
|
if (expected.numbers === false) {
|
|
sanitized.toThrow();
|
|
} else if (expected.numbers === true) {
|
|
sanitized.toStrictEqual(input);
|
|
} else {
|
|
sanitized.toStrictEqual(expected.numbers);
|
|
}
|
|
});
|
|
it.for(testCases)(
|
|
"number array.min(2) with $input",
|
|
({ input, expected }) => {
|
|
const sanitized = expect(
|
|
expected.numbersMin === false
|
|
? () => sanitize(numbersArrayMin2Schema, input)
|
|
: sanitize(numbersArrayMin2Schema, input),
|
|
);
|
|
|
|
if (expected.numbersMin === false) {
|
|
sanitized.toThrow();
|
|
} else if (expected.numbersMin === true) {
|
|
sanitized.toStrictEqual(input);
|
|
} else {
|
|
sanitized.toStrictEqual(expected.numbersMin);
|
|
}
|
|
},
|
|
);
|
|
});
|
|
describe("objects", () => {
|
|
const objectSchema = z.object({
|
|
name: z.string(),
|
|
age: z.number().positive(),
|
|
tags: z.array(z.string()),
|
|
enumArray: z.array(z.enum(["one", "two"])).min(2),
|
|
});
|
|
const objectSchemaFullPartial = objectSchema.partial().strip();
|
|
const objectSchemaWithOptional = objectSchema.partial({
|
|
tags: true,
|
|
enumArray: true,
|
|
});
|
|
|
|
const testCases: {
|
|
input: z.infer<typeof objectSchemaFullPartial>;
|
|
expected: {
|
|
mandatory: z.infer<typeof objectSchema> | boolean;
|
|
partial: z.infer<typeof objectSchemaFullPartial> | boolean;
|
|
optional: z.infer<typeof objectSchemaWithOptional> | boolean;
|
|
};
|
|
}[] = [
|
|
{
|
|
input: {},
|
|
expected: { mandatory: false, partial: true, optional: false },
|
|
},
|
|
{
|
|
input: {
|
|
name: "Alice",
|
|
age: 23,
|
|
tags: ["one", "two"],
|
|
enumArray: ["one", "two"],
|
|
},
|
|
expected: { mandatory: true, partial: true, optional: true },
|
|
},
|
|
{
|
|
input: {
|
|
name: "Alice",
|
|
age: 23,
|
|
},
|
|
expected: { mandatory: false, partial: true, optional: true },
|
|
},
|
|
{
|
|
input: {
|
|
name: "Alice",
|
|
age: "sixty" as any,
|
|
},
|
|
expected: {
|
|
mandatory: false,
|
|
partial: { name: "Alice" },
|
|
optional: false,
|
|
},
|
|
},
|
|
{
|
|
input: {
|
|
name: "Alice",
|
|
age: 23,
|
|
tags: ["one", 2 as any],
|
|
enumArray: "one" as any,
|
|
},
|
|
expected: {
|
|
mandatory: false,
|
|
partial: { name: "Alice", age: 23, tags: ["one"] },
|
|
optional: { name: "Alice", age: 23, tags: ["one"] },
|
|
},
|
|
},
|
|
{
|
|
input: {
|
|
name: "Alice",
|
|
age: 23,
|
|
tags: [1, 2] as any,
|
|
enumArray: [1, 2] as any,
|
|
},
|
|
expected: {
|
|
mandatory: false,
|
|
partial: { name: "Alice", age: 23 },
|
|
optional: { name: "Alice", age: 23 },
|
|
},
|
|
},
|
|
{
|
|
input: {
|
|
name: "Alice",
|
|
age: 23,
|
|
extraArray: [],
|
|
extraObject: {},
|
|
extraString: "",
|
|
} as any,
|
|
expected: {
|
|
mandatory: false,
|
|
partial: { name: "Alice", age: 23 },
|
|
optional: { name: "Alice", age: 23 },
|
|
},
|
|
},
|
|
{
|
|
input: {
|
|
name: "Alice",
|
|
//results in two errors on the same path. array with invalid value and not enough items
|
|
enumArray: ["invalid" as any],
|
|
},
|
|
expected: {
|
|
mandatory: false,
|
|
partial: { name: "Alice" }, //enumArray is removed
|
|
optional: false,
|
|
},
|
|
},
|
|
];
|
|
|
|
it.for(testCases)("object mandatory with $input", ({ input, expected }) => {
|
|
const sanitized = expect(
|
|
expected.mandatory === false
|
|
? () => sanitize(objectSchema, input as any)
|
|
: sanitize(objectSchema, input as any),
|
|
);
|
|
|
|
if (expected.mandatory === false) {
|
|
sanitized.toThrow();
|
|
} else if (expected.mandatory === true) {
|
|
sanitized.toStrictEqual(input);
|
|
} else {
|
|
sanitized.toStrictEqual(expected.mandatory);
|
|
}
|
|
});
|
|
it.for(testCases)(
|
|
"object full partial with $input",
|
|
({ input, expected }) => {
|
|
const sanitized = expect(
|
|
expected.partial === false
|
|
? () => sanitize(objectSchemaFullPartial, input as any)
|
|
: sanitize(objectSchemaFullPartial, input as any),
|
|
);
|
|
|
|
if (expected.partial === false) {
|
|
sanitized.toThrow();
|
|
} else if (expected.partial === true) {
|
|
sanitized.toStrictEqual(input);
|
|
} else {
|
|
sanitized.toStrictEqual(expected.partial);
|
|
}
|
|
},
|
|
);
|
|
it.for(testCases)("object optional with $input", ({ input, expected }) => {
|
|
const sanitized = expect(
|
|
expected.optional === false
|
|
? () => sanitize(objectSchemaWithOptional, input as any)
|
|
: sanitize(objectSchemaWithOptional, input as any),
|
|
);
|
|
|
|
if (expected.optional === false) {
|
|
sanitized.toThrow();
|
|
} else if (expected.optional === true) {
|
|
sanitized.toStrictEqual(input);
|
|
} else {
|
|
sanitized.toStrictEqual(expected.optional);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("nested", () => {
|
|
const itemSchema = z.object({
|
|
name: z.string(),
|
|
enumArray: z.array(z.enum(["one", "two"])).min(2),
|
|
});
|
|
const nestedSchema = z.object({
|
|
nested: z.array(itemSchema),
|
|
});
|
|
|
|
const nestedSchemaFullPartial = z
|
|
.object({
|
|
nested: z.array(itemSchema.partial()),
|
|
})
|
|
.partial();
|
|
const nestedSchemaWithMin2Array = z.object({
|
|
nested: z.array(itemSchema).min(2),
|
|
});
|
|
|
|
const testCases: {
|
|
input: z.infer<typeof nestedSchema>;
|
|
expected: {
|
|
mandatory: z.infer<typeof nestedSchema> | boolean;
|
|
partial: z.infer<typeof nestedSchemaFullPartial> | boolean;
|
|
minArray: z.infer<typeof nestedSchemaWithMin2Array> | boolean;
|
|
};
|
|
}[] = [
|
|
{
|
|
input: {} as any,
|
|
expected: {
|
|
mandatory: false,
|
|
partial: true,
|
|
minArray: false,
|
|
},
|
|
},
|
|
{
|
|
input: {
|
|
nested: [
|
|
{ name: "Alice", enumArray: ["one", "two"] },
|
|
{ name: "Bob", enumArray: ["one", "two"] },
|
|
],
|
|
},
|
|
expected: {
|
|
mandatory: true,
|
|
partial: true,
|
|
minArray: true,
|
|
},
|
|
},
|
|
{
|
|
input: {
|
|
nested: [
|
|
{ name: "Alice", enumArray: ["one", "two"] },
|
|
{ name: "Bob" } as any,
|
|
],
|
|
},
|
|
expected: {
|
|
mandatory: {
|
|
nested: [{ name: "Alice", enumArray: ["one", "two"] }],
|
|
},
|
|
partial: true,
|
|
minArray: false,
|
|
},
|
|
},
|
|
{
|
|
input: {
|
|
nested: [
|
|
{ enumArray: ["one", "two"] } as any,
|
|
{ name: "Bob" } as any,
|
|
],
|
|
},
|
|
expected: {
|
|
mandatory: false,
|
|
partial: true,
|
|
minArray: false,
|
|
},
|
|
},
|
|
];
|
|
|
|
it.for(testCases)("nested mandatory with $input", ({ input, expected }) => {
|
|
const sanitized = expect(
|
|
expected.mandatory === false
|
|
? () => sanitize(nestedSchema, input as any)
|
|
: sanitize(nestedSchema, input as any),
|
|
);
|
|
|
|
if (expected.mandatory === false) {
|
|
sanitized.toThrow();
|
|
} else if (expected.mandatory === true) {
|
|
sanitized.toStrictEqual(input);
|
|
} else {
|
|
sanitized.toStrictEqual(expected.mandatory);
|
|
}
|
|
});
|
|
it.for(testCases)("nested partial with $input", ({ input, expected }) => {
|
|
const sanitized = expect(
|
|
expected.partial === false
|
|
? () => sanitize(nestedSchemaFullPartial, input as any)
|
|
: sanitize(nestedSchemaFullPartial, input as any),
|
|
);
|
|
|
|
if (expected.partial === false) {
|
|
sanitized.toThrow();
|
|
} else if (expected.partial === true) {
|
|
sanitized.toStrictEqual(input);
|
|
} else {
|
|
sanitized.toStrictEqual(expected.partial);
|
|
}
|
|
});
|
|
it.for(testCases)(
|
|
"nested array min(2) with $input",
|
|
({ input, expected }) => {
|
|
const sanitized = expect(
|
|
expected.minArray === false
|
|
? () => sanitize(nestedSchemaWithMin2Array, input as any)
|
|
: sanitize(nestedSchemaWithMin2Array, input as any),
|
|
);
|
|
|
|
if (expected.minArray === false) {
|
|
sanitized.toThrow();
|
|
} else if (expected.minArray === true) {
|
|
sanitized.toStrictEqual(input);
|
|
} else {
|
|
sanitized.toStrictEqual(expected.minArray);
|
|
}
|
|
},
|
|
);
|
|
});
|
|
|
|
const schema = z
|
|
.object({
|
|
name: z.string(),
|
|
age: z.number().positive(),
|
|
tags: z.array(z.string()),
|
|
enumArray: z.array(z.enum(["one", "two"])).min(2),
|
|
})
|
|
.partial()
|
|
.strip();
|
|
|
|
it("should strip extra keys", () => {
|
|
const obj = {
|
|
name: "bob",
|
|
age: 30,
|
|
tags: ["developer", "coder"],
|
|
powerLevel: 9001,
|
|
} as any;
|
|
const stripped = sanitize(schema.strip(), obj);
|
|
expect(stripped).not.toHaveProperty("powerLevel");
|
|
});
|
|
it("should strip extra keys on error", () => {
|
|
const obj = {
|
|
name: "bob",
|
|
age: 30,
|
|
powerLevel: 9001,
|
|
} as any;
|
|
const stripped = sanitize(schema.strip(), obj);
|
|
expect(stripped).not.toHaveProperty("powerLevel");
|
|
});
|
|
it("should provide a readable error message", () => {
|
|
const obj = {
|
|
arrayOneTwo: ["one", "nonexistent"],
|
|
} as any;
|
|
expect(() => {
|
|
sanitize(schema.required().strip(), obj);
|
|
}).toThrow(
|
|
"unable to sanitize: name: Required, age: Required, tags: Required, enumArray: Required",
|
|
);
|
|
});
|
|
});
|