@20syldev/api 4.8.0 → 5.0.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.
Files changed (126) hide show
  1. package/README.md +93 -55
  2. package/dist/config/versions.js +16 -0
  3. package/dist/config/versions.js.map +1 -1
  4. package/dist/constants.js +5 -0
  5. package/dist/constants.js.map +1 -1
  6. package/dist/modules/v5/chart.js +379 -0
  7. package/dist/modules/v5/chart.js.map +1 -0
  8. package/dist/modules/v5/evaluate.js +281 -0
  9. package/dist/modules/v5/evaluate.js.map +1 -0
  10. package/dist/modules/v5/matrix.js +238 -0
  11. package/dist/modules/v5/matrix.js.map +1 -0
  12. package/dist/modules/v5.js +5 -0
  13. package/dist/modules/v5.js.map +1 -0
  14. package/dist/routes/get.js +191 -162
  15. package/dist/routes/get.js.map +1 -1
  16. package/dist/routes/post.js +69 -0
  17. package/dist/routes/post.js.map +1 -1
  18. package/package.json +4 -2
  19. package/recap.md +98 -0
  20. package/.github/FUNDING.yml +0 -1
  21. package/.prettierrc +0 -7
  22. package/eslint.config.js +0 -24
  23. package/robots.txt +0 -74
  24. package/src/app.ts +0 -53
  25. package/src/config/env.ts +0 -87
  26. package/src/config/plans.ts +0 -61
  27. package/src/config/versions.ts +0 -121
  28. package/src/constants.ts +0 -61
  29. package/src/favicon.ico +0 -0
  30. package/src/middleware/cors.ts +0 -23
  31. package/src/middleware/error.ts +0 -16
  32. package/src/middleware/json.ts +0 -9
  33. package/src/middleware/logger.ts +0 -32
  34. package/src/middleware/ratelimit.ts +0 -89
  35. package/src/middleware/version.ts +0 -40
  36. package/src/modules/v3/algorithms.js +0 -218
  37. package/src/modules/v3/captcha.js +0 -60
  38. package/src/modules/v3/chat.js +0 -111
  39. package/src/modules/v3/color.js +0 -60
  40. package/src/modules/v3/convert.js +0 -39
  41. package/src/modules/v3/domain.js +0 -86
  42. package/src/modules/v3/hash.js +0 -16
  43. package/src/modules/v3/hyperplanning.js +0 -56
  44. package/src/modules/v3/levenshtein.js +0 -46
  45. package/src/modules/v3/personal.js +0 -139
  46. package/src/modules/v3/qrcode.js +0 -20
  47. package/src/modules/v3/tic_tac_toe.js +0 -242
  48. package/src/modules/v3/time.js +0 -98
  49. package/src/modules/v3/token.js +0 -54
  50. package/src/modules/v3/username.js +0 -94
  51. package/src/modules/v3/utils.js +0 -77
  52. package/src/modules/v3.js +0 -15
  53. package/src/modules/v4/address.ts +0 -412
  54. package/src/modules/v4/agent.ts +0 -104
  55. package/src/modules/v4/algorithms.ts +0 -276
  56. package/src/modules/v4/captcha.ts +0 -114
  57. package/src/modules/v4/chat.ts +0 -130
  58. package/src/modules/v4/color.ts +0 -75
  59. package/src/modules/v4/convert.ts +0 -127
  60. package/src/modules/v4/cron.ts +0 -221
  61. package/src/modules/v4/dice.ts +0 -46
  62. package/src/modules/v4/domain.ts +0 -86
  63. package/src/modules/v4/encode.ts +0 -241
  64. package/src/modules/v4/geo.ts +0 -64
  65. package/src/modules/v4/hash.ts +0 -32
  66. package/src/modules/v4/hyperplanning.ts +0 -93
  67. package/src/modules/v4/ip.ts +0 -134
  68. package/src/modules/v4/levenshtein.ts +0 -46
  69. package/src/modules/v4/palette.ts +0 -71
  70. package/src/modules/v4/password.ts +0 -379
  71. package/src/modules/v4/personal.ts +0 -140
  72. package/src/modules/v4/placeholder.ts +0 -197
  73. package/src/modules/v4/qrcode.ts +0 -121
  74. package/src/modules/v4/regex.ts +0 -67
  75. package/src/modules/v4/statistics.ts +0 -62
  76. package/src/modules/v4/text.ts +0 -330
  77. package/src/modules/v4/tic_tac_toe.ts +0 -264
  78. package/src/modules/v4/time.ts +0 -149
  79. package/src/modules/v4/token.ts +0 -53
  80. package/src/modules/v4/username.ts +0 -94
  81. package/src/modules/v4/validate.ts +0 -76
  82. package/src/modules/v4.ts +0 -29
  83. package/src/routes/delete.ts +0 -72
  84. package/src/routes/get.ts +0 -856
  85. package/src/routes/index.ts +0 -59
  86. package/src/routes/patch.ts +0 -45
  87. package/src/routes/post.ts +0 -209
  88. package/src/storage/index.ts +0 -16
  89. package/src/types/express.d.ts +0 -17
  90. package/src/types/storage.ts +0 -48
  91. package/src/utils/colors.ts +0 -91
  92. package/src/utils/helpers.ts +0 -90
  93. package/src/utils/response.ts +0 -28
  94. package/tests/integration/api.test.ts +0 -772
  95. package/tests/tsconfig.json +0 -3
  96. package/tests/unit/address.test.ts +0 -84
  97. package/tests/unit/agent.test.ts +0 -113
  98. package/tests/unit/algorithms.test.ts +0 -121
  99. package/tests/unit/captcha.test.ts +0 -62
  100. package/tests/unit/chat.test.ts +0 -54
  101. package/tests/unit/color.test.ts +0 -54
  102. package/tests/unit/convert.test.ts +0 -68
  103. package/tests/unit/cron.test.ts +0 -110
  104. package/tests/unit/dice.test.ts +0 -58
  105. package/tests/unit/domain.test.ts +0 -48
  106. package/tests/unit/encode.test.ts +0 -109
  107. package/tests/unit/geo.test.ts +0 -46
  108. package/tests/unit/hash.test.ts +0 -45
  109. package/tests/unit/hyperplanning.test.ts +0 -97
  110. package/tests/unit/ip.test.ts +0 -140
  111. package/tests/unit/levenshtein.test.ts +0 -35
  112. package/tests/unit/palette.test.ts +0 -54
  113. package/tests/unit/password.test.ts +0 -86
  114. package/tests/unit/personal.test.ts +0 -66
  115. package/tests/unit/placeholder.test.ts +0 -58
  116. package/tests/unit/qrcode.test.ts +0 -83
  117. package/tests/unit/regex.test.ts +0 -95
  118. package/tests/unit/statistics.test.ts +0 -58
  119. package/tests/unit/text.test.ts +0 -108
  120. package/tests/unit/tic_tac_toe.test.ts +0 -56
  121. package/tests/unit/time.test.ts +0 -115
  122. package/tests/unit/token.test.ts +0 -62
  123. package/tests/unit/username.test.ts +0 -35
  124. package/tests/unit/validate.test.ts +0 -72
  125. package/tsconfig.json +0 -17
  126. package/tsconfig.test.json +0 -9
@@ -1,86 +0,0 @@
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
- });
@@ -1,66 +0,0 @@
1
- import { strict as assert } from 'node:assert';
2
- import { describe, test } from 'node:test';
3
-
4
- import personal from '../../src/modules/v4/personal.js';
5
-
6
- describe('personal', () => {
7
- test('returns expected fields', () => {
8
- const result = personal();
9
- const fields = [
10
- 'name',
11
- 'email',
12
- 'phone',
13
- 'job',
14
- 'language',
15
- 'card',
16
- 'cvc',
17
- 'expiration',
18
- 'address',
19
- 'birthday',
20
- 'civil_status',
21
- 'social_profiles',
22
- 'emergency_contacts',
23
- 'subscriptions',
24
- 'pets',
25
- ];
26
- for (const f of fields) {
27
- assert.ok(f in result, `missing field: ${f}`);
28
- }
29
- });
30
-
31
- test('card has 4 groups of 4 digits', () => {
32
- const result = personal();
33
- assert.match(result.card as string, /^\d{4} \d{4} \d{4} \d{4}$/);
34
- });
35
-
36
- test('cvc is 3 digits', () => {
37
- const result = personal();
38
- const cvc = result.cvc as number;
39
- assert.ok(cvc >= 100 && cvc <= 999);
40
- });
41
-
42
- test('expiration MM/YY format', () => {
43
- const result = personal();
44
- assert.match(result.expiration as string, /^\d{2}\/\d{2}$/);
45
- });
46
-
47
- test('phone has international code', () => {
48
- const result = personal();
49
- assert.match(result.phone as string, /^\+\d+ /);
50
- });
51
-
52
- test('social_profiles has 4 platforms', () => {
53
- const result = personal();
54
- const social = result.social_profiles as Record<string, string>;
55
- assert.ok('twitter' in social);
56
- assert.ok('facebook' in social);
57
- assert.ok('linkedin' in social);
58
- assert.ok('instagram' in social);
59
- });
60
-
61
- test('emergency_contacts non-empty', () => {
62
- const result = personal();
63
- const contacts = result.emergency_contacts as unknown[];
64
- assert.ok(contacts.length >= 1);
65
- });
66
- });
@@ -1,58 +0,0 @@
1
- import { strict as assert } from 'node:assert';
2
- import { describe, test } from 'node:test';
3
-
4
- import placeholder from '../../src/modules/v4/placeholder.js';
5
-
6
- describe('placeholder', () => {
7
- test('image returns SVG with text', () => {
8
- const result = placeholder('image', { width: '200', height: '100', text: 'Hello' });
9
- assert.equal(result.type, 'image');
10
- assert.equal(result.contentType, 'image/svg+xml');
11
- assert.match(result.body, /<svg/);
12
- assert.match(result.body, /Hello/);
13
- });
14
-
15
- test('skeleton returns SVG with animation', () => {
16
- const result = placeholder('skeleton', { width: '300', height: '200' });
17
- assert.equal(result.type, 'skeleton');
18
- assert.equal(result.contentType, 'image/svg+xml');
19
- assert.match(result.body, /<svg/);
20
- });
21
-
22
- test('default dimensions 800x600', () => {
23
- const result = placeholder('image', {});
24
- assert.match(result.body, /width="800"/);
25
- assert.match(result.body, /height="600"/);
26
- });
27
-
28
- test('skeleton with avatar', () => {
29
- const result = placeholder('skeleton', { avatar: 'circle' });
30
- assert.match(result.body, /<circle/);
31
- });
32
-
33
- test('skeleton with rows', () => {
34
- const result = placeholder('skeleton', { rows: '5' });
35
- assert.match(result.body, /<rect/);
36
- });
37
-
38
- test('animate none removes animation', () => {
39
- const result = placeholder('skeleton', { animate: 'none' });
40
- assert.ok(!result.body.includes('<animate'));
41
- });
42
-
43
- test('throws on invalid type', () => {
44
- assert.throws(() => placeholder('video', {}), /Type must be one of/);
45
- });
46
-
47
- test('throws on invalid width', () => {
48
- assert.throws(() => placeholder('image', { width: '5000' }), /width/);
49
- });
50
-
51
- test('throws on invalid avatar shape', () => {
52
- assert.throws(() => placeholder('skeleton', { avatar: 'hexagon' }), /avatar/);
53
- });
54
-
55
- test('throws on invalid speed', () => {
56
- assert.throws(() => placeholder('skeleton', { speed: '99' }), /speed/);
57
- });
58
- });
@@ -1,83 +0,0 @@
1
- import { strict as assert } from 'node:assert';
2
- import { describe, test } from 'node:test';
3
-
4
- import qrcode from '../../src/modules/v4/qrcode.js';
5
-
6
- const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
7
-
8
- describe('qrcode', () => {
9
- test('returns PNG buffer by default', async () => {
10
- const result = await qrcode({ url: 'https://example.com' });
11
- assert.equal(result.contentType, 'image/png');
12
- assert.ok(Buffer.isBuffer(result.body));
13
- assert.ok((result.body as Buffer).subarray(0, 4).equals(PNG_MAGIC));
14
- });
15
-
16
- test('returns data URL with format=base64', async () => {
17
- const result = await qrcode({ url: 'https://example.com', format: 'base64' });
18
- assert.equal(result.contentType, 'application/json');
19
- assert.match(result.body as string, /^data:image\/png;base64,/);
20
- });
21
-
22
- test('throws on invalid format', async () => {
23
- await assert.rejects(() => qrcode({ url: 'https://example.com', format: 'jpg' as 'png' }), /format/);
24
- });
25
-
26
- test('throws on missing url', async () => {
27
- await assert.rejects(() => qrcode({ url: '' }));
28
- });
29
-
30
- test('accepts custom size', async () => {
31
- const result = await qrcode({ url: 'https://example.com', size: 400 });
32
- assert.equal(result.contentType, 'image/png');
33
- });
34
-
35
- test('throws on invalid size', async () => {
36
- await assert.rejects(() => qrcode({ url: 'https://example.com', size: 9999 }), /size/);
37
- });
38
-
39
- test('accepts custom colors', async () => {
40
- const result = await qrcode({ url: 'https://example.com', dark: 'ff0000', light: 'ffffff' });
41
- assert.equal(result.contentType, 'image/png');
42
- });
43
-
44
- test('throws on invalid dark color', async () => {
45
- await assert.rejects(() => qrcode({ url: 'https://example.com', dark: 'xyz' }), /color/i);
46
- });
47
-
48
- test('throws on invalid light color', async () => {
49
- await assert.rejects(() => qrcode({ url: 'https://example.com', light: 'nope' }), /color/i);
50
- });
51
-
52
- test('accepts correction level', async () => {
53
- const result = await qrcode({ url: 'https://example.com', correction: 'H' });
54
- assert.equal(result.contentType, 'image/png');
55
- });
56
-
57
- test('throws on invalid correction level', async () => {
58
- await assert.rejects(() => qrcode({ url: 'https://example.com', correction: 'X' as 'L' }), /correction/);
59
- });
60
-
61
- test('accepts custom margin', async () => {
62
- const result = await qrcode({ url: 'https://example.com', margin: 0 });
63
- assert.equal(result.contentType, 'image/png');
64
- });
65
-
66
- test('throws on invalid margin', async () => {
67
- await assert.rejects(() => qrcode({ url: 'https://example.com', margin: 99 }), /margin/);
68
- });
69
-
70
- test('throws on non-HTTPS icon URL', async () => {
71
- await assert.rejects(
72
- () => qrcode({ url: 'https://example.com', icon: 'http://example.com/icon.png' }),
73
- /HTTPS/,
74
- );
75
- });
76
-
77
- test('throws on unreachable icon URL', async () => {
78
- await assert.rejects(
79
- () => qrcode({ url: 'https://example.com', icon: 'https://localhost:1/nope.png' }),
80
- /Failed to fetch icon|fetch/i,
81
- );
82
- });
83
- });
@@ -1,95 +0,0 @@
1
- import { strict as assert } from 'node:assert';
2
- import { describe, test } from 'node:test';
3
-
4
- import regex from '../../src/modules/v4/regex.js';
5
-
6
- describe('regex', () => {
7
- describe('basic matching', () => {
8
- test('finds a simple match', () => {
9
- const r = regex('hello', 'hello world');
10
- assert.equal(r.valid, true);
11
- assert.equal(r.count, 1);
12
- assert.equal(r.matches[0]!.match, 'hello');
13
- assert.equal(r.matches[0]!.index, 0);
14
- });
15
-
16
- test('finds multiple matches', () => {
17
- const r = regex('\\d+', 'abc 42 def 7');
18
- assert.equal(r.count, 2);
19
- assert.equal(r.matches[0]!.match, '42');
20
- assert.equal(r.matches[1]!.match, '7');
21
- });
22
-
23
- test('no match returns empty array', () => {
24
- const r = regex('xyz', 'hello world');
25
- assert.equal(r.valid, true);
26
- assert.equal(r.count, 0);
27
- assert.deepEqual(r.matches, []);
28
- });
29
- });
30
-
31
- describe('flags', () => {
32
- test('flag i enables case-insensitive matching', () => {
33
- const r = regex('hello', 'HELLO world', 'i');
34
- assert.equal(r.count, 1);
35
- assert.equal(r.matches[0]!.match, 'HELLO');
36
- });
37
-
38
- test('invalid flag x is silently ignored', () => {
39
- const r = regex('hello', 'hello world', 'x');
40
- assert.equal(r.valid, true);
41
- assert.equal(r.count, 1);
42
- });
43
-
44
- test('g flag is always present', () => {
45
- const r = regex('a', 'aaa', '');
46
- assert.ok(r.flags.includes('g'));
47
- });
48
- });
49
-
50
- describe('capture groups', () => {
51
- test('numbered groups are returned', () => {
52
- const r = regex('(\\d{4})-(\\d{2})', '2026-05');
53
- assert.deepEqual(r.matches[0]!.groups, ['2026', '05']);
54
- });
55
-
56
- test('named groups are returned', () => {
57
- const r = regex('(?<year>\\d{4})-(?<month>\\d{2})', '2026-05');
58
- assert.equal(r.matches[0]!.namedGroups.year, '2026');
59
- assert.equal(r.matches[0]!.namedGroups.month, '05');
60
- });
61
-
62
- test('no groups returns empty arrays', () => {
63
- const r = regex('hello', 'hello');
64
- assert.deepEqual(r.matches[0]!.groups, []);
65
- assert.deepEqual(r.matches[0]!.namedGroups, {});
66
- });
67
- });
68
-
69
- describe('invalid pattern', () => {
70
- test('invalid regex returns valid: false', () => {
71
- const r = regex('[invalid', 'hello');
72
- assert.equal(r.valid, false);
73
- assert.equal(r.count, 0);
74
- assert.deepEqual(r.matches, []);
75
- });
76
- });
77
-
78
- describe('errors', () => {
79
- test('missing pattern throws', () => {
80
- assert.throws(() => regex('', 'hello'), /pattern/);
81
- });
82
-
83
- test('missing text throws', () => {
84
- assert.throws(() => regex('hello', ''), /text/);
85
- });
86
-
87
- test('pattern too long throws', () => {
88
- assert.throws(() => regex('a'.repeat(201), 'hello'), /200/);
89
- });
90
-
91
- test('text too long throws', () => {
92
- assert.throws(() => regex('hello', 'a'.repeat(1001)), /1000/);
93
- });
94
- });
95
- });
@@ -1,58 +0,0 @@
1
- import { strict as assert } from 'node:assert';
2
- import { describe, test } from 'node:test';
3
-
4
- import statistics from '../../src/modules/v4/statistics.js';
5
-
6
- describe('statistics', () => {
7
- test('basic computation on integers', () => {
8
- const result = statistics('1,2,3,4,5');
9
- assert.equal(result.count, 5);
10
- assert.equal(result.sum, 15);
11
- assert.equal(result.min, 1);
12
- assert.equal(result.max, 5);
13
- assert.equal(result.range, 4);
14
- assert.equal(result.mean, 3);
15
- assert.equal(result.median, 3);
16
- });
17
-
18
- test('median on even count', () => {
19
- const result = statistics('1,2,3,4');
20
- assert.equal(result.median, 2.5);
21
- });
22
-
23
- test('mode is empty when all values unique', () => {
24
- const result = statistics('1,2,3,4,5');
25
- assert.deepEqual(result.mode, []);
26
- });
27
-
28
- test('mode detects most frequent value', () => {
29
- const result = statistics('1,2,2,3,3,3');
30
- assert.deepEqual(result.mode, [3]);
31
- });
32
-
33
- test('multiple modes', () => {
34
- const result = statistics('1,1,2,2,3');
35
- assert.deepEqual(result.mode.sort(), [1, 2]);
36
- });
37
-
38
- test('stddev of identical values is 0', () => {
39
- const result = statistics('5,5,5,5');
40
- assert.equal(result.stddev, 0);
41
- assert.equal(result.variance, 0);
42
- });
43
-
44
- test('handles negative and decimal values', () => {
45
- const result = statistics('-1.5,0,1.5');
46
- assert.equal(result.mean, 0);
47
- assert.equal(result.min, -1.5);
48
- assert.equal(result.max, 1.5);
49
- });
50
-
51
- test('throws on non-numeric values', () => {
52
- assert.throws(() => statistics('1,abc,3'), /must contain only numbers/);
53
- });
54
-
55
- test('throws on missing values', () => {
56
- assert.throws(() => statistics(''), /required/);
57
- });
58
- });
@@ -1,108 +0,0 @@
1
- import { strict as assert } from 'node:assert';
2
- import { describe, test } from 'node:test';
3
-
4
- import { lorem, number, slug, stats } from '../../src/modules/v4/text.js';
5
-
6
- describe('text', () => {
7
- describe('slug', () => {
8
- test('basic slug', () => {
9
- assert.equal(slug('Hello World'), 'hello-world');
10
- });
11
-
12
- test('strips diacritics', () => {
13
- assert.equal(slug('Crème Brûlée'), 'creme-brulee');
14
- });
15
-
16
- test('strips special chars', () => {
17
- assert.equal(slug('Mon Article #1 !'), 'mon-article-1');
18
- });
19
-
20
- test('collapses multiple spaces', () => {
21
- assert.equal(slug('a b c'), 'a-b-c');
22
- });
23
- });
24
-
25
- describe('stats', () => {
26
- test('counts characters and words', () => {
27
- const result = stats('Hello world');
28
- assert.equal(result.characters, 11);
29
- assert.equal(result.charactersNoSpaces, 10);
30
- assert.equal(result.words, 2);
31
- });
32
-
33
- test('counts sentences', () => {
34
- const result = stats('Hi. How are you? Fine!');
35
- assert.equal(result.sentences, 3);
36
- });
37
-
38
- test('reading time format', () => {
39
- const result = stats('a '.repeat(200).trim());
40
- assert.match(result.readingTime, /min/);
41
- });
42
-
43
- test('short text returns seconds', () => {
44
- const result = stats('hello world');
45
- assert.match(result.readingTime, /s$/);
46
- });
47
- });
48
-
49
- describe('lorem', () => {
50
- test('words count', () => {
51
- const result = lorem('words', '10');
52
- assert.equal(result.split(' ').length, 10);
53
- });
54
-
55
- test('sentences end with period', () => {
56
- const result = lorem('sentences', '3');
57
- const sentences = result.split('. ');
58
- assert.equal(sentences.length, 3);
59
- });
60
-
61
- test('paragraphs separated by double newline', () => {
62
- const result = lorem('paragraphs', '2');
63
- assert.equal(result.split('\n\n').length, 2);
64
- });
65
-
66
- test('throws on invalid type', () => {
67
- assert.throws(() => lorem('verses', '5'), /Type must be one of/);
68
- });
69
- });
70
-
71
- describe('number', () => {
72
- test('English: 42', () => {
73
- assert.equal(number('42', 'en'), 'forty-two');
74
- });
75
-
76
- test('English: 100', () => {
77
- assert.equal(number('100', 'en'), 'one hundred');
78
- });
79
-
80
- test('English: 1234', () => {
81
- assert.equal(number('1234', 'en'), 'one thousand two hundred thirty-four');
82
- });
83
-
84
- test('French: 21', () => {
85
- assert.equal(number('21', 'fr'), 'vingt et un');
86
- });
87
-
88
- test('French: 80', () => {
89
- assert.equal(number('80', 'fr'), 'quatre-vingts');
90
- });
91
-
92
- test('French: 0', () => {
93
- assert.equal(number('0', 'fr'), 'zéro');
94
- });
95
-
96
- test('English: negative', () => {
97
- assert.equal(number('-5', 'en'), 'minus five');
98
- });
99
-
100
- test('throws on invalid lang', () => {
101
- assert.throws(() => number('42', 'es'), /Lang must be one of/);
102
- });
103
-
104
- test('throws on non-integer', () => {
105
- assert.throws(() => number('3.14', 'en'), /must be an integer/);
106
- });
107
- });
108
- });
@@ -1,56 +0,0 @@
1
- import { strict as assert } from 'node:assert';
2
- import { describe, test } from 'node:test';
3
-
4
- import tic_tac_toe from '../../src/modules/v4/tic_tac_toe.js';
5
- import type { TicTacToeStorage } from '../../src/types/storage.js';
6
-
7
- function makeStorage(): TicTacToeStorage {
8
- return { games: {}, sessions: {}, rateLimits: {} };
9
- }
10
-
11
- describe('tic_tac_toe', () => {
12
- test('list returns empty games array', () => {
13
- const result = tic_tac_toe('list', { storage: makeStorage() });
14
- assert.ok(Array.isArray(result.games));
15
- });
16
-
17
- test('play creates a game and makes a move', () => {
18
- const storage = makeStorage();
19
- const result = tic_tac_toe('play', { username: 'alice', move: '1-1', session: 'a1', game: 'TEST1', storage });
20
- assert.ok((result.message as string).includes('Move sent'));
21
- });
22
-
23
- test('fetch returns game state', () => {
24
- const storage = makeStorage();
25
- tic_tac_toe('play', { username: 'alice', move: '1-1', session: 'a1', game: 'TEST2', storage });
26
- const result = tic_tac_toe('fetch', { username: 'alice', game: 'TEST2', storage });
27
- assert.equal(result.id, 'TEST2');
28
- });
29
-
30
- test('forfeit throws on non-player', () => {
31
- const storage = makeStorage();
32
- tic_tac_toe('play', { username: 'alice', move: '1-1', session: 'a1', game: 'TEST3', storage });
33
- assert.throws(
34
- () => tic_tac_toe('forfeit', { username: 'charlie', session: 'c1', game: 'TEST3', storage }),
35
- /not a player/,
36
- );
37
- });
38
-
39
- test('throws on missing username', () => {
40
- assert.throws(
41
- () => tic_tac_toe('play', { move: '1-1', session: 's', game: 'G', storage: makeStorage() }),
42
- /username/,
43
- );
44
- });
45
-
46
- test('throws on missing move', () => {
47
- assert.throws(
48
- () => tic_tac_toe('play', { username: 'alice', session: 's', game: 'G', storage: makeStorage() }),
49
- /move/i,
50
- );
51
- });
52
-
53
- test('throws on invalid action', () => {
54
- assert.throws(() => tic_tac_toe('reset', { username: 'alice', storage: makeStorage() }), /Invalid action/);
55
- });
56
- });