@20syldev/api 4.6.0 → 4.8.0
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/README.md +5 -3
- package/dist/app.js +6 -6
- package/dist/app.js.map +1 -1
- package/dist/config/env.js.map +1 -1
- package/dist/config/versions.js +22 -3
- package/dist/config/versions.js.map +1 -1
- package/dist/constants.js +27 -12
- package/dist/constants.js.map +1 -1
- package/dist/middleware/error.js +2 -2
- package/dist/middleware/error.js.map +1 -1
- package/dist/middleware/logger.js.map +1 -1
- package/dist/middleware/ratelimit.js +2 -2
- package/dist/middleware/ratelimit.js.map +1 -1
- package/dist/middleware/version.js.map +1 -1
- package/dist/modules/v3.js +1 -1
- package/dist/modules/v3.js.map +1 -1
- package/dist/modules/v4/address.js +381 -0
- package/dist/modules/v4/address.js.map +1 -0
- package/dist/modules/v4/agent.js +92 -0
- package/dist/modules/v4/agent.js.map +1 -0
- package/dist/modules/v4/algorithms.js +79 -0
- package/dist/modules/v4/algorithms.js.map +1 -1
- package/dist/modules/v4/avatar.js +136 -0
- package/dist/modules/v4/avatar.js.map +1 -0
- package/dist/modules/v4/barcode.js +440 -0
- package/dist/modules/v4/barcode.js.map +1 -0
- package/dist/modules/v4/captcha.js +7 -0
- package/dist/modules/v4/captcha.js.map +1 -1
- package/dist/modules/v4/chat.js +9 -1
- package/dist/modules/v4/chat.js.map +1 -1
- package/dist/modules/v4/color.js +8 -1
- package/dist/modules/v4/color.js.map +1 -1
- package/dist/modules/v4/convert.js +9 -0
- package/dist/modules/v4/convert.js.map +1 -1
- package/dist/modules/v4/credit.js +104 -0
- package/dist/modules/v4/credit.js.map +1 -0
- package/dist/modules/v4/cron.js +202 -0
- package/dist/modules/v4/cron.js.map +1 -0
- package/dist/modules/v4/dice.js +7 -0
- package/dist/modules/v4/dice.js.map +1 -1
- package/dist/modules/v4/domain.js +6 -1
- package/dist/modules/v4/domain.js.map +1 -1
- package/dist/modules/v4/encode.js +71 -0
- package/dist/modules/v4/encode.js.map +1 -1
- package/dist/modules/v4/geo.js +11 -1
- package/dist/modules/v4/geo.js.map +1 -1
- package/dist/modules/v4/hash.js +10 -1
- package/dist/modules/v4/hash.js.map +1 -1
- package/dist/modules/v4/hyperplanning.js +9 -1
- package/dist/modules/v4/hyperplanning.js.map +1 -1
- package/dist/modules/v4/ip.js +132 -0
- package/dist/modules/v4/ip.js.map +1 -0
- package/dist/modules/v4/levenshtein.js +8 -0
- package/dist/modules/v4/levenshtein.js.map +1 -1
- package/dist/modules/v4/palette.js +9 -1
- package/dist/modules/v4/palette.js.map +1 -1
- package/dist/modules/v4/password.js +343 -0
- package/dist/modules/v4/password.js.map +1 -0
- package/dist/modules/v4/personal.js +14 -18
- package/dist/modules/v4/personal.js.map +1 -1
- package/dist/modules/v4/placeholder.js +8 -0
- package/dist/modules/v4/placeholder.js.map +1 -1
- package/dist/modules/v4/qrcode.js +10 -3
- package/dist/modules/v4/qrcode.js.map +1 -1
- package/dist/modules/v4/regex.js +48 -0
- package/dist/modules/v4/regex.js.map +1 -0
- package/dist/modules/v4/statistics.js +7 -0
- package/dist/modules/v4/statistics.js.map +1 -1
- package/dist/modules/v4/text.js +30 -0
- package/dist/modules/v4/text.js.map +1 -1
- package/dist/modules/v4/tic_tac_toe.js +9 -1
- package/dist/modules/v4/tic_tac_toe.js.map +1 -1
- package/dist/modules/v4/time.js +62 -11
- package/dist/modules/v4/time.js.map +1 -1
- package/dist/modules/v4/token.js +17 -8
- package/dist/modules/v4/token.js.map +1 -1
- package/dist/modules/v4/username.js +5 -0
- package/dist/modules/v4/username.js.map +1 -1
- package/dist/modules/v4/validate.js +21 -0
- package/dist/modules/v4/validate.js.map +1 -1
- package/dist/modules/v4.js +10 -1
- package/dist/modules/v4.js.map +1 -1
- package/dist/routes/delete.js.map +1 -1
- package/dist/routes/get.js +208 -48
- package/dist/routes/get.js.map +1 -1
- package/dist/routes/index.js +2 -2
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/patch.js.map +1 -1
- package/dist/routes/post.js +2 -2
- package/dist/routes/post.js.map +1 -1
- package/dist/utils/response.js.map +1 -1
- package/package.json +1 -1
- package/src/config/versions.ts +8 -1
- package/src/constants.ts +27 -12
- package/src/modules/v4/address.ts +412 -0
- package/src/modules/v4/cron.ts +221 -0
- package/src/modules/v4/geo.ts +2 -1
- package/src/modules/v4/password.ts +379 -0
- package/src/modules/v4/personal.ts +4 -16
- package/src/modules/v4/qrcode.ts +2 -2
- package/src/modules/v4/regex.ts +67 -0
- package/src/modules/v4/time.ts +63 -17
- package/src/modules/v4/token.ts +8 -7
- package/src/modules/v4.ts +4 -0
- package/src/routes/get.ts +125 -46
- package/tests/integration/api.test.ts +111 -0
- package/tests/unit/address.test.ts +84 -0
- package/tests/unit/cron.test.ts +110 -0
- package/tests/unit/password.test.ts +86 -0
- package/tests/unit/regex.test.ts +95 -0
- package/tests/unit/time.test.ts +47 -0
package/src/routes/get.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { type Request, type Response, Router } from 'express';
|
|
|
3
3
|
import { env } from '../config/env.js';
|
|
4
4
|
import { versions } from '../config/versions.js';
|
|
5
5
|
import { DOCS_URL, GITHUB_CACHE_TTL } from '../constants.js';
|
|
6
|
+
import type { AddressResult } from '../modules/v4/address.js';
|
|
6
7
|
import type { UserAgentResult } from '../modules/v4/agent.js';
|
|
7
8
|
import type { CaptchaOptions, CaptchaResult } from '../modules/v4/captcha.js';
|
|
8
9
|
import type { ColorResult } from '../modules/v4/color.js';
|
|
9
10
|
import type { IpResult } from '../modules/v4/ip.js';
|
|
11
|
+
import type { PasswordResult } from '../modules/v4/password.js';
|
|
10
12
|
import type { QRCodeOptions, QRCodeResult } from '../modules/v4/qrcode.js';
|
|
11
13
|
import { chatStorage, ipLimits } from '../storage/index.js';
|
|
12
14
|
import { since } from '../utils/helpers.js';
|
|
@@ -176,6 +178,27 @@ router.get('/:version/convert', (req: Request, res: Response) => {
|
|
|
176
178
|
}
|
|
177
179
|
});
|
|
178
180
|
|
|
181
|
+
// Generate a fictional postal address
|
|
182
|
+
router.get('/:version/address', (req: Request, res: Response) => {
|
|
183
|
+
const { country, count } = req.query;
|
|
184
|
+
const { version } = req.params;
|
|
185
|
+
|
|
186
|
+
const addressFn = (req.module as { address?: (c?: string, n?: number) => AddressResult }).address;
|
|
187
|
+
if (!addressFn) {
|
|
188
|
+
error(res, 404, `Endpoint not available in ${version}.`, `${version}/address`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const parsedCount = count !== undefined ? parseInt(count as string, 10) : 1;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const result = addressFn(country as string | undefined, parsedCount);
|
|
196
|
+
res.jsonResponse(result);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
error(res, 400, (err as Error).message, `${req.version}/address`);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
179
202
|
// Echo request headers
|
|
180
203
|
router.get('/:version/headers', (req: Request, res: Response) => {
|
|
181
204
|
const redacted = new Set(['authorization', 'cookie', 'set-cookie', 'proxy-authorization']);
|
|
@@ -234,6 +257,34 @@ router.get('/:version/domain', (req: Request, res: Response) => {
|
|
|
234
257
|
}
|
|
235
258
|
});
|
|
236
259
|
|
|
260
|
+
// Parse a cron expression and compute next execution dates
|
|
261
|
+
router.get('/:version/cron', (req: Request, res: Response) => {
|
|
262
|
+
const { expr, count, from, timezone } = req.query;
|
|
263
|
+
const { version } = req.params;
|
|
264
|
+
|
|
265
|
+
const cronFn = (req.module as { cron?: (e: string, n?: number, f?: string, tz?: string) => unknown }).cron;
|
|
266
|
+
if (!cronFn) {
|
|
267
|
+
error(res, 404, `Endpoint not available in ${version}.`, `${version}/cron`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (!expr) {
|
|
271
|
+
error(res, 400, 'Please provide a cron expression (?expr=* * * * *)', `${version}/cron`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const result = cronFn(
|
|
277
|
+
expr as string,
|
|
278
|
+
count !== undefined ? parseInt(count as string, 10) : 5,
|
|
279
|
+
from as string | undefined,
|
|
280
|
+
(timezone as string) ?? 'UTC',
|
|
281
|
+
);
|
|
282
|
+
res.jsonResponse(result);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
error(res, 400, (err as Error).message, `${req.version}/cron`);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
237
288
|
// RPG Dice roller
|
|
238
289
|
router.get('/:version/dice', (req: Request, res: Response) => {
|
|
239
290
|
const { roll } = req.query;
|
|
@@ -376,6 +427,41 @@ router.get('/:version/palette', (req: Request, res: Response) => {
|
|
|
376
427
|
}
|
|
377
428
|
});
|
|
378
429
|
|
|
430
|
+
// Generate a password or passphrase
|
|
431
|
+
router.get('/:version/password', (req: Request, res: Response) => {
|
|
432
|
+
const { type, length, uppercase, lowercase, digits, symbols, exclude, count, separator } = req.query;
|
|
433
|
+
const { version } = req.params;
|
|
434
|
+
|
|
435
|
+
const passwordFn = (
|
|
436
|
+
req.module as { password?: (t: string, l: number, o: Record<string, unknown>) => PasswordResult }
|
|
437
|
+
).password;
|
|
438
|
+
if (!passwordFn) {
|
|
439
|
+
error(res, 404, `Endpoint not available in ${version}.`, `${version}/password`);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const parseBool = (v: unknown, def: boolean): boolean => (v === undefined ? def : v !== 'false');
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
const result = passwordFn(
|
|
447
|
+
(type as string) ?? 'random',
|
|
448
|
+
length !== undefined ? parseInt(length as string, 10) : 16,
|
|
449
|
+
{
|
|
450
|
+
uppercase: parseBool(uppercase, true),
|
|
451
|
+
lowercase: parseBool(lowercase, true),
|
|
452
|
+
digits: parseBool(digits, true),
|
|
453
|
+
symbols: parseBool(symbols, false),
|
|
454
|
+
exclude: (exclude as string) ?? '',
|
|
455
|
+
count: count !== undefined ? parseInt(count as string, 10) : 1,
|
|
456
|
+
separator: (separator as string) ?? '-',
|
|
457
|
+
},
|
|
458
|
+
);
|
|
459
|
+
res.jsonResponse(result);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
error(res, 400, (err as Error).message, `${req.version}/password`);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
379
465
|
// Generate personal data
|
|
380
466
|
router.get('/:version/personal', (req: Request, res: Response) => {
|
|
381
467
|
try {
|
|
@@ -451,6 +537,33 @@ router.get('/:version/qrcode', async (req: Request, res: Response) => {
|
|
|
451
537
|
}
|
|
452
538
|
});
|
|
453
539
|
|
|
540
|
+
// Test a regex pattern against a text
|
|
541
|
+
router.get('/:version/regex', (req: Request, res: Response) => {
|
|
542
|
+
const { pattern, text, flags } = req.query;
|
|
543
|
+
const { version } = req.params;
|
|
544
|
+
|
|
545
|
+
const regexFn = (req.module as { regex?: (p: string, t: string, f?: string) => unknown }).regex;
|
|
546
|
+
if (!regexFn) {
|
|
547
|
+
error(res, 404, `Endpoint not available in ${version}.`, `${version}/regex`);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (!pattern) {
|
|
551
|
+
error(res, 400, 'Please provide a pattern (?pattern={regex})', `${version}/regex`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (!text) {
|
|
555
|
+
error(res, 400, 'Please provide a text (&text={string})', `${version}/regex`);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
const result = regexFn(pattern as string, text as string, flags as string | undefined);
|
|
561
|
+
res.jsonResponse(result);
|
|
562
|
+
} catch (err) {
|
|
563
|
+
error(res, 400, (err as Error).message, `${req.version}/regex`);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
454
567
|
// Statistics on a list of numbers
|
|
455
568
|
router.get('/:version/statistics', (req: Request, res: Response) => {
|
|
456
569
|
const { values } = req.query;
|
|
@@ -551,60 +664,26 @@ router.get('/:version/tic-tac-toe/list', (_req: Request, res: Response) => {
|
|
|
551
664
|
error(res, 405, 'This endpoint only supports POST requests.');
|
|
552
665
|
});
|
|
553
666
|
|
|
554
|
-
// Display or generate time informations
|
|
667
|
+
// Display or generate time informations, or compute a countdown
|
|
555
668
|
router.get('/:version/time', (req: Request, res: Response) => {
|
|
556
|
-
const { type = 'live', start, end, format, timezone } = req.query;
|
|
557
|
-
|
|
558
|
-
const validFormats = [
|
|
559
|
-
'iso',
|
|
560
|
-
'utc',
|
|
561
|
-
'timestamp',
|
|
562
|
-
'locale',
|
|
563
|
-
'date',
|
|
564
|
-
'time',
|
|
565
|
-
'year',
|
|
566
|
-
'month',
|
|
567
|
-
'day',
|
|
568
|
-
'hour',
|
|
569
|
-
'minute',
|
|
570
|
-
'second',
|
|
571
|
-
'ms',
|
|
572
|
-
'dayOfWeek',
|
|
573
|
-
'dayOfYear',
|
|
574
|
-
'weekNumber',
|
|
575
|
-
'timezone',
|
|
576
|
-
'timezoneOffset',
|
|
577
|
-
];
|
|
578
|
-
const validTimezones = ['UTC', 'America/New_York', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney'];
|
|
579
|
-
|
|
580
|
-
if (type !== 'live' && type !== 'random') {
|
|
581
|
-
error(res, 400, 'Please provide a valid type (?type={type})', `${req.version}/time`);
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
if (start && !Date.parse(start as string)) {
|
|
585
|
-
error(res, 400, 'Please provide a valid start date (?start={YYYY-MM-DD})', `${req.version}/time`);
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
if (end && !Date.parse(end as string)) {
|
|
589
|
-
error(res, 400, 'Please provide a valid end date (?end={YYYY-MM-DD})', `${req.version}/time`);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
if (format && !validFormats.includes(format as string)) {
|
|
593
|
-
error(res, 400, 'Please provide a valid format (?format={format})', `${req.version}/time`);
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
if (timezone && !validTimezones.includes(timezone as string)) {
|
|
597
|
-
error(res, 400, 'Please provide a valid timezone (?timezone={timezone})', `${req.version}/time`);
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
669
|
+
const { type = 'live', start, end, format, timezone, target } = req.query;
|
|
600
670
|
|
|
601
671
|
try {
|
|
602
|
-
const
|
|
672
|
+
const timeFn = req.module.time as (
|
|
673
|
+
t: string,
|
|
674
|
+
s?: string,
|
|
675
|
+
e?: string,
|
|
676
|
+
f?: string,
|
|
677
|
+
tz?: string,
|
|
678
|
+
target?: string,
|
|
679
|
+
) => Record<string, unknown>;
|
|
680
|
+
const time = timeFn(
|
|
603
681
|
type as string,
|
|
604
682
|
start as string,
|
|
605
683
|
end as string,
|
|
606
684
|
format as string,
|
|
607
685
|
timezone as string,
|
|
686
|
+
target as string,
|
|
608
687
|
);
|
|
609
688
|
res.jsonResponse(time);
|
|
610
689
|
} catch (err) {
|
|
@@ -331,6 +331,19 @@ describe('GET /v4/time', () => {
|
|
|
331
331
|
assert.ok(ts >= new Date('2020-01-01').getTime());
|
|
332
332
|
assert.ok(ts <= new Date('2020-12-31').getTime());
|
|
333
333
|
});
|
|
334
|
+
|
|
335
|
+
test('countdown future returns direction and remaining', async () => {
|
|
336
|
+
const future = new Date(Date.now() + 10 * 24 * 3600 * 1000).toISOString();
|
|
337
|
+
const { status, body } = await getJson(`/v4/time?type=countdown&target=${encodeURIComponent(future)}`);
|
|
338
|
+
assert.equal(status, 200);
|
|
339
|
+
assert.equal(body.direction, 'future');
|
|
340
|
+
assert.ok(typeof body.human === 'string');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test('countdown missing target returns 400', async () => {
|
|
344
|
+
const { status } = await getJson('/v4/time?type=countdown');
|
|
345
|
+
assert.equal(status, 400);
|
|
346
|
+
});
|
|
334
347
|
});
|
|
335
348
|
|
|
336
349
|
describe('GET /v4/username', () => {
|
|
@@ -643,6 +656,104 @@ describe('GET /v4/agent', () => {
|
|
|
643
656
|
});
|
|
644
657
|
});
|
|
645
658
|
|
|
659
|
+
describe('GET /v4/address', () => {
|
|
660
|
+
test('returns one address by default', async () => {
|
|
661
|
+
const { status, body } = await getJson('/v4/address');
|
|
662
|
+
assert.equal(status, 200);
|
|
663
|
+
assert.equal((body.addresses as unknown[]).length, 1);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test('country=fr returns a French address', async () => {
|
|
667
|
+
const { body } = await getJson('/v4/address?country=fr');
|
|
668
|
+
const a = (body.addresses as Record<string, string>[])[0]!;
|
|
669
|
+
assert.equal(a.countryCode, 'FR');
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test('count=3 returns 3 addresses', async () => {
|
|
673
|
+
const { body } = await getJson('/v4/address?count=3');
|
|
674
|
+
assert.equal((body.addresses as unknown[]).length, 3);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
test('unknown country returns 400', async () => {
|
|
678
|
+
const { status } = await getJson('/v4/address?country=xx');
|
|
679
|
+
assert.equal(status, 400);
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
describe('GET /v4/password', () => {
|
|
684
|
+
test('returns one password by default', async () => {
|
|
685
|
+
const { status, body } = await getJson('/v4/password');
|
|
686
|
+
assert.equal(status, 200);
|
|
687
|
+
assert.equal((body.passwords as string[]).length, 1);
|
|
688
|
+
assert.equal((body.passwords as string[])[0]!.length, 16);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
test('length param is respected', async () => {
|
|
692
|
+
const { body } = await getJson('/v4/password?length=32');
|
|
693
|
+
assert.equal((body.passwords as string[])[0]!.length, 32);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test('passphrase mode returns words', async () => {
|
|
697
|
+
const { body } = await getJson('/v4/password?type=passphrase&length=4');
|
|
698
|
+
assert.equal(body.type, 'passphrase');
|
|
699
|
+
assert.equal((body.passwords as string[])[0]!.split('-').length, 4);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
test('count=5 returns 5 passwords', async () => {
|
|
703
|
+
const { body } = await getJson('/v4/password?count=5');
|
|
704
|
+
assert.equal((body.passwords as string[]).length, 5);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test('no charset active returns 400', async () => {
|
|
708
|
+
const { status } = await getJson('/v4/password?uppercase=false&lowercase=false&digits=false&symbols=false');
|
|
709
|
+
assert.equal(status, 400);
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
describe('GET /v4/cron', () => {
|
|
714
|
+
test('returns next dates for * * * * *', async () => {
|
|
715
|
+
const { status, body } = await getJson('/v4/cron?expr=*%20*%20*%20*%20*');
|
|
716
|
+
assert.equal(status, 200);
|
|
717
|
+
assert.equal((body.next as string[]).length, 5);
|
|
718
|
+
assert.ok(typeof body.description === 'string');
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test('count param is respected', async () => {
|
|
722
|
+
const { body } = await getJson('/v4/cron?expr=*%20*%20*%20*%20*&count=3');
|
|
723
|
+
assert.equal((body.next as string[]).length, 3);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
test('invalid expression returns 400', async () => {
|
|
727
|
+
const { status } = await getJson('/v4/cron?expr=invalid');
|
|
728
|
+
assert.equal(status, 400);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
test('missing expr returns 400', async () => {
|
|
732
|
+
const { status } = await getJson('/v4/cron');
|
|
733
|
+
assert.equal(status, 400);
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
describe('GET /v4/regex', () => {
|
|
738
|
+
test('basic match returns count and matches', async () => {
|
|
739
|
+
const { status, body } = await getJson('/v4/regex?pattern=hello&text=hello%20world');
|
|
740
|
+
assert.equal(status, 200);
|
|
741
|
+
assert.equal(body.valid, true);
|
|
742
|
+
assert.ok((body.count as number) >= 1);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test('invalid pattern returns valid: false with 200', async () => {
|
|
746
|
+
const { status, body } = await getJson('/v4/regex?pattern=%5Binvalid&text=hello');
|
|
747
|
+
assert.equal(status, 200);
|
|
748
|
+
assert.equal(body.valid, false);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
test('missing pattern returns 400', async () => {
|
|
752
|
+
const { status } = await getJson('/v4/regex?text=hello');
|
|
753
|
+
assert.equal(status, 400);
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
|
|
646
757
|
describe('Prototype access on dynamic endpoints', () => {
|
|
647
758
|
test('algorithms?method=toString returns 400', async () => {
|
|
648
759
|
const { status } = await getJson('/v4/algorithms?method=toString');
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { strict as assert } from 'node:assert';
|
|
2
|
+
import { describe, test } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import address from '../../src/modules/v4/address.js';
|
|
5
|
+
|
|
6
|
+
describe('address', () => {
|
|
7
|
+
describe('default behavior', () => {
|
|
8
|
+
test('returns an addresses array with one entry by default', () => {
|
|
9
|
+
const r = address();
|
|
10
|
+
assert.equal(r.addresses.length, 1);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('each address has required fields', () => {
|
|
14
|
+
const r = address();
|
|
15
|
+
const a = r.addresses[0]!;
|
|
16
|
+
assert.ok(a.street);
|
|
17
|
+
assert.ok(a.city);
|
|
18
|
+
assert.ok(a.zip);
|
|
19
|
+
assert.ok(a.state);
|
|
20
|
+
assert.ok(a.country);
|
|
21
|
+
assert.ok(a.countryCode);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('country filtering', () => {
|
|
26
|
+
test('fr returns a French address', () => {
|
|
27
|
+
const r = address('fr');
|
|
28
|
+
assert.equal(r.addresses[0]!.countryCode, 'FR');
|
|
29
|
+
assert.equal(r.addresses[0]!.country, 'France');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('us returns a US address', () => {
|
|
33
|
+
const r = address('us');
|
|
34
|
+
assert.equal(r.addresses[0]!.countryCode, 'US');
|
|
35
|
+
assert.equal(r.addresses[0]!.country, 'United States');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('uk returns a UK address', () => {
|
|
39
|
+
const r = address('uk');
|
|
40
|
+
assert.equal(r.addresses[0]!.countryCode, 'UK');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('de returns a German address', () => {
|
|
44
|
+
const r = address('de');
|
|
45
|
+
assert.equal(r.addresses[0]!.countryCode, 'DE');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('es returns a Spanish address', () => {
|
|
49
|
+
const r = address('es');
|
|
50
|
+
assert.equal(r.addresses[0]!.countryCode, 'ES');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('country code is case-insensitive', () => {
|
|
54
|
+
const r = address('FR');
|
|
55
|
+
assert.equal(r.addresses[0]!.countryCode, 'FR');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('count', () => {
|
|
60
|
+
test('count=5 returns 5 addresses', () => {
|
|
61
|
+
const r = address('fr', 5);
|
|
62
|
+
assert.equal(r.addresses.length, 5);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('count=10 returns 10 addresses', () => {
|
|
66
|
+
const r = address('us', 10);
|
|
67
|
+
assert.equal(r.addresses.length, 10);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('errors', () => {
|
|
72
|
+
test('unknown country throws', () => {
|
|
73
|
+
assert.throws(() => address('xx'), /Unknown country code/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('count=0 throws', () => {
|
|
77
|
+
assert.throws(() => address('fr', 0), /Count must be between/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('count=11 throws', () => {
|
|
81
|
+
assert.throws(() => address('fr', 11), /Count must be between/);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { strict as assert } from 'node:assert';
|
|
2
|
+
import { describe, test } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import cron from '../../src/modules/v4/cron.js';
|
|
5
|
+
|
|
6
|
+
// Fixed reference point: a known Monday at 08:45 UTC
|
|
7
|
+
const FROM = '2026-01-05T08:45:00Z'; // Monday
|
|
8
|
+
|
|
9
|
+
describe('cron', () => {
|
|
10
|
+
describe('every minute', () => {
|
|
11
|
+
test('* * * * * returns 5 consecutive minutes', () => {
|
|
12
|
+
const r = cron('* * * * *', 5, FROM);
|
|
13
|
+
assert.equal(r.next.length, 5);
|
|
14
|
+
// First result should be the next minute after FROM
|
|
15
|
+
assert.ok(r.next[0]!.startsWith('2026-01-05T08:46'));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('description is "Every minute"', () => {
|
|
19
|
+
const r = cron('* * * * *', 1, FROM);
|
|
20
|
+
assert.equal(r.description, 'Every minute');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('every N minutes', () => {
|
|
25
|
+
test('*/15 * * * * returns results 15 minutes apart', () => {
|
|
26
|
+
const r = cron('*/15 * * * *', 3, FROM);
|
|
27
|
+
assert.equal(r.next.length, 3);
|
|
28
|
+
assert.ok(r.next[0]!.includes('T09:00'));
|
|
29
|
+
assert.ok(r.next[1]!.includes('T09:15'));
|
|
30
|
+
assert.ok(r.next[2]!.includes('T09:30'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('description mentions every 15 minutes', () => {
|
|
34
|
+
const r = cron('*/15 * * * *', 1, FROM);
|
|
35
|
+
assert.match(r.description, /Every 15 minutes/);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('at fixed time', () => {
|
|
40
|
+
test('0 9 * * 1-5 fires at 09:00 on weekdays', () => {
|
|
41
|
+
const r = cron('0 9 * * 1-5', 3, FROM);
|
|
42
|
+
// Next Monday 09:00 is same day
|
|
43
|
+
assert.ok(r.next[0]!.includes('T09:00'));
|
|
44
|
+
// All results should be on weekdays (Mon–Fri)
|
|
45
|
+
for (const date of r.next) {
|
|
46
|
+
const d = new Date(date);
|
|
47
|
+
const dow = d.getUTCDay();
|
|
48
|
+
assert.ok(dow >= 1 && dow <= 5, `Expected weekday, got ${dow}`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('description includes Monday through Friday', () => {
|
|
53
|
+
const r = cron('0 9 * * 1-5', 1, FROM);
|
|
54
|
+
assert.match(r.description, /Monday through Friday/);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('plan example', () => {
|
|
59
|
+
test('*/15 9-17 * * 1-5 fires between 09:00 and 17:45 on weekdays', () => {
|
|
60
|
+
const r = cron('*/15 9-17 * * 1-5', 5, FROM);
|
|
61
|
+
assert.equal(r.next.length, 5);
|
|
62
|
+
for (const date of r.next) {
|
|
63
|
+
const d = new Date(date);
|
|
64
|
+
const hour = d.getUTCHours();
|
|
65
|
+
assert.ok(hour >= 9 && hour <= 17);
|
|
66
|
+
const dow = d.getUTCDay();
|
|
67
|
+
assert.ok(dow >= 1 && dow <= 5);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('description includes "Every 15 minutes"', () => {
|
|
72
|
+
const r = cron('*/15 9-17 * * 1-5', 1, FROM);
|
|
73
|
+
assert.match(r.description, /Every 15 minutes/);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('count', () => {
|
|
78
|
+
test('count=10 returns 10 results', () => {
|
|
79
|
+
const r = cron('* * * * *', 10, FROM);
|
|
80
|
+
assert.equal(r.next.length, 10);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('count=1 returns 1 result', () => {
|
|
84
|
+
const r = cron('*/30 * * * *', 1, FROM);
|
|
85
|
+
assert.equal(r.next.length, 1);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('errors', () => {
|
|
90
|
+
test('missing expr throws', () => {
|
|
91
|
+
assert.throws(() => cron(''), /expression/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('too few fields throws', () => {
|
|
95
|
+
assert.throws(() => cron('* * * *'), /5 fields/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('invalid range throws', () => {
|
|
99
|
+
assert.throws(() => cron('60-70 * * * *'), /valid: 0-59/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('invalid timezone throws', () => {
|
|
103
|
+
assert.throws(() => cron('* * * * *', 5, undefined, 'Mars/Olympus'), /timezone/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('count out of range throws', () => {
|
|
107
|
+
assert.throws(() => cron('* * * * *', 21), /Count/);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { strict as assert } from 'node:assert';
|
|
2
|
+
import { describe, test } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import password from '../../src/modules/v4/password.js';
|
|
5
|
+
|
|
6
|
+
describe('password', () => {
|
|
7
|
+
describe('random mode', () => {
|
|
8
|
+
test('default returns one password of length 16', () => {
|
|
9
|
+
const r = password();
|
|
10
|
+
assert.equal(r.passwords.length, 1);
|
|
11
|
+
assert.equal(r.passwords[0]!.length, 16);
|
|
12
|
+
assert.equal(r.type, 'random');
|
|
13
|
+
assert.equal(r.length, 16);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('length is respected', () => {
|
|
17
|
+
const r = password('random', 32);
|
|
18
|
+
assert.equal(r.passwords[0]!.length, 32);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('no symbols when symbols=false', () => {
|
|
22
|
+
const r = password('random', 64, { symbols: false });
|
|
23
|
+
assert.ok(!/[!@#$%^&*()\-_=+[\]{}|;:,.<>?]/.test(r.passwords[0]!));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('only digits when only digits enabled', () => {
|
|
27
|
+
const r = password('random', 16, { uppercase: false, lowercase: false, digits: true, symbols: false });
|
|
28
|
+
assert.ok(/^\d+$/.test(r.passwords[0]!));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('exclude chars are absent from password', () => {
|
|
32
|
+
const r = password('random', 64, { exclude: 'aeiou' });
|
|
33
|
+
assert.ok(!/[aeiou]/.test(r.passwords[0]!));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('count=5 returns 5 passwords', () => {
|
|
37
|
+
const r = password('random', 16, { count: 5 });
|
|
38
|
+
assert.equal(r.passwords.length, 5);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('entropy and strength are present', () => {
|
|
42
|
+
const r = password();
|
|
43
|
+
assert.ok(typeof r.entropy === 'number');
|
|
44
|
+
assert.ok(['weak', 'moderate', 'strong', 'very_strong'].includes(r.strength));
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('passphrase mode', () => {
|
|
49
|
+
test('returns words joined by separator', () => {
|
|
50
|
+
const r = password('passphrase', 4, { separator: '-' });
|
|
51
|
+
assert.equal(r.type, 'passphrase');
|
|
52
|
+
const words = r.passwords[0]!.split('-');
|
|
53
|
+
assert.equal(words.length, 4);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('custom separator is used', () => {
|
|
57
|
+
const r = password('passphrase', 3, { separator: '.' });
|
|
58
|
+
assert.ok(r.passwords[0]!.includes('.'));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('errors', () => {
|
|
63
|
+
test('length below 8 throws', () => {
|
|
64
|
+
assert.throws(() => password('random', 4), /Length must be between/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('length above 128 throws', () => {
|
|
68
|
+
assert.throws(() => password('random', 200), /Length must be between/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('no charset active throws', () => {
|
|
72
|
+
assert.throws(
|
|
73
|
+
() => password('random', 16, { uppercase: false, lowercase: false, digits: false, symbols: false }),
|
|
74
|
+
/At least one character set/,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('count above 20 throws', () => {
|
|
79
|
+
assert.throws(() => password('random', 16, { count: 21 }), /Count must be between/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('invalid type throws', () => {
|
|
83
|
+
assert.throws(() => password('invalid'), /Type must be/);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|