@nestia/e2e 7.0.0 → 7.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.
@@ -2,18 +2,57 @@ import { RandomGenerator } from "./RandomGenerator";
2
2
  import { json_equal_to } from "./internal/json_equal_to";
3
3
 
4
4
  /**
5
- * Test validator.
5
+ * A comprehensive collection of E2E validation utilities for testing
6
+ * applications.
6
7
  *
7
- * `TestValidator` is a collection gathering E2E validation functions.
8
+ * TestValidator provides type-safe validation functions for common testing
9
+ * scenarios including condition checking, equality validation, error testing,
10
+ * HTTP error validation, pagination testing, search functionality validation,
11
+ * and sorting validation.
12
+ *
13
+ * All functions follow a currying pattern to enable reusable test
14
+ * configurations and provide detailed error messages for debugging failed
15
+ * assertions.
8
16
  *
9
17
  * @author Jeongho Nam - https://github.com/samchon
18
+ * @example
19
+ * ```typescript
20
+ * // Basic condition testing
21
+ * TestValidator.predicate("user should be authenticated")(user.isAuthenticated);
22
+ *
23
+ * // Equality validation
24
+ * TestValidator.equals("API response should match expected")(expected)(actual);
25
+ *
26
+ * // Error validation
27
+ * TestValidator.error("should throw on invalid input")(() => validateInput(""));
28
+ * ```;
10
29
  */
11
30
  export namespace TestValidator {
12
31
  /**
13
- * Test whether condition is satisfied.
32
+ * Validates that a given condition evaluates to true.
33
+ *
34
+ * Supports synchronous boolean values, synchronous functions returning
35
+ * boolean, and asynchronous functions returning Promise<boolean>. The return
36
+ * type is automatically inferred based on the input type.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * // Synchronous boolean
41
+ * TestValidator.predicate("user should exist")(user !== null);
42
+ *
43
+ * // Synchronous function
44
+ * TestValidator.predicate("array should be empty")(() => arr.length === 0);
14
45
  *
15
- * @param title Title of error message when condition is not satisfied
16
- * @return Currying function
46
+ * // Asynchronous function
47
+ * await TestValidator.predicate("database should be connected")(
48
+ * async () => await db.ping()
49
+ * );
50
+ * ```;
51
+ *
52
+ * @param title - Descriptive title used in error messages when validation
53
+ * fails
54
+ * @returns A currying function that accepts the condition to validate
55
+ * @throws Error with descriptive message when condition is not satisfied
17
56
  */
18
57
  export const predicate =
19
58
  (title: string) =>
@@ -48,21 +87,38 @@ export namespace TestValidator {
48
87
  };
49
88
 
50
89
  /**
51
- * Test whether two values are equal.
90
+ * Validates deep equality between two values using JSON comparison.
91
+ *
92
+ * Performs recursive comparison of objects and arrays. Supports an optional
93
+ * exception filter to ignore specific keys during comparison. Useful for
94
+ * validating API responses, data transformations, and object state changes.
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * // Basic equality
99
+ * TestValidator.equals("response should match expected")(expectedUser)(actualUser);
52
100
  *
53
- * If you want to validate `covers` relationship,
54
- * call smaller first and then larger.
101
+ * // Ignore timestamps in comparison
102
+ * TestValidator.equals("user data should match", (key) => key === "updatedAt")(
103
+ * expectedUser
104
+ * )(actualUser);
55
105
  *
56
- * Otherwise you wanna non equals validator, combine with {@link error}.
106
+ * // Validate API response structure
107
+ * const validateResponse = TestValidator.equals("API response structure");
108
+ * validateResponse({ id: 1, name: "John" })({ id: 1, name: "John" });
109
+ * ```;
57
110
  *
58
- * @param title Title of error message when different
59
- * @param exception Exception filter for ignoring some keys
60
- * @returns Currying function
111
+ * @param title - Descriptive title used in error messages when values differ
112
+ * @param exception - Optional filter function to exclude specific keys from
113
+ * comparison
114
+ * @returns A currying function chain: first accepts expected value, then
115
+ * actual value
116
+ * @throws Error with detailed diff information when values are not equal
61
117
  */
62
118
  export const equals =
63
119
  (title: string, exception: (key: string) => boolean = () => false) =>
64
120
  <T>(x: T) =>
65
- (y: T) => {
121
+ (y: T | null | undefined) => {
66
122
  const diff: string[] = json_equal_to(exception)(x)(y);
67
123
  if (diff.length)
68
124
  throw new Error(
@@ -75,13 +131,80 @@ export namespace TestValidator {
75
131
  };
76
132
 
77
133
  /**
78
- * Test whether error occurs.
134
+ * Validates deep inequality between two values using JSON comparison.
135
+ *
136
+ * Performs recursive comparison of objects and arrays to ensure they are NOT
137
+ * equal. Supports an optional exception filter to ignore specific keys during
138
+ * comparison. Useful for validating that data has changed, objects are
139
+ * different, or mutations have occurred.
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * // Basic inequality
144
+ * TestValidator.notEquals("user should be different after update")(originalUser)(updatedUser);
145
+ *
146
+ * // Ignore timestamps in comparison
147
+ * TestValidator.notEquals("user data should differ", (key) => key === "updatedAt")(
148
+ * originalUser
149
+ * )(modifiedUser);
150
+ *
151
+ * // Validate state changes
152
+ * const validateStateChange = TestValidator.notEquals("state should have changed");
153
+ * validateStateChange(initialState)(currentState);
154
+ * ```;
155
+ *
156
+ * @param title - Descriptive title used in error messages when values are
157
+ * equal
158
+ * @param exception - Optional filter function to exclude specific keys from
159
+ * comparison
160
+ * @returns A currying function chain: first accepts expected value, then
161
+ * actual value
162
+ * @throws Error when values are equal (indicating validation failure)
163
+ */
164
+ export const notEquals =
165
+ (title: string, exception: (key: string) => boolean = () => false) =>
166
+ <T>(x: T) =>
167
+ (y: T | null | undefined) => {
168
+ const diff: string[] = json_equal_to(exception)(x)(y);
169
+ if (diff.length === 0)
170
+ throw new Error(
171
+ [
172
+ `Bug on ${title}: values should be different but are equal:`,
173
+ "\n",
174
+ JSON.stringify({ x, y }, null, 2),
175
+ ].join("\n"),
176
+ );
177
+ };
178
+
179
+ /**
180
+ * Validates that a function throws an error or rejects when executed.
79
181
  *
80
- * If error occurs, nothing would be happened.
182
+ * Expects the provided function to fail. If the function executes
183
+ * successfully without throwing an error or rejecting, this validator will
184
+ * throw an exception. Supports both synchronous and asynchronous functions.
81
185
  *
82
- * However, no error exists, then exception would be thrown.
186
+ * @example
187
+ * ```typescript
188
+ * // Synchronous error validation
189
+ * TestValidator.error("should reject invalid email")(
190
+ * () => validateEmail("invalid-email")
191
+ * );
83
192
  *
84
- * @param title Title of exception because of no error exists
193
+ * // Asynchronous error validation
194
+ * await TestValidator.error("should reject unauthorized access")(
195
+ * async () => await api.functional.getSecretData()
196
+ * );
197
+ *
198
+ * // Validate input validation
199
+ * TestValidator.error("should throw on empty string")(
200
+ * () => processRequiredInput("")
201
+ * );
202
+ * ```;
203
+ *
204
+ * @param title - Descriptive title used in error messages when no error
205
+ * occurs
206
+ * @returns A currying function that accepts the task function to validate
207
+ * @throws Error when the task function does not throw an error or reject
85
208
  */
86
209
  export const error =
87
210
  (title: string) =>
@@ -99,6 +222,37 @@ export namespace TestValidator {
99
222
  }
100
223
  };
101
224
 
225
+ /**
226
+ * Validates that a function throws an HTTP error with specific status codes.
227
+ *
228
+ * Specialized error validator for HTTP operations. Validates that the
229
+ * function throws an HttpError with one of the specified status codes. Useful
230
+ * for testing API endpoints, authentication, and authorization logic.
231
+ *
232
+ * @example
233
+ * ```typescript
234
+ * // Validate 401 Unauthorized
235
+ * await TestValidator.httpError("should return 401 for invalid token")(401)(
236
+ * async () => await api.functional.getProtectedResource("invalid-token")
237
+ * );
238
+ *
239
+ * // Validate multiple possible error codes
240
+ * await TestValidator.httpError("should return client error")(400, 404, 422)(
241
+ * async () => await api.functional.updateNonexistentResource(data)
242
+ * );
243
+ *
244
+ * // Validate server errors
245
+ * TestValidator.httpError("should handle server errors")(500, 502, 503)(
246
+ * () => callFaultyEndpoint()
247
+ * );
248
+ * ```;
249
+ *
250
+ * @param title - Descriptive title used in error messages
251
+ * @returns A currying function that accepts status codes, then the task
252
+ * function
253
+ * @throws Error when function doesn't throw HttpError or status code doesn't
254
+ * match
255
+ */
102
256
  export const httpError =
103
257
  (title: string) =>
104
258
  (...statuses: number[]) =>
@@ -143,6 +297,37 @@ export namespace TestValidator {
143
297
  }
144
298
  };
145
299
 
300
+ /**
301
+ * Safely executes a function and captures any errors without throwing.
302
+ *
303
+ * Utility function for error handling in tests. Executes the provided
304
+ * function and returns any error that occurs, or null if successful. Supports
305
+ * both synchronous and asynchronous functions. Useful for testing error
306
+ * conditions without stopping test execution.
307
+ *
308
+ * @example
309
+ * ```typescript
310
+ * // Synchronous error capture
311
+ * const error = TestValidator.proceed(() => {
312
+ * throw new Error("Something went wrong");
313
+ * });
314
+ * console.log(error?.message); // "Something went wrong"
315
+ *
316
+ * // Asynchronous error capture
317
+ * const asyncError = await TestValidator.proceed(async () => {
318
+ * await failingAsyncOperation();
319
+ * });
320
+ *
321
+ * // Success case
322
+ * const noError = TestValidator.proceed(() => {
323
+ * return "success";
324
+ * });
325
+ * console.log(noError); // null
326
+ * ```;
327
+ *
328
+ * @param task - Function to execute safely
329
+ * @returns Error object if function throws/rejects, null if successful
330
+ */
146
331
  export function proceed(task: () => Promise<any>): Promise<Error | null>;
147
332
  export function proceed(task: () => any): Error | null;
148
333
  export function proceed(
@@ -163,16 +348,36 @@ export namespace TestValidator {
163
348
  }
164
349
 
165
350
  /**
166
- * Validate index API.
351
+ * Validates pagination index API results against expected entity order.
352
+ *
353
+ * Compares the order of entities returned by a pagination API with manually
354
+ * sorted expected results. Validates that entity IDs appear in the correct
355
+ * sequence. Commonly used for testing database queries, search results, and
356
+ * any paginated data APIs.
357
+ *
358
+ * @example
359
+ * ```typescript
360
+ * // Test article pagination
361
+ * const expectedArticles = await db.articles.findAll({ order: 'created_at DESC' });
362
+ * const actualArticles = await api.functional.getArticles({ page: 1, limit: 10 });
167
363
  *
168
- * Test whether two indexed values are equal.
364
+ * TestValidator.index("article pagination order")(expectedArticles)(
365
+ * actualArticles,
366
+ * true // enable trace logging
367
+ * );
169
368
  *
170
- * If two values are different, then exception would be thrown.
369
+ * // Test user search results
370
+ * const manuallyFilteredUsers = allUsers.filter(u => u.name.includes("John"));
371
+ * const apiSearchResults = await api.functional.searchUsers({ query: "John" });
171
372
  *
172
- * @param title Title of error message when different
173
- * @return Currying function
373
+ * TestValidator.index("user search results")(manuallyFilteredUsers)(
374
+ * apiSearchResults
375
+ * );
376
+ * ```;
174
377
  *
175
- * @example https://github.com/samchon/nestia-template/blob/master/src/test/features/api/bbs/test_api_bbs_article_index_search.ts
378
+ * @param title - Descriptive title used in error messages when order differs
379
+ * @returns A currying function chain: expected entities, then actual entities
380
+ * @throws Error when entity order differs between expected and actual results
176
381
  */
177
382
  export const index =
178
383
  (title: string) =>
@@ -203,31 +408,53 @@ export namespace TestValidator {
203
408
  };
204
409
 
205
410
  /**
206
- * Valiate search options.
411
+ * Validates search functionality by testing API results against manual
412
+ * filtering.
207
413
  *
208
- * Test a pagination API supporting search options.
414
+ * Comprehensive search validation that samples entities from a complete
415
+ * dataset, extracts search values, applies manual filtering, calls the search
416
+ * API, and compares results. Validates that search APIs return the correct
417
+ * subset of data matching the search criteria.
209
418
  *
210
- * @param title Title of error message when searching is invalid
211
- * @returns Currying function
419
+ * @example
420
+ * ```typescript
421
+ * // Test article search functionality
422
+ * const allArticles = await db.articles.findAll();
423
+ * const searchValidator = TestValidator.search("article search API")(
424
+ * (req) => api.searchArticles(req)
425
+ * )(allArticles, 5); // test with 5 random samples
212
426
  *
213
- * @example https://github.com/samchon/nestia-template/blob/master/src/test/features/api/bbs/test_api_bbs_article_index_search.ts
427
+ * await searchValidator({
428
+ * fields: ["title", "content"],
429
+ * values: (article) => [article.title.split(" ")[0]], // first word
430
+ * filter: (article, [keyword]) =>
431
+ * article.title.includes(keyword) || article.content.includes(keyword),
432
+ * request: ([keyword]) => ({ q: keyword })
433
+ * });
434
+ *
435
+ * // Test user search with multiple criteria
436
+ * await TestValidator.search("user search with filters")(
437
+ * (req) => api.getUsers(req)
438
+ * )(allUsers, 3)({
439
+ * fields: ["status", "role"],
440
+ * values: (user) => [user.status, user.role],
441
+ * filter: (user, [status, role]) =>
442
+ * user.status === status && user.role === role,
443
+ * request: ([status, role]) => ({ status, role })
444
+ * });
445
+ * ```;
446
+ *
447
+ * @param title - Descriptive title used in error messages when search fails
448
+ * @returns A currying function chain: API getter function, then dataset and
449
+ * sample count
450
+ * @throws Error when API search results don't match manual filtering results
214
451
  */
215
452
  export const search =
216
453
  (title: string) =>
217
- /**
218
- * @param getter A pagination API function to be called
219
- */
220
454
  <Entity extends IEntity<any>, Request>(
221
455
  getter: (input: Request) => Promise<Entity[]>,
222
456
  ) =>
223
- /**
224
- * @param total Total entity records for comparison
225
- * @param sampleCount Sampling count. Default is 1
226
- */
227
457
  (total: Entity[], sampleCount: number = 1) =>
228
- /**
229
- * @param props Search properties
230
- */
231
458
  async <Values extends any[]>(
232
459
  props: ISearchProps<Entity, Values, Request>,
233
460
  ): Promise<void> => {
@@ -245,34 +472,94 @@ export namespace TestValidator {
245
472
  }
246
473
  };
247
474
 
475
+ /**
476
+ * Configuration interface for search validation functionality.
477
+ *
478
+ * Defines the structure needed to validate search operations by specifying
479
+ * how to extract search values from entities, filter the dataset manually,
480
+ * and construct API requests.
481
+ *
482
+ * @template Entity - Type of entities being searched, must have an ID field
483
+ * @template Values - Tuple type representing the search values extracted from
484
+ * entities
485
+ * @template Request - Type of the API request object
486
+ */
248
487
  export interface ISearchProps<
249
488
  Entity extends IEntity<any>,
250
489
  Values extends any[],
251
490
  Request,
252
491
  > {
492
+ /** Field names being searched, used in error messages for identification */
253
493
  fields: string[];
494
+
495
+ /**
496
+ * Extracts search values from a sample entity
497
+ *
498
+ * @param entity - The entity to extract search values from
499
+ * @returns Tuple of values used for searching
500
+ */
254
501
  values(entity: Entity): Values;
502
+
503
+ /**
504
+ * Manual filter function to determine if an entity matches search criteria
505
+ *
506
+ * @param entity - Entity to test against criteria
507
+ * @param values - Search values to match against
508
+ * @returns True if entity matches the search criteria
509
+ */
255
510
  filter(entity: Entity, values: Values): boolean;
511
+
512
+ /**
513
+ * Constructs API request object from search values
514
+ *
515
+ * @param values - Search values to include in request
516
+ * @returns Request object for the search API
517
+ */
256
518
  request(values: Values): Request;
257
519
  }
258
520
 
259
521
  /**
260
- * Validate sorting options.
522
+ * Validates sorting functionality of pagination APIs.
523
+ *
524
+ * Tests sorting operations by calling the API with sort parameters and
525
+ * validating that results are correctly ordered. Supports multiple fields,
526
+ * ascending/descending order, and optional filtering. Provides detailed error
527
+ * reporting for sorting failures.
528
+ *
529
+ * @example
530
+ * ```typescript
531
+ * // Test single field sorting
532
+ * const sortValidator = TestValidator.sort("article sorting")(
533
+ * (sortable) => api.getArticles({ sort: sortable })
534
+ * )("created_at")(
535
+ * (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
536
+ * );
261
537
  *
262
- * Test a pagination API supporting sorting options.
538
+ * await sortValidator("+"); // ascending
539
+ * await sortValidator("-"); // descending
263
540
  *
264
- * You can validate detailed sorting options both ascending and descending orders
265
- * with multiple fields. However, as it forms a complicate currying function,
266
- * I recommend you to see below example code before using.
541
+ * // Test multi-field sorting with filtering
542
+ * const userSortValidator = TestValidator.sort("user sorting")(
543
+ * (sortable) => api.getUsers({ sort: sortable })
544
+ * )("status", "created_at")(
545
+ * (a, b) => {
546
+ * if (a.status !== b.status) return a.status.localeCompare(b.status);
547
+ * return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
548
+ * },
549
+ * (user) => user.isActive // only test active users
550
+ * );
267
551
  *
268
- * @param title Title of error message when sorting is invalid
269
- * @example https://github.com/samchon/nestia-template/blob/master/src/test/features/api/bbs/test_api_bbs_article_index_sort.ts
552
+ * await userSortValidator("+", true); // ascending with trace logging
553
+ * ```;
554
+ *
555
+ * @param title - Descriptive title used in error messages when sorting fails
556
+ * @returns A currying function chain: API getter, field names, comparator,
557
+ * then direction
558
+ * @throws Error when API results are not properly sorted according to
559
+ * specification
270
560
  */
271
561
  export const sort =
272
562
  (title: string) =>
273
- /**
274
- * @param getter A pagination API function to be called
275
- */
276
563
  <
277
564
  T extends object,
278
565
  Fields extends string,
@@ -282,18 +569,8 @@ export namespace TestValidator {
282
569
  >(
283
570
  getter: (sortable: Sortable) => Promise<T[]>,
284
571
  ) =>
285
- /**
286
- * @param fields List of fields to be sorted
287
- */
288
572
  (...fields: Fields[]) =>
289
- /**
290
- * @param comp Comparator function for validation
291
- * @param filter Filter function for data if required
292
- */
293
573
  (comp: (x: T, y: T) => number, filter?: (elem: T) => boolean) =>
294
- /**
295
- * @param direction "+" means ascending order, and "-" means descending order
296
- */
297
574
  async (direction: "+" | "-", trace: boolean = false) => {
298
575
  let data: T[] = await getter(
299
576
  fields.map((field) => `${direction}${field}` as const) as Sortable,
@@ -318,6 +595,22 @@ export namespace TestValidator {
318
595
  }
319
596
  };
320
597
 
598
+ /**
599
+ * Type alias for sortable field specifications.
600
+ *
601
+ * Represents an array of sort field specifications where each field can be
602
+ * prefixed with '+' for ascending order or '-' for descending order.
603
+ *
604
+ * @example
605
+ * ```typescript
606
+ * type UserSortable = TestValidator.Sortable<"name" | "email" | "created_at">;
607
+ * // Results in: Array<"-name" | "+name" | "-email" | "+email" | "-created_at" | "+created_at">
608
+ *
609
+ * const userSort: UserSortable = ["+name", "-created_at"];
610
+ * ```;
611
+ *
612
+ * @template Literal - String literal type representing available field names
613
+ */
321
614
  export type Sortable<Literal extends string> = Array<
322
615
  `-${Literal}` | `+${Literal}`
323
616
  >;
@@ -1,7 +1,7 @@
1
1
  export const json_equal_to =
2
2
  (exception: (key: string) => boolean) =>
3
3
  <T>(x: T) =>
4
- (y: T): string[] => {
4
+ (y: T | null | undefined): string[] => {
5
5
  const container: string[] = [];
6
6
  const iterate =
7
7
  (accessor: string) =>