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