@rog0x/mcp-testing-tools 1.0.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.
- package/README.md +109 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +250 -0
- package/dist/tools/api-mock.d.ts +30 -0
- package/dist/tools/api-mock.js +193 -0
- package/dist/tools/assertion-helper.d.ts +25 -0
- package/dist/tools/assertion-helper.js +223 -0
- package/dist/tools/mock-data.d.ts +14 -0
- package/dist/tools/mock-data.js +191 -0
- package/dist/tools/test-coverage-analyzer.d.ts +30 -0
- package/dist/tools/test-coverage-analyzer.js +247 -0
- package/dist/tools/test-generator.d.ts +7 -0
- package/dist/tools/test-generator.js +278 -0
- package/package.json +26 -0
- package/src/index.ts +311 -0
- package/src/tools/api-mock.ts +221 -0
- package/src/tools/assertion-helper.ts +273 -0
- package/src/tools/mock-data.ts +241 -0
- package/src/tools/test-coverage-analyzer.ts +283 -0
- package/src/tools/test-generator.ts +300 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Given expected vs actual values, generate detailed assertion code
|
|
3
|
+
* with descriptive messages. Supports deep object comparison,
|
|
4
|
+
* array comparison, and type checking.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
interface AssertionOptions {
|
|
8
|
+
expected: string;
|
|
9
|
+
actual: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
framework?: "jest" | "vitest" | "chai";
|
|
12
|
+
deep?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DiffEntry {
|
|
16
|
+
path: string;
|
|
17
|
+
expected: unknown;
|
|
18
|
+
actual: unknown;
|
|
19
|
+
type: "missing" | "extra" | "type_mismatch" | "value_mismatch";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function safeJsonParse(value: string): { parsed: unknown; isJson: boolean } {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(value);
|
|
25
|
+
return { parsed, isJson: true };
|
|
26
|
+
} catch {
|
|
27
|
+
return { parsed: value, isJson: false };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function typeOf(value: unknown): string {
|
|
32
|
+
if (value === null) return "null";
|
|
33
|
+
if (value === undefined) return "undefined";
|
|
34
|
+
if (Array.isArray(value)) return "array";
|
|
35
|
+
return typeof value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function deepDiff(expected: unknown, actual: unknown, path: string = "$"): DiffEntry[] {
|
|
39
|
+
const diffs: DiffEntry[] = [];
|
|
40
|
+
const expType = typeOf(expected);
|
|
41
|
+
const actType = typeOf(actual);
|
|
42
|
+
|
|
43
|
+
if (expType !== actType) {
|
|
44
|
+
diffs.push({ path, expected, actual, type: "type_mismatch" });
|
|
45
|
+
return diffs;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (expType === "array" && actType === "array") {
|
|
49
|
+
const expArr = expected as unknown[];
|
|
50
|
+
const actArr = actual as unknown[];
|
|
51
|
+
const maxLen = Math.max(expArr.length, actArr.length);
|
|
52
|
+
if (expArr.length !== actArr.length) {
|
|
53
|
+
diffs.push({
|
|
54
|
+
path: `${path}.length`,
|
|
55
|
+
expected: expArr.length,
|
|
56
|
+
actual: actArr.length,
|
|
57
|
+
type: "value_mismatch",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
for (let i = 0; i < maxLen; i++) {
|
|
61
|
+
if (i >= expArr.length) {
|
|
62
|
+
diffs.push({ path: `${path}[${i}]`, expected: undefined, actual: actArr[i], type: "extra" });
|
|
63
|
+
} else if (i >= actArr.length) {
|
|
64
|
+
diffs.push({ path: `${path}[${i}]`, expected: expArr[i], actual: undefined, type: "missing" });
|
|
65
|
+
} else {
|
|
66
|
+
diffs.push(...deepDiff(expArr[i], actArr[i], `${path}[${i}]`));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return diffs;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (expType === "object" && actType === "object" && expected !== null && actual !== null) {
|
|
73
|
+
const expObj = expected as Record<string, unknown>;
|
|
74
|
+
const actObj = actual as Record<string, unknown>;
|
|
75
|
+
const allKeys = new Set([...Object.keys(expObj), ...Object.keys(actObj)]);
|
|
76
|
+
|
|
77
|
+
for (const key of allKeys) {
|
|
78
|
+
const childPath = `${path}.${key}`;
|
|
79
|
+
if (!(key in expObj)) {
|
|
80
|
+
diffs.push({ path: childPath, expected: undefined, actual: actObj[key], type: "extra" });
|
|
81
|
+
} else if (!(key in actObj)) {
|
|
82
|
+
diffs.push({ path: childPath, expected: expObj[key], actual: undefined, type: "missing" });
|
|
83
|
+
} else {
|
|
84
|
+
diffs.push(...deepDiff(expObj[key], actObj[key], childPath));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return diffs;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (expected !== actual) {
|
|
91
|
+
diffs.push({ path, expected, actual, type: "value_mismatch" });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return diffs;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatValue(value: unknown): string {
|
|
98
|
+
if (value === undefined) return "undefined";
|
|
99
|
+
if (value === null) return "null";
|
|
100
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
101
|
+
if (typeof value === "object") return JSON.stringify(value, null, 2);
|
|
102
|
+
return String(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function generateJestAssertions(
|
|
106
|
+
expected: unknown,
|
|
107
|
+
actual: unknown,
|
|
108
|
+
expectedExpr: string,
|
|
109
|
+
actualExpr: string,
|
|
110
|
+
label: string,
|
|
111
|
+
diffs: DiffEntry[],
|
|
112
|
+
deep: boolean
|
|
113
|
+
): string {
|
|
114
|
+
const lines: string[] = [];
|
|
115
|
+
const expType = typeOf(expected);
|
|
116
|
+
const actType = typeOf(actual);
|
|
117
|
+
|
|
118
|
+
lines.push(`// Assertions for: ${label}`);
|
|
119
|
+
lines.push(`const expected = ${formatValue(expected)};`);
|
|
120
|
+
lines.push(`const actual = ${formatValue(actual)};`);
|
|
121
|
+
lines.push("");
|
|
122
|
+
|
|
123
|
+
// Type check
|
|
124
|
+
lines.push(`// Type check`);
|
|
125
|
+
lines.push(`expect(typeof actual).toBe(${JSON.stringify(actType === "array" ? "object" : actType)});`);
|
|
126
|
+
|
|
127
|
+
if (expType === "array" && actType === "array") {
|
|
128
|
+
lines.push(`expect(Array.isArray(actual)).toBe(true);`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (diffs.length === 0) {
|
|
132
|
+
lines.push("");
|
|
133
|
+
lines.push("// Values match");
|
|
134
|
+
if (deep && (expType === "object" || expType === "array")) {
|
|
135
|
+
lines.push(`expect(actual).toEqual(expected);`);
|
|
136
|
+
} else {
|
|
137
|
+
lines.push(`expect(actual).toBe(expected);`);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
lines.push("");
|
|
141
|
+
lines.push(`// ${diffs.length} difference(s) found:`);
|
|
142
|
+
|
|
143
|
+
for (const diff of diffs) {
|
|
144
|
+
lines.push("");
|
|
145
|
+
switch (diff.type) {
|
|
146
|
+
case "type_mismatch":
|
|
147
|
+
lines.push(`// Type mismatch at ${diff.path}`);
|
|
148
|
+
lines.push(`// Expected type: ${typeOf(diff.expected)}, Actual type: ${typeOf(diff.actual)}`);
|
|
149
|
+
lines.push(`expect(typeof ${pathToAccessor(diff.path, "actual")}).toBe(${JSON.stringify(typeOf(diff.expected))});`);
|
|
150
|
+
break;
|
|
151
|
+
case "value_mismatch":
|
|
152
|
+
lines.push(`// Value mismatch at ${diff.path}`);
|
|
153
|
+
lines.push(`expect(${pathToAccessor(diff.path, "actual")}).toBe(${formatValue(diff.expected)});`);
|
|
154
|
+
lines.push(`// Actual value: ${formatValue(diff.actual)}`);
|
|
155
|
+
break;
|
|
156
|
+
case "missing":
|
|
157
|
+
lines.push(`// Missing at ${diff.path}`);
|
|
158
|
+
lines.push(`// Expected: ${formatValue(diff.expected)}`);
|
|
159
|
+
lines.push(`expect(${pathToAccessor(diff.path, "actual")}).toBeDefined();`);
|
|
160
|
+
break;
|
|
161
|
+
case "extra":
|
|
162
|
+
lines.push(`// Unexpected property at ${diff.path}`);
|
|
163
|
+
lines.push(`// Actual value: ${formatValue(diff.actual)}`);
|
|
164
|
+
lines.push(`expect(${pathToAccessor(diff.path, "actual")}).toBeUndefined();`);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
lines.push("");
|
|
170
|
+
lines.push("// Full deep equality (will fail if differences exist)");
|
|
171
|
+
if (deep && (expType === "object" || expType === "array")) {
|
|
172
|
+
lines.push(`expect(actual).toEqual(expected);`);
|
|
173
|
+
} else {
|
|
174
|
+
lines.push(`expect(actual).toBe(expected);`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return lines.join("\n");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function generateChaiAssertions(
|
|
182
|
+
expected: unknown,
|
|
183
|
+
actual: unknown,
|
|
184
|
+
label: string,
|
|
185
|
+
diffs: DiffEntry[],
|
|
186
|
+
deep: boolean
|
|
187
|
+
): string {
|
|
188
|
+
const lines: string[] = [];
|
|
189
|
+
const expType = typeOf(expected);
|
|
190
|
+
|
|
191
|
+
lines.push(`// Assertions for: ${label}`);
|
|
192
|
+
lines.push(`const expected = ${formatValue(expected)};`);
|
|
193
|
+
lines.push(`const actual = ${formatValue(actual)};`);
|
|
194
|
+
lines.push("");
|
|
195
|
+
|
|
196
|
+
if (diffs.length === 0) {
|
|
197
|
+
lines.push("// Values match");
|
|
198
|
+
if (deep && (expType === "object" || expType === "array")) {
|
|
199
|
+
lines.push(`expect(actual).to.deep.equal(expected);`);
|
|
200
|
+
} else {
|
|
201
|
+
lines.push(`expect(actual).to.equal(expected);`);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
lines.push(`// ${diffs.length} difference(s) found:`);
|
|
205
|
+
for (const diff of diffs) {
|
|
206
|
+
lines.push("");
|
|
207
|
+
switch (diff.type) {
|
|
208
|
+
case "type_mismatch":
|
|
209
|
+
lines.push(`// Type mismatch at ${diff.path}`);
|
|
210
|
+
lines.push(`expect(${pathToAccessor(diff.path, "actual")}).to.be.a(${JSON.stringify(typeOf(diff.expected))});`);
|
|
211
|
+
break;
|
|
212
|
+
case "value_mismatch":
|
|
213
|
+
lines.push(`// Value mismatch at ${diff.path}`);
|
|
214
|
+
lines.push(`expect(${pathToAccessor(diff.path, "actual")}).to.equal(${formatValue(diff.expected)});`);
|
|
215
|
+
break;
|
|
216
|
+
case "missing":
|
|
217
|
+
lines.push(`// Missing at ${diff.path}`);
|
|
218
|
+
lines.push(`expect(${pathToAccessor(diff.path, "actual")}).to.exist;`);
|
|
219
|
+
break;
|
|
220
|
+
case "extra":
|
|
221
|
+
lines.push(`// Unexpected at ${diff.path}`);
|
|
222
|
+
lines.push(`expect(${pathToAccessor(diff.path, "actual")}).to.not.exist;`);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
lines.push("");
|
|
227
|
+
lines.push("// Full equality");
|
|
228
|
+
lines.push(`expect(actual).to.deep.equal(expected);`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return lines.join("\n");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function pathToAccessor(path: string, varName: string): string {
|
|
235
|
+
if (path === "$") return varName;
|
|
236
|
+
// Convert $.foo[0].bar to varName.foo[0].bar
|
|
237
|
+
return varName + path.slice(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function generateAssertions(options: AssertionOptions): {
|
|
241
|
+
code: string;
|
|
242
|
+
diffs: DiffEntry[];
|
|
243
|
+
summary: string;
|
|
244
|
+
} {
|
|
245
|
+
const {
|
|
246
|
+
expected: expectedStr,
|
|
247
|
+
actual: actualStr,
|
|
248
|
+
label = "value comparison",
|
|
249
|
+
framework = "jest",
|
|
250
|
+
deep = true,
|
|
251
|
+
} = options;
|
|
252
|
+
|
|
253
|
+
const { parsed: expected } = safeJsonParse(expectedStr);
|
|
254
|
+
const { parsed: actual } = safeJsonParse(actualStr);
|
|
255
|
+
|
|
256
|
+
const diffs = deepDiff(expected, actual);
|
|
257
|
+
|
|
258
|
+
let code: string;
|
|
259
|
+
|
|
260
|
+
if (framework === "chai") {
|
|
261
|
+
code = generateChaiAssertions(expected, actual, label, diffs, deep);
|
|
262
|
+
} else {
|
|
263
|
+
// jest and vitest use the same API
|
|
264
|
+
code = generateJestAssertions(expected, actual, "expected", "actual", label, diffs, deep);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const summary =
|
|
268
|
+
diffs.length === 0
|
|
269
|
+
? "Values are identical. All assertions will pass."
|
|
270
|
+
: `Found ${diffs.length} difference(s): ${diffs.map((d) => `${d.type} at ${d.path}`).join(", ")}`;
|
|
271
|
+
|
|
272
|
+
return { code, diffs, summary };
|
|
273
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate realistic mock data: names, emails, addresses, dates, UUIDs,
|
|
3
|
+
* phone numbers, company names, credit card numbers (fake), IP addresses.
|
|
4
|
+
* Configurable count and locale.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
interface MockDataOptions {
|
|
8
|
+
type: string;
|
|
9
|
+
count: number;
|
|
10
|
+
locale: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Seeded pseudo-random for deterministic output within a call
|
|
14
|
+
class SeededRandom {
|
|
15
|
+
private state: number;
|
|
16
|
+
constructor(seed: number) {
|
|
17
|
+
this.state = seed;
|
|
18
|
+
}
|
|
19
|
+
next(): number {
|
|
20
|
+
this.state = (this.state * 1664525 + 1013904223) & 0x7fffffff;
|
|
21
|
+
return this.state / 0x7fffffff;
|
|
22
|
+
}
|
|
23
|
+
int(min: number, max: number): number {
|
|
24
|
+
return Math.floor(this.next() * (max - min + 1)) + min;
|
|
25
|
+
}
|
|
26
|
+
pick<T>(arr: readonly T[]): T {
|
|
27
|
+
return arr[this.int(0, arr.length - 1)];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const FIRST_NAMES_EN = [
|
|
32
|
+
"James", "Mary", "Robert", "Patricia", "John", "Jennifer", "Michael", "Linda",
|
|
33
|
+
"David", "Elizabeth", "William", "Barbara", "Richard", "Susan", "Joseph", "Jessica",
|
|
34
|
+
"Thomas", "Sarah", "Christopher", "Karen", "Charles", "Lisa", "Daniel", "Nancy",
|
|
35
|
+
"Matthew", "Betty", "Anthony", "Margaret", "Mark", "Sandra", "Steven", "Ashley",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
const FIRST_NAMES_ES = [
|
|
39
|
+
"Carlos", "Maria", "Jose", "Ana", "Luis", "Carmen", "Miguel", "Laura",
|
|
40
|
+
"Juan", "Isabel", "Pedro", "Rosa", "Diego", "Elena", "Pablo", "Sofia",
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
const LAST_NAMES_EN = [
|
|
44
|
+
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis",
|
|
45
|
+
"Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson",
|
|
46
|
+
"Thomas", "Taylor", "Moore", "Jackson", "Martin", "Lee", "Thompson", "White",
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
const LAST_NAMES_ES = [
|
|
50
|
+
"Garcia", "Rodriguez", "Martinez", "Lopez", "Gonzalez", "Hernandez", "Perez",
|
|
51
|
+
"Sanchez", "Ramirez", "Torres", "Flores", "Rivera", "Gomez", "Diaz",
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
const STREET_NAMES = [
|
|
55
|
+
"Main St", "Oak Ave", "Cedar Ln", "Elm St", "Pine Rd", "Maple Dr",
|
|
56
|
+
"Washington Blvd", "Park Ave", "Lake Dr", "River Rd", "Hill St", "Forest Way",
|
|
57
|
+
"Sunset Blvd", "Highland Ave", "Valley Rd", "Spring St", "Meadow Ln",
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
60
|
+
const CITIES_EN = [
|
|
61
|
+
"New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "Philadelphia",
|
|
62
|
+
"San Antonio", "San Diego", "Dallas", "Austin", "Portland", "Denver",
|
|
63
|
+
"Seattle", "Boston", "Nashville", "Atlanta", "Miami", "Minneapolis",
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
66
|
+
const CITIES_ES = [
|
|
67
|
+
"Madrid", "Barcelona", "Valencia", "Sevilla", "Bilbao", "Malaga",
|
|
68
|
+
"Zaragoza", "Alicante", "Cordoba", "Granada", "Murcia", "Palma",
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
const STATES = ["CA", "TX", "NY", "FL", "IL", "PA", "OH", "GA", "NC", "MI", "WA", "OR", "CO", "AZ"] as const;
|
|
72
|
+
|
|
73
|
+
const DOMAINS = ["example.com", "testmail.org", "mockdata.net", "sampledomain.io", "fakeinbox.dev"] as const;
|
|
74
|
+
|
|
75
|
+
const COMPANY_SUFFIXES = ["Inc", "LLC", "Corp", "Group", "Solutions", "Technologies", "Systems", "Labs", "Ventures", "Partners"] as const;
|
|
76
|
+
|
|
77
|
+
const COMPANY_WORDS = [
|
|
78
|
+
"Apex", "Horizon", "Vertex", "Pinnacle", "Atlas", "Nova", "Stellar",
|
|
79
|
+
"Crimson", "Azure", "Cobalt", "Onyx", "Prism", "Quantum", "Vector",
|
|
80
|
+
"Zenith", "Fusion", "Nexus", "Summit", "Cipher", "Vanguard",
|
|
81
|
+
] as const;
|
|
82
|
+
|
|
83
|
+
const TLD_BY_LOCALE: Record<string, string[]> = {
|
|
84
|
+
en: [".com", ".org", ".net", ".io"],
|
|
85
|
+
es: [".es", ".com", ".org"],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function getFirstNames(locale: string): readonly string[] {
|
|
89
|
+
return locale === "es" ? FIRST_NAMES_ES : FIRST_NAMES_EN;
|
|
90
|
+
}
|
|
91
|
+
function getLastNames(locale: string): readonly string[] {
|
|
92
|
+
return locale === "es" ? LAST_NAMES_ES : LAST_NAMES_EN;
|
|
93
|
+
}
|
|
94
|
+
function getCities(locale: string): readonly string[] {
|
|
95
|
+
return locale === "es" ? CITIES_ES : CITIES_EN;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function generateName(rng: SeededRandom, locale: string): string {
|
|
99
|
+
return `${rng.pick(getFirstNames(locale))} ${rng.pick(getLastNames(locale))}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function generateEmail(rng: SeededRandom, locale: string): string {
|
|
103
|
+
const first = rng.pick(getFirstNames(locale)).toLowerCase();
|
|
104
|
+
const last = rng.pick(getLastNames(locale)).toLowerCase();
|
|
105
|
+
const sep = rng.pick([".", "_", ""]);
|
|
106
|
+
const num = rng.int(1, 999);
|
|
107
|
+
const domain = rng.pick(DOMAINS);
|
|
108
|
+
return `${first}${sep}${last}${num}@${domain}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function generateAddress(rng: SeededRandom, locale: string): string {
|
|
112
|
+
const num = rng.int(100, 9999);
|
|
113
|
+
const street = rng.pick(STREET_NAMES);
|
|
114
|
+
const city = rng.pick(getCities(locale));
|
|
115
|
+
const state = rng.pick(STATES);
|
|
116
|
+
const zip = String(rng.int(10000, 99999));
|
|
117
|
+
return `${num} ${street}, ${city}, ${state} ${zip}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function generateDate(rng: SeededRandom): string {
|
|
121
|
+
const year = rng.int(1970, 2030);
|
|
122
|
+
const month = String(rng.int(1, 12)).padStart(2, "0");
|
|
123
|
+
const day = String(rng.int(1, 28)).padStart(2, "0");
|
|
124
|
+
return `${year}-${month}-${day}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function generateUUID(rng: SeededRandom): string {
|
|
128
|
+
const hex = () => rng.int(0, 15).toString(16);
|
|
129
|
+
const seg = (len: number) => Array.from({ length: len }, hex).join("");
|
|
130
|
+
return `${seg(8)}-${seg(4)}-4${seg(3)}-${rng.pick(["8", "9", "a", "b"])}${seg(3)}-${seg(12)}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function generatePhone(rng: SeededRandom, locale: string): string {
|
|
134
|
+
if (locale === "es") {
|
|
135
|
+
return `+34 ${rng.int(600, 699)} ${rng.int(100, 999)} ${rng.int(100, 999)}`;
|
|
136
|
+
}
|
|
137
|
+
const area = rng.int(200, 999);
|
|
138
|
+
const mid = rng.int(200, 999);
|
|
139
|
+
const last = rng.int(1000, 9999);
|
|
140
|
+
return `+1 (${area}) ${mid}-${last}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function generateCompanyName(rng: SeededRandom): string {
|
|
144
|
+
const numWords = rng.int(1, 2);
|
|
145
|
+
const words = Array.from({ length: numWords }, () => rng.pick(COMPANY_WORDS));
|
|
146
|
+
return `${words.join(" ")} ${rng.pick(COMPANY_SUFFIXES)}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function generateCreditCard(rng: SeededRandom): string {
|
|
150
|
+
// Fake Visa-like number starting with 4, fails Luhn check intentionally
|
|
151
|
+
const groups = [
|
|
152
|
+
`4${rng.int(100, 999)}`,
|
|
153
|
+
String(rng.int(1000, 9999)),
|
|
154
|
+
String(rng.int(1000, 9999)),
|
|
155
|
+
String(rng.int(1000, 9999)),
|
|
156
|
+
];
|
|
157
|
+
return groups.join(" ");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function generateIP(rng: SeededRandom): string {
|
|
161
|
+
return `${rng.int(1, 254)}.${rng.int(0, 255)}.${rng.int(0, 255)}.${rng.int(1, 254)}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
type GeneratorFn = (rng: SeededRandom, locale: string) => string;
|
|
165
|
+
|
|
166
|
+
const GENERATORS: Record<string, GeneratorFn> = {
|
|
167
|
+
name: generateName,
|
|
168
|
+
email: generateEmail,
|
|
169
|
+
address: generateAddress,
|
|
170
|
+
date: generateDate,
|
|
171
|
+
uuid: (rng) => generateUUID(rng),
|
|
172
|
+
phone: generatePhone,
|
|
173
|
+
company: (rng) => generateCompanyName(rng),
|
|
174
|
+
credit_card: (rng) => generateCreditCard(rng),
|
|
175
|
+
ip: (rng) => generateIP(rng),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const SUPPORTED_TYPES = Object.keys(GENERATORS);
|
|
179
|
+
|
|
180
|
+
export function generateMockData(type: string, count: number = 10, locale: string = "en"): {
|
|
181
|
+
type: string;
|
|
182
|
+
locale: string;
|
|
183
|
+
count: number;
|
|
184
|
+
data: string[];
|
|
185
|
+
supportedTypes: string[];
|
|
186
|
+
} {
|
|
187
|
+
const normalizedType = type.toLowerCase().replace(/[\s-]/g, "_");
|
|
188
|
+
|
|
189
|
+
if (!GENERATORS[normalizedType]) {
|
|
190
|
+
return {
|
|
191
|
+
type: normalizedType,
|
|
192
|
+
locale,
|
|
193
|
+
count: 0,
|
|
194
|
+
data: [],
|
|
195
|
+
supportedTypes: SUPPORTED_TYPES,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const safeCount = Math.min(Math.max(1, count), 1000);
|
|
200
|
+
const rng = new SeededRandom(Date.now() % 100000);
|
|
201
|
+
const generator = GENERATORS[normalizedType];
|
|
202
|
+
const data: string[] = [];
|
|
203
|
+
|
|
204
|
+
for (let i = 0; i < safeCount; i++) {
|
|
205
|
+
data.push(generator(rng, locale));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
type: normalizedType,
|
|
210
|
+
locale,
|
|
211
|
+
count: safeCount,
|
|
212
|
+
data,
|
|
213
|
+
supportedTypes: SUPPORTED_TYPES,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function generateMixedMockData(
|
|
218
|
+
types: string[],
|
|
219
|
+
count: number = 5,
|
|
220
|
+
locale: string = "en"
|
|
221
|
+
): Record<string, string>[] {
|
|
222
|
+
const safeCount = Math.min(Math.max(1, count), 1000);
|
|
223
|
+
const rng = new SeededRandom(Date.now() % 100000);
|
|
224
|
+
const validTypes = types
|
|
225
|
+
.map((t) => t.toLowerCase().replace(/[\s-]/g, "_"))
|
|
226
|
+
.filter((t) => GENERATORS[t]);
|
|
227
|
+
|
|
228
|
+
if (validTypes.length === 0) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const records: Record<string, string>[] = [];
|
|
233
|
+
for (let i = 0; i < safeCount; i++) {
|
|
234
|
+
const record: Record<string, string> = {};
|
|
235
|
+
for (const t of validTypes) {
|
|
236
|
+
record[t] = GENERATORS[t](rng, locale);
|
|
237
|
+
}
|
|
238
|
+
records.push(record);
|
|
239
|
+
}
|
|
240
|
+
return records;
|
|
241
|
+
}
|