@schmock/faker 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/overrides.d.ts +1 -1
- package/dist/overrides.d.ts.map +1 -1
- package/dist/overrides.js +24 -27
- package/dist/test-utils.d.ts.map +1 -1
- package/dist/test-utils.js +7 -3
- package/package.json +6 -6
- package/src/index.test.ts +2 -1
- package/src/index.ts +23 -36
- package/src/overrides.property.test.ts +172 -0
- package/src/overrides.test.ts +38 -0
- package/src/overrides.ts +46 -55
- package/src/plugin-integration.test.ts +2 -1
- package/src/post-process.test.ts +152 -0
- package/src/schema-enhancement.test.ts +216 -0
- package/src/steps/deterministic-seeds.steps.ts +0 -21
- package/src/steps/faker-plugin.steps.ts +1 -100
- package/src/test-utils.ts +7 -3
- package/src/validation.ts +2 -2
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import fc from "fast-check";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { applyOverrides, determineArrayCount } from "./overrides";
|
|
4
|
+
|
|
5
|
+
describe("applyOverrides — property-based tests", () => {
|
|
6
|
+
it("never adds keys not in data or overrides (shape preservation)", () => {
|
|
7
|
+
// Use simple alphanumeric keys to avoid dot-path splitting edge cases,
|
|
8
|
+
// since keys containing "." are treated as nested paths by applyOverrides.
|
|
9
|
+
const safeKey = fc.stringMatching(/^[a-z][a-zA-Z0-9]{0,9}$/);
|
|
10
|
+
|
|
11
|
+
fc.assert(
|
|
12
|
+
fc.property(
|
|
13
|
+
fc.dictionary(safeKey, fc.jsonValue()),
|
|
14
|
+
fc.dictionary(safeKey, fc.jsonValue()),
|
|
15
|
+
(data, overrides) => {
|
|
16
|
+
const result = applyOverrides(data, overrides) as Record<
|
|
17
|
+
string,
|
|
18
|
+
unknown
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
const allowedKeys = new Set([
|
|
22
|
+
...Object.keys(data),
|
|
23
|
+
...Object.keys(overrides),
|
|
24
|
+
]);
|
|
25
|
+
for (const key of Object.keys(result)) {
|
|
26
|
+
expect(allowedKeys.has(key)).toBe(true);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
),
|
|
30
|
+
{ numRuns: 100 },
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("with empty overrides returns structuredClone of data", () => {
|
|
35
|
+
fc.assert(
|
|
36
|
+
fc.property(fc.dictionary(fc.string(), fc.jsonValue()), (data) => {
|
|
37
|
+
const result = applyOverrides(data, {});
|
|
38
|
+
|
|
39
|
+
// Should be deep-equal but not the same reference
|
|
40
|
+
expect(result).toEqual(data);
|
|
41
|
+
if (Object.keys(data).length > 0) {
|
|
42
|
+
expect(result).not.toBe(data);
|
|
43
|
+
}
|
|
44
|
+
}),
|
|
45
|
+
{ numRuns: 100 },
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("processTemplate idempotency: processing an already-resolved template is a no-op", () => {
|
|
50
|
+
// Resolved values contain no {{ }} markers, so re-applying overrides
|
|
51
|
+
// with the same non-template values should be idempotent
|
|
52
|
+
fc.assert(
|
|
53
|
+
fc.property(
|
|
54
|
+
fc.record({
|
|
55
|
+
name: fc.string().filter((s) => !s.includes("{{")),
|
|
56
|
+
age: fc.integer({ min: 0, max: 200 }),
|
|
57
|
+
}),
|
|
58
|
+
(data) => {
|
|
59
|
+
const overrides = { name: "Alice", age: 42 };
|
|
60
|
+
|
|
61
|
+
const first = applyOverrides(data, overrides);
|
|
62
|
+
const second = applyOverrides(
|
|
63
|
+
first as Record<string, unknown>,
|
|
64
|
+
overrides,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(second).toEqual(first);
|
|
68
|
+
},
|
|
69
|
+
),
|
|
70
|
+
{ numRuns: 100 },
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("DANGEROUS_KEYS (__proto__, constructor, prototype) are always rejected in setNestedProperty", () => {
|
|
75
|
+
const dangerousKeys = ["__proto__", "constructor", "prototype"];
|
|
76
|
+
|
|
77
|
+
for (const dangerous of dangerousKeys) {
|
|
78
|
+
// Flat key — dangerous key is skipped entirely, so no own property is set
|
|
79
|
+
const flatResult = applyOverrides(
|
|
80
|
+
{ safe: "value" },
|
|
81
|
+
{ [dangerous]: "injected" },
|
|
82
|
+
) as Record<string, unknown>;
|
|
83
|
+
expect(Object.hasOwn(flatResult, dangerous)).toBe(false);
|
|
84
|
+
|
|
85
|
+
// Nested path with dangerous key at start — entire path is skipped
|
|
86
|
+
const nestedStartResult = applyOverrides(
|
|
87
|
+
{ safe: "value" },
|
|
88
|
+
{ [`${dangerous}.nested`]: "injected" },
|
|
89
|
+
) as Record<string, unknown>;
|
|
90
|
+
expect(Object.hasOwn(nestedStartResult, dangerous)).toBe(false);
|
|
91
|
+
|
|
92
|
+
// Nested path with dangerous key in middle — traversal aborts
|
|
93
|
+
const nestedMiddleResult = applyOverrides(
|
|
94
|
+
{ a: { b: "original" } },
|
|
95
|
+
{ [`a.${dangerous}.c`]: "injected" },
|
|
96
|
+
) as Record<string, unknown>;
|
|
97
|
+
const inner = nestedMiddleResult.a as Record<string, unknown>;
|
|
98
|
+
expect(Object.hasOwn(inner, dangerous)).toBe(false);
|
|
99
|
+
|
|
100
|
+
// Nested path with dangerous key at end — final key set is blocked
|
|
101
|
+
const nestedEndResult = applyOverrides(
|
|
102
|
+
{ a: { b: "original" } },
|
|
103
|
+
{ [`a.${dangerous}`]: "injected" },
|
|
104
|
+
) as Record<string, unknown>;
|
|
105
|
+
const innerEnd = nestedEndResult.a as Record<string, unknown>;
|
|
106
|
+
expect(Object.hasOwn(innerEnd, dangerous)).toBe(false);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("applyOverrides — edge cases", () => {
|
|
112
|
+
it("deeply nested path 'a.b.c.d.e' creates intermediate objects", () => {
|
|
113
|
+
const data = { existing: "value" };
|
|
114
|
+
const result = applyOverrides(data, {
|
|
115
|
+
"a.b.c.d.e": "deep",
|
|
116
|
+
}) as Record<string, unknown>;
|
|
117
|
+
|
|
118
|
+
expect(result.existing).toBe("value");
|
|
119
|
+
const a = result.a as Record<string, unknown>;
|
|
120
|
+
const b = a.b as Record<string, unknown>;
|
|
121
|
+
const c = b.c as Record<string, unknown>;
|
|
122
|
+
const d = c.d as Record<string, unknown>;
|
|
123
|
+
expect(d.e).toBe("deep");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("template '{{params.id}}' resolves correctly", () => {
|
|
127
|
+
const data = { userId: "placeholder" };
|
|
128
|
+
const result = applyOverrides(
|
|
129
|
+
data,
|
|
130
|
+
{ userId: "{{params.id}}" },
|
|
131
|
+
{ id: "abc-123" },
|
|
132
|
+
) as Record<string, unknown>;
|
|
133
|
+
|
|
134
|
+
expect(result.userId).toBe("abc-123");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("mixed template 'Hello {{params.name}}!' resolves to string", () => {
|
|
138
|
+
const data = { greeting: "placeholder" };
|
|
139
|
+
const result = applyOverrides(
|
|
140
|
+
data,
|
|
141
|
+
{ greeting: "Hello {{params.name}}!" },
|
|
142
|
+
{ name: "World" },
|
|
143
|
+
) as Record<string, unknown>;
|
|
144
|
+
|
|
145
|
+
expect(result.greeting).toBe("Hello World!");
|
|
146
|
+
expect(typeof result.greeting).toBe("string");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("on non-object data (null) returns data unchanged", () => {
|
|
150
|
+
expect(applyOverrides(null, { key: "value" })).toBe(null);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("on non-object data (string) returns data unchanged", () => {
|
|
154
|
+
expect(applyOverrides("hello", { key: "value" })).toBe("hello");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("on non-object data (number) returns data unchanged", () => {
|
|
158
|
+
expect(applyOverrides(42, { key: "value" })).toBe(42);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("determineArrayCount — edge cases", () => {
|
|
163
|
+
it("with explicitCount=0 returns 0", () => {
|
|
164
|
+
expect(determineArrayCount({}, 0)).toBe(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("with negative explicitCount returns 0", () => {
|
|
168
|
+
expect(determineArrayCount({}, -1)).toBe(0);
|
|
169
|
+
expect(determineArrayCount({}, -100)).toBe(0);
|
|
170
|
+
expect(determineArrayCount({}, Number.NEGATIVE_INFINITY)).toBe(0);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { JSONSchema7 } from "json-schema";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { determineArrayCount } from "./overrides";
|
|
4
|
+
|
|
5
|
+
describe("determineArrayCount", () => {
|
|
6
|
+
it("returns explicit count when provided", () => {
|
|
7
|
+
expect(determineArrayCount({}, 5)).toBe(5);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("returns 0 for negative explicit count", () => {
|
|
11
|
+
expect(determineArrayCount({}, -3)).toBe(0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("returns value within minItems/maxItems range", () => {
|
|
15
|
+
const schema: JSONSchema7 = { minItems: 2, maxItems: 5 };
|
|
16
|
+
for (let i = 0; i < 50; i++) {
|
|
17
|
+
const count = determineArrayCount(schema);
|
|
18
|
+
expect(count).toBeGreaterThanOrEqual(2);
|
|
19
|
+
expect(count).toBeLessThanOrEqual(5);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("handles minItems greater than maxItems without producing garbage", () => {
|
|
24
|
+
const schema: JSONSchema7 = { minItems: 10, maxItems: 3 };
|
|
25
|
+
const count = determineArrayCount(schema);
|
|
26
|
+
|
|
27
|
+
// Should not produce negative numbers or unreasonably large values
|
|
28
|
+
expect(count).toBeGreaterThanOrEqual(0);
|
|
29
|
+
expect(Number.isFinite(count)).toBe(true);
|
|
30
|
+
expect(Number.isNaN(count)).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("handles minItems equal to maxItems", () => {
|
|
34
|
+
const schema: JSONSchema7 = { minItems: 5, maxItems: 5 };
|
|
35
|
+
const count = determineArrayCount(schema);
|
|
36
|
+
expect(count).toBe(5);
|
|
37
|
+
});
|
|
38
|
+
});
|
package/src/overrides.ts
CHANGED
|
@@ -24,10 +24,9 @@ export function determineArrayCount(
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
if (schema.minItems !== undefined && schema.maxItems !== undefined) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
);
|
|
27
|
+
const min = schema.minItems;
|
|
28
|
+
const max = Math.max(min, schema.maxItems);
|
|
29
|
+
return Math.floor(random() * (max - min + 1)) + min;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
if (schema.minItems !== undefined) {
|
|
@@ -52,15 +51,16 @@ export function determineArrayCount(
|
|
|
52
51
|
* @returns Data with overrides applied
|
|
53
52
|
*/
|
|
54
53
|
export function applyOverrides(
|
|
55
|
-
data:
|
|
56
|
-
overrides?: Record<string,
|
|
54
|
+
data: unknown,
|
|
55
|
+
overrides?: Record<string, unknown>,
|
|
57
56
|
params?: Record<string, string>,
|
|
58
|
-
state?:
|
|
57
|
+
state?: Record<string, unknown>,
|
|
59
58
|
query?: Record<string, string>,
|
|
60
|
-
):
|
|
59
|
+
): unknown {
|
|
61
60
|
if (!overrides) return data;
|
|
61
|
+
if (typeof data !== "object" || data === null) return data;
|
|
62
62
|
|
|
63
|
-
const result = structuredClone(data)
|
|
63
|
+
const result = structuredClone(data) as Record<string, unknown>;
|
|
64
64
|
|
|
65
65
|
for (const [key, value] of Object.entries(overrides)) {
|
|
66
66
|
if (DANGEROUS_KEYS.has(key)) continue;
|
|
@@ -74,17 +74,18 @@ export function applyOverrides(
|
|
|
74
74
|
value !== null &&
|
|
75
75
|
!Array.isArray(value)
|
|
76
76
|
) {
|
|
77
|
+
const nested = value as Record<string, unknown>;
|
|
77
78
|
// Recursively apply nested overrides
|
|
78
79
|
if (result[key] && typeof result[key] === "object") {
|
|
79
80
|
result[key] = applyOverrides(
|
|
80
81
|
result[key],
|
|
81
|
-
|
|
82
|
+
nested,
|
|
82
83
|
params,
|
|
83
84
|
state,
|
|
84
85
|
query,
|
|
85
86
|
);
|
|
86
87
|
} else {
|
|
87
|
-
result[key] = applyOverrides({},
|
|
88
|
+
result[key] = applyOverrides({}, nested, params, state, query);
|
|
88
89
|
}
|
|
89
90
|
} else if (typeof value === "string" && value.includes("{{")) {
|
|
90
91
|
// Template processing
|
|
@@ -99,17 +100,13 @@ export function applyOverrides(
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
function setNestedProperty(
|
|
102
|
-
obj:
|
|
103
|
+
obj: Record<string, unknown>,
|
|
103
104
|
path: string,
|
|
104
|
-
value:
|
|
105
|
-
context:
|
|
106
|
-
params?: Record<string, string>;
|
|
107
|
-
state?: any;
|
|
108
|
-
query?: Record<string, string>;
|
|
109
|
-
},
|
|
105
|
+
value: unknown,
|
|
106
|
+
context: TemplateContext,
|
|
110
107
|
): void {
|
|
111
108
|
const parts = path.split(".");
|
|
112
|
-
let current = obj;
|
|
109
|
+
let current: Record<string, unknown> = obj;
|
|
113
110
|
|
|
114
111
|
// Navigate to the parent of the target property
|
|
115
112
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
@@ -122,7 +119,7 @@ function setNestedProperty(
|
|
|
122
119
|
) {
|
|
123
120
|
current[part] = {};
|
|
124
121
|
}
|
|
125
|
-
current = current[part]
|
|
122
|
+
current = current[part] as Record<string, unknown>;
|
|
126
123
|
}
|
|
127
124
|
|
|
128
125
|
// Set the final property
|
|
@@ -135,51 +132,45 @@ function setNestedProperty(
|
|
|
135
132
|
}
|
|
136
133
|
}
|
|
137
134
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
135
|
+
interface TemplateContext {
|
|
136
|
+
params?: Record<string, string>;
|
|
137
|
+
state?: Record<string, unknown>;
|
|
138
|
+
query?: Record<string, string>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveTemplatePath(
|
|
142
|
+
context: TemplateContext,
|
|
143
|
+
expression: string,
|
|
144
|
+
): unknown {
|
|
145
|
+
const parts = expression.trim().split(".");
|
|
146
|
+
let result: unknown = context;
|
|
147
|
+
|
|
148
|
+
for (const part of parts) {
|
|
149
|
+
if (result && typeof result === "object") {
|
|
150
|
+
result = (result as Record<string, unknown>)[part];
|
|
151
|
+
} else {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function processTemplate(template: string, context: TemplateContext): unknown {
|
|
146
160
|
// Check if the template is just a single template expression
|
|
147
161
|
const singleTemplateMatch = template.match(/^\{\{\s*([^}]+)\s*\}\}$/);
|
|
148
162
|
if (singleTemplateMatch) {
|
|
149
163
|
// For single templates, return the actual value without string conversion
|
|
150
|
-
const
|
|
151
|
-
const parts = expression.trim().split(".");
|
|
152
|
-
let result: any = context;
|
|
153
|
-
|
|
154
|
-
for (const part of parts) {
|
|
155
|
-
if (result && typeof result === "object") {
|
|
156
|
-
result = result[part];
|
|
157
|
-
} else {
|
|
158
|
-
return template; // Return original if can't resolve
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
164
|
+
const result = resolveTemplatePath(context, singleTemplateMatch[1]);
|
|
162
165
|
return result !== undefined ? result : template;
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
// For templates mixed with other text, do string replacement
|
|
166
|
-
|
|
169
|
+
return template.replace(
|
|
167
170
|
/\{\{\s*([^}]+)\s*\}\}/g,
|
|
168
|
-
(match, expression) => {
|
|
169
|
-
const
|
|
170
|
-
let result: any = context;
|
|
171
|
-
|
|
172
|
-
for (const part of parts) {
|
|
173
|
-
if (result && typeof result === "object") {
|
|
174
|
-
result = result[part];
|
|
175
|
-
} else {
|
|
176
|
-
return match; // Return original if can't resolve
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
171
|
+
(match, expression: string) => {
|
|
172
|
+
const result = resolveTemplatePath(context, expression);
|
|
180
173
|
return result !== undefined ? String(result) : match;
|
|
181
174
|
},
|
|
182
175
|
);
|
|
183
|
-
|
|
184
|
-
return processed;
|
|
185
176
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { version as packageVersion } from "../package.json";
|
|
2
3
|
import { fakerPlugin } from "./index";
|
|
3
4
|
import { schemas, validators } from "./test-utils";
|
|
4
5
|
|
|
@@ -11,7 +12,7 @@ describe("Schema Plugin Integration", () => {
|
|
|
11
12
|
});
|
|
12
13
|
expect(validPlugin).toBeDefined();
|
|
13
14
|
expect(validPlugin.name).toBe("faker");
|
|
14
|
-
expect(validPlugin.version).toBe(
|
|
15
|
+
expect(validPlugin.version).toBe(packageVersion);
|
|
15
16
|
|
|
16
17
|
// Invalid schema should throw immediately
|
|
17
18
|
expect(() => {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { JSONSchema7 } from "json-schema";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { NULLABLE_NULL_PROBABILITY } from "./constants";
|
|
4
|
+
import { generateFromSchema } from "./index";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tests for postProcessGenerated behavior.
|
|
8
|
+
* Since postProcessGenerated is not exported, we test it indirectly via generateFromSchema.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
describe("postProcessGenerated — schmockNullable", () => {
|
|
12
|
+
it("schema with schmockNullable: true — over 100 runs, some should be null", async () => {
|
|
13
|
+
// We use a schema where the field has schmockNullable set.
|
|
14
|
+
// Since enhanceSchemaWithSmartMapping may add this marker for nullable fields,
|
|
15
|
+
// we test by adding the marker directly through a schema that triggers it.
|
|
16
|
+
// The simplest approach: craft a schema with the x-nullable pattern that
|
|
17
|
+
// enhanceSchemaWithSmartMapping would process, or use a type that gets
|
|
18
|
+
// the schmockNullable marker applied.
|
|
19
|
+
//
|
|
20
|
+
// Since schmockNullable is set by the enhancement layer for nullable fields,
|
|
21
|
+
// we test via a schema with nullable: true (OpenAPI pattern) or anyOf with null.
|
|
22
|
+
// However, the safest approach is to test a field that we know goes through
|
|
23
|
+
// postProcessGenerated with the schmockNullable flag.
|
|
24
|
+
|
|
25
|
+
// Direct approach: construct a schema that includes schmockNullable at the property level
|
|
26
|
+
const schema: JSONSchema7 & Record<string, unknown> = {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
value: {
|
|
30
|
+
type: "string",
|
|
31
|
+
schmockNullable: true,
|
|
32
|
+
} as JSONSchema7 & { schmockNullable: boolean },
|
|
33
|
+
},
|
|
34
|
+
required: ["value"],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let nullCount = 0;
|
|
38
|
+
const runs = 200;
|
|
39
|
+
for (let i = 0; i < runs; i++) {
|
|
40
|
+
const result = (await generateFromSchema({ schema })) as Record<
|
|
41
|
+
string,
|
|
42
|
+
unknown
|
|
43
|
+
>;
|
|
44
|
+
if (result.value === null) {
|
|
45
|
+
nullCount++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// With NULLABLE_NULL_PROBABILITY = 0.05, over 200 runs we expect ~10 nulls.
|
|
50
|
+
// We use a loose check: at least 1 null (very unlikely to get 0 in 200 trials at 5%)
|
|
51
|
+
expect(nullCount).toBeGreaterThan(0);
|
|
52
|
+
// And not all nulls
|
|
53
|
+
expect(nullCount).toBeLessThan(runs);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("postProcessGenerated — schmockTrueProbability", () => {
|
|
58
|
+
it("schema with schmockTrueProbability: 0.8 — over 100 runs, ~80% should be true", async () => {
|
|
59
|
+
const schema: JSONSchema7 & Record<string, unknown> = {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
isActive: {
|
|
63
|
+
type: "boolean",
|
|
64
|
+
schmockTrueProbability: 0.8,
|
|
65
|
+
} as JSONSchema7 & { schmockTrueProbability: number },
|
|
66
|
+
},
|
|
67
|
+
required: ["isActive"],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
let trueCount = 0;
|
|
71
|
+
const runs = 200;
|
|
72
|
+
for (let i = 0; i < runs; i++) {
|
|
73
|
+
const result = (await generateFromSchema({ schema })) as Record<
|
|
74
|
+
string,
|
|
75
|
+
unknown
|
|
76
|
+
>;
|
|
77
|
+
if (result.isActive === true) {
|
|
78
|
+
trueCount++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const trueRate = trueCount / runs;
|
|
83
|
+
// With probability 0.8, expect trueRate to be roughly in [0.65, 0.95]
|
|
84
|
+
expect(trueRate).toBeGreaterThan(0.55);
|
|
85
|
+
expect(trueRate).toBeLessThan(0.95);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("postProcessGenerated — recursive processing", () => {
|
|
90
|
+
it("nested object properties are recursively processed", async () => {
|
|
91
|
+
const schema: JSONSchema7 = {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
outer: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
inner: {
|
|
98
|
+
type: "boolean",
|
|
99
|
+
schmockTrueProbability: 1.0,
|
|
100
|
+
} as JSONSchema7 & { schmockTrueProbability: number },
|
|
101
|
+
},
|
|
102
|
+
required: ["inner"],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
required: ["outer"],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// With schmockTrueProbability = 1.0, every run should yield true
|
|
109
|
+
for (let i = 0; i < 10; i++) {
|
|
110
|
+
const result = (await generateFromSchema({ schema })) as {
|
|
111
|
+
outer: { inner: boolean };
|
|
112
|
+
};
|
|
113
|
+
expect(result.outer.inner).toBe(true);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("array items are recursively processed", async () => {
|
|
118
|
+
const schema: JSONSchema7 = {
|
|
119
|
+
type: "array",
|
|
120
|
+
items: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
flag: {
|
|
124
|
+
type: "boolean",
|
|
125
|
+
schmockTrueProbability: 1.0,
|
|
126
|
+
} as JSONSchema7 & { schmockTrueProbability: number },
|
|
127
|
+
},
|
|
128
|
+
required: ["flag"],
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const result = (await generateFromSchema({ schema, count: 5 })) as Array<{
|
|
133
|
+
flag: boolean;
|
|
134
|
+
}>;
|
|
135
|
+
|
|
136
|
+
expect(Array.isArray(result)).toBe(true);
|
|
137
|
+
expect(result).toHaveLength(5);
|
|
138
|
+
for (const item of result) {
|
|
139
|
+
expect(item.flag).toBe(true);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("non-object data passes through unchanged", async () => {
|
|
144
|
+
// A string schema should pass through postProcessGenerated without issues
|
|
145
|
+
const schema: JSONSchema7 = {
|
|
146
|
+
type: "string",
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const result = await generateFromSchema({ schema });
|
|
150
|
+
expect(typeof result).toBe("string");
|
|
151
|
+
});
|
|
152
|
+
});
|