@nestia/e2e 6.0.6 → 7.0.0-dev.20250604

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.
@@ -1,347 +1,347 @@
1
- import { RandomGenerator } from "./RandomGenerator";
2
- import { json_equal_to } from "./internal/json_equal_to";
3
-
4
- /**
5
- * Test validator.
6
- *
7
- * `TestValidator` is a collection gathering E2E validation functions.
8
- *
9
- * @author Jeongho Nam - https://github.com/samchon
10
- */
11
- export namespace TestValidator {
12
- /**
13
- * Test whether condition is satisfied.
14
- *
15
- * @param title Title of error message when condition is not satisfied
16
- * @return Currying function
17
- */
18
- export const predicate =
19
- (title: string) =>
20
- <T extends boolean | (() => boolean) | (() => Promise<boolean>)>(
21
- condition: T,
22
- ): T extends () => Promise<boolean> ? Promise<void> : void => {
23
- const message = () =>
24
- `Bug on ${title}: expected condition is not satisfied.`;
25
-
26
- // SCALAR
27
- if (typeof condition === "boolean") {
28
- if (condition !== true) throw new Error(message());
29
- return undefined as any;
30
- }
31
-
32
- // CLOSURE
33
- const output: boolean | Promise<boolean> = condition();
34
- if (typeof output === "boolean") {
35
- if (output !== true) throw new Error(message());
36
- return undefined as any;
37
- }
38
-
39
- // ASYNCHRONOUS
40
- return new Promise<void>((resolve, reject) => {
41
- output
42
- .then((flag) => {
43
- if (flag === true) resolve();
44
- else reject(message());
45
- })
46
- .catch(reject);
47
- }) as any;
48
- };
49
-
50
- /**
51
- * Test whether two values are equal.
52
- *
53
- * If you want to validate `covers` relationship,
54
- * call smaller first and then larger.
55
- *
56
- * Otherwise you wanna non equals validator, combine with {@link error}.
57
- *
58
- * @param title Title of error message when different
59
- * @param exception Exception filter for ignoring some keys
60
- * @returns Currying function
61
- */
62
- export const equals =
63
- (title: string, exception: (key: string) => boolean = () => false) =>
64
- <T>(x: T) =>
65
- (y: T) => {
66
- const diff: string[] = json_equal_to(exception)(x)(y);
67
- if (diff.length)
68
- throw new Error(
69
- [
70
- `Bug on ${title}: found different values - [${diff.join(", ")}]:`,
71
- "\n",
72
- JSON.stringify({ x, y }, null, 2),
73
- ].join("\n"),
74
- );
75
- };
76
-
77
- /**
78
- * Test whether error occurs.
79
- *
80
- * If error occurs, nothing would be happened.
81
- *
82
- * However, no error exists, then exception would be thrown.
83
- *
84
- * @param title Title of exception because of no error exists
85
- */
86
- export const error =
87
- (title: string) =>
88
- <T>(task: () => T): T extends Promise<any> ? Promise<void> : void => {
89
- const message = () => `Bug on ${title}: exception must be thrown.`;
90
- try {
91
- const output: T = task();
92
- if (is_promise(output))
93
- return new Promise<void>((resolve, reject) =>
94
- output.catch(() => resolve()).then(() => reject(message())),
95
- ) as any;
96
- else throw new Error(message());
97
- } catch {
98
- return undefined as any;
99
- }
100
- };
101
-
102
- export const httpError =
103
- (title: string) =>
104
- (...statuses: number[]) =>
105
- <T>(task: () => T): T extends Promise<any> ? Promise<void> : void => {
106
- const message = (actual?: number) =>
107
- typeof actual === "number"
108
- ? `Bug on ${title}: status code must be ${statuses.join(
109
- " or ",
110
- )}, but ${actual}.`
111
- : `Bug on ${title}: status code must be ${statuses.join(
112
- " or ",
113
- )}, but succeeded.`;
114
- const predicate = (exp: any): Error | null =>
115
- typeof exp === "object" &&
116
- exp.constructor.name === "HttpError" &&
117
- statuses.some((val) => val === exp.status)
118
- ? null
119
- : new Error(
120
- message(
121
- typeof exp === "object" && exp.constructor.name === "HttpError"
122
- ? exp.status
123
- : undefined,
124
- ),
125
- );
126
- try {
127
- const output: T = task();
128
- if (is_promise(output))
129
- return new Promise<void>((resolve, reject) =>
130
- output
131
- .catch((exp) => {
132
- const res: Error | null = predicate(exp);
133
- if (res) reject(res);
134
- else resolve();
135
- })
136
- .then(() => reject(new Error(message()))),
137
- ) as any;
138
- else throw new Error(message());
139
- } catch (exp) {
140
- const res: Error | null = predicate(exp);
141
- if (res) throw res;
142
- return undefined!;
143
- }
144
- };
145
-
146
- export function proceed(task: () => Promise<any>): Promise<Error | null>;
147
- export function proceed(task: () => any): Error | null;
148
- export function proceed(
149
- task: () => any,
150
- ): Promise<Error | null> | (Error | null) {
151
- try {
152
- const output: any = task();
153
- if (is_promise(output))
154
- return new Promise<Error | null>((resolve) =>
155
- output
156
- .catch((exp) => resolve(exp as Error))
157
- .then(() => resolve(null)),
158
- );
159
- } catch (exp) {
160
- return exp as Error;
161
- }
162
- return null;
163
- }
164
-
165
- /**
166
- * Validate index API.
167
- *
168
- * Test whether two indexed values are equal.
169
- *
170
- * If two values are different, then exception would be thrown.
171
- *
172
- * @param title Title of error message when different
173
- * @return Currying function
174
- *
175
- * @example https://github.com/samchon/nestia-template/blob/master/src/test/features/api/bbs/test_api_bbs_article_index_search.ts
176
- */
177
- export const index =
178
- (title: string) =>
179
- <Solution extends IEntity<any>>(expected: Solution[]) =>
180
- <Summary extends IEntity<any>>(
181
- gotten: Summary[],
182
- trace: boolean = false,
183
- ): void => {
184
- const length: number = Math.min(expected.length, gotten.length);
185
- expected = expected.slice(0, length);
186
- gotten = gotten.slice(0, length);
187
-
188
- const xIds: string[] = get_ids(expected).slice(0, length);
189
- const yIds: string[] = get_ids(gotten)
190
- .filter((id) => id >= xIds[0])
191
- .slice(0, length);
192
-
193
- const equals: boolean = xIds.every((x, i) => x === yIds[i]);
194
- if (equals === true) return;
195
- else if (trace === true)
196
- console.log({
197
- expected: xIds,
198
- gotten: yIds,
199
- });
200
- throw new Error(
201
- `Bug on ${title}: result of the index is different with manual aggregation.`,
202
- );
203
- };
204
-
205
- /**
206
- * Valiate search options.
207
- *
208
- * Test a pagination API supporting search options.
209
- *
210
- * @param title Title of error message when searching is invalid
211
- * @returns Currying function
212
- *
213
- * @example https://github.com/samchon/nestia-template/blob/master/src/test/features/api/bbs/test_api_bbs_article_index_search.ts
214
- */
215
- export const search =
216
- (title: string) =>
217
- /**
218
- * @param getter A pagination API function to be called
219
- */
220
- <Entity extends IEntity<any>, Request>(
221
- getter: (input: Request) => Promise<Entity[]>,
222
- ) =>
223
- /**
224
- * @param total Total entity records for comparison
225
- * @param sampleCount Sampling count. Default is 1
226
- */
227
- (total: Entity[], sampleCount: number = 1) =>
228
- /**
229
- * @param props Search properties
230
- */
231
- async <Values extends any[]>(
232
- props: ISearchProps<Entity, Values, Request>,
233
- ): Promise<void> => {
234
- const samples: Entity[] = RandomGenerator.sample(total)(sampleCount);
235
- for (const s of samples) {
236
- const values: Values = props.values(s);
237
- const filtered: Entity[] = total.filter((entity) =>
238
- props.filter(entity, values),
239
- );
240
- const gotten: Entity[] = await getter(props.request(values));
241
-
242
- TestValidator.index(`${title} (${props.fields.join(", ")})`)(filtered)(
243
- gotten,
244
- );
245
- }
246
- };
247
-
248
- export interface ISearchProps<
249
- Entity extends IEntity<any>,
250
- Values extends any[],
251
- Request,
252
- > {
253
- fields: string[];
254
- values(entity: Entity): Values;
255
- filter(entity: Entity, values: Values): boolean;
256
- request(values: Values): Request;
257
- }
258
-
259
- /**
260
- * Validate sorting options.
261
- *
262
- * Test a pagination API supporting sorting options.
263
- *
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.
267
- *
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
270
- */
271
- export const sort =
272
- (title: string) =>
273
- /**
274
- * @param getter A pagination API function to be called
275
- */
276
- <
277
- T extends object,
278
- Fields extends string,
279
- Sortable extends Array<`-${Fields}` | `+${Fields}`> = Array<
280
- `-${Fields}` | `+${Fields}`
281
- >,
282
- >(
283
- getter: (sortable: Sortable) => Promise<T[]>,
284
- ) =>
285
- /**
286
- * @param fields List of fields to be sorted
287
- */
288
- (...fields: Fields[]) =>
289
- /**
290
- * @param comp Comparator function for validation
291
- * @param filter Filter function for data if required
292
- */
293
- (comp: (x: T, y: T) => number, filter?: (elem: T) => boolean) =>
294
- /**
295
- * @param direction "+" means ascending order, and "-" means descending order
296
- */
297
- async (direction: "+" | "-", trace: boolean = false) => {
298
- let data: T[] = await getter(
299
- fields.map((field) => `${direction}${field}` as const) as Sortable,
300
- );
301
- if (filter) data = data.filter(filter);
302
-
303
- const reversed: typeof comp =
304
- direction === "+" ? comp : (x, y) => comp(y, x);
305
- if (is_sorted(data, reversed) === false) {
306
- if (
307
- fields.length === 1 &&
308
- data.length &&
309
- (data as any)[0][fields[0]] !== undefined &&
310
- trace
311
- )
312
- console.log(data.map((elem) => (elem as any)[fields[0]]));
313
- throw new Error(
314
- `Bug on ${title}: wrong sorting on ${direction}(${fields.join(
315
- ", ",
316
- )}).`,
317
- );
318
- }
319
- };
320
-
321
- export type Sortable<Literal extends string> = Array<
322
- `-${Literal}` | `+${Literal}`
323
- >;
324
- }
325
-
326
- interface IEntity<Type extends string | number | bigint> {
327
- id: Type;
328
- }
329
-
330
- function get_ids<Entity extends IEntity<any>>(entities: Entity[]): string[] {
331
- return entities.map((entity) => entity.id).sort((x, y) => (x < y ? -1 : 1));
332
- }
333
-
334
- function is_promise(input: any): input is Promise<any> {
335
- return (
336
- typeof input === "object" &&
337
- input !== null &&
338
- typeof (input as any).then === "function" &&
339
- typeof (input as any).catch === "function"
340
- );
341
- }
342
-
343
- function is_sorted<T>(data: T[], comp: (x: T, y: T) => number): boolean {
344
- for (let i: number = 1; i < data.length; ++i)
345
- if (comp(data[i - 1], data[i]) > 0) return false;
346
- return true;
347
- }
1
+ import { RandomGenerator } from "./RandomGenerator";
2
+ import { json_equal_to } from "./internal/json_equal_to";
3
+
4
+ /**
5
+ * Test validator.
6
+ *
7
+ * `TestValidator` is a collection gathering E2E validation functions.
8
+ *
9
+ * @author Jeongho Nam - https://github.com/samchon
10
+ */
11
+ export namespace TestValidator {
12
+ /**
13
+ * Test whether condition is satisfied.
14
+ *
15
+ * @param title Title of error message when condition is not satisfied
16
+ * @return Currying function
17
+ */
18
+ export const predicate =
19
+ (title: string) =>
20
+ <T extends boolean | (() => boolean) | (() => Promise<boolean>)>(
21
+ condition: T,
22
+ ): T extends () => Promise<boolean> ? Promise<void> : void => {
23
+ const message = () =>
24
+ `Bug on ${title}: expected condition is not satisfied.`;
25
+
26
+ // SCALAR
27
+ if (typeof condition === "boolean") {
28
+ if (condition !== true) throw new Error(message());
29
+ return undefined as any;
30
+ }
31
+
32
+ // CLOSURE
33
+ const output: boolean | Promise<boolean> = condition();
34
+ if (typeof output === "boolean") {
35
+ if (output !== true) throw new Error(message());
36
+ return undefined as any;
37
+ }
38
+
39
+ // ASYNCHRONOUS
40
+ return new Promise<void>((resolve, reject) => {
41
+ output
42
+ .then((flag) => {
43
+ if (flag === true) resolve();
44
+ else reject(message());
45
+ })
46
+ .catch(reject);
47
+ }) as any;
48
+ };
49
+
50
+ /**
51
+ * Test whether two values are equal.
52
+ *
53
+ * If you want to validate `covers` relationship,
54
+ * call smaller first and then larger.
55
+ *
56
+ * Otherwise you wanna non equals validator, combine with {@link error}.
57
+ *
58
+ * @param title Title of error message when different
59
+ * @param exception Exception filter for ignoring some keys
60
+ * @returns Currying function
61
+ */
62
+ export const equals =
63
+ (title: string, exception: (key: string) => boolean = () => false) =>
64
+ <T>(x: T) =>
65
+ (y: T) => {
66
+ const diff: string[] = json_equal_to(exception)(x)(y);
67
+ if (diff.length)
68
+ throw new Error(
69
+ [
70
+ `Bug on ${title}: found different values - [${diff.join(", ")}]:`,
71
+ "\n",
72
+ JSON.stringify({ x, y }, null, 2),
73
+ ].join("\n"),
74
+ );
75
+ };
76
+
77
+ /**
78
+ * Test whether error occurs.
79
+ *
80
+ * If error occurs, nothing would be happened.
81
+ *
82
+ * However, no error exists, then exception would be thrown.
83
+ *
84
+ * @param title Title of exception because of no error exists
85
+ */
86
+ export const error =
87
+ (title: string) =>
88
+ <T>(task: () => T): T extends Promise<any> ? Promise<void> : void => {
89
+ const message = () => `Bug on ${title}: exception must be thrown.`;
90
+ try {
91
+ const output: T = task();
92
+ if (is_promise(output))
93
+ return new Promise<void>((resolve, reject) =>
94
+ output.catch(() => resolve()).then(() => reject(message())),
95
+ ) as any;
96
+ else throw new Error(message());
97
+ } catch {
98
+ return undefined as any;
99
+ }
100
+ };
101
+
102
+ export const httpError =
103
+ (title: string) =>
104
+ (...statuses: number[]) =>
105
+ <T>(task: () => T): T extends Promise<any> ? Promise<void> : void => {
106
+ const message = (actual?: number) =>
107
+ typeof actual === "number"
108
+ ? `Bug on ${title}: status code must be ${statuses.join(
109
+ " or ",
110
+ )}, but ${actual}.`
111
+ : `Bug on ${title}: status code must be ${statuses.join(
112
+ " or ",
113
+ )}, but succeeded.`;
114
+ const predicate = (exp: any): Error | null =>
115
+ typeof exp === "object" &&
116
+ exp.constructor.name === "HttpError" &&
117
+ statuses.some((val) => val === exp.status)
118
+ ? null
119
+ : new Error(
120
+ message(
121
+ typeof exp === "object" && exp.constructor.name === "HttpError"
122
+ ? exp.status
123
+ : undefined,
124
+ ),
125
+ );
126
+ try {
127
+ const output: T = task();
128
+ if (is_promise(output))
129
+ return new Promise<void>((resolve, reject) =>
130
+ output
131
+ .catch((exp) => {
132
+ const res: Error | null = predicate(exp);
133
+ if (res) reject(res);
134
+ else resolve();
135
+ })
136
+ .then(() => reject(new Error(message()))),
137
+ ) as any;
138
+ else throw new Error(message());
139
+ } catch (exp) {
140
+ const res: Error | null = predicate(exp);
141
+ if (res) throw res;
142
+ return undefined!;
143
+ }
144
+ };
145
+
146
+ export function proceed(task: () => Promise<any>): Promise<Error | null>;
147
+ export function proceed(task: () => any): Error | null;
148
+ export function proceed(
149
+ task: () => any,
150
+ ): Promise<Error | null> | (Error | null) {
151
+ try {
152
+ const output: any = task();
153
+ if (is_promise(output))
154
+ return new Promise<Error | null>((resolve) =>
155
+ output
156
+ .catch((exp) => resolve(exp as Error))
157
+ .then(() => resolve(null)),
158
+ );
159
+ } catch (exp) {
160
+ return exp as Error;
161
+ }
162
+ return null;
163
+ }
164
+
165
+ /**
166
+ * Validate index API.
167
+ *
168
+ * Test whether two indexed values are equal.
169
+ *
170
+ * If two values are different, then exception would be thrown.
171
+ *
172
+ * @param title Title of error message when different
173
+ * @return Currying function
174
+ *
175
+ * @example https://github.com/samchon/nestia-template/blob/master/src/test/features/api/bbs/test_api_bbs_article_index_search.ts
176
+ */
177
+ export const index =
178
+ (title: string) =>
179
+ <Solution extends IEntity<any>>(expected: Solution[]) =>
180
+ <Summary extends IEntity<any>>(
181
+ gotten: Summary[],
182
+ trace: boolean = false,
183
+ ): void => {
184
+ const length: number = Math.min(expected.length, gotten.length);
185
+ expected = expected.slice(0, length);
186
+ gotten = gotten.slice(0, length);
187
+
188
+ const xIds: string[] = get_ids(expected).slice(0, length);
189
+ const yIds: string[] = get_ids(gotten)
190
+ .filter((id) => id >= xIds[0])
191
+ .slice(0, length);
192
+
193
+ const equals: boolean = xIds.every((x, i) => x === yIds[i]);
194
+ if (equals === true) return;
195
+ else if (trace === true)
196
+ console.log({
197
+ expected: xIds,
198
+ gotten: yIds,
199
+ });
200
+ throw new Error(
201
+ `Bug on ${title}: result of the index is different with manual aggregation.`,
202
+ );
203
+ };
204
+
205
+ /**
206
+ * Valiate search options.
207
+ *
208
+ * Test a pagination API supporting search options.
209
+ *
210
+ * @param title Title of error message when searching is invalid
211
+ * @returns Currying function
212
+ *
213
+ * @example https://github.com/samchon/nestia-template/blob/master/src/test/features/api/bbs/test_api_bbs_article_index_search.ts
214
+ */
215
+ export const search =
216
+ (title: string) =>
217
+ /**
218
+ * @param getter A pagination API function to be called
219
+ */
220
+ <Entity extends IEntity<any>, Request>(
221
+ getter: (input: Request) => Promise<Entity[]>,
222
+ ) =>
223
+ /**
224
+ * @param total Total entity records for comparison
225
+ * @param sampleCount Sampling count. Default is 1
226
+ */
227
+ (total: Entity[], sampleCount: number = 1) =>
228
+ /**
229
+ * @param props Search properties
230
+ */
231
+ async <Values extends any[]>(
232
+ props: ISearchProps<Entity, Values, Request>,
233
+ ): Promise<void> => {
234
+ const samples: Entity[] = RandomGenerator.sample(total)(sampleCount);
235
+ for (const s of samples) {
236
+ const values: Values = props.values(s);
237
+ const filtered: Entity[] = total.filter((entity) =>
238
+ props.filter(entity, values),
239
+ );
240
+ const gotten: Entity[] = await getter(props.request(values));
241
+
242
+ TestValidator.index(`${title} (${props.fields.join(", ")})`)(filtered)(
243
+ gotten,
244
+ );
245
+ }
246
+ };
247
+
248
+ export interface ISearchProps<
249
+ Entity extends IEntity<any>,
250
+ Values extends any[],
251
+ Request,
252
+ > {
253
+ fields: string[];
254
+ values(entity: Entity): Values;
255
+ filter(entity: Entity, values: Values): boolean;
256
+ request(values: Values): Request;
257
+ }
258
+
259
+ /**
260
+ * Validate sorting options.
261
+ *
262
+ * Test a pagination API supporting sorting options.
263
+ *
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.
267
+ *
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
270
+ */
271
+ export const sort =
272
+ (title: string) =>
273
+ /**
274
+ * @param getter A pagination API function to be called
275
+ */
276
+ <
277
+ T extends object,
278
+ Fields extends string,
279
+ Sortable extends Array<`-${Fields}` | `+${Fields}`> = Array<
280
+ `-${Fields}` | `+${Fields}`
281
+ >,
282
+ >(
283
+ getter: (sortable: Sortable) => Promise<T[]>,
284
+ ) =>
285
+ /**
286
+ * @param fields List of fields to be sorted
287
+ */
288
+ (...fields: Fields[]) =>
289
+ /**
290
+ * @param comp Comparator function for validation
291
+ * @param filter Filter function for data if required
292
+ */
293
+ (comp: (x: T, y: T) => number, filter?: (elem: T) => boolean) =>
294
+ /**
295
+ * @param direction "+" means ascending order, and "-" means descending order
296
+ */
297
+ async (direction: "+" | "-", trace: boolean = false) => {
298
+ let data: T[] = await getter(
299
+ fields.map((field) => `${direction}${field}` as const) as Sortable,
300
+ );
301
+ if (filter) data = data.filter(filter);
302
+
303
+ const reversed: typeof comp =
304
+ direction === "+" ? comp : (x, y) => comp(y, x);
305
+ if (is_sorted(data, reversed) === false) {
306
+ if (
307
+ fields.length === 1 &&
308
+ data.length &&
309
+ (data as any)[0][fields[0]] !== undefined &&
310
+ trace
311
+ )
312
+ console.log(data.map((elem) => (elem as any)[fields[0]]));
313
+ throw new Error(
314
+ `Bug on ${title}: wrong sorting on ${direction}(${fields.join(
315
+ ", ",
316
+ )}).`,
317
+ );
318
+ }
319
+ };
320
+
321
+ export type Sortable<Literal extends string> = Array<
322
+ `-${Literal}` | `+${Literal}`
323
+ >;
324
+ }
325
+
326
+ interface IEntity<Type extends string | number | bigint> {
327
+ id: Type;
328
+ }
329
+
330
+ function get_ids<Entity extends IEntity<any>>(entities: Entity[]): string[] {
331
+ return entities.map((entity) => entity.id).sort((x, y) => (x < y ? -1 : 1));
332
+ }
333
+
334
+ function is_promise(input: any): input is Promise<any> {
335
+ return (
336
+ typeof input === "object" &&
337
+ input !== null &&
338
+ typeof (input as any).then === "function" &&
339
+ typeof (input as any).catch === "function"
340
+ );
341
+ }
342
+
343
+ function is_sorted<T>(data: T[], comp: (x: T, y: T) => number): boolean {
344
+ for (let i: number = 1; i < data.length; ++i)
345
+ if (comp(data[i - 1], data[i]) > 0) return false;
346
+ return true;
347
+ }