@nestia/e2e 14.0.1 → 14.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ArrayUtil.d.ts +152 -70
- package/lib/ArrayUtil.js +168 -74
- package/lib/ArrayUtil.js.map +1 -1
- package/lib/DynamicExecutor.d.ts +139 -11
- package/lib/DynamicExecutor.js +98 -10
- package/lib/DynamicExecutor.js.map +1 -1
- package/lib/GaffComparator.d.ts +176 -73
- package/lib/GaffComparator.js +189 -76
- package/lib/GaffComparator.js.map +1 -1
- package/lib/MapUtil.d.ts +37 -6
- package/lib/MapUtil.js +37 -6
- package/lib/MapUtil.js.map +1 -1
- package/lib/RandomGenerator.d.ts +157 -84
- package/lib/RandomGenerator.js +173 -89
- package/lib/RandomGenerator.js.map +1 -1
- package/lib/TestValidator.d.ts +248 -74
- package/lib/TestValidator.js +252 -89
- package/lib/TestValidator.js.map +1 -1
- package/lib/internal/json_equal_to.d.ts +21 -0
- package/lib/internal/json_equal_to.js +26 -6
- package/lib/internal/json_equal_to.js.map +1 -1
- package/package.json +3 -2
- package/src/ArrayUtil.ts +172 -76
- package/src/DynamicExecutor.ts +158 -15
- package/src/GaffComparator.ts +187 -77
- package/src/MapUtil.ts +37 -6
- package/src/RandomGenerator.ts +172 -89
- package/src/TestValidator.ts +276 -98
- package/src/internal/json_equal_to.ts +25 -4
package/src/TestValidator.ts
CHANGED
|
@@ -14,18 +14,31 @@ import { json_equal_to } from "./internal/json_equal_to";
|
|
|
14
14
|
* maintain currying patterns for advanced composition. All provide detailed
|
|
15
15
|
* error messages for debugging failed assertions.
|
|
16
16
|
*
|
|
17
|
+
* Processing cost: Scalar assertions use constant bookkeeping; JSON assertions
|
|
18
|
+
* traverse the expected structure, index compares the shared page prefix,
|
|
19
|
+
* search scans the dataset once per sample and sort checks adjacent results.
|
|
20
|
+
* External callbacks own their computation costs.
|
|
21
|
+
*
|
|
17
22
|
* @author Jeongho Nam - https://github.com/samchon
|
|
18
23
|
* @example
|
|
19
|
-
* ```typescript
|
|
20
24
|
* // Basic condition testing
|
|
21
|
-
* TestValidator.predicate(
|
|
25
|
+
* TestValidator.predicate(
|
|
26
|
+
* "user should be authenticated",
|
|
27
|
+
* user.isAuthenticated,
|
|
28
|
+
* );
|
|
22
29
|
*
|
|
23
30
|
* // Equality validation
|
|
24
31
|
* TestValidator.equals("API response should match expected", x, y);
|
|
25
32
|
*
|
|
26
33
|
* // Error validation
|
|
27
|
-
* TestValidator.error("should throw on invalid input", () =>
|
|
28
|
-
*
|
|
34
|
+
* TestValidator.error("should throw on invalid input", () =>
|
|
35
|
+
* assertInput(""),
|
|
36
|
+
* );
|
|
37
|
+
*
|
|
38
|
+
* @evidence contracts/common.md#principled-implementation Each validator runs a check and throws an `Error` whose message begins with `Bug on <title>`, returning a promise only when its input is asynchronous, so the same function serves synchronous and asynchronous tests; the comparison helpers read values through their JSON form, which is the form an HTTP API delivers.
|
|
39
|
+
* @evidence contracts/common.md#clear-and-simple-design One function per assertion kind, sharing the JSON comparison (`json_equal_to`) and two small module helpers (`is_promise` and `is_sorted`), with the curried forms only where a request must be prepared once and run twice.
|
|
40
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Assertions compare the values they are given; none consults a fixture, an entity name, or an environment.
|
|
41
|
+
* @evidence contracts/common.md#meaningful-documentation The namespace prose lists the assertion families and the error message convention, with examples.
|
|
29
42
|
*/
|
|
30
43
|
export namespace TestValidator {
|
|
31
44
|
/**
|
|
@@ -35,19 +48,33 @@ export namespace TestValidator {
|
|
|
35
48
|
* boolean, and asynchronous functions returning Promise<boolean>. The return
|
|
36
49
|
* type is automatically inferred based on the input type.
|
|
37
50
|
*
|
|
51
|
+
* Processing cost: The condition is evaluated once and asynchronous
|
|
52
|
+
* settlement adds constant bookkeeping without polling or repeating the
|
|
53
|
+
* condition.
|
|
54
|
+
*
|
|
55
|
+
* Reuse: Each invocation must evaluate its supplied task or request to
|
|
56
|
+
* observe current behavior; prior results cannot replace these effectful
|
|
57
|
+
* checks.
|
|
58
|
+
*
|
|
59
|
+
* Resource ownership: The returned promise retains invocation-local state
|
|
60
|
+
* until settlement. The callback owns its external resources, and this
|
|
61
|
+
* validator provides no cancellation or timeout.
|
|
62
|
+
*
|
|
38
63
|
* @example
|
|
39
|
-
* ```typescript
|
|
40
64
|
* // Synchronous boolean
|
|
41
65
|
* TestValidator.predicate("user should exist", user !== null);
|
|
42
66
|
*
|
|
43
67
|
* // Synchronous function
|
|
44
|
-
* TestValidator.predicate(
|
|
68
|
+
* TestValidator.predicate(
|
|
69
|
+
* "array should be empty",
|
|
70
|
+
* () => arr.length === 0,
|
|
71
|
+
* );
|
|
45
72
|
*
|
|
46
73
|
* // Asynchronous function
|
|
47
|
-
* await TestValidator.predicate(
|
|
48
|
-
*
|
|
74
|
+
* await TestValidator.predicate(
|
|
75
|
+
* "database should be connected",
|
|
76
|
+
* async () => await db.ping(),
|
|
49
77
|
* );
|
|
50
|
-
* ```;
|
|
51
78
|
*
|
|
52
79
|
* @param title - Descriptive title used in error messages when validation
|
|
53
80
|
* fails
|
|
@@ -55,6 +82,10 @@ export namespace TestValidator {
|
|
|
55
82
|
* function)
|
|
56
83
|
* @returns Void or Promise<void> based on the input type
|
|
57
84
|
* @throws Error with descriptive message when condition is not satisfied
|
|
85
|
+
* @evidence contracts/common.md#principled-implementation A boolean, a function returning a boolean, or a function returning a promise of a boolean is evaluated, and only the exact value `true` passes; a false result throws the same `Error` in all three forms, and the asynchronous form rejects with it.
|
|
86
|
+
* @evidence contracts/common.md#clear-and-simple-design One function whose three branches differ only in when the value is available.
|
|
87
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The check is on the value the caller gives, with no truthiness coercion.
|
|
88
|
+
* @evidence contracts/common.md#meaningful-documentation The comment documents the three forms, the parameters, the return type rule, and the thrown error, with examples.
|
|
58
89
|
*/
|
|
59
90
|
export function predicate<
|
|
60
91
|
T extends boolean | (() => boolean) | (() => Promise<boolean>),
|
|
@@ -92,30 +123,45 @@ export namespace TestValidator {
|
|
|
92
123
|
/**
|
|
93
124
|
* Validates deep equality between two values using JSON comparison.
|
|
94
125
|
*
|
|
95
|
-
* Performs recursive comparison of objects and arrays
|
|
126
|
+
* Performs recursive comparison of objects and arrays through their JSON
|
|
127
|
+
* form. The comparison walks the keys of the first value, so a key that only
|
|
128
|
+
* the second value has is ignored, which is why the second value may be a
|
|
129
|
+
* subtype of the first. Keys whose value in the first value is `undefined`
|
|
130
|
+
* are skipped, and arrays must have the same length. Supports an optional
|
|
96
131
|
* exception filter to ignore specific keys during comparison. Useful for
|
|
97
132
|
* validating API responses, data transformations, and object state changes.
|
|
98
133
|
*
|
|
134
|
+
* Processing cost: The shared JSON walker traverses the first value and its
|
|
135
|
+
* corresponding second-value positions once, with recursion and accessor
|
|
136
|
+
* output proportional to visited structure; serialization occurs only on
|
|
137
|
+
* failure.
|
|
138
|
+
*
|
|
99
139
|
* @example
|
|
100
|
-
* ```typescript
|
|
101
140
|
* // Basic equality
|
|
102
|
-
* TestValidator.equals(
|
|
141
|
+
* TestValidator.equals(
|
|
142
|
+
* "response should match expected",
|
|
143
|
+
* expectedUser,
|
|
144
|
+
* actualUser,
|
|
145
|
+
* );
|
|
103
146
|
*
|
|
104
147
|
* // Ignore timestamps in comparison
|
|
105
|
-
* TestValidator.equals(
|
|
106
|
-
*
|
|
148
|
+
* TestValidator.equals(
|
|
149
|
+
* "user data should match",
|
|
150
|
+
* expectedUser,
|
|
151
|
+
* actualUser,
|
|
152
|
+
* (key) => key === "updatedAt",
|
|
107
153
|
* );
|
|
108
154
|
*
|
|
109
155
|
* // Validate API response structure
|
|
110
|
-
* TestValidator.equals(
|
|
156
|
+
* TestValidator.equals(
|
|
157
|
+
* "API response structure",
|
|
158
|
+
* { id: 1, name: "John" },
|
|
111
159
|
* { id: 1, name: "John" },
|
|
112
|
-
* { id: 1, name: "John" }
|
|
113
160
|
* );
|
|
114
161
|
*
|
|
115
162
|
* // Type-safe nullable comparisons
|
|
116
163
|
* const nullableData: { name: string } | null = getData();
|
|
117
164
|
* TestValidator.equals("nullable check", nullableData, null);
|
|
118
|
-
* ```;
|
|
119
165
|
*
|
|
120
166
|
* @param title - Descriptive title used in error messages when values differ
|
|
121
167
|
* @param X - The first value to compare
|
|
@@ -123,6 +169,10 @@ export namespace TestValidator {
|
|
|
123
169
|
* @param exception - Optional filter function to exclude specific keys from
|
|
124
170
|
* comparison
|
|
125
171
|
* @throws Error with detailed diff information when values are not equal
|
|
172
|
+
* @evidence contracts/common.md#principled-implementation Both values are compared through `json_equal_to`, which walks the keys of the first value, so the second value may carry extra keys and the typing `Y extends X` states that; the differing accessors are listed in the error with both values serialized.
|
|
173
|
+
* @evidence contracts/common.md#clear-and-simple-design A thin wrapper over the shared comparison that adds the message.
|
|
174
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The exception filter is the caller's; the function contains no key name of its own.
|
|
175
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the JSON form, the key-walking rule with its consequence for extra keys, the array length rule, and the exception filter, with examples.
|
|
126
176
|
*/
|
|
127
177
|
export function equals<X, Y extends X = X>(
|
|
128
178
|
title: string,
|
|
@@ -145,27 +195,42 @@ export namespace TestValidator {
|
|
|
145
195
|
* Validates deep inequality between two values using JSON comparison.
|
|
146
196
|
*
|
|
147
197
|
* Performs recursive comparison of objects and arrays to ensure they are NOT
|
|
148
|
-
* equal
|
|
149
|
-
*
|
|
198
|
+
* equal, by the same rules as {@link equals}: only the keys of the first value
|
|
199
|
+
* are compared, so a key that only the second value has does not make them
|
|
200
|
+
* different. Supports an optional exception filter to ignore specific keys
|
|
201
|
+
* during comparison. Useful for validating that data has changed, objects are
|
|
150
202
|
* different, or mutations have occurred.
|
|
151
203
|
*
|
|
204
|
+
* Processing cost: The shared JSON walker visits the first value and
|
|
205
|
+
* corresponding positions once; a failure additionally serializes both values
|
|
206
|
+
* for diagnostics. Space follows visited structure and reported differences.
|
|
207
|
+
*
|
|
152
208
|
* @example
|
|
153
|
-
* ```typescript
|
|
154
209
|
* // Basic inequality
|
|
155
|
-
* TestValidator.notEquals(
|
|
210
|
+
* TestValidator.notEquals(
|
|
211
|
+
* "user should be different after update",
|
|
212
|
+
* originalUser,
|
|
213
|
+
* updatedUser,
|
|
214
|
+
* );
|
|
156
215
|
*
|
|
157
216
|
* // Ignore timestamps in comparison
|
|
158
|
-
* TestValidator.notEquals(
|
|
159
|
-
*
|
|
217
|
+
* TestValidator.notEquals(
|
|
218
|
+
* "user data should differ",
|
|
219
|
+
* originalUser,
|
|
220
|
+
* modifiedUser,
|
|
221
|
+
* (key) => key === "updatedAt",
|
|
160
222
|
* );
|
|
161
223
|
*
|
|
162
224
|
* // Validate state changes
|
|
163
|
-
* TestValidator.notEquals(
|
|
225
|
+
* TestValidator.notEquals(
|
|
226
|
+
* "state should have changed",
|
|
227
|
+
* initialState,
|
|
228
|
+
* currentState,
|
|
229
|
+
* );
|
|
164
230
|
*
|
|
165
231
|
* // Type-safe nullable comparisons
|
|
166
232
|
* const mutableData: { count: number } | null = getMutableData();
|
|
167
233
|
* TestValidator.notEquals("should have changed", mutableData, null);
|
|
168
|
-
* ```;
|
|
169
234
|
*
|
|
170
235
|
* @param title - Descriptive title used in error messages when values are
|
|
171
236
|
* equal
|
|
@@ -174,6 +239,10 @@ export namespace TestValidator {
|
|
|
174
239
|
* @param exception - Optional filter function to exclude specific keys from
|
|
175
240
|
* comparison
|
|
176
241
|
* @throws Error when values are equal (indicating validation failure)
|
|
242
|
+
* @evidence contracts/common.md#principled-implementation It runs the same comparison as `equals` and fails when the comparison finds no difference, so two values differing only by extra keys of the second are considered equal and fail.
|
|
243
|
+
* @evidence contracts/common.md#clear-and-simple-design A thin wrapper over the shared comparison with the opposite outcome.
|
|
244
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The exception filter is the caller's; the function contains no key name of its own.
|
|
245
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states that the rules are those of `equals`, and gives examples.
|
|
177
246
|
*/
|
|
178
247
|
export function notEquals<X, Y extends X = X>(
|
|
179
248
|
title: string,
|
|
@@ -199,29 +268,43 @@ export namespace TestValidator {
|
|
|
199
268
|
* successfully without throwing an error or rejecting, this validator will
|
|
200
269
|
* throw an exception. Supports both synchronous and asynchronous functions.
|
|
201
270
|
*
|
|
271
|
+
* Processing cost: The task is invoked once and its settlement is classified
|
|
272
|
+
* with constant bookkeeping; no retry or repeated execution occurs.
|
|
273
|
+
*
|
|
274
|
+
* Reuse: Each invocation must evaluate its supplied task or request to
|
|
275
|
+
* observe current behavior; prior results cannot replace these effectful
|
|
276
|
+
* checks.
|
|
277
|
+
*
|
|
278
|
+
* Resource ownership: The returned promise retains invocation-local state
|
|
279
|
+
* until settlement. The callback owns its external resources, and this
|
|
280
|
+
* validator provides no cancellation or timeout.
|
|
281
|
+
*
|
|
202
282
|
* @example
|
|
203
|
-
* ```typescript
|
|
204
283
|
* // Synchronous error validation
|
|
205
|
-
* TestValidator.error("should reject invalid email",
|
|
206
|
-
*
|
|
284
|
+
* TestValidator.error("should reject invalid email", () =>
|
|
285
|
+
* validateEmail("invalid-email"),
|
|
207
286
|
* );
|
|
208
287
|
*
|
|
209
288
|
* // Asynchronous error validation
|
|
210
|
-
* await TestValidator.error(
|
|
211
|
-
*
|
|
289
|
+
* await TestValidator.error(
|
|
290
|
+
* "should reject unauthorized access",
|
|
291
|
+
* async () => await api.functional.getSecretData(),
|
|
212
292
|
* );
|
|
213
293
|
*
|
|
214
294
|
* // Validate input validation
|
|
215
|
-
* TestValidator.error("should throw on empty string",
|
|
216
|
-
*
|
|
295
|
+
* TestValidator.error("should throw on empty string", () =>
|
|
296
|
+
* processRequiredInput(""),
|
|
217
297
|
* );
|
|
218
|
-
* ```;
|
|
219
298
|
*
|
|
220
299
|
* @param title - Descriptive title used in error messages when no error
|
|
221
300
|
* occurs
|
|
222
301
|
* @param task - The function that should throw an error or reject
|
|
223
302
|
* @returns Void or Promise<void> based on the input type
|
|
224
303
|
* @throws Error when the task function does not throw an error or reject
|
|
304
|
+
* @evidence contracts/common.md#principled-implementation The task is run inside a `try` so that only its own throw counts, a promise is awaited and a rejection counts as the expected error, and a task that neither throws nor rejects raises the failure outside the `try`, so the failure cannot be swallowed.
|
|
305
|
+
* @evidence contracts/common.md#clear-and-simple-design One function with a synchronous and an asynchronous path selected by the returned value.
|
|
306
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Any error satisfies it, as documented, and no error type is special-cased.
|
|
307
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states that an exception is required, the return type rule, and the thrown error, with examples.
|
|
225
308
|
*/
|
|
226
309
|
export function error<T>(
|
|
227
310
|
title: string,
|
|
@@ -249,23 +332,40 @@ export namespace TestValidator {
|
|
|
249
332
|
* function throws an HttpError with one of the specified status codes. Useful
|
|
250
333
|
* for testing API endpoints, authentication, and authorization logic.
|
|
251
334
|
*
|
|
335
|
+
* Processing cost: The task is invoked once. Classification reads the thrown
|
|
336
|
+
* shape once and scans the expected status list until a match, using O(S)
|
|
337
|
+
* worst-case status comparisons and constant auxiliary state.
|
|
338
|
+
*
|
|
339
|
+
* Reuse: Each invocation must evaluate its supplied task or request to
|
|
340
|
+
* observe current behavior; prior results cannot replace these effectful
|
|
341
|
+
* checks.
|
|
342
|
+
*
|
|
343
|
+
* Resource ownership: The returned promise retains invocation-local state
|
|
344
|
+
* until settlement. The callback owns its external resources, and this
|
|
345
|
+
* validator provides no cancellation or timeout.
|
|
346
|
+
*
|
|
252
347
|
* @example
|
|
253
|
-
* ```typescript
|
|
254
348
|
* // Validate 401 Unauthorized
|
|
255
|
-
* await TestValidator.httpError(
|
|
256
|
-
*
|
|
349
|
+
* await TestValidator.httpError(
|
|
350
|
+
* "should return 401 for invalid token",
|
|
351
|
+
* 401,
|
|
352
|
+
* async () =>
|
|
353
|
+
* await api.functional.getProtectedResource("invalid-token"),
|
|
257
354
|
* );
|
|
258
355
|
*
|
|
259
356
|
* // Validate multiple possible error codes
|
|
260
|
-
* await TestValidator.httpError(
|
|
261
|
-
*
|
|
357
|
+
* await TestValidator.httpError(
|
|
358
|
+
* "should return client error",
|
|
359
|
+
* [400, 404, 422],
|
|
360
|
+
* async () => await api.functional.updateNonexistentResource(data),
|
|
262
361
|
* );
|
|
263
362
|
*
|
|
264
363
|
* // Validate server errors
|
|
265
|
-
* TestValidator.httpError(
|
|
266
|
-
*
|
|
364
|
+
* TestValidator.httpError(
|
|
365
|
+
* "should handle server errors",
|
|
366
|
+
* [500, 502, 503],
|
|
367
|
+
* () => callFaultyEndpoint(),
|
|
267
368
|
* );
|
|
268
|
-
* ```;
|
|
269
369
|
*
|
|
270
370
|
* @param title - Descriptive title used in error messages
|
|
271
371
|
* @param status - Expected status code(s), can be a single number or array
|
|
@@ -273,6 +373,10 @@ export namespace TestValidator {
|
|
|
273
373
|
* @returns Void or Promise<void> based on the input type
|
|
274
374
|
* @throws Error when function doesn't throw HttpError or status code doesn't
|
|
275
375
|
* match
|
|
376
|
+
* @evidence contracts/common.md#principled-implementation The thrown value must expose a constructor named `HttpError` and one expected numeric status, matched by name because generated SDKs define their own classes. Missing or throwing properties make classification fail instead of escaping the expectation. The returned promise chain propagates rejection-handler failures without leaving a separately bridged promise pending.
|
|
377
|
+
* @evidence contracts/common.md#clear-and-simple-design One predicate reads the thrown value once and is shared by the synchronous and asynchronous paths; asynchronous settlement uses the task's own promise chain.
|
|
378
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The check follows the documented error shape, not a specific SDK or status list.
|
|
379
|
+
* @evidence contracts/common.md#meaningful-documentation The comment documents the status argument, the two task kinds, and the thrown error, with examples.
|
|
276
380
|
*/
|
|
277
381
|
export function httpError<T>(
|
|
278
382
|
title: string,
|
|
@@ -288,29 +392,36 @@ export namespace TestValidator {
|
|
|
288
392
|
: `Bug on ${title}: status code must be ${status.join(
|
|
289
393
|
" or ",
|
|
290
394
|
)}, but succeeded.`;
|
|
291
|
-
const predicate = (exp: any): Error | null =>
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
395
|
+
const predicate = (exp: any): Error | null => {
|
|
396
|
+
let actual: number | undefined;
|
|
397
|
+
try {
|
|
398
|
+
if (
|
|
399
|
+
typeof exp === "object" &&
|
|
400
|
+
exp !== null &&
|
|
401
|
+
exp.constructor?.name === "HttpError"
|
|
402
|
+
) {
|
|
403
|
+
const value: unknown = exp.status;
|
|
404
|
+
if (typeof value === "number") actual = value;
|
|
405
|
+
}
|
|
406
|
+
} catch {
|
|
407
|
+
// A thrown value with unreadable properties cannot establish an
|
|
408
|
+
// HTTP status; it fails the same expectation as any other value.
|
|
409
|
+
}
|
|
410
|
+
return actual !== undefined && status.some((val) => val === actual)
|
|
295
411
|
? null
|
|
296
|
-
: new Error(
|
|
297
|
-
|
|
298
|
-
typeof exp === "object" && exp.constructor.name === "HttpError"
|
|
299
|
-
? exp.status
|
|
300
|
-
: undefined,
|
|
301
|
-
),
|
|
302
|
-
);
|
|
412
|
+
: new Error(message(actual));
|
|
413
|
+
};
|
|
303
414
|
try {
|
|
304
415
|
const output: T = task();
|
|
305
416
|
if (is_promise(output))
|
|
306
|
-
return
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
417
|
+
return output.then(
|
|
418
|
+
() => {
|
|
419
|
+
throw new Error(message());
|
|
420
|
+
},
|
|
421
|
+
(exp) => {
|
|
422
|
+
const res: Error | null = predicate(exp);
|
|
423
|
+
if (res) throw res;
|
|
424
|
+
},
|
|
314
425
|
) as any;
|
|
315
426
|
else throw new Error(message());
|
|
316
427
|
} catch (exp) {
|
|
@@ -332,22 +443,40 @@ export namespace TestValidator {
|
|
|
332
443
|
* is compared. An empty result against expected entities, or the reverse,
|
|
333
444
|
* fails.
|
|
334
445
|
*
|
|
446
|
+
* Processing cost: Only the common prefix of P entities is sliced, converted
|
|
447
|
+
* to id strings and compared, giving O(P) entries and temporary storage;
|
|
448
|
+
* string conversion and comparison add their text costs.
|
|
449
|
+
*
|
|
335
450
|
* @example
|
|
336
|
-
* ```typescript
|
|
337
451
|
* // Test article pagination
|
|
338
|
-
* const expectedArticles = await db.articles.findAll({
|
|
339
|
-
*
|
|
452
|
+
* const expectedArticles = await db.articles.findAll({
|
|
453
|
+
* order: "created_at DESC",
|
|
454
|
+
* });
|
|
455
|
+
* const actualArticles = await api.functional.getArticles({
|
|
456
|
+
* page: 1,
|
|
457
|
+
* limit: 10,
|
|
458
|
+
* });
|
|
340
459
|
*
|
|
341
|
-
* TestValidator.index(
|
|
342
|
-
*
|
|
460
|
+
* TestValidator.index(
|
|
461
|
+
* "article pagination order",
|
|
462
|
+
* expectedArticles,
|
|
463
|
+
* actualArticles,
|
|
464
|
+
* true, // enable trace logging
|
|
343
465
|
* );
|
|
344
466
|
*
|
|
345
467
|
* // Test user search results
|
|
346
|
-
* const manuallyFilteredUsers = allUsers.filter(u =>
|
|
347
|
-
*
|
|
468
|
+
* const manuallyFilteredUsers = allUsers.filter((u) =>
|
|
469
|
+
* u.name.includes("John"),
|
|
470
|
+
* );
|
|
471
|
+
* const apiSearchResults = await api.functional.searchUsers({
|
|
472
|
+
* query: "John",
|
|
473
|
+
* });
|
|
348
474
|
*
|
|
349
|
-
* TestValidator.index(
|
|
350
|
-
*
|
|
475
|
+
* TestValidator.index(
|
|
476
|
+
* "user search results",
|
|
477
|
+
* manuallyFilteredUsers,
|
|
478
|
+
* apiSearchResults,
|
|
479
|
+
* );
|
|
351
480
|
*
|
|
352
481
|
* @param title - Descriptive title used in error messages when order differs
|
|
353
482
|
* @param expected - The expected entities in correct order
|
|
@@ -355,6 +484,10 @@ export namespace TestValidator {
|
|
|
355
484
|
* @param trace - Optional flag to enable debug logging (default: false)
|
|
356
485
|
* @throws Error when entity order differs between expected and actual results
|
|
357
486
|
* over their common prefix, or when exactly one of them is empty
|
|
487
|
+
* @evidence contracts/common.md#principled-implementation A page holds the first entities of the expected list, so the two id lists are compared over their common prefix, and one side being empty while the other is not is a difference because there is no prefix to compare; ids are compared as strings.
|
|
488
|
+
* @evidence contracts/common.md#clear-and-simple-design One function over the id lists produced by a private helper, with an optional trace flag.
|
|
489
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Only ids are compared, with no entity type or field named in the function.
|
|
490
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the prefix rule, the empty rule, the trace flag, and the thrown error, with examples.
|
|
358
491
|
*/
|
|
359
492
|
export const index = <X extends IEntity<any>, Y extends X = X>(
|
|
360
493
|
title: string,
|
|
@@ -375,11 +508,8 @@ export namespace TestValidator {
|
|
|
375
508
|
);
|
|
376
509
|
}
|
|
377
510
|
const length: number = Math.min(expected.length, gotten.length);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const xIds: string[] = get_ids(expected).slice(0, length);
|
|
382
|
-
const yIds: string[] = get_ids(gotten).slice(0, length);
|
|
511
|
+
const xIds: string[] = get_ids(expected.slice(0, length));
|
|
512
|
+
const yIds: string[] = get_ids(gotten.slice(0, length));
|
|
383
513
|
|
|
384
514
|
const equals: boolean = xIds.every((x, i) => x === yIds[i]);
|
|
385
515
|
if (equals === true) return;
|
|
@@ -402,15 +532,27 @@ export namespace TestValidator {
|
|
|
402
532
|
* API, and compares results. Validates that search APIs return the correct
|
|
403
533
|
* subset of data matching the search criteria.
|
|
404
534
|
*
|
|
535
|
+
* Processing cost: For K sampled entities from N entries, sparse sampling
|
|
536
|
+
* costs O(K), each reference filter scans N entries and each returned page
|
|
537
|
+
* prefix is compared once. Calls are sequential and one sample result is live
|
|
538
|
+
* at a time.
|
|
539
|
+
*
|
|
540
|
+
* Reuse: Each invocation must evaluate its supplied task or request to
|
|
541
|
+
* observe current behavior; prior results cannot replace these effectful
|
|
542
|
+
* checks.
|
|
543
|
+
*
|
|
544
|
+
* Resource ownership: The returned promise retains invocation-local state
|
|
545
|
+
* until settlement. The callback owns its external resources, and this
|
|
546
|
+
* validator provides no cancellation or timeout.
|
|
547
|
+
*
|
|
405
548
|
* @example
|
|
406
|
-
* ```typescript
|
|
407
549
|
* // Test article search functionality with exact matching
|
|
408
550
|
* const allArticles = await db.articles.findAll();
|
|
409
551
|
* const searchValidator = TestValidator.search(
|
|
410
552
|
* "article search API",
|
|
411
553
|
* (req) => api.searchArticles(req),
|
|
412
554
|
* allArticles,
|
|
413
|
-
* 5 // test with 5 random samples
|
|
555
|
+
* 5, // test with 5 random samples
|
|
414
556
|
* );
|
|
415
557
|
*
|
|
416
558
|
* // Test exact match search
|
|
@@ -418,7 +560,7 @@ export namespace TestValidator {
|
|
|
418
560
|
* fields: ["title"],
|
|
419
561
|
* values: (article) => [article.title], // full title for exact match
|
|
420
562
|
* filter: (article, [title]) => article.title === title, // exact match
|
|
421
|
-
* request: ([title]) => ({ search: { title } })
|
|
563
|
+
* request: ([title]) => ({ search: { title } }),
|
|
422
564
|
* });
|
|
423
565
|
*
|
|
424
566
|
* // Test partial match search with includes
|
|
@@ -426,7 +568,7 @@ export namespace TestValidator {
|
|
|
426
568
|
* fields: ["content"],
|
|
427
569
|
* values: (article) => [article.content.substring(0, 20)], // partial content
|
|
428
570
|
* filter: (article, [keyword]) => article.content.includes(keyword),
|
|
429
|
-
* request: ([keyword]) => ({ q: keyword })
|
|
571
|
+
* request: ([keyword]) => ({ q: keyword }),
|
|
430
572
|
* });
|
|
431
573
|
*
|
|
432
574
|
* // Test multi-field search with exact matching
|
|
@@ -435,9 +577,8 @@ export namespace TestValidator {
|
|
|
435
577
|
* values: (article) => [article.writer, article.title],
|
|
436
578
|
* filter: (article, [writer, title]) =>
|
|
437
579
|
* article.writer === writer && article.title === title,
|
|
438
|
-
* request: ([writer, title]) => ({ search: { writer, title } })
|
|
580
|
+
* request: ([writer, title]) => ({ search: { writer, title } }),
|
|
439
581
|
* });
|
|
440
|
-
* ```;
|
|
441
582
|
*
|
|
442
583
|
* @param title - Descriptive title used in error messages when search fails
|
|
443
584
|
* @param getter - API function that performs the search
|
|
@@ -445,6 +586,10 @@ export namespace TestValidator {
|
|
|
445
586
|
* @param sampleCount - Number of random samples to test (default: 1)
|
|
446
587
|
* @returns A function that accepts search configuration properties
|
|
447
588
|
* @throws Error when API search results don't match manual filtering results
|
|
589
|
+
* @evidence contracts/common.md#principled-implementation A sample of entities is drawn, the search values are read from each, the expected set is computed by a manual filter over the whole dataset, the API is called with the request built from the values, and `index` compares the two lists, so the API is checked against a filter the test author wrote independently.
|
|
590
|
+
* @evidence contracts/common.md#clear-and-simple-design The function is curried so the dataset and the API are given once and each search configuration is run against them, and the comparison is delegated to `index`.
|
|
591
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The sample, values, filter, and request all come from the caller's configuration.
|
|
592
|
+
* @evidence contracts/common.md#meaningful-documentation The comment documents the steps, the parameters, and the thrown error, with examples.
|
|
448
593
|
*/
|
|
449
594
|
export const search =
|
|
450
595
|
<Entity extends IEntity<any>, Request>(
|
|
@@ -482,6 +627,10 @@ export namespace TestValidator {
|
|
|
482
627
|
* @template Values - Tuple type representing the search values extracted from
|
|
483
628
|
* entities
|
|
484
629
|
* @template Request - Type of the API request object
|
|
630
|
+
* @evidence contracts/common.md#principled-implementation The record supplies what the check needs from a test author: the field names for the message and the three functions that extract values, filter, and build the request.
|
|
631
|
+
* @evidence contracts/common.md#clear-and-simple-design A four-member record with the functions as methods.
|
|
632
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It is a type and adds no runtime behavior.
|
|
633
|
+
* @evidence contracts/common.md#meaningful-documentation The comment names the role of the record and each member documents itself.
|
|
485
634
|
*/
|
|
486
635
|
export interface ISearchProps<
|
|
487
636
|
Entity extends IEntity<any>,
|
|
@@ -496,6 +645,10 @@ export namespace TestValidator {
|
|
|
496
645
|
*
|
|
497
646
|
* @param entity - The entity to extract search values from
|
|
498
647
|
* @returns Tuple of values used for searching
|
|
648
|
+
* @evidence contracts/common.md#principled-implementation The search values are read from a sampled entity, so the search is exercised with values that occur in the dataset.
|
|
649
|
+
* @evidence contracts/common.md#clear-and-simple-design One method with one entity argument.
|
|
650
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It is caller code.
|
|
651
|
+
* @evidence contracts/common.md#meaningful-documentation The comment documents the parameter and the return value.
|
|
499
652
|
*/
|
|
500
653
|
values(entity: Entity): Values;
|
|
501
654
|
|
|
@@ -505,6 +658,10 @@ export namespace TestValidator {
|
|
|
505
658
|
* @param entity - Entity to test against criteria
|
|
506
659
|
* @param values - Search values to match against
|
|
507
660
|
* @returns True if entity matches the search criteria
|
|
661
|
+
* @evidence contracts/common.md#principled-implementation The predicate decides whether an entity matches the values, which is the manual reference against which the API is compared.
|
|
662
|
+
* @evidence contracts/common.md#clear-and-simple-design One predicate over an entity and the values.
|
|
663
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It is caller code.
|
|
664
|
+
* @evidence contracts/common.md#meaningful-documentation The comment documents the parameters and the return value.
|
|
508
665
|
*/
|
|
509
666
|
filter(entity: Entity, values: Values): boolean;
|
|
510
667
|
|
|
@@ -513,6 +670,10 @@ export namespace TestValidator {
|
|
|
513
670
|
*
|
|
514
671
|
* @param values - Search values to include in request
|
|
515
672
|
* @returns Request object for the search API
|
|
673
|
+
* @evidence contracts/common.md#principled-implementation The function builds the API request from the values, so the same values drive the manual filter and the call.
|
|
674
|
+
* @evidence contracts/common.md#clear-and-simple-design One method from values to the request.
|
|
675
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It is caller code.
|
|
676
|
+
* @evidence contracts/common.md#meaningful-documentation The comment documents the parameter and the return value.
|
|
516
677
|
*/
|
|
517
678
|
request(values: Values): Request;
|
|
518
679
|
}
|
|
@@ -525,15 +686,24 @@ export namespace TestValidator {
|
|
|
525
686
|
* ascending/descending order, and optional filtering. Provides detailed error
|
|
526
687
|
* reporting for sorting failures.
|
|
527
688
|
*
|
|
689
|
+
* Processing cost: After one getter call, an optional filter scans N results
|
|
690
|
+
* and sortedness checks at most N minus one adjacent pairs. Signed field
|
|
691
|
+
* construction costs O(F); filter output costs O(N) space when requested.
|
|
692
|
+
*
|
|
693
|
+
* Reuse: Each invocation must evaluate its supplied task or request to
|
|
694
|
+
* observe current behavior; prior results cannot replace these effectful
|
|
695
|
+
* checks.
|
|
696
|
+
*
|
|
697
|
+
* Resource ownership: The returned promise retains invocation-local state
|
|
698
|
+
* until settlement. The callback owns its external resources, and this
|
|
699
|
+
* validator provides no cancellation or timeout.
|
|
700
|
+
*
|
|
528
701
|
* @example
|
|
529
|
-
* ```typescript
|
|
530
702
|
* // Test single field sorting with GaffComparator
|
|
531
703
|
* const sortValidator = TestValidator.sort(
|
|
532
704
|
* "article sorting",
|
|
533
|
-
* (sortable) => api.getArticles({ sort: sortable })
|
|
534
|
-
* )("created_at")(
|
|
535
|
-
* GaffComparator.dates((a) => a.created_at)
|
|
536
|
-
* );
|
|
705
|
+
* (sortable) => api.getArticles({ sort: sortable }),
|
|
706
|
+
* )("created_at")(GaffComparator.dates((a) => a.created_at));
|
|
537
707
|
*
|
|
538
708
|
* await sortValidator("+"); // ascending
|
|
539
709
|
* await sortValidator("-"); // descending
|
|
@@ -541,10 +711,10 @@ export namespace TestValidator {
|
|
|
541
711
|
* // Test multi-field sorting with GaffComparator
|
|
542
712
|
* const userSortValidator = TestValidator.sort(
|
|
543
713
|
* "user sorting",
|
|
544
|
-
* (sortable) => api.getUsers({ sort: sortable })
|
|
714
|
+
* (sortable) => api.getUsers({ sort: sortable }),
|
|
545
715
|
* )("lastName", "firstName")(
|
|
546
716
|
* GaffComparator.strings((user) => [user.lastName, user.firstName]),
|
|
547
|
-
* (user) => user.isActive // only test active users
|
|
717
|
+
* (user) => user.isActive, // only test active users
|
|
548
718
|
* );
|
|
549
719
|
*
|
|
550
720
|
* await userSortValidator("+", true); // ascending with trace logging
|
|
@@ -552,20 +722,24 @@ export namespace TestValidator {
|
|
|
552
722
|
* // Custom comparator for complex logic
|
|
553
723
|
* const customSortValidator = TestValidator.sort(
|
|
554
724
|
* "custom sorting",
|
|
555
|
-
* (sortable) => api.getProducts({ sort: sortable })
|
|
556
|
-
* )(
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
725
|
+
* (sortable) => api.getProducts({ sort: sortable }),
|
|
726
|
+
* )(
|
|
727
|
+
* "price",
|
|
728
|
+
* "rating",
|
|
729
|
+
* )((a, b) => {
|
|
730
|
+
* const priceDiff = a.price - b.price;
|
|
731
|
+
* return priceDiff !== 0 ? priceDiff : b.rating - a.rating; // price asc, rating desc
|
|
732
|
+
* });
|
|
563
733
|
*
|
|
564
734
|
* @param title - Descriptive title used in error messages when sorting fails
|
|
565
735
|
* @param getter - API function that fetches sorted data
|
|
566
736
|
* @returns A currying function chain: field names, comparator, then direction
|
|
567
737
|
* @throws Error when API results are not properly sorted according to
|
|
568
738
|
* specification
|
|
739
|
+
* @evidence contracts/common.md#principled-implementation The API is called with the signed field names, its result is optionally filtered, and the comparator (reversed for the descending direction) is applied to adjacent pairs, so any adjacent pair out of order fails; checking adjacent pairs suffices for a total preorder.
|
|
740
|
+
* @evidence contracts/common.md#clear-and-simple-design The function is curried in three steps so the request, the comparator, and the direction can be given at the moments each is known.
|
|
741
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The order is judged by the caller's comparator, with no field or direction special-cased.
|
|
742
|
+
* @evidence contracts/common.md#meaningful-documentation The comment documents each stage, the parameters, and the thrown error, with examples.
|
|
569
743
|
*/
|
|
570
744
|
export const sort =
|
|
571
745
|
<
|
|
@@ -611,14 +785,18 @@ export namespace TestValidator {
|
|
|
611
785
|
* prefixed with '+' for ascending order or '-' for descending order.
|
|
612
786
|
*
|
|
613
787
|
* @example
|
|
614
|
-
*
|
|
615
|
-
*
|
|
788
|
+
* type UserSortable = TestValidator.Sortable<
|
|
789
|
+
* "name" | "email" | "created_at"
|
|
790
|
+
* >;
|
|
616
791
|
* // Results in: Array<"-name" | "+name" | "-email" | "+email" | "-created_at" | "+created_at">
|
|
617
792
|
*
|
|
618
793
|
* const userSort: UserSortable = ["+name", "-created_at"];
|
|
619
|
-
* ```;
|
|
620
794
|
*
|
|
621
795
|
* @template Literal - String literal type representing available field names
|
|
796
|
+
* @evidence contracts/common.md#principled-implementation A sortable is an array of field names with a leading `+` or `-`, the notation the API under test receives.
|
|
797
|
+
* @evidence contracts/common.md#clear-and-simple-design A single template-literal type.
|
|
798
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It is a type and adds no runtime behavior.
|
|
799
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the notation.
|
|
622
800
|
*/
|
|
623
801
|
export type Sortable<Literal extends string> = Array<
|
|
624
802
|
`-${Literal}` | `+${Literal}`
|