@nestia/e2e 7.3.3 → 8.0.0-dev.20250829
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/LICENSE +21 -21
- package/README.md +93 -93
- package/lib/ArrayUtil.d.ts +47 -43
- package/lib/ArrayUtil.js +122 -134
- package/lib/ArrayUtil.js.map +1 -1
- package/lib/GaffComparator.d.ts +19 -12
- package/lib/GaffComparator.js +19 -12
- package/lib/GaffComparator.js.map +1 -1
- package/lib/MapUtil.d.ts +79 -0
- package/lib/MapUtil.js +92 -0
- package/lib/MapUtil.js.map +1 -0
- package/lib/RandomGenerator.d.ts +107 -71
- package/lib/RandomGenerator.js +124 -109
- package/lib/RandomGenerator.js.map +1 -1
- package/lib/TestValidator.d.ts +107 -145
- package/lib/TestValidator.js +308 -351
- package/lib/TestValidator.js.map +1 -1
- package/lib/module.d.ts +2 -1
- package/lib/module.js +2 -1
- package/lib/module.js.map +1 -1
- package/package.json +1 -1
- package/src/ArrayUtil.ts +87 -88
- package/src/GaffComparator.ts +19 -12
- package/src/MapUtil.ts +86 -0
- package/src/RandomGenerator.ts +138 -101
- package/src/TestValidator.ts +251 -294
- package/src/module.ts +3 -1
package/lib/TestValidator.js
CHANGED
|
@@ -84,21 +84,21 @@ var json_equal_to_1 = require("./internal/json_equal_to");
|
|
|
84
84
|
* HTTP error validation, pagination testing, search functionality validation,
|
|
85
85
|
* and sorting validation.
|
|
86
86
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* assertions.
|
|
87
|
+
* Most functions use direct parameter passing for simplicity, while some
|
|
88
|
+
* maintain currying patterns for advanced composition. All provide detailed
|
|
89
|
+
* error messages for debugging failed assertions.
|
|
90
90
|
*
|
|
91
91
|
* @author Jeongho Nam - https://github.com/samchon
|
|
92
92
|
* @example
|
|
93
93
|
* ```typescript
|
|
94
94
|
* // Basic condition testing
|
|
95
|
-
* TestValidator.predicate("user should be authenticated"
|
|
95
|
+
* TestValidator.predicate("user should be authenticated", user.isAuthenticated);
|
|
96
96
|
*
|
|
97
97
|
* // Equality validation
|
|
98
|
-
* TestValidator.equals("API response should match expected")
|
|
98
|
+
* TestValidator.equals("API response should match expected", x, y);
|
|
99
99
|
*
|
|
100
100
|
* // Error validation
|
|
101
|
-
* TestValidator.error("should throw on invalid input"
|
|
101
|
+
* TestValidator.error("should throw on invalid input", () => assertInput(""));
|
|
102
102
|
* ```;
|
|
103
103
|
*/
|
|
104
104
|
var TestValidator;
|
|
@@ -114,53 +114,54 @@ var TestValidator;
|
|
|
114
114
|
* @example
|
|
115
115
|
* ```typescript
|
|
116
116
|
* // Synchronous boolean
|
|
117
|
-
* TestValidator.predicate("user should exist"
|
|
117
|
+
* TestValidator.predicate("user should exist", user !== null);
|
|
118
118
|
*
|
|
119
119
|
* // Synchronous function
|
|
120
|
-
* TestValidator.predicate("array should be empty"
|
|
120
|
+
* TestValidator.predicate("array should be empty", () => arr.length === 0);
|
|
121
121
|
*
|
|
122
122
|
* // Asynchronous function
|
|
123
|
-
* await TestValidator.predicate("database should be connected"
|
|
123
|
+
* await TestValidator.predicate("database should be connected",
|
|
124
124
|
* async () => await db.ping()
|
|
125
125
|
* );
|
|
126
126
|
* ```;
|
|
127
127
|
*
|
|
128
128
|
* @param title - Descriptive title used in error messages when validation
|
|
129
129
|
* fails
|
|
130
|
-
* @
|
|
130
|
+
* @param condition - The condition to validate (boolean, function, or async
|
|
131
|
+
* function)
|
|
132
|
+
* @returns Void or Promise<void> based on the input type
|
|
131
133
|
* @throws Error with descriptive message when condition is not satisfied
|
|
132
134
|
*/
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return "Bug on ".concat(title, ": expected condition is not satisfied.");
|
|
137
|
-
};
|
|
138
|
-
// SCALAR
|
|
139
|
-
if (typeof condition === "boolean") {
|
|
140
|
-
if (condition !== true)
|
|
141
|
-
throw new Error(message());
|
|
142
|
-
return undefined;
|
|
143
|
-
}
|
|
144
|
-
// CLOSURE
|
|
145
|
-
var output = condition();
|
|
146
|
-
if (typeof output === "boolean") {
|
|
147
|
-
if (output !== true)
|
|
148
|
-
throw new Error(message());
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
// ASYNCHRONOUS
|
|
152
|
-
return new Promise(function (resolve, reject) {
|
|
153
|
-
output
|
|
154
|
-
.then(function (flag) {
|
|
155
|
-
if (flag === true)
|
|
156
|
-
resolve();
|
|
157
|
-
else
|
|
158
|
-
reject(message());
|
|
159
|
-
})
|
|
160
|
-
.catch(reject);
|
|
161
|
-
});
|
|
135
|
+
function predicate(title, condition) {
|
|
136
|
+
var message = function () {
|
|
137
|
+
return "Bug on ".concat(title, ": expected condition is not satisfied.");
|
|
162
138
|
};
|
|
163
|
-
|
|
139
|
+
// SCALAR
|
|
140
|
+
if (typeof condition === "boolean") {
|
|
141
|
+
if (condition !== true)
|
|
142
|
+
throw new Error(message());
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
// CLOSURE
|
|
146
|
+
var output = condition();
|
|
147
|
+
if (typeof output === "boolean") {
|
|
148
|
+
if (output !== true)
|
|
149
|
+
throw new Error(message());
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
// ASYNCHRONOUS
|
|
153
|
+
return new Promise(function (resolve, reject) {
|
|
154
|
+
output
|
|
155
|
+
.then(function (flag) {
|
|
156
|
+
if (flag === true)
|
|
157
|
+
resolve();
|
|
158
|
+
else
|
|
159
|
+
reject(message());
|
|
160
|
+
})
|
|
161
|
+
.catch(reject);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
TestValidator.predicate = predicate;
|
|
164
165
|
/**
|
|
165
166
|
* Validates deep equality between two values using JSON comparison.
|
|
166
167
|
*
|
|
@@ -168,61 +169,44 @@ var TestValidator;
|
|
|
168
169
|
* exception filter to ignore specific keys during comparison. Useful for
|
|
169
170
|
* validating API responses, data transformations, and object state changes.
|
|
170
171
|
*
|
|
171
|
-
* **Type Safety Notes:**
|
|
172
|
-
*
|
|
173
|
-
* - The generic type T is inferred from the `actual` parameter (first in the
|
|
174
|
-
* currying chain)
|
|
175
|
-
* - The `expected` parameter must be assignable to `T | null | undefined`
|
|
176
|
-
* - For objects, `expected` must have the same or subset of properties as
|
|
177
|
-
* `actual`
|
|
178
|
-
* - For union types like `string | null`, ensure proper type compatibility:
|
|
179
|
-
*
|
|
180
|
-
* ```typescript
|
|
181
|
-
* const x: string | null;
|
|
182
|
-
* TestValidator.equals("works")(x)(null); // ✅ Works: null is assignable to string | null
|
|
183
|
-
* TestValidator.equals("error")(null)(x); // ❌ Error: x might be string, but expected is null
|
|
184
|
-
* ```
|
|
185
|
-
*
|
|
186
172
|
* @example
|
|
187
173
|
* ```typescript
|
|
188
174
|
* // Basic equality
|
|
189
|
-
* TestValidator.equals("response should match expected"
|
|
175
|
+
* TestValidator.equals("response should match expected", expectedUser, actualUser);
|
|
190
176
|
*
|
|
191
177
|
* // Ignore timestamps in comparison
|
|
192
|
-
* TestValidator.equals("user data should match",
|
|
193
|
-
*
|
|
194
|
-
* )
|
|
178
|
+
* TestValidator.equals("user data should match", expectedUser, actualUser,
|
|
179
|
+
* (key) => key === "updatedAt"
|
|
180
|
+
* );
|
|
195
181
|
*
|
|
196
182
|
* // Validate API response structure
|
|
197
|
-
*
|
|
198
|
-
*
|
|
183
|
+
* TestValidator.equals("API response structure",
|
|
184
|
+
* { id: 1, name: "John" },
|
|
185
|
+
* { id: 1, name: "John" }
|
|
186
|
+
* );
|
|
199
187
|
*
|
|
200
188
|
* // Type-safe nullable comparisons
|
|
201
189
|
* const nullableData: { name: string } | null = getData();
|
|
202
|
-
* TestValidator.equals("nullable check"
|
|
190
|
+
* TestValidator.equals("nullable check", nullableData, null);
|
|
203
191
|
* ```;
|
|
204
192
|
*
|
|
205
193
|
* @param title - Descriptive title used in error messages when values differ
|
|
194
|
+
* @param x - The first value to compare
|
|
195
|
+
* @param y - The second value to compare (can be null or undefined)
|
|
206
196
|
* @param exception - Optional filter function to exclude specific keys from
|
|
207
197
|
* comparison
|
|
208
|
-
* @returns A currying function chain: first accepts expected value, then
|
|
209
|
-
* actual value
|
|
210
198
|
* @throws Error with detailed diff information when values are not equal
|
|
211
199
|
*/
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
].join("\n"));
|
|
223
|
-
};
|
|
224
|
-
};
|
|
225
|
-
};
|
|
200
|
+
function equals(title, x, y, exception) {
|
|
201
|
+
var diff = (0, json_equal_to_1.json_equal_to)(exception !== null && exception !== void 0 ? exception : (function () { return false; }))(x)(y);
|
|
202
|
+
if (diff.length)
|
|
203
|
+
throw new Error([
|
|
204
|
+
"Bug on ".concat(title, ": found different values - [").concat(diff.join(", "), "]:"),
|
|
205
|
+
"\n",
|
|
206
|
+
JSON.stringify({ x: x, y: y }, null, 2),
|
|
207
|
+
].join("\n"));
|
|
208
|
+
}
|
|
209
|
+
TestValidator.equals = equals;
|
|
226
210
|
/**
|
|
227
211
|
* Validates deep inequality between two values using JSON comparison.
|
|
228
212
|
*
|
|
@@ -231,62 +215,42 @@ var TestValidator;
|
|
|
231
215
|
* comparison. Useful for validating that data has changed, objects are
|
|
232
216
|
* different, or mutations have occurred.
|
|
233
217
|
*
|
|
234
|
-
* **Type Safety Notes:**
|
|
235
|
-
*
|
|
236
|
-
* - The generic type T is inferred from the `actual` parameter (first in the
|
|
237
|
-
* currying chain)
|
|
238
|
-
* - The `expected` parameter must be assignable to `T | null | undefined`
|
|
239
|
-
* - For objects, `expected` must have the same or subset of properties as
|
|
240
|
-
* `actual`
|
|
241
|
-
* - For union types like `string | null`, ensure proper type compatibility:
|
|
242
|
-
*
|
|
243
|
-
* ```typescript
|
|
244
|
-
* const x: string | null;
|
|
245
|
-
* TestValidator.notEquals("works")(x)(null); // ✅ Works: null is assignable to string | null
|
|
246
|
-
* TestValidator.notEquals("error")(null)(x); // ❌ Error: x might be string, but expected is null
|
|
247
|
-
* ```
|
|
248
|
-
*
|
|
249
218
|
* @example
|
|
250
219
|
* ```typescript
|
|
251
220
|
* // Basic inequality
|
|
252
|
-
* TestValidator.notEquals("user should be different after update"
|
|
221
|
+
* TestValidator.notEquals("user should be different after update", originalUser, updatedUser);
|
|
253
222
|
*
|
|
254
223
|
* // Ignore timestamps in comparison
|
|
255
|
-
* TestValidator.notEquals("user data should differ",
|
|
256
|
-
*
|
|
257
|
-
* )
|
|
224
|
+
* TestValidator.notEquals("user data should differ", originalUser, modifiedUser,
|
|
225
|
+
* (key) => key === "updatedAt"
|
|
226
|
+
* );
|
|
258
227
|
*
|
|
259
228
|
* // Validate state changes
|
|
260
|
-
*
|
|
261
|
-
* validateStateChange(initialState)(currentState);
|
|
229
|
+
* TestValidator.notEquals("state should have changed", initialState, currentState);
|
|
262
230
|
*
|
|
263
231
|
* // Type-safe nullable comparisons
|
|
264
232
|
* const mutableData: { count: number } | null = getMutableData();
|
|
265
|
-
* TestValidator.notEquals("should have changed"
|
|
233
|
+
* TestValidator.notEquals("should have changed", mutableData, null);
|
|
266
234
|
* ```;
|
|
267
235
|
*
|
|
268
236
|
* @param title - Descriptive title used in error messages when values are
|
|
269
237
|
* equal
|
|
238
|
+
* @param x - The first value to compare
|
|
239
|
+
* @param y - The second value to compare (can be null or undefined)
|
|
270
240
|
* @param exception - Optional filter function to exclude specific keys from
|
|
271
241
|
* comparison
|
|
272
|
-
* @returns A currying function chain: first accepts expected value, then
|
|
273
|
-
* actual value
|
|
274
242
|
* @throws Error when values are equal (indicating validation failure)
|
|
275
243
|
*/
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
].join("\n"));
|
|
287
|
-
};
|
|
288
|
-
};
|
|
289
|
-
};
|
|
244
|
+
function notEquals(title, x, y, exception) {
|
|
245
|
+
var diff = (0, json_equal_to_1.json_equal_to)(exception !== null && exception !== void 0 ? exception : (function () { return false; }))(x)(y);
|
|
246
|
+
if (diff.length === 0)
|
|
247
|
+
throw new Error([
|
|
248
|
+
"Bug on ".concat(title, ": values should be different but are equal:"),
|
|
249
|
+
"\n",
|
|
250
|
+
JSON.stringify({ x: x, y: y }, null, 2),
|
|
251
|
+
].join("\n"));
|
|
252
|
+
}
|
|
253
|
+
TestValidator.notEquals = notEquals;
|
|
290
254
|
/**
|
|
291
255
|
* Validates that a function throws an error or rejects when executed.
|
|
292
256
|
*
|
|
@@ -297,43 +261,43 @@ var TestValidator;
|
|
|
297
261
|
* @example
|
|
298
262
|
* ```typescript
|
|
299
263
|
* // Synchronous error validation
|
|
300
|
-
* TestValidator.error("should reject invalid email"
|
|
264
|
+
* TestValidator.error("should reject invalid email",
|
|
301
265
|
* () => validateEmail("invalid-email")
|
|
302
266
|
* );
|
|
303
267
|
*
|
|
304
268
|
* // Asynchronous error validation
|
|
305
|
-
* await TestValidator.error("should reject unauthorized access"
|
|
269
|
+
* await TestValidator.error("should reject unauthorized access",
|
|
306
270
|
* async () => await api.functional.getSecretData()
|
|
307
271
|
* );
|
|
308
272
|
*
|
|
309
273
|
* // Validate input validation
|
|
310
|
-
* TestValidator.error("should throw on empty string"
|
|
274
|
+
* TestValidator.error("should throw on empty string",
|
|
311
275
|
* () => processRequiredInput("")
|
|
312
276
|
* );
|
|
313
277
|
* ```;
|
|
314
278
|
*
|
|
315
279
|
* @param title - Descriptive title used in error messages when no error
|
|
316
280
|
* occurs
|
|
317
|
-
* @
|
|
281
|
+
* @param task - The function that should throw an error or reject
|
|
282
|
+
* @returns Void or Promise<void> based on the input type
|
|
318
283
|
* @throws Error when the task function does not throw an error or reject
|
|
319
284
|
*/
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
};
|
|
285
|
+
function error(title, task) {
|
|
286
|
+
var message = function () { return "Bug on ".concat(title, ": exception must be thrown."); };
|
|
287
|
+
try {
|
|
288
|
+
var output_1 = task();
|
|
289
|
+
if (is_promise(output_1))
|
|
290
|
+
return new Promise(function (resolve, reject) {
|
|
291
|
+
return output_1.catch(function () { return resolve(); }).then(function () { return reject(message()); });
|
|
292
|
+
});
|
|
293
|
+
else
|
|
294
|
+
throw new Error(message());
|
|
295
|
+
}
|
|
296
|
+
catch (_a) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
TestValidator.error = error;
|
|
337
301
|
/**
|
|
338
302
|
* Validates that a function throws an HTTP error with specific status codes.
|
|
339
303
|
*
|
|
@@ -344,90 +308,70 @@ var TestValidator;
|
|
|
344
308
|
* @example
|
|
345
309
|
* ```typescript
|
|
346
310
|
* // Validate 401 Unauthorized
|
|
347
|
-
* await TestValidator.httpError("should return 401 for invalid token"
|
|
311
|
+
* await TestValidator.httpError("should return 401 for invalid token", 401,
|
|
348
312
|
* async () => await api.functional.getProtectedResource("invalid-token")
|
|
349
313
|
* );
|
|
350
314
|
*
|
|
351
315
|
* // Validate multiple possible error codes
|
|
352
|
-
* await TestValidator.httpError("should return client error"
|
|
316
|
+
* await TestValidator.httpError("should return client error", [400, 404, 422],
|
|
353
317
|
* async () => await api.functional.updateNonexistentResource(data)
|
|
354
318
|
* );
|
|
355
319
|
*
|
|
356
320
|
* // Validate server errors
|
|
357
|
-
* TestValidator.httpError("should handle server errors"
|
|
321
|
+
* TestValidator.httpError("should handle server errors", [500, 502, 503],
|
|
358
322
|
* () => callFaultyEndpoint()
|
|
359
323
|
* );
|
|
360
324
|
* ```;
|
|
361
325
|
*
|
|
362
326
|
* @param title - Descriptive title used in error messages
|
|
363
|
-
* @
|
|
364
|
-
*
|
|
327
|
+
* @param status - Expected status code(s), can be a single number or array
|
|
328
|
+
* @param task - The function that should throw an HttpError
|
|
329
|
+
* @returns Void or Promise<void> based on the input type
|
|
365
330
|
* @throws Error when function doesn't throw HttpError or status code doesn't
|
|
366
331
|
* match
|
|
367
332
|
*/
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
? null
|
|
385
|
-
: new Error(message(typeof exp === "object" && exp.constructor.name === "HttpError"
|
|
386
|
-
? exp.status
|
|
387
|
-
: undefined));
|
|
388
|
-
};
|
|
389
|
-
try {
|
|
390
|
-
var output_2 = task();
|
|
391
|
-
if (is_promise(output_2))
|
|
392
|
-
return new Promise(function (resolve, reject) {
|
|
393
|
-
return output_2
|
|
394
|
-
.catch(function (exp) {
|
|
395
|
-
var res = predicate(exp);
|
|
396
|
-
if (res)
|
|
397
|
-
reject(res);
|
|
398
|
-
else
|
|
399
|
-
resolve();
|
|
400
|
-
})
|
|
401
|
-
.then(function () { return reject(new Error(message())); });
|
|
402
|
-
});
|
|
403
|
-
else
|
|
404
|
-
throw new Error(message());
|
|
405
|
-
}
|
|
406
|
-
catch (exp) {
|
|
407
|
-
var res = predicate(exp);
|
|
408
|
-
if (res)
|
|
409
|
-
throw res;
|
|
410
|
-
return undefined;
|
|
411
|
-
}
|
|
412
|
-
};
|
|
333
|
+
function httpError(title, status, task) {
|
|
334
|
+
if (typeof status === "number")
|
|
335
|
+
status = [status];
|
|
336
|
+
var message = function (actual) {
|
|
337
|
+
return typeof actual === "number"
|
|
338
|
+
? "Bug on ".concat(title, ": status code must be ").concat(status.join(" or "), ", but ").concat(actual, ".")
|
|
339
|
+
: "Bug on ".concat(title, ": status code must be ").concat(status.join(" or "), ", but succeeded.");
|
|
340
|
+
};
|
|
341
|
+
var predicate = function (exp) {
|
|
342
|
+
return typeof exp === "object" &&
|
|
343
|
+
exp.constructor.name === "HttpError" &&
|
|
344
|
+
status.some(function (val) { return val === exp.status; })
|
|
345
|
+
? null
|
|
346
|
+
: new Error(message(typeof exp === "object" && exp.constructor.name === "HttpError"
|
|
347
|
+
? exp.status
|
|
348
|
+
: undefined));
|
|
413
349
|
};
|
|
414
|
-
};
|
|
415
|
-
function proceed(task) {
|
|
416
350
|
try {
|
|
417
|
-
var
|
|
418
|
-
if (is_promise(
|
|
419
|
-
return new Promise(function (resolve) {
|
|
420
|
-
return
|
|
421
|
-
.catch(function (exp) {
|
|
422
|
-
|
|
351
|
+
var output_2 = task();
|
|
352
|
+
if (is_promise(output_2))
|
|
353
|
+
return new Promise(function (resolve, reject) {
|
|
354
|
+
return output_2
|
|
355
|
+
.catch(function (exp) {
|
|
356
|
+
var res = predicate(exp);
|
|
357
|
+
if (res)
|
|
358
|
+
reject(res);
|
|
359
|
+
else
|
|
360
|
+
resolve();
|
|
361
|
+
})
|
|
362
|
+
.then(function () { return reject(new Error(message())); });
|
|
423
363
|
});
|
|
364
|
+
else
|
|
365
|
+
throw new Error(message());
|
|
424
366
|
}
|
|
425
367
|
catch (exp) {
|
|
426
|
-
|
|
368
|
+
var res = predicate(exp);
|
|
369
|
+
if (res)
|
|
370
|
+
throw res;
|
|
371
|
+
return undefined;
|
|
427
372
|
}
|
|
428
|
-
return null;
|
|
429
373
|
}
|
|
430
|
-
TestValidator.
|
|
374
|
+
TestValidator.httpError = httpError;
|
|
431
375
|
/**
|
|
432
376
|
* Validates pagination index API results against expected entity order.
|
|
433
377
|
*
|
|
@@ -442,8 +386,7 @@ var TestValidator;
|
|
|
442
386
|
* const expectedArticles = await db.articles.findAll({ order: 'created_at DESC' });
|
|
443
387
|
* const actualArticles = await api.functional.getArticles({ page: 1, limit: 10 });
|
|
444
388
|
*
|
|
445
|
-
* TestValidator.index("article pagination order"
|
|
446
|
-
* actualArticles,
|
|
389
|
+
* TestValidator.index("article pagination order", expectedArticles, actualArticles,
|
|
447
390
|
* true // enable trace logging
|
|
448
391
|
* );
|
|
449
392
|
*
|
|
@@ -451,37 +394,33 @@ var TestValidator;
|
|
|
451
394
|
* const manuallyFilteredUsers = allUsers.filter(u => u.name.includes("John"));
|
|
452
395
|
* const apiSearchResults = await api.functional.searchUsers({ query: "John" });
|
|
453
396
|
*
|
|
454
|
-
* TestValidator.index("user search results"
|
|
455
|
-
* apiSearchResults
|
|
456
|
-
* );
|
|
397
|
+
* TestValidator.index("user search results", manuallyFilteredUsers, apiSearchResults);
|
|
457
398
|
* ```;
|
|
458
399
|
*
|
|
459
400
|
* @param title - Descriptive title used in error messages when order differs
|
|
460
|
-
* @
|
|
401
|
+
* @param expected - The expected entities in correct order
|
|
402
|
+
* @param gotten - The actual entities returned by the API
|
|
403
|
+
* @param trace - Optional flag to enable debug logging (default: false)
|
|
461
404
|
* @throws Error when entity order differs between expected and actual results
|
|
462
405
|
*/
|
|
463
|
-
TestValidator.index = function (title) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
});
|
|
482
|
-
throw new Error("Bug on ".concat(title, ": result of the index is different with manual aggregation."));
|
|
483
|
-
};
|
|
484
|
-
};
|
|
406
|
+
TestValidator.index = function (title, expected, gotten, trace) {
|
|
407
|
+
if (trace === void 0) { trace = false; }
|
|
408
|
+
var length = Math.min(expected.length, gotten.length);
|
|
409
|
+
expected = expected.slice(0, length);
|
|
410
|
+
gotten = gotten.slice(0, length);
|
|
411
|
+
var xIds = get_ids(expected).slice(0, length);
|
|
412
|
+
var yIds = get_ids(gotten)
|
|
413
|
+
.filter(function (id) { return id >= xIds[0]; })
|
|
414
|
+
.slice(0, length);
|
|
415
|
+
var equals = xIds.every(function (x, i) { return x === yIds[i]; });
|
|
416
|
+
if (equals === true)
|
|
417
|
+
return;
|
|
418
|
+
else if (trace === true)
|
|
419
|
+
console.log({
|
|
420
|
+
expected: xIds,
|
|
421
|
+
gotten: yIds,
|
|
422
|
+
});
|
|
423
|
+
throw new Error("Bug on ".concat(title, ": result of the index is different with manual aggregation."));
|
|
485
424
|
};
|
|
486
425
|
/**
|
|
487
426
|
* Validates search functionality by testing API results against manual
|
|
@@ -494,97 +433,104 @@ var TestValidator;
|
|
|
494
433
|
*
|
|
495
434
|
* @example
|
|
496
435
|
* ```typescript
|
|
497
|
-
* // Test article search functionality
|
|
436
|
+
* // Test article search functionality with exact matching
|
|
498
437
|
* const allArticles = await db.articles.findAll();
|
|
499
|
-
* const searchValidator = TestValidator.search(
|
|
500
|
-
*
|
|
501
|
-
*
|
|
438
|
+
* const searchValidator = TestValidator.search(
|
|
439
|
+
* "article search API",
|
|
440
|
+
* (req) => api.searchArticles(req),
|
|
441
|
+
* allArticles,
|
|
442
|
+
* 5 // test with 5 random samples
|
|
443
|
+
* );
|
|
444
|
+
*
|
|
445
|
+
* // Test exact match search
|
|
446
|
+
* await searchValidator({
|
|
447
|
+
* fields: ["title"],
|
|
448
|
+
* values: (article) => [article.title], // full title for exact match
|
|
449
|
+
* filter: (article, [title]) => article.title === title, // exact match
|
|
450
|
+
* request: ([title]) => ({ search: { title } })
|
|
451
|
+
* });
|
|
502
452
|
*
|
|
453
|
+
* // Test partial match search with includes
|
|
503
454
|
* await searchValidator({
|
|
504
|
-
* fields: ["
|
|
505
|
-
* values: (article) => [article.
|
|
506
|
-
* filter: (article, [keyword]) =>
|
|
507
|
-
* article.title.includes(keyword) || article.content.includes(keyword),
|
|
455
|
+
* fields: ["content"],
|
|
456
|
+
* values: (article) => [article.content.substring(0, 20)], // partial content
|
|
457
|
+
* filter: (article, [keyword]) => article.content.includes(keyword),
|
|
508
458
|
* request: ([keyword]) => ({ q: keyword })
|
|
509
459
|
* });
|
|
510
460
|
*
|
|
511
|
-
* // Test
|
|
512
|
-
* await
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
* user.status === status && user.role === role,
|
|
519
|
-
* request: ([status, role]) => ({ status, role })
|
|
461
|
+
* // Test multi-field search with exact matching
|
|
462
|
+
* await searchValidator({
|
|
463
|
+
* fields: ["writer", "title"],
|
|
464
|
+
* values: (article) => [article.writer, article.title],
|
|
465
|
+
* filter: (article, [writer, title]) =>
|
|
466
|
+
* article.writer === writer && article.title === title,
|
|
467
|
+
* request: ([writer, title]) => ({ search: { writer, title } })
|
|
520
468
|
* });
|
|
521
469
|
* ```;
|
|
522
470
|
*
|
|
523
471
|
* @param title - Descriptive title used in error messages when search fails
|
|
524
|
-
* @
|
|
525
|
-
*
|
|
472
|
+
* @param getter - API function that performs the search
|
|
473
|
+
* @param total - Complete dataset to sample from for testing
|
|
474
|
+
* @param sampleCount - Number of random samples to test (default: 1)
|
|
475
|
+
* @returns A function that accepts search configuration properties
|
|
526
476
|
* @throws Error when API search results don't match manual filtering results
|
|
527
477
|
*/
|
|
528
|
-
TestValidator.search = function (title) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
TestValidator.index("".concat(title, " (").concat(props.fields.join(", "), ")"))(filtered)(gotten);
|
|
552
|
-
return [2 /*return*/];
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
};
|
|
556
|
-
_b.label = 1;
|
|
557
|
-
case 1:
|
|
558
|
-
_b.trys.push([1, 6, 7, 8]);
|
|
559
|
-
samples_1 = __values(samples), samples_1_1 = samples_1.next();
|
|
560
|
-
_b.label = 2;
|
|
561
|
-
case 2:
|
|
562
|
-
if (!!samples_1_1.done) return [3 /*break*/, 5];
|
|
563
|
-
s = samples_1_1.value;
|
|
564
|
-
return [5 /*yield**/, _loop_1(s)];
|
|
565
|
-
case 3:
|
|
566
|
-
_b.sent();
|
|
567
|
-
_b.label = 4;
|
|
568
|
-
case 4:
|
|
569
|
-
samples_1_1 = samples_1.next();
|
|
570
|
-
return [3 /*break*/, 2];
|
|
571
|
-
case 5: return [3 /*break*/, 8];
|
|
572
|
-
case 6:
|
|
573
|
-
e_1_1 = _b.sent();
|
|
574
|
-
e_1 = { error: e_1_1 };
|
|
575
|
-
return [3 /*break*/, 8];
|
|
576
|
-
case 7:
|
|
577
|
-
try {
|
|
578
|
-
if (samples_1_1 && !samples_1_1.done && (_a = samples_1.return)) _a.call(samples_1);
|
|
478
|
+
TestValidator.search = function (title, getter, total, sampleCount) {
|
|
479
|
+
if (sampleCount === void 0) { sampleCount = 1; }
|
|
480
|
+
return function (props) { return __awaiter(_this, void 0, void 0, function () {
|
|
481
|
+
var samples, _loop_1, samples_1, samples_1_1, s, e_1_1;
|
|
482
|
+
var e_1, _a;
|
|
483
|
+
return __generator(this, function (_b) {
|
|
484
|
+
switch (_b.label) {
|
|
485
|
+
case 0:
|
|
486
|
+
samples = RandomGenerator_1.RandomGenerator.sample(total, sampleCount);
|
|
487
|
+
_loop_1 = function (s) {
|
|
488
|
+
var values, filtered, gotten;
|
|
489
|
+
return __generator(this, function (_c) {
|
|
490
|
+
switch (_c.label) {
|
|
491
|
+
case 0:
|
|
492
|
+
values = props.values(s);
|
|
493
|
+
filtered = total.filter(function (entity) {
|
|
494
|
+
return props.filter(entity, values);
|
|
495
|
+
});
|
|
496
|
+
return [4 /*yield*/, getter(props.request(values))];
|
|
497
|
+
case 1:
|
|
498
|
+
gotten = _c.sent();
|
|
499
|
+
TestValidator.index("".concat(title, " (").concat(props.fields.join(", "), ")"), filtered, gotten);
|
|
500
|
+
return [2 /*return*/];
|
|
579
501
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
502
|
+
});
|
|
503
|
+
};
|
|
504
|
+
_b.label = 1;
|
|
505
|
+
case 1:
|
|
506
|
+
_b.trys.push([1, 6, 7, 8]);
|
|
507
|
+
samples_1 = __values(samples), samples_1_1 = samples_1.next();
|
|
508
|
+
_b.label = 2;
|
|
509
|
+
case 2:
|
|
510
|
+
if (!!samples_1_1.done) return [3 /*break*/, 5];
|
|
511
|
+
s = samples_1_1.value;
|
|
512
|
+
return [5 /*yield**/, _loop_1(s)];
|
|
513
|
+
case 3:
|
|
514
|
+
_b.sent();
|
|
515
|
+
_b.label = 4;
|
|
516
|
+
case 4:
|
|
517
|
+
samples_1_1 = samples_1.next();
|
|
518
|
+
return [3 /*break*/, 2];
|
|
519
|
+
case 5: return [3 /*break*/, 8];
|
|
520
|
+
case 6:
|
|
521
|
+
e_1_1 = _b.sent();
|
|
522
|
+
e_1 = { error: e_1_1 };
|
|
523
|
+
return [3 /*break*/, 8];
|
|
524
|
+
case 7:
|
|
525
|
+
try {
|
|
526
|
+
if (samples_1_1 && !samples_1_1.done && (_a = samples_1.return)) _a.call(samples_1);
|
|
583
527
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
528
|
+
finally { if (e_1) throw e_1.error; }
|
|
529
|
+
return [7 /*endfinally*/];
|
|
530
|
+
case 8: return [2 /*return*/];
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}); };
|
|
588
534
|
};
|
|
589
535
|
/**
|
|
590
536
|
* Validates sorting functionality of pagination APIs.
|
|
@@ -596,87 +542,98 @@ var TestValidator;
|
|
|
596
542
|
*
|
|
597
543
|
* @example
|
|
598
544
|
* ```typescript
|
|
599
|
-
* // Test single field sorting
|
|
600
|
-
* const sortValidator = TestValidator.sort(
|
|
545
|
+
* // Test single field sorting with GaffComparator
|
|
546
|
+
* const sortValidator = TestValidator.sort(
|
|
547
|
+
* "article sorting",
|
|
601
548
|
* (sortable) => api.getArticles({ sort: sortable })
|
|
602
549
|
* )("created_at")(
|
|
603
|
-
* (a
|
|
550
|
+
* GaffComparator.dates((a) => a.created_at)
|
|
604
551
|
* );
|
|
605
552
|
*
|
|
606
553
|
* await sortValidator("+"); // ascending
|
|
607
554
|
* await sortValidator("-"); // descending
|
|
608
555
|
*
|
|
609
|
-
* // Test multi-field sorting with
|
|
610
|
-
* const userSortValidator = TestValidator.sort(
|
|
556
|
+
* // Test multi-field sorting with GaffComparator
|
|
557
|
+
* const userSortValidator = TestValidator.sort(
|
|
558
|
+
* "user sorting",
|
|
611
559
|
* (sortable) => api.getUsers({ sort: sortable })
|
|
612
|
-
* )("
|
|
613
|
-
* (
|
|
614
|
-
* if (a.status !== b.status) return a.status.localeCompare(b.status);
|
|
615
|
-
* return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
|
616
|
-
* },
|
|
560
|
+
* )("lastName", "firstName")(
|
|
561
|
+
* GaffComparator.strings((user) => [user.lastName, user.firstName]),
|
|
617
562
|
* (user) => user.isActive // only test active users
|
|
618
563
|
* );
|
|
619
564
|
*
|
|
620
565
|
* await userSortValidator("+", true); // ascending with trace logging
|
|
566
|
+
*
|
|
567
|
+
* // Custom comparator for complex logic
|
|
568
|
+
* const customSortValidator = TestValidator.sort(
|
|
569
|
+
* "custom sorting",
|
|
570
|
+
* (sortable) => api.getProducts({ sort: sortable })
|
|
571
|
+
* )("price", "rating")(
|
|
572
|
+
* (a, b) => {
|
|
573
|
+
* const priceDiff = a.price - b.price;
|
|
574
|
+
* return priceDiff !== 0 ? priceDiff : b.rating - a.rating; // price asc, rating desc
|
|
575
|
+
* }
|
|
576
|
+
* );
|
|
621
577
|
* ```;
|
|
622
578
|
*
|
|
623
579
|
* @param title - Descriptive title used in error messages when sorting fails
|
|
624
|
-
* @
|
|
625
|
-
*
|
|
580
|
+
* @param getter - API function that fetches sorted data
|
|
581
|
+
* @returns A currying function chain: field names, comparator, then direction
|
|
626
582
|
* @throws Error when API results are not properly sorted according to
|
|
627
583
|
* specification
|
|
628
584
|
*/
|
|
629
|
-
TestValidator.sort = function (title) {
|
|
630
|
-
return function (
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
return function (
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
}
|
|
663
|
-
});
|
|
585
|
+
TestValidator.sort = function (title, getter) {
|
|
586
|
+
return function () {
|
|
587
|
+
var fields = [];
|
|
588
|
+
for (var _i = 0; _i < arguments.length; _i++) {
|
|
589
|
+
fields[_i] = arguments[_i];
|
|
590
|
+
}
|
|
591
|
+
return function (comp, filter) {
|
|
592
|
+
return function (direction_1) {
|
|
593
|
+
var args_1 = [];
|
|
594
|
+
for (var _i = 1; _i < arguments.length; _i++) {
|
|
595
|
+
args_1[_i - 1] = arguments[_i];
|
|
596
|
+
}
|
|
597
|
+
return __awaiter(_this, __spreadArray([direction_1], __read(args_1), false), void 0, function (direction, trace) {
|
|
598
|
+
var data, reversed;
|
|
599
|
+
if (trace === void 0) { trace = false; }
|
|
600
|
+
return __generator(this, function (_a) {
|
|
601
|
+
switch (_a.label) {
|
|
602
|
+
case 0: return [4 /*yield*/, getter(fields.map(function (field) { return "".concat(direction).concat(field); }))];
|
|
603
|
+
case 1:
|
|
604
|
+
data = _a.sent();
|
|
605
|
+
if (filter)
|
|
606
|
+
data = data.filter(filter);
|
|
607
|
+
reversed = direction === "+" ? comp : function (x, y) { return comp(y, x); };
|
|
608
|
+
if (is_sorted(data, reversed) === false) {
|
|
609
|
+
if (fields.length === 1 &&
|
|
610
|
+
data.length &&
|
|
611
|
+
data[0][fields[0]] !== undefined &&
|
|
612
|
+
trace)
|
|
613
|
+
console.log(data.map(function (elem) { return elem[fields[0]]; }));
|
|
614
|
+
throw new Error("Bug on ".concat(title, ": wrong sorting on ").concat(direction, "(").concat(fields.join(", "), ")."));
|
|
615
|
+
}
|
|
616
|
+
return [2 /*return*/];
|
|
617
|
+
}
|
|
664
618
|
});
|
|
665
|
-
};
|
|
619
|
+
});
|
|
666
620
|
};
|
|
667
621
|
};
|
|
668
622
|
};
|
|
669
623
|
};
|
|
670
624
|
})(TestValidator || (exports.TestValidator = TestValidator = {}));
|
|
625
|
+
/** @internal */
|
|
671
626
|
function get_ids(entities) {
|
|
672
627
|
return entities.map(function (entity) { return entity.id; }).sort(function (x, y) { return (x < y ? -1 : 1); });
|
|
673
628
|
}
|
|
629
|
+
/** @internal */
|
|
674
630
|
function is_promise(input) {
|
|
675
631
|
return (typeof input === "object" &&
|
|
676
632
|
input !== null &&
|
|
677
633
|
typeof input.then === "function" &&
|
|
678
634
|
typeof input.catch === "function");
|
|
679
635
|
}
|
|
636
|
+
/** @internal */
|
|
680
637
|
function is_sorted(data, comp) {
|
|
681
638
|
for (var i = 1; i < data.length; ++i)
|
|
682
639
|
if (comp(data[i - 1], data[i]) > 0)
|