@schmock/faker 1.4.0

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,160 @@
1
+ import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
+ import { expect } from "vitest";
3
+ import { generateFromSchema, fakerPlugin } from "../index";
4
+
5
+ const feature = await loadFeature("../../features/faker-plugin.feature");
6
+
7
+ describeFeature(feature, ({ Scenario }) => {
8
+ let generated: any;
9
+ let error: Error | null = null;
10
+
11
+ Scenario("Generate object from simple schema", ({ Given, When, Then, And }) => {
12
+ let schema: any;
13
+
14
+ Given("I create a schema plugin with:", (_, docString: string) => {
15
+ schema = JSON.parse(docString);
16
+ });
17
+
18
+ When("I generate data from the schema", () => {
19
+ generated = generateFromSchema({ schema });
20
+ });
21
+
22
+ Then("the generated data should have property {string} of type {string}", (_, prop: string, type: string) => {
23
+ expect(generated).toHaveProperty(prop);
24
+ expect(typeof generated[prop]).toBe(type);
25
+ });
26
+
27
+ And("the generated data should have property {string} of type {string}", (_, prop: string, type: string) => {
28
+ expect(generated).toHaveProperty(prop);
29
+ expect(typeof generated[prop]).toBe(type);
30
+ });
31
+ });
32
+
33
+ Scenario("Generate array of items with explicit count", ({ Given, When, Then }) => {
34
+ let schema: any;
35
+ let count: number;
36
+
37
+ Given("I create a schema plugin for array with count {int}:", (_, cnt: number, docString: string) => {
38
+ schema = JSON.parse(docString);
39
+ count = cnt;
40
+ });
41
+
42
+ When("I generate data from the schema", () => {
43
+ generated = generateFromSchema({ schema, count });
44
+ });
45
+
46
+ Then("the generated data should be an array of length {int}", (_, length: number) => {
47
+ expect(Array.isArray(generated)).toBe(true);
48
+ expect(generated).toHaveLength(length);
49
+ });
50
+ });
51
+
52
+ Scenario("Template preserves string values for mixed templates", ({ Given, When, Then }) => {
53
+ let template: string;
54
+ let result: any;
55
+
56
+ Given("I create a schema plugin with template override {string}", (_, tmpl: string) => {
57
+ template = tmpl;
58
+ });
59
+
60
+ When("I generate data with param {string} set to {string}", (_, paramName: string, paramValue: string) => {
61
+ const schema = {
62
+ type: "object" as const,
63
+ properties: {
64
+ value: { type: "string" as const },
65
+ },
66
+ };
67
+ result = generateFromSchema({
68
+ schema,
69
+ overrides: { value: template },
70
+ params: { [paramName]: paramValue },
71
+ });
72
+ });
73
+
74
+ Then("the template result should be the string {string}", (_, expected: string) => {
75
+ expect(result.value).toBe(expected);
76
+ expect(typeof result.value).toBe("string");
77
+ });
78
+ });
79
+
80
+ Scenario("Multiple schema plugin instances do not share state", ({ Given, When, Then }) => {
81
+ let plugin1: any;
82
+ let plugin2: any;
83
+ let data1: any;
84
+ let data2: any;
85
+
86
+ Given("I create two separate schema plugin instances", () => {
87
+ const schema = {
88
+ type: "object" as const,
89
+ properties: {
90
+ name: { type: "string" as const },
91
+ },
92
+ required: ["name"],
93
+ };
94
+ plugin1 = fakerPlugin({ schema });
95
+ plugin2 = fakerPlugin({ schema });
96
+ });
97
+
98
+ When("I generate data from both instances", async () => {
99
+ const ctx = {
100
+ path: "/test",
101
+ route: {},
102
+ method: "GET" as const,
103
+ params: {},
104
+ query: {},
105
+ headers: {},
106
+ state: new Map(),
107
+ };
108
+ const result1 = await plugin1.process(ctx);
109
+ const result2 = await plugin2.process(ctx);
110
+ data1 = result1.response;
111
+ data2 = result2.response;
112
+ });
113
+
114
+ Then("the data from each instance should be independently generated", () => {
115
+ expect(data1).toBeDefined();
116
+ expect(data2).toBeDefined();
117
+ expect(typeof data1.name).toBe("string");
118
+ expect(typeof data2.name).toBe("string");
119
+ });
120
+ });
121
+
122
+ Scenario("Invalid schema is rejected at plugin creation time", ({ Given, Then }) => {
123
+ Given("I attempt to create a schema plugin with invalid schema", () => {
124
+ error = null;
125
+ try {
126
+ fakerPlugin({ schema: {} as any });
127
+ } catch (e) {
128
+ error = e as Error;
129
+ }
130
+ });
131
+
132
+ Then("it should throw a SchemaValidationError", () => {
133
+ expect(error).not.toBeNull();
134
+ expect(error!.name).toBe("SchemaValidationError");
135
+ });
136
+ });
137
+
138
+ Scenario("Faker method validation checks actual method existence", ({ Given, Then }) => {
139
+ Given("I attempt to create a schema with faker method {string}", (_, fakerMethod: string) => {
140
+ error = null;
141
+ try {
142
+ const schema = {
143
+ type: "object" as const,
144
+ properties: {
145
+ field: { type: "string" as const, faker: fakerMethod },
146
+ },
147
+ };
148
+ fakerPlugin({ schema: schema as any });
149
+ } catch (e) {
150
+ error = e as Error;
151
+ }
152
+ });
153
+
154
+ Then("it should throw a SchemaValidationError with message {string}", (_, message: string) => {
155
+ expect(error).not.toBeNull();
156
+ expect(error!.name).toBe("SchemaValidationError");
157
+ expect(error!.message).toContain(message);
158
+ });
159
+ });
160
+ });
@@ -0,0 +1,345 @@
1
+ import type { JSONSchema7 } from "json-schema";
2
+ import { expect } from "vitest";
3
+ import { generateFromSchema } from "./index";
4
+
5
+ interface FakerSchema extends JSONSchema7 {
6
+ faker?: string;
7
+ }
8
+
9
+ // Schema Factory Functions
10
+ export const schemas = {
11
+ simple: {
12
+ string: (): JSONSchema7 => ({
13
+ type: "string",
14
+ }),
15
+
16
+ number: (): JSONSchema7 => ({
17
+ type: "number",
18
+ }),
19
+
20
+ object: (properties: Record<string, JSONSchema7> = {}): JSONSchema7 => ({
21
+ type: "object",
22
+ properties,
23
+ }),
24
+
25
+ array: (
26
+ items: JSONSchema7,
27
+ constraints?: { minItems?: number; maxItems?: number },
28
+ ): JSONSchema7 => ({
29
+ type: "array",
30
+ items,
31
+ ...constraints,
32
+ }),
33
+ },
34
+
35
+ withFaker: (type: JSONSchema7["type"], fakerMethod: string): FakerSchema => ({
36
+ type,
37
+ faker: fakerMethod,
38
+ }),
39
+
40
+ nested: {
41
+ deep: (
42
+ depth: number,
43
+ leafSchema: JSONSchema7 = schemas.simple.string(),
44
+ ): JSONSchema7 => {
45
+ if (depth <= 0) return leafSchema;
46
+ return {
47
+ type: "object",
48
+ properties: {
49
+ nested: schemas.nested.deep(depth - 1, leafSchema),
50
+ },
51
+ };
52
+ },
53
+
54
+ wide: (
55
+ width: number,
56
+ propertySchema: JSONSchema7 = schemas.simple.string(),
57
+ ): JSONSchema7 => ({
58
+ type: "object",
59
+ properties: Object.fromEntries(
60
+ Array.from({ length: width }, (_, i) => [`prop${i}`, propertySchema]),
61
+ ),
62
+ }),
63
+ },
64
+
65
+ complex: {
66
+ user: (): JSONSchema7 => ({
67
+ type: "object",
68
+ properties: {
69
+ id: { type: "string", format: "uuid" },
70
+ email: { type: "string" },
71
+ firstName: { type: "string" },
72
+ lastName: { type: "string" },
73
+ createdAt: { type: "string" },
74
+ },
75
+ required: ["id", "email"],
76
+ }),
77
+
78
+ apiResponse: (): JSONSchema7 => ({
79
+ type: "object",
80
+ properties: {
81
+ success: { type: "boolean" },
82
+ data: {
83
+ type: "array",
84
+ items: schemas.complex.user(),
85
+ },
86
+ meta: {
87
+ type: "object",
88
+ properties: {
89
+ page: { type: "number" },
90
+ total: { type: "number" },
91
+ },
92
+ },
93
+ },
94
+ }),
95
+ },
96
+ };
97
+
98
+ // Validation Helpers
99
+ export const validators = {
100
+ // Check if a field was mapped to a faker method by comparing with unmapped behavior
101
+ isFieldMapped: async (
102
+ fieldName: string,
103
+ fieldType: JSONSchema7["type"] = "string",
104
+ ): Promise<boolean> => {
105
+ const mappedSchema: JSONSchema7 = {
106
+ type: "object",
107
+ properties: {
108
+ [fieldName]: { type: fieldType },
109
+ },
110
+ };
111
+
112
+ const unmappedSchema: JSONSchema7 = {
113
+ type: "object",
114
+ properties: {
115
+ unmappedRandomField12345: { type: fieldType },
116
+ },
117
+ };
118
+
119
+ // Generate multiple samples to check for patterns
120
+ const mappedSamples = Array.from(
121
+ { length: 10 },
122
+ () => generateFromSchema({ schema: mappedSchema })[fieldName],
123
+ );
124
+
125
+ const unmappedSamples = Array.from(
126
+ { length: 10 },
127
+ () =>
128
+ generateFromSchema({ schema: unmappedSchema }).unmappedRandomField12345,
129
+ );
130
+
131
+ // If field is mapped to a specific faker method, it should have different characteristics
132
+ // than the generic unmapped field
133
+ return (
134
+ analyzeDataCharacteristics(mappedSamples) !==
135
+ analyzeDataCharacteristics(unmappedSamples)
136
+ );
137
+ },
138
+
139
+ // Analyze uniqueness of generated data
140
+ uniquenessRatio: (samples: any[]): number => {
141
+ const unique = new Set(samples);
142
+ return unique.size / samples.length;
143
+ },
144
+
145
+ // Check if all samples match a basic pattern without being too specific
146
+ allMatch: (samples: any[], validator: (sample: any) => boolean): boolean => {
147
+ return samples.every(validator);
148
+ },
149
+
150
+ // Check if data appears to be from a specific faker category
151
+ appearsToBeFromCategory: (
152
+ samples: string[],
153
+ category: "email" | "name" | "phone" | "address" | "uuid" | "date",
154
+ ): boolean => {
155
+ switch (category) {
156
+ case "email":
157
+ return validators.allMatch(
158
+ samples,
159
+ (s) => typeof s === "string" && s.includes("@") && s.includes("."),
160
+ );
161
+ case "name":
162
+ return validators.allMatch(
163
+ samples,
164
+ (s) =>
165
+ typeof s === "string" &&
166
+ s.length > 1 &&
167
+ s.length < 50 &&
168
+ /^[A-Z]/.test(s),
169
+ );
170
+ case "phone":
171
+ return validators.allMatch(
172
+ samples,
173
+ (s) => typeof s === "string" && /\d/.test(s) && s.length > 10,
174
+ );
175
+ case "address":
176
+ return validators.allMatch(
177
+ samples,
178
+ (s) =>
179
+ typeof s === "string" &&
180
+ s.length > 10 &&
181
+ /\d/.test(s) &&
182
+ /[A-Z]/.test(s),
183
+ );
184
+ case "uuid":
185
+ return validators.allMatch(
186
+ samples,
187
+ (s) =>
188
+ typeof s === "string" &&
189
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
190
+ s,
191
+ ),
192
+ );
193
+ case "date":
194
+ return validators.allMatch(
195
+ samples,
196
+ (s) => typeof s === "string" && !Number.isNaN(Date.parse(s)),
197
+ );
198
+ default:
199
+ return false;
200
+ }
201
+ },
202
+ };
203
+
204
+ // Performance Testing Utilities
205
+ export const performance = {
206
+ measure: async <T>(
207
+ fn: () => T | Promise<T>,
208
+ ): Promise<{ result: T; duration: number }> => {
209
+ const start = Date.now();
210
+ const result = await fn();
211
+ const duration = Date.now() - start;
212
+ return { result, duration };
213
+ },
214
+
215
+ measureMemory: (fn: () => void): number => {
216
+ if (globalThis.gc) {
217
+ globalThis.gc();
218
+ }
219
+ const before = process.memoryUsage().heapUsed;
220
+ fn();
221
+ const after = process.memoryUsage().heapUsed;
222
+ return after - before;
223
+ },
224
+
225
+ benchmark: async (
226
+ _name: string,
227
+ fn: () => any,
228
+ iterations = 100,
229
+ ): Promise<{ mean: number; min: number; max: number }> => {
230
+ const times: number[] = [];
231
+
232
+ for (let i = 0; i < iterations; i++) {
233
+ const start = Date.now();
234
+ await fn();
235
+ const duration = Date.now() - start;
236
+ times.push(duration);
237
+ }
238
+
239
+ return {
240
+ mean: times.reduce((a, b) => a + b, 0) / times.length,
241
+ min: Math.min(...times),
242
+ max: Math.max(...times),
243
+ };
244
+ },
245
+ };
246
+
247
+ // Test Data Generators
248
+ export const generate = {
249
+ samples: <T>(schema: JSONSchema7, count = 10, options?: any): T[] => {
250
+ return Array.from({ length: count }, () =>
251
+ generateFromSchema({ schema, ...options }),
252
+ );
253
+ },
254
+
255
+ withSeed: (schema: JSONSchema7, _seed?: number): any => {
256
+ // Note: faker.js doesn't support seeding in the same way,
257
+ // but we can at least ensure consistent test behavior
258
+ return generateFromSchema({ schema });
259
+ },
260
+ };
261
+
262
+ // Statistical Analysis
263
+ export const stats = {
264
+ distribution: (samples: any[]): Map<any, number> => {
265
+ const dist = new Map<any, number>();
266
+ for (const sample of samples) {
267
+ const key = JSON.stringify(sample);
268
+ dist.set(key, (dist.get(key) || 0) + 1);
269
+ }
270
+ return dist;
271
+ },
272
+
273
+ entropy: (samples: any[]): number => {
274
+ const dist = stats.distribution(samples);
275
+ const total = samples.length;
276
+ let entropy = 0;
277
+
278
+ for (const count of dist.values()) {
279
+ const p = count / total;
280
+ if (p > 0) {
281
+ entropy -= p * Math.log2(p);
282
+ }
283
+ }
284
+
285
+ return entropy;
286
+ },
287
+ };
288
+
289
+ // Schema Validation Test Helpers
290
+ export const schemaTests = {
291
+ expectValid: (schema: JSONSchema7): void => {
292
+ expect(() => generateFromSchema({ schema })).not.toThrow();
293
+ },
294
+
295
+ expectInvalid: (schema: any, errorMessage?: string | RegExp): void => {
296
+ if (errorMessage) {
297
+ expect(() => generateFromSchema({ schema })).toThrow(errorMessage);
298
+ } else {
299
+ expect(() => generateFromSchema({ schema })).toThrow();
300
+ }
301
+ },
302
+
303
+ expectSchemaError: (schema: any, path: string, issue?: string): void => {
304
+ try {
305
+ generateFromSchema({ schema });
306
+ throw new Error("Expected schema validation to fail");
307
+ } catch (error: any) {
308
+ expect(error.name).toBe("SchemaValidationError");
309
+ // The schemaPath is in the context
310
+ if (error.context?.schemaPath) {
311
+ expect(error.context.schemaPath).toBe(path);
312
+ }
313
+ if (issue) {
314
+ expect(error.message).toContain(issue);
315
+ }
316
+ }
317
+ },
318
+ };
319
+
320
+ // Helper to analyze data characteristics without hardcoding patterns
321
+ function analyzeDataCharacteristics(samples: any[]): string {
322
+ if (samples.length === 0) return "empty";
323
+
324
+ const first = samples[0];
325
+ const type = typeof first;
326
+
327
+ if (type !== "string") return type;
328
+
329
+ // Analyze string characteristics
330
+ const characteristics: string[] = [type];
331
+
332
+ // Check common patterns without being too specific
333
+ if (samples.every((s) => s.includes("@"))) characteristics.push("has-at");
334
+ if (samples.every((s) => /^\d+$/.test(s))) characteristics.push("numeric");
335
+ if (samples.every((s) => /^[0-9a-f-]+$/i.test(s)))
336
+ characteristics.push("hex-like");
337
+ if (samples.every((s) => s.length > 50)) characteristics.push("long");
338
+ if (samples.every((s) => s.length < 10)) characteristics.push("short");
339
+ if (validators.uniquenessRatio(samples) > 0.8)
340
+ characteristics.push("high-entropy");
341
+ if (validators.uniquenessRatio(samples) < 0.2)
342
+ characteristics.push("low-entropy");
343
+
344
+ return characteristics.join("-");
345
+ }