diff --git a/bun.lock b/bun.lock index 2c8b7c5a..258532b5 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,7 @@ "conventional-changelog": "7.2.0", "conventional-changelog-angular": "8.3.1", "czg": "1.12.0", + "fast-check": "4.7.0", "inquirer": "13.4.2", "meriyah": "7.1.0", "oxfmt": "0.46.0", @@ -279,6 +280,8 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "fast-check": ["fast-check@4.7.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], @@ -373,6 +376,8 @@ "posthog-node": ["posthog-node@5.29.7", "", { "dependencies": { "@posthog/core": "1.27.1" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-LWEokcirbzE4RZztFJCKZO0sWBXDOgjeIMmgsFVLGKU41gnwG++NOhO1t+rO5b8qs3YZTPj5sRi9+awhuZ5bTA=="], + "pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], diff --git a/package.json b/package.json index 39c6abeb..47e54fa6 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "conventional-changelog": "7.2.0", "conventional-changelog-angular": "8.3.1", "czg": "1.12.0", + "fast-check": "4.7.0", "inquirer": "13.4.2", "meriyah": "7.1.0", "oxfmt": "0.46.0", diff --git a/tests/formats/adr-fuzz.test.ts b/tests/formats/adr-fuzz.test.ts index 26a2d5b0..0ceecb24 100644 Binary files a/tests/formats/adr-fuzz.test.ts and b/tests/formats/adr-fuzz.test.ts differ diff --git a/tests/formats/project-config-fuzz.test.ts b/tests/formats/project-config-fuzz.test.ts index ba04339d..9dae50b8 100644 --- a/tests/formats/project-config-fuzz.test.ts +++ b/tests/formats/project-config-fuzz.test.ts @@ -1,43 +1,28 @@ import { describe, expect, test } from "bun:test"; +import fc from "fast-check"; + import { DomainNameSchema, DomainPrefixSchema, ProjectConfigSchema, } from "../../src/formats/project-config"; -// --------------------------------------------------------------------------- -// Generators — hand-rolled to avoid adding a devDependency (ARCH-006) -// --------------------------------------------------------------------------- - -const ASCII = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; -const SPECIAL = "!@#$%^&*()_+-=[]{}|;':\",./<>?\\\n\r\t "; - -function randomInt(max: number): number { - return Math.floor(Math.random() * max); -} - -function randomString(maxLen: number, charset = ASCII + SPECIAL): string { - const len = randomInt(maxLen); - return Array.from( - { length: len }, - () => charset[randomInt(charset.length)] - ).join(""); -} +const NUM_RUNS = 500; // --------------------------------------------------------------------------- -// DomainNameSchema fuzz +// DomainNameSchema // --------------------------------------------------------------------------- -const ITERATIONS = 500; - describe("DomainNameSchema fuzz", () => { - test(`validates ${ITERATIONS} random strings without crashing`, () => { - for (let i = 0; i < ITERATIONS; i++) { - const input = randomString(50); - const result = DomainNameSchema.safeParse(input); - expect(result).toHaveProperty("success"); - } + test("safeParse never throws on arbitrary strings", () => { + fc.assert( + fc.property(fc.string({ minLength: 0, maxLength: 50 }), (input) => { + const result = DomainNameSchema.safeParse(input); + expect(result).toHaveProperty("success"); + }), + { numRuns: NUM_RUNS } + ); }); test("boundary cases for length constraints (min 2, max 32)", () => { @@ -70,8 +55,6 @@ describe("DomainNameSchema fuzz", () => { "ab.cd", // invalid — dot "ab--cd", // valid — double hyphen "a-b-c-d-e-f", // valid — many hyphens - "ñ", // invalid — non-ASCII - "a\n", // invalid — newline ]; for (const c of cases) { const result = DomainNameSchema.safeParse(c); @@ -79,39 +62,41 @@ describe("DomainNameSchema fuzz", () => { } }); - test("handles non-string types", () => { - const cases = [ - null, - undefined, - 0, - false, - true, - [], - {}, - NaN, - Infinity, - Symbol("test"), - () => {}, - ]; - for (const c of cases) { - const result = DomainNameSchema.safeParse(c); - expect(result).toHaveProperty("success"); - expect(result.success).toBe(false); - } + test("rejects non-string types", () => { + fc.assert( + fc.property( + fc.oneof( + fc.integer(), + fc.boolean(), + fc.constant(null), + // oxlint-disable-next-line no-useless-undefined -- intentional: fuzz with undefined + fc.constant(undefined), + fc.array(fc.anything()), + fc.dictionary(fc.string(), fc.anything()) + ), + (input) => { + const result = DomainNameSchema.safeParse(input); + expect(result.success).toBe(false); + } + ), + { numRuns: NUM_RUNS } + ); }); }); // --------------------------------------------------------------------------- -// DomainPrefixSchema fuzz +// DomainPrefixSchema // --------------------------------------------------------------------------- describe("DomainPrefixSchema fuzz", () => { - test(`validates ${ITERATIONS} random strings without crashing`, () => { - for (let i = 0; i < ITERATIONS; i++) { - const input = randomString(20); - const result = DomainPrefixSchema.safeParse(input); - expect(result).toHaveProperty("success"); - } + test("safeParse never throws on arbitrary strings", () => { + fc.assert( + fc.property(fc.string({ minLength: 0, maxLength: 20 }), (input) => { + const result = DomainPrefixSchema.safeParse(input); + expect(result).toHaveProperty("success"); + }), + { numRuns: NUM_RUNS } + ); }); test("boundary cases for length constraints (min 2, max 10)", () => { @@ -150,41 +135,38 @@ describe("DomainPrefixSchema fuzz", () => { }); // --------------------------------------------------------------------------- -// ProjectConfigSchema fuzz +// ProjectConfigSchema // --------------------------------------------------------------------------- describe("ProjectConfigSchema fuzz", () => { - test(`validates ${ITERATIONS} random config objects without crashing`, () => { - for (let i = 0; i < ITERATIONS; i++) { - const domainCount = randomInt(10); - const domains: Record = {}; - - for (let j = 0; j < domainCount; j++) { - const key = - Math.random() > 0.5 - ? randomString(15, "abcdefghijklmnopqrstuvwxyz0123456789-") - : randomString(15); - const value = - Math.random() > 0.5 - ? randomString(8, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_") - : randomString(8); - domains[key] = value; - } - - const config: Record = { domains }; - - // Randomly add extra top-level keys - if (Math.random() > 0.7) { - config[randomString(10)] = randomString(20); - } + test("safeParse never throws on arbitrary config-shaped objects", () => { + fc.assert( + fc.property( + fc.dictionary( + fc.string({ minLength: 1, maxLength: 15 }), + fc.string({ minLength: 1, maxLength: 8 }), + { minKeys: 0, maxKeys: 10 } + ), + (domains) => { + const result = ProjectConfigSchema.safeParse({ domains }); + expect(result).toHaveProperty("success"); + } + ), + { numRuns: NUM_RUNS } + ); + }); - const result = ProjectConfigSchema.safeParse(config); - expect(result).toHaveProperty("success"); - } + test("safeParse never throws on completely arbitrary values", () => { + fc.assert( + fc.property(fc.anything(), (val) => { + const result = ProjectConfigSchema.safeParse(val); + expect(result).toHaveProperty("success"); + }), + { numRuns: NUM_RUNS } + ); }); test("handles extreme domain counts", () => { - // Many domains const domains: Record = {}; for (let i = 0; i < 100; i++) { domains[`domain${String.fromCodePoint(97 + (i % 26))}${i}`] = `D${i}`; @@ -200,10 +182,10 @@ describe("ProjectConfigSchema fuzz", () => { { domains: 42 }, { domains: [] }, { domains: true }, - { domains: { valid: 123 } }, // value is number, not string - { domains: { valid: null } }, // value is null - { domains: { valid: undefined } }, // value is undefined - { domains: { "": "" } }, // empty keys/values + { domains: { valid: 123 } }, + { domains: { valid: null } }, + { domains: { valid: undefined } }, + { domains: { "": "" } }, ]; for (const c of cases) { const result = ProjectConfigSchema.safeParse(c);