zod
Version:
TypeScript-first schema declaration and validation library with static type inference
77 lines (65 loc) • 2.94 kB
text/typescript
import { describe, expect, test } from "vitest";
import * as z from "zod";
describe("z.creditCard", () => {
// Luhn-valid PANs. The last four are the schemes an issuer allowlist would turn away — validation is shape plus Luhn only, so the BIN is never consulted.
const valid: [string, string][] = [
["Visa", "4111111111111111"],
["Visa (13)", "4007000000027"],
["Mastercard", "5555555555554444"],
["AmEx", "378282246310005"],
["Discover", "6011111111111117"],
["JCB", "3530111333300000"],
["Diners Club", "30569309025904"],
["UnionPay", "6200000000000005"],
["Mir", "2200000000000004"],
["Elo", "6362000000000009"],
["Discover 644-649", "6450000000000002"],
["Troy", "9792000000000003"],
];
test.each(valid)("accepts %s", (_label, n) => {
expect(z.creditCard().parse(n)).toBe(n);
});
test("accepts single space or hyphen separators", () => {
expect(z.creditCard().parse("4111 1111 1111 1111")).toBe("4111 1111 1111 1111");
expect(z.creditCard().parse("4111-1111-1111-1111")).toBe("4111-1111-1111-1111");
});
test("rejects other separator shapes", () => {
for (const n of ["4111 1111 1111 1111", " 4111111111111111", "4111111111111111 ", "4111.1111.1111.1111"]) {
expect(() => z.creditCard().parse(n)).toThrow();
}
});
test("accepts the 12- and 19-digit length bounds", () => {
expect(z.creditCard().parse("400000000002")).toBe("400000000002");
expect(z.creditCard().parse("6250000000000000005")).toBe("6250000000000000005");
});
test("rejects Luhn-valid input outside 12-19 digits", () => {
expect(() => z.creditCard().parse("40000000006")).toThrow();
expect(() => z.creditCard().parse("40000000000000000002")).toThrow();
});
test("rejects when Luhn fails (right shape, wrong checksum)", () => {
expect(() => z.creditCard().parse("4111111111111112")).toThrow();
});
test("rejects non-digit input", () => {
expect(() => z.creditCard().parse("not-a-card")).toThrow();
expect(() => z.creditCard().parse("")).toThrow();
});
test("carries the shape regex as its JSON Schema pattern", () => {
// Luhn is not expressible as a pattern, so `pattern` covers length and separators only.
expect(z.toJSONSchema(z.creditCard())).toMatchObject({
format: "credit_card",
pattern: z.regexes.creditCard.source,
});
});
test("round-trips through JSON Schema with the checksum intact", () => {
const schema = z.fromJSONSchema(z.toJSONSchema(z.creditCard()));
expect(schema.safeParse("4111111111111111").success).toBe(true);
expect(schema.safeParse("4111111111111112").success).toBe(false);
});
test("error message references credit card", () => {
const result = z.creditCard().safeParse("not-a-card");
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0]?.message).toMatch(/credit card/i);
}
});
});