@20syldev/api 4.0.0 → 4.2.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 +9 -4
- package/package.json +5 -4
- package/src/app.ts +7 -3
- package/src/config/versions.ts +25 -10
- package/src/constants.ts +19 -0
- package/src/modules/v4/algorithms.ts +43 -1
- package/src/modules/v4/dice.ts +39 -0
- package/src/modules/v4/encode.ts +170 -0
- package/src/modules/v4/statistics.ts +55 -0
- package/src/modules/v4/text.ts +300 -0
- package/src/modules/v4/validate.ts +55 -0
- package/src/modules/v4.ts +5 -0
- package/src/routes/get.ts +134 -0
- package/src/routes/index.ts +2 -2
- package/tests/integration/api.test.ts +450 -0
- package/tests/tsconfig.json +3 -0
- package/tests/unit/algorithms.test.ts +120 -0
- package/tests/unit/color.test.ts +33 -0
- package/tests/unit/convert.test.ts +42 -0
- package/tests/unit/dice.test.ts +57 -0
- package/tests/unit/domain.test.ts +47 -0
- package/tests/unit/encode.test.ts +108 -0
- package/tests/unit/hash.test.ts +37 -0
- package/tests/unit/hyperplanning.test.ts +77 -0
- package/tests/unit/levenshtein.test.ts +34 -0
- package/tests/unit/personal.test.ts +65 -0
- package/tests/unit/statistics.test.ts +57 -0
- package/tests/unit/text.test.ts +107 -0
- package/tests/unit/time.test.ts +67 -0
- package/tests/unit/token.test.ts +61 -0
- package/tests/unit/username.test.ts +34 -0
- package/tests/unit/validate.test.ts +71 -0
- package/tsconfig.test.json +9 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { test, describe, before, after } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import type { AddressInfo } from 'node:net';
|
|
4
|
+
import type { Server } from 'node:http';
|
|
5
|
+
import app from '../../src/app.js';
|
|
6
|
+
|
|
7
|
+
let server: Server;
|
|
8
|
+
let baseUrl: string;
|
|
9
|
+
|
|
10
|
+
before(() => {
|
|
11
|
+
return new Promise<void>((resolve) => {
|
|
12
|
+
server = app.listen(0, () => {
|
|
13
|
+
const port = (server.address() as AddressInfo).port;
|
|
14
|
+
baseUrl = `http://127.0.0.1:${port}`;
|
|
15
|
+
resolve();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
after(() => {
|
|
21
|
+
return new Promise<void>((resolve) => {
|
|
22
|
+
server.close(() => resolve());
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
async function getJson(path: string): Promise<{ status: number; body: Record<string, unknown> }> {
|
|
27
|
+
const res = await fetch(`${baseUrl}${path}`);
|
|
28
|
+
const body = (await res.json()) as Record<string, unknown>;
|
|
29
|
+
return { status: res.status, body };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function sendJson(
|
|
33
|
+
method: string,
|
|
34
|
+
path: string,
|
|
35
|
+
body: Record<string, unknown>,
|
|
36
|
+
): Promise<{ status: number; body: Record<string, unknown> }> {
|
|
37
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
38
|
+
method,
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify(body),
|
|
41
|
+
});
|
|
42
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
43
|
+
return { status: res.status, body: data };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- GET endpoints ---
|
|
47
|
+
|
|
48
|
+
describe('GET / (version listing)', () => {
|
|
49
|
+
test('v4 endpoints listed', async () => {
|
|
50
|
+
const { body } = await getJson('/v4');
|
|
51
|
+
assert.equal(body.version, 'v4');
|
|
52
|
+
const endpoints = body.endpoints as Record<string, Record<string, string>>;
|
|
53
|
+
assert.ok('dice' in endpoints.get!);
|
|
54
|
+
assert.ok('encode' in endpoints.get!);
|
|
55
|
+
assert.ok('statistics' in endpoints.get!);
|
|
56
|
+
assert.ok('text' in endpoints.get!);
|
|
57
|
+
assert.ok('validate' in endpoints.get!);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('invalid version returns 404', async () => {
|
|
61
|
+
const { status } = await getJson('/v99');
|
|
62
|
+
assert.equal(status, 404);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('GET /v4/algorithms', () => {
|
|
67
|
+
test('fibonacci', async () => {
|
|
68
|
+
const { status, body } = await getJson('/v4/algorithms?method=fibonacci&value=10');
|
|
69
|
+
assert.equal(status, 200);
|
|
70
|
+
assert.deepEqual(body.answer, [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('isprime', async () => {
|
|
74
|
+
const { body } = await getJson('/v4/algorithms?method=isprime&value=17');
|
|
75
|
+
assert.equal(body.answer, true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('roman 2024 to MMXXIV', async () => {
|
|
79
|
+
const { body } = await getJson('/v4/algorithms?method=roman&value=2024');
|
|
80
|
+
assert.equal(body.answer, 'MMXXIV');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('roman MMXXIV to 2024', async () => {
|
|
84
|
+
const { body } = await getJson('/v4/algorithms?method=roman&value=MMXXIV');
|
|
85
|
+
assert.equal(body.answer, 2024);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('GET /v4/captcha', () => {
|
|
90
|
+
test('returns PNG', async () => {
|
|
91
|
+
const res = await fetch(`${baseUrl}/v4/captcha?text=hello`);
|
|
92
|
+
assert.equal(res.status, 200);
|
|
93
|
+
assert.match(res.headers.get('content-type') ?? '', /image\/png/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('without text returns 400', async () => {
|
|
97
|
+
const { status } = await getJson('/v4/captcha');
|
|
98
|
+
assert.equal(status, 400);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('GET /v4/chat', () => {
|
|
103
|
+
test('fetches public messages', async () => {
|
|
104
|
+
await sendJson('POST', '/v4/chat', {
|
|
105
|
+
username: 'get-chat-user',
|
|
106
|
+
message: 'ping',
|
|
107
|
+
session: `get-chat-${Date.now()}`,
|
|
108
|
+
});
|
|
109
|
+
const res = await fetch(`${baseUrl}/v4/chat`);
|
|
110
|
+
assert.equal(res.status, 200);
|
|
111
|
+
const body = (await res.json()) as { username: string; message: string }[];
|
|
112
|
+
assert.ok(Array.isArray(body));
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('GET /v4/color', () => {
|
|
117
|
+
test('returns color formats', async () => {
|
|
118
|
+
const { status, body } = await getJson('/v4/color');
|
|
119
|
+
assert.equal(status, 200);
|
|
120
|
+
assert.match(body.hex as string, /^#[0-9a-f]{6}$/);
|
|
121
|
+
assert.ok('rgb' in body);
|
|
122
|
+
assert.ok('hsl' in body);
|
|
123
|
+
assert.ok('cmyk' in body);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('GET /v4/convert', () => {
|
|
128
|
+
test('celsius to fahrenheit', async () => {
|
|
129
|
+
const { status, body } = await getJson('/v4/convert?value=100&from=celsius&to=fahrenheit');
|
|
130
|
+
assert.equal(status, 200);
|
|
131
|
+
assert.equal(body.result, 212);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('missing value returns 400', async () => {
|
|
135
|
+
const { status } = await getJson('/v4/convert?from=celsius&to=fahrenheit');
|
|
136
|
+
assert.equal(status, 400);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('GET /v4/dice', () => {
|
|
141
|
+
test('rolls 2d6+3', async () => {
|
|
142
|
+
const { body } = await getJson('/v4/dice?roll=2d6%2B3');
|
|
143
|
+
assert.equal(body.count, 2);
|
|
144
|
+
assert.equal(body.sides, 6);
|
|
145
|
+
assert.equal(body.modifier, 3);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('rejects invalid notation', async () => {
|
|
149
|
+
const { status } = await getJson('/v4/dice?roll=foo');
|
|
150
|
+
assert.equal(status, 400);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('GET /v4/encode', () => {
|
|
155
|
+
test('base64 encode', async () => {
|
|
156
|
+
const { status, body } = await getJson('/v4/encode?method=base64encode&text=hello');
|
|
157
|
+
assert.equal(status, 200);
|
|
158
|
+
assert.equal(body.result, 'aGVsbG8=');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('rot13', async () => {
|
|
162
|
+
const { body } = await getJson('/v4/encode?method=rot13&text=Hello');
|
|
163
|
+
assert.equal(body.result, 'Uryyb');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('missing method returns 400', async () => {
|
|
167
|
+
const { status } = await getJson('/v4/encode?text=hello');
|
|
168
|
+
assert.equal(status, 400);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('GET /v4/domain', () => {
|
|
173
|
+
test('has TLD', async () => {
|
|
174
|
+
const { status, body } = await getJson('/v4/domain');
|
|
175
|
+
assert.equal(status, 200);
|
|
176
|
+
assert.match(body.domain as string, /\./);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('GET /v4/infos', () => {
|
|
181
|
+
test('returns API infos', async () => {
|
|
182
|
+
const { status, body } = await getJson('/v4/infos');
|
|
183
|
+
assert.equal(status, 200);
|
|
184
|
+
assert.ok('endpoints' in body);
|
|
185
|
+
assert.ok('documentation' in body);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('GET /v4/levenshtein', () => {
|
|
190
|
+
test('computes distance', async () => {
|
|
191
|
+
const { status, body } = await getJson('/v4/levenshtein?str1=kitten&str2=sitting');
|
|
192
|
+
assert.equal(status, 200);
|
|
193
|
+
assert.equal(body.distance, 3);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('GET /v4/personal', () => {
|
|
198
|
+
test('returns generated identity', async () => {
|
|
199
|
+
const { status, body } = await getJson('/v4/personal');
|
|
200
|
+
assert.equal(status, 200);
|
|
201
|
+
assert.ok('name' in body);
|
|
202
|
+
assert.ok('email' in body);
|
|
203
|
+
assert.ok('card' in body);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('GET /v4/qrcode', () => {
|
|
208
|
+
test('returns data URL', async () => {
|
|
209
|
+
const { status, body } = await getJson('/v4/qrcode?url=https%3A%2F%2Fexample.com');
|
|
210
|
+
assert.equal(status, 200);
|
|
211
|
+
assert.match(body as unknown as string, /^data:image\/png;base64,/);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('GET /v4/statistics', () => {
|
|
216
|
+
test('basic stats', async () => {
|
|
217
|
+
const { body } = await getJson('/v4/statistics?values=1,2,3,4,5');
|
|
218
|
+
assert.equal(body.mean, 3);
|
|
219
|
+
assert.equal(body.median, 3);
|
|
220
|
+
assert.equal(body.count, 5);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('missing values returns 400', async () => {
|
|
224
|
+
const { status } = await getJson('/v4/statistics');
|
|
225
|
+
assert.equal(status, 400);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('GET /v4/text', () => {
|
|
230
|
+
test('slug', async () => {
|
|
231
|
+
const { body } = await getJson('/v4/text?method=slug&value=Hello%20World');
|
|
232
|
+
assert.equal(body.result, 'hello-world');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('number en', async () => {
|
|
236
|
+
const { body } = await getJson('/v4/text?method=number&value=42&lang=en');
|
|
237
|
+
assert.equal(body.result, 'forty-two');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('lorem words', async () => {
|
|
241
|
+
const { body } = await getJson('/v4/text?method=lorem&type=words&count=5');
|
|
242
|
+
assert.equal((body.result as string).split(' ').length, 5);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('GET /v4/time', () => {
|
|
247
|
+
test('live', async () => {
|
|
248
|
+
const { status, body } = await getJson('/v4/time');
|
|
249
|
+
assert.equal(status, 200);
|
|
250
|
+
assert.ok('iso' in body);
|
|
251
|
+
assert.ok('timestamp' in body);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('random in range', async () => {
|
|
255
|
+
const { status, body } = await getJson('/v4/time?type=random&start=2020-01-01&end=2020-12-31');
|
|
256
|
+
assert.equal(status, 200);
|
|
257
|
+
const ts = body.timestamp as number;
|
|
258
|
+
assert.ok(ts >= new Date('2020-01-01').getTime());
|
|
259
|
+
assert.ok(ts <= new Date('2020-12-31').getTime());
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('GET /v4/validate', () => {
|
|
264
|
+
test('valid luhn', async () => {
|
|
265
|
+
const { body } = await getJson('/v4/validate?type=luhn&value=4111111111111111');
|
|
266
|
+
assert.equal(body.valid, true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('valid email', async () => {
|
|
270
|
+
const { body } = await getJson('/v4/validate?type=email&value=hello%40example.com');
|
|
271
|
+
assert.equal(body.valid, true);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('invalid type returns 400', async () => {
|
|
275
|
+
const { status } = await getJson('/v4/validate?type=foo&value=bar');
|
|
276
|
+
assert.equal(status, 400);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('GET /v4/username', () => {
|
|
281
|
+
test('has fields', async () => {
|
|
282
|
+
const { status, body } = await getJson('/v4/username');
|
|
283
|
+
assert.equal(status, 200);
|
|
284
|
+
assert.ok('username' in body);
|
|
285
|
+
assert.ok('adjective' in body);
|
|
286
|
+
assert.ok('animal' in body);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('GET /v4/website', () => {
|
|
291
|
+
const originalFetch = globalThis.fetch;
|
|
292
|
+
|
|
293
|
+
after(() => {
|
|
294
|
+
globalThis.fetch = originalFetch;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('returns aggregated payload (with mocked GitHub API)', async () => {
|
|
298
|
+
globalThis.fetch = (async (input: string | URL | Request) => {
|
|
299
|
+
const url = typeof input === 'string' ? input : input.toString();
|
|
300
|
+
if (url.includes('api.github.com')) {
|
|
301
|
+
return new Response(
|
|
302
|
+
JSON.stringify({
|
|
303
|
+
data: {
|
|
304
|
+
user: {
|
|
305
|
+
contributionsCollection: {
|
|
306
|
+
contributionCalendar: {
|
|
307
|
+
weeks: [
|
|
308
|
+
{
|
|
309
|
+
firstDay: '2026-01-01',
|
|
310
|
+
contributionDays: [{ date: '2026-01-01', contributionCount: 5 }],
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
}),
|
|
318
|
+
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
return originalFetch(input);
|
|
322
|
+
}) as typeof fetch;
|
|
323
|
+
|
|
324
|
+
const { status, body } = await getJson('/v4/website');
|
|
325
|
+
assert.equal(status, 200);
|
|
326
|
+
assert.ok('versions' in body);
|
|
327
|
+
assert.ok('stats' in body);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('?key=stats.5 returns sub-key', async () => {
|
|
331
|
+
const { status, body } = await getJson('/v4/website?key=stats.5');
|
|
332
|
+
assert.equal(status, 200);
|
|
333
|
+
assert.ok('stats.5' in body);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('?key=invalid.path returns 404', async () => {
|
|
337
|
+
const { status } = await getJson('/v4/website?key=does.not.exist');
|
|
338
|
+
assert.equal(status, 404);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('rejects prototype pollution keys', async () => {
|
|
342
|
+
const { status } = await getJson('/v4/website?key=__proto__');
|
|
343
|
+
assert.equal(status, 400);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// --- POST endpoints ---
|
|
348
|
+
|
|
349
|
+
describe('POST /v4/chat', () => {
|
|
350
|
+
const session = `chat-${Date.now()}`;
|
|
351
|
+
|
|
352
|
+
test('sends a public message', async () => {
|
|
353
|
+
const { status, body } = await sendJson('POST', '/v4/chat', {
|
|
354
|
+
username: 'alice',
|
|
355
|
+
message: 'public hello',
|
|
356
|
+
session,
|
|
357
|
+
});
|
|
358
|
+
assert.equal(status, 200);
|
|
359
|
+
assert.match(body.message as string, /sent/);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('fetches private chat (legacy)', async () => {
|
|
363
|
+
const token = `tok-${Date.now()}`;
|
|
364
|
+
await sendJson('POST', '/v4/chat', {
|
|
365
|
+
username: 'bob',
|
|
366
|
+
message: 'secret',
|
|
367
|
+
session: `chat-bob-${Date.now()}`,
|
|
368
|
+
token,
|
|
369
|
+
});
|
|
370
|
+
const { status, body } = await sendJson('POST', '/v4/chat/private', {
|
|
371
|
+
username: 'bob',
|
|
372
|
+
token,
|
|
373
|
+
});
|
|
374
|
+
assert.equal(status, 200);
|
|
375
|
+
assert.ok(Array.isArray(body));
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe('POST /v4/hash', () => {
|
|
380
|
+
test('sha256', async () => {
|
|
381
|
+
const { status, body } = await sendJson('POST', '/v4/hash', { text: 'hello', method: 'sha256' });
|
|
382
|
+
assert.equal(status, 200);
|
|
383
|
+
assert.equal(body.hash, '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('md5', async () => {
|
|
387
|
+
const { body } = await sendJson('POST', '/v4/hash', { text: 'hello', method: 'md5' });
|
|
388
|
+
assert.equal(body.hash, '5d41402abc4b2a76b9719d911017c592');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test('missing text returns 400', async () => {
|
|
392
|
+
const { status } = await sendJson('POST', '/v4/hash', { method: 'sha256' });
|
|
393
|
+
assert.equal(status, 400);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe('POST /v4/tic-tac-toe', () => {
|
|
398
|
+
const game = 'TTT' + Date.now().toString(36).slice(-3).toUpperCase();
|
|
399
|
+
const session = `ttt-${Date.now()}`;
|
|
400
|
+
|
|
401
|
+
test('list returns games', async () => {
|
|
402
|
+
const { status, body } = await sendJson('POST', '/v4/tic-tac-toe/list', {});
|
|
403
|
+
assert.equal(status, 200);
|
|
404
|
+
assert.ok(Array.isArray(body.games));
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('plays a move', async () => {
|
|
408
|
+
const { status, body } = await sendJson('POST', '/v4/tic-tac-toe', {
|
|
409
|
+
username: 'alice',
|
|
410
|
+
move: '1-1',
|
|
411
|
+
session,
|
|
412
|
+
game,
|
|
413
|
+
});
|
|
414
|
+
assert.equal(status, 200);
|
|
415
|
+
assert.match(body.message as string, /Move sent/);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test('fetch returns game state', async () => {
|
|
419
|
+
const { status, body } = await sendJson('POST', '/v4/tic-tac-toe/fetch', {
|
|
420
|
+
username: 'alice',
|
|
421
|
+
game,
|
|
422
|
+
});
|
|
423
|
+
assert.equal(status, 200);
|
|
424
|
+
assert.equal(body.id, game);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('missing move returns 400', async () => {
|
|
428
|
+
const { status } = await sendJson('POST', '/v4/tic-tac-toe', {
|
|
429
|
+
username: 'alice',
|
|
430
|
+
session,
|
|
431
|
+
game,
|
|
432
|
+
});
|
|
433
|
+
assert.equal(status, 400);
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
describe('POST /v4/token', () => {
|
|
438
|
+
test('alpha 24', async () => {
|
|
439
|
+
const { status, body } = await sendJson('POST', '/v4/token', { len: 24, type: 'alpha' });
|
|
440
|
+
assert.equal(status, 200);
|
|
441
|
+
assert.equal(typeof body.token, 'string');
|
|
442
|
+
assert.equal((body.token as string).length, 24);
|
|
443
|
+
assert.match(body.token as string, /^[a-zA-Z]+$/);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('below min length returns 400', async () => {
|
|
447
|
+
const { status } = await sendJson('POST', '/v4/token', { len: 5, type: 'alpha' });
|
|
448
|
+
assert.equal(status, 400);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
anagram,
|
|
5
|
+
factorial,
|
|
6
|
+
fibonacci,
|
|
7
|
+
gcd,
|
|
8
|
+
isprime,
|
|
9
|
+
palindrome,
|
|
10
|
+
primefactors,
|
|
11
|
+
reverse,
|
|
12
|
+
roman,
|
|
13
|
+
} from '../../src/modules/v4/algorithms.js';
|
|
14
|
+
|
|
15
|
+
describe('algorithms', () => {
|
|
16
|
+
describe('anagram', () => {
|
|
17
|
+
test('listen / silent', () => {
|
|
18
|
+
assert.equal(anagram('listen', 'silent'), true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('hello / world', () => {
|
|
22
|
+
assert.equal(anagram('hello', 'world'), false);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('factorial', () => {
|
|
27
|
+
test('5! = 120', () => {
|
|
28
|
+
assert.equal(factorial(5), 120);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('0! = 1', () => {
|
|
32
|
+
assert.equal(factorial(0), 1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('throws on negative', () => {
|
|
36
|
+
assert.throws(() => factorial(-1), /positive/);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('fibonacci', () => {
|
|
41
|
+
test('first 10 values', () => {
|
|
42
|
+
assert.deepEqual(fibonacci(10), [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('gcd', () => {
|
|
47
|
+
test('gcd(12, 18) = 6', () => {
|
|
48
|
+
assert.equal(gcd(12, 18), 6);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('gcd(7, 13) = 1', () => {
|
|
52
|
+
assert.equal(gcd(7, 13), 1);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('isprime', () => {
|
|
57
|
+
test('17 is prime', () => {
|
|
58
|
+
assert.equal(isprime(17), true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('15 is not prime', () => {
|
|
62
|
+
assert.equal(isprime(15), false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('palindrome', () => {
|
|
67
|
+
test('madam is palindrome', () => {
|
|
68
|
+
assert.equal(palindrome('madam'), true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('hello is not', () => {
|
|
72
|
+
assert.equal(palindrome('hello'), false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('primefactors', () => {
|
|
77
|
+
test('factors of 60', () => {
|
|
78
|
+
assert.deepEqual(primefactors(60), [2, 2, 3, 5]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('reverse', () => {
|
|
83
|
+
test('reverses a string', () => {
|
|
84
|
+
assert.equal(reverse('abc'), 'cba');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('roman', () => {
|
|
89
|
+
test('2024 → MMXXIV', () => {
|
|
90
|
+
assert.equal(roman('2024'), 'MMXXIV');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('MMXXIV → 2024', () => {
|
|
94
|
+
assert.equal(roman('MMXXIV'), 2024);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('1 → I', () => {
|
|
98
|
+
assert.equal(roman('1'), 'I');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('3999 → MMMCMXCIX', () => {
|
|
102
|
+
assert.equal(roman('3999'), 'MMMCMXCIX');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('round-trip 1..100', () => {
|
|
106
|
+
for (let i = 1; i <= 100; i++) {
|
|
107
|
+
const r = roman(String(i));
|
|
108
|
+
assert.equal(roman(r as string), i);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('throws on out of range', () => {
|
|
113
|
+
assert.throws(() => roman('4000'), /between 1 and 3999/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('throws on invalid roman', () => {
|
|
117
|
+
assert.throws(() => roman('XYZ'), /Invalid Roman/);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import color from '../../src/modules/v4/color.js';
|
|
4
|
+
|
|
5
|
+
describe('color', () => {
|
|
6
|
+
test('returns all 6 formats', () => {
|
|
7
|
+
const result = color();
|
|
8
|
+
assert.ok('hex' in result);
|
|
9
|
+
assert.ok('rgb' in result);
|
|
10
|
+
assert.ok('hsl' in result);
|
|
11
|
+
assert.ok('hsv' in result);
|
|
12
|
+
assert.ok('hwb' in result);
|
|
13
|
+
assert.ok('cmyk' in result);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('hex matches #RRGGBB', () => {
|
|
17
|
+
for (let i = 0; i < 20; i++) {
|
|
18
|
+
assert.match(color().hex!, /^#[0-9a-f]{6}$/);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('rgb format', () => {
|
|
23
|
+
assert.match(color().rgb!, /^rgb\(\d{1,3}, \d{1,3}, \d{1,3}\)$/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('hsl format', () => {
|
|
27
|
+
assert.match(color().hsl!, /^hsl\(\d+(\.\d+)?, \d+(\.\d+)?%, \d+(\.\d+)?%\)$/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('cmyk format with 4 channels', () => {
|
|
31
|
+
assert.match(color().cmyk!, /^cmyk\(/);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import convert from '../../src/modules/v4/convert.js';
|
|
4
|
+
|
|
5
|
+
describe('convert', () => {
|
|
6
|
+
test('celsius to fahrenheit', () => {
|
|
7
|
+
const result = convert(0, 'celsius', 'fahrenheit');
|
|
8
|
+
assert.equal(result.result, 32);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('celsius to kelvin', () => {
|
|
12
|
+
const result = convert(0, 'celsius', 'kelvin');
|
|
13
|
+
assert.equal(result.result, 273.15);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('fahrenheit to celsius', () => {
|
|
17
|
+
const result = convert(32, 'fahrenheit', 'celsius');
|
|
18
|
+
assert.equal(result.result, 0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('kelvin to celsius', () => {
|
|
22
|
+
const result = convert(273.15, 'kelvin', 'celsius');
|
|
23
|
+
assert.equal(result.result, 0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('case-insensitive units', () => {
|
|
27
|
+
const result = convert(100, 'CELSIUS', 'Fahrenheit');
|
|
28
|
+
assert.equal(result.result, 212);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('throws on invalid unit', () => {
|
|
32
|
+
assert.throws(() => convert(100, 'celsius', 'banana'), /Invalid conversion/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('throws on absolute zero violation', () => {
|
|
36
|
+
assert.throws(() => convert(-300, 'celsius', 'kelvin'), /absolute zero/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('throws on non-numeric value', () => {
|
|
40
|
+
assert.throws(() => convert('abc', 'celsius', 'kelvin'), /must be a number/);
|
|
41
|
+
});
|
|
42
|
+
});
|