@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.
@@ -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
- return (
28
- Math.floor(random() * (schema.maxItems - schema.minItems + 1)) +
29
- schema.minItems
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: any,
56
- overrides?: Record<string, any>,
54
+ data: unknown,
55
+ overrides?: Record<string, unknown>,
57
56
  params?: Record<string, string>,
58
- state?: any,
57
+ state?: Record<string, unknown>,
59
58
  query?: Record<string, string>,
60
- ): any {
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
- value,
82
+ nested,
82
83
  params,
83
84
  state,
84
85
  query,
85
86
  );
86
87
  } else {
87
- result[key] = applyOverrides({}, value, params, state, query);
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: any,
103
+ obj: Record<string, unknown>,
103
104
  path: string,
104
- value: any,
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
- function processTemplate(
139
- template: string,
140
- context: {
141
- params?: Record<string, string>;
142
- state?: any;
143
- query?: Record<string, string>;
144
- },
145
- ): any {
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 expression = singleTemplateMatch[1];
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
- const processed = template.replace(
169
+ return template.replace(
167
170
  /\{\{\s*([^}]+)\s*\}\}/g,
168
- (match, expression) => {
169
- const parts = expression.trim().split(".");
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("1.0.1");
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
+ });