@20syldev/api 4.2.0 → 4.4.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 +3 -3
- package/dist/app.js +4 -0
- package/dist/app.js.map +1 -1
- package/dist/config/versions.js +19 -0
- package/dist/config/versions.js.map +1 -1
- package/dist/constants.js +2 -2
- package/dist/constants.js.map +1 -1
- package/dist/modules/v3/chat.js +1 -1
- package/dist/modules/v3/chat.js.map +1 -1
- package/dist/modules/v3/domain.js +1 -1
- package/dist/modules/v3/domain.js.map +1 -1
- package/dist/modules/v3/hyperplanning.js +1 -1
- package/dist/modules/v3/hyperplanning.js.map +1 -1
- package/dist/modules/v3/personal.js +1 -1
- package/dist/modules/v3/personal.js.map +1 -1
- package/dist/modules/v3/username.js +1 -1
- package/dist/modules/v3/username.js.map +1 -1
- package/dist/modules/v4/captcha.js +60 -23
- package/dist/modules/v4/captcha.js.map +1 -1
- package/dist/modules/v4/chat.js +1 -1
- package/dist/modules/v4/chat.js.map +1 -1
- package/dist/modules/v4/color.js +44 -35
- package/dist/modules/v4/color.js.map +1 -1
- package/dist/modules/v4/colors.js +54 -0
- package/dist/modules/v4/colors.js.map +1 -0
- package/dist/modules/v4/convert.js +93 -19
- package/dist/modules/v4/convert.js.map +1 -1
- package/dist/modules/v4/domain.js +1 -1
- package/dist/modules/v4/domain.js.map +1 -1
- package/dist/modules/v4/hash.js +11 -4
- package/dist/modules/v4/hash.js.map +1 -1
- package/dist/modules/v4/hyperplanning.js +1 -1
- package/dist/modules/v4/hyperplanning.js.map +1 -1
- package/dist/modules/v4/palette.js +2 -43
- package/dist/modules/v4/palette.js.map +1 -1
- package/dist/modules/v4/personal.js +1 -1
- package/dist/modules/v4/personal.js.map +1 -1
- package/dist/modules/v4/placeholder.js +108 -52
- package/dist/modules/v4/placeholder.js.map +1 -1
- package/dist/modules/v4/qrcode.js +79 -6
- package/dist/modules/v4/qrcode.js.map +1 -1
- package/dist/modules/v4/username.js +1 -1
- package/dist/modules/v4/username.js.map +1 -1
- package/dist/modules/v4.js +6 -0
- package/dist/modules/v4.js.map +1 -1
- package/dist/routes/delete.js +16 -17
- package/dist/routes/delete.js.map +1 -1
- package/dist/routes/get.js +212 -13
- 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 +2 -3
- package/dist/routes/patch.js.map +1 -1
- package/dist/routes/post.js +10 -3
- package/dist/routes/post.js.map +1 -1
- package/dist/utils/colors.js +54 -0
- package/dist/utils/colors.js.map +1 -0
- package/dist/utils/helpers.js +26 -0
- package/dist/utils/helpers.js.map +1 -0
- package/package.json +1 -1
- package/src/app.ts +4 -0
- package/src/config/versions.ts +16 -0
- package/src/constants.ts +0 -2
- package/src/middleware/cors.ts +1 -1
- package/src/modules/v4/captcha.ts +84 -27
- package/src/modules/v4/chat.ts +25 -2
- package/src/modules/v4/color.ts +56 -52
- package/src/modules/v4/convert.ts +100 -20
- package/src/modules/v4/domain.ts +1 -1
- package/src/modules/v4/geo.ts +53 -0
- package/src/modules/v4/hash.ts +18 -4
- package/src/modules/v4/hyperplanning.ts +1 -1
- package/src/modules/v4/palette.ts +63 -0
- package/src/modules/v4/personal.ts +1 -1
- package/src/modules/v4/placeholder.ts +189 -0
- package/src/modules/v4/qrcode.ts +108 -6
- package/src/modules/v4/tic_tac_toe.ts +33 -1
- package/src/modules/v4/username.ts +1 -1
- package/src/modules/v4.ts +3 -0
- package/src/routes/delete.ts +71 -0
- package/src/routes/get.ts +147 -15
- package/src/routes/patch.ts +44 -0
- package/src/routes/post.ts +11 -3
- package/src/utils/colors.ts +91 -0
- package/src/utils/helpers.ts +90 -0
- package/src/utils/response.ts +8 -0
- package/tests/integration/api.test.ts +113 -25
- package/tests/unit/captcha.test.ts +61 -0
- package/tests/unit/chat.test.ts +53 -0
- package/tests/unit/color.test.ts +24 -4
- package/tests/unit/convert.test.ts +26 -1
- package/tests/unit/geo.test.ts +45 -0
- package/tests/unit/hash.test.ts +17 -10
- package/tests/unit/palette.test.ts +53 -0
- package/tests/unit/placeholder.test.ts +57 -0
- package/tests/unit/qrcode.test.ts +82 -0
- package/tests/unit/tic_tac_toe.test.ts +55 -0
- package/src/modules/v4/utils.ts +0 -38
|
@@ -52,6 +52,9 @@ describe('GET / (version listing)', () => {
|
|
|
52
52
|
const endpoints = body.endpoints as Record<string, Record<string, string>>;
|
|
53
53
|
assert.ok('dice' in endpoints.get!);
|
|
54
54
|
assert.ok('encode' in endpoints.get!);
|
|
55
|
+
assert.ok('geo' in endpoints.get!);
|
|
56
|
+
assert.ok('palette' in endpoints.get!);
|
|
57
|
+
assert.ok('placeholder' in endpoints.get!);
|
|
55
58
|
assert.ok('statistics' in endpoints.get!);
|
|
56
59
|
assert.ok('text' in endpoints.get!);
|
|
57
60
|
assert.ok('validate' in endpoints.get!);
|
|
@@ -87,15 +90,23 @@ describe('GET /v4/algorithms', () => {
|
|
|
87
90
|
});
|
|
88
91
|
|
|
89
92
|
describe('GET /v4/captcha', () => {
|
|
90
|
-
test('returns PNG', async () => {
|
|
93
|
+
test('returns PNG with provided text', async () => {
|
|
91
94
|
const res = await fetch(`${baseUrl}/v4/captcha?text=hello`);
|
|
92
95
|
assert.equal(res.status, 200);
|
|
93
96
|
assert.match(res.headers.get('content-type') ?? '', /image\/png/);
|
|
97
|
+
assert.equal(res.headers.get('x-captcha-text'), 'hello');
|
|
94
98
|
});
|
|
95
99
|
|
|
96
|
-
test('
|
|
97
|
-
const
|
|
98
|
-
assert.equal(status,
|
|
100
|
+
test('auto-generates text when none provided', async () => {
|
|
101
|
+
const res = await fetch(`${baseUrl}/v4/captcha`);
|
|
102
|
+
assert.equal(res.status, 200);
|
|
103
|
+
assert.match(res.headers.get('content-type') ?? '', /image\/png/);
|
|
104
|
+
assert.ok(res.headers.get('x-captcha-text')!.length > 0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('accepts noise and dimensions', async () => {
|
|
108
|
+
const res = await fetch(`${baseUrl}/v4/captcha?text=AB&noise=high&width=200&height=80`);
|
|
109
|
+
assert.equal(res.status, 200);
|
|
99
110
|
});
|
|
100
111
|
});
|
|
101
112
|
|
|
@@ -114,7 +125,7 @@ describe('GET /v4/chat', () => {
|
|
|
114
125
|
});
|
|
115
126
|
|
|
116
127
|
describe('GET /v4/color', () => {
|
|
117
|
-
test('returns color formats', async () => {
|
|
128
|
+
test('returns random color formats', async () => {
|
|
118
129
|
const { status, body } = await getJson('/v4/color');
|
|
119
130
|
assert.equal(status, 200);
|
|
120
131
|
assert.match(body.hex as string, /^#[0-9a-f]{6}$/);
|
|
@@ -122,6 +133,12 @@ describe('GET /v4/color', () => {
|
|
|
122
133
|
assert.ok('hsl' in body);
|
|
123
134
|
assert.ok('cmyk' in body);
|
|
124
135
|
});
|
|
136
|
+
|
|
137
|
+
test('accepts hex input', async () => {
|
|
138
|
+
const { status, body } = await getJson('/v4/color?hex=ff6600');
|
|
139
|
+
assert.equal(status, 200);
|
|
140
|
+
assert.equal(body.hex, '#ff6600');
|
|
141
|
+
});
|
|
125
142
|
});
|
|
126
143
|
|
|
127
144
|
describe('GET /v4/convert', () => {
|
|
@@ -131,6 +148,12 @@ describe('GET /v4/convert', () => {
|
|
|
131
148
|
assert.equal(body.result, 212);
|
|
132
149
|
});
|
|
133
150
|
|
|
151
|
+
test('km to mi', async () => {
|
|
152
|
+
const { status, body } = await getJson('/v4/convert?value=1&from=km&to=mi');
|
|
153
|
+
assert.equal(status, 200);
|
|
154
|
+
assert.ok(Math.abs((body.result as number) - 0.621371) < 0.001);
|
|
155
|
+
});
|
|
156
|
+
|
|
134
157
|
test('missing value returns 400', async () => {
|
|
135
158
|
const { status } = await getJson('/v4/convert?from=celsius&to=fahrenheit');
|
|
136
159
|
assert.equal(status, 400);
|
|
@@ -151,6 +174,14 @@ describe('GET /v4/dice', () => {
|
|
|
151
174
|
});
|
|
152
175
|
});
|
|
153
176
|
|
|
177
|
+
describe('GET /v4/domain', () => {
|
|
178
|
+
test('has TLD', async () => {
|
|
179
|
+
const { status, body } = await getJson('/v4/domain');
|
|
180
|
+
assert.equal(status, 200);
|
|
181
|
+
assert.match(body.domain as string, /\./);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
154
185
|
describe('GET /v4/encode', () => {
|
|
155
186
|
test('base64 encode', async () => {
|
|
156
187
|
const { status, body } = await getJson('/v4/encode?method=base64encode&text=hello');
|
|
@@ -169,11 +200,16 @@ describe('GET /v4/encode', () => {
|
|
|
169
200
|
});
|
|
170
201
|
});
|
|
171
202
|
|
|
172
|
-
describe('GET /v4/
|
|
173
|
-
test('
|
|
174
|
-
const {
|
|
175
|
-
|
|
176
|
-
assert.
|
|
203
|
+
describe('GET /v4/geo', () => {
|
|
204
|
+
test('Paris-NYC distance', async () => {
|
|
205
|
+
const { body } = await getJson('/v4/geo?lat1=48.8566&lon1=2.3522&lat2=40.7128&lon2=-74.006');
|
|
206
|
+
const distance = body.distance as { km: number };
|
|
207
|
+
assert.ok(Math.abs(distance.km - 5837) < 50);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('missing param returns 400', async () => {
|
|
211
|
+
const { status } = await getJson('/v4/geo?lat1=0&lon1=0');
|
|
212
|
+
assert.equal(status, 400);
|
|
177
213
|
});
|
|
178
214
|
});
|
|
179
215
|
|
|
@@ -194,6 +230,18 @@ describe('GET /v4/levenshtein', () => {
|
|
|
194
230
|
});
|
|
195
231
|
});
|
|
196
232
|
|
|
233
|
+
describe('GET /v4/palette', () => {
|
|
234
|
+
test('triadic returns 3 colors', async () => {
|
|
235
|
+
const { body } = await getJson('/v4/palette?color=%23ff6600&type=triadic');
|
|
236
|
+
assert.equal((body.colors as unknown[]).length, 3);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('invalid type returns 400', async () => {
|
|
240
|
+
const { status } = await getJson('/v4/palette?color=%23ff6600&type=rainbow');
|
|
241
|
+
assert.equal(status, 400);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
197
245
|
describe('GET /v4/personal', () => {
|
|
198
246
|
test('returns generated identity', async () => {
|
|
199
247
|
const { status, body } = await getJson('/v4/personal');
|
|
@@ -204,9 +252,33 @@ describe('GET /v4/personal', () => {
|
|
|
204
252
|
});
|
|
205
253
|
});
|
|
206
254
|
|
|
255
|
+
describe('GET /v4/placeholder', () => {
|
|
256
|
+
test('image returns SVG', async () => {
|
|
257
|
+
const res = await fetch(`${baseUrl}/v4/placeholder?type=image&width=100&height=50`);
|
|
258
|
+
assert.equal(res.status, 200);
|
|
259
|
+
assert.match(res.headers.get('content-type') ?? '', /image\/svg\+xml/);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('skeleton returns SVG', async () => {
|
|
263
|
+
const res = await fetch(`${baseUrl}/v4/placeholder?type=skeleton&width=200&height=100`);
|
|
264
|
+
assert.equal(res.status, 200);
|
|
265
|
+
assert.match(res.headers.get('content-type') ?? '', /image\/svg\+xml/);
|
|
266
|
+
const body = await res.text();
|
|
267
|
+
assert.match(body, /<svg/);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
207
271
|
describe('GET /v4/qrcode', () => {
|
|
208
|
-
test('returns
|
|
209
|
-
const
|
|
272
|
+
test('returns PNG image by default', async () => {
|
|
273
|
+
const res = await fetch(`${baseUrl}/v4/qrcode?url=https%3A%2F%2Fexample.com`);
|
|
274
|
+
assert.equal(res.status, 200);
|
|
275
|
+
assert.match(res.headers.get('content-type') ?? '', /image\/png/);
|
|
276
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
277
|
+
assert.ok(buf.subarray(0, 4).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47])));
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('returns data URL with format=base64', async () => {
|
|
281
|
+
const { status, body } = await getJson('/v4/qrcode?url=https%3A%2F%2Fexample.com&format=base64');
|
|
210
282
|
assert.equal(status, 200);
|
|
211
283
|
assert.match(body as unknown as string, /^data:image\/png;base64,/);
|
|
212
284
|
});
|
|
@@ -260,6 +332,16 @@ describe('GET /v4/time', () => {
|
|
|
260
332
|
});
|
|
261
333
|
});
|
|
262
334
|
|
|
335
|
+
describe('GET /v4/username', () => {
|
|
336
|
+
test('has fields', async () => {
|
|
337
|
+
const { status, body } = await getJson('/v4/username');
|
|
338
|
+
assert.equal(status, 200);
|
|
339
|
+
assert.ok('username' in body);
|
|
340
|
+
assert.ok('adjective' in body);
|
|
341
|
+
assert.ok('animal' in body);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
263
345
|
describe('GET /v4/validate', () => {
|
|
264
346
|
test('valid luhn', async () => {
|
|
265
347
|
const { body } = await getJson('/v4/validate?type=luhn&value=4111111111111111');
|
|
@@ -277,16 +359,6 @@ describe('GET /v4/validate', () => {
|
|
|
277
359
|
});
|
|
278
360
|
});
|
|
279
361
|
|
|
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
362
|
describe('GET /v4/website', () => {
|
|
291
363
|
const originalFetch = globalThis.fetch;
|
|
292
364
|
|
|
@@ -327,10 +399,10 @@ describe('GET /v4/website', () => {
|
|
|
327
399
|
assert.ok('stats' in body);
|
|
328
400
|
});
|
|
329
401
|
|
|
330
|
-
test('?key=
|
|
331
|
-
const { status, body } = await getJson('/v4/website?key=
|
|
402
|
+
test('?key=active returns sub-key', async () => {
|
|
403
|
+
const { status, body } = await getJson('/v4/website?key=active');
|
|
332
404
|
assert.equal(status, 200);
|
|
333
|
-
assert.ok('
|
|
405
|
+
assert.ok('active' in body);
|
|
334
406
|
});
|
|
335
407
|
|
|
336
408
|
test('?key=invalid.path returns 404', async () => {
|
|
@@ -381,6 +453,7 @@ describe('POST /v4/hash', () => {
|
|
|
381
453
|
const { status, body } = await sendJson('POST', '/v4/hash', { text: 'hello', method: 'sha256' });
|
|
382
454
|
assert.equal(status, 200);
|
|
383
455
|
assert.equal(body.hash, '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
|
|
456
|
+
assert.equal(body.encoding, 'hex');
|
|
384
457
|
});
|
|
385
458
|
|
|
386
459
|
test('md5', async () => {
|
|
@@ -388,6 +461,21 @@ describe('POST /v4/hash', () => {
|
|
|
388
461
|
assert.equal(body.hash, '5d41402abc4b2a76b9719d911017c592');
|
|
389
462
|
});
|
|
390
463
|
|
|
464
|
+
test('base64 encoding', async () => {
|
|
465
|
+
const { status, body } = await sendJson('POST', '/v4/hash', {
|
|
466
|
+
text: 'hello',
|
|
467
|
+
method: 'sha256',
|
|
468
|
+
encoding: 'base64',
|
|
469
|
+
});
|
|
470
|
+
assert.equal(status, 200);
|
|
471
|
+
assert.equal(body.encoding, 'base64');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test('unsupported method returns 400', async () => {
|
|
475
|
+
const { status } = await sendJson('POST', '/v4/hash', { text: 'hello', method: 'fakehash' });
|
|
476
|
+
assert.equal(status, 400);
|
|
477
|
+
});
|
|
478
|
+
|
|
391
479
|
test('missing text returns 400', async () => {
|
|
392
480
|
const { status } = await sendJson('POST', '/v4/hash', { method: 'sha256' });
|
|
393
481
|
assert.equal(status, 400);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import captcha from '../../src/modules/v4/captcha.js';
|
|
4
|
+
|
|
5
|
+
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
|
|
6
|
+
|
|
7
|
+
describe('captcha', () => {
|
|
8
|
+
test('returns PNG buffer with provided text', () => {
|
|
9
|
+
const result = captcha({ text: 'hello' });
|
|
10
|
+
assert.equal(result.contentType, 'image/png');
|
|
11
|
+
assert.ok(Buffer.isBuffer(result.body));
|
|
12
|
+
assert.ok(result.body.subarray(0, 4).equals(PNG_MAGIC));
|
|
13
|
+
assert.equal(result.text, 'hello');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('auto-generates text when none provided', () => {
|
|
17
|
+
const result = captcha({});
|
|
18
|
+
assert.equal(result.text.length, 6);
|
|
19
|
+
assert.ok(Buffer.isBuffer(result.body));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('respects custom length', () => {
|
|
23
|
+
const result = captcha({ length: 10 });
|
|
24
|
+
assert.equal(result.text.length, 10);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('throws on invalid length', () => {
|
|
28
|
+
assert.throws(() => captcha({ length: 0 }), /length/);
|
|
29
|
+
assert.throws(() => captcha({ length: 99 }), /length/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('accepts custom dimensions', () => {
|
|
33
|
+
const result = captcha({ text: 'AB', width: 200, height: 80 });
|
|
34
|
+
assert.ok(Buffer.isBuffer(result.body));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('throws on invalid dimensions', () => {
|
|
38
|
+
assert.throws(() => captcha({ text: 'AB', width: 10 }), /width/);
|
|
39
|
+
assert.throws(() => captcha({ text: 'AB', height: 500 }), /height/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('accepts noise levels', () => {
|
|
43
|
+
for (const noise of ['low', 'medium', 'high'] as const) {
|
|
44
|
+
const result = captcha({ text: 'test', noise });
|
|
45
|
+
assert.ok(Buffer.isBuffer(result.body));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('throws on invalid noise', () => {
|
|
50
|
+
assert.throws(() => captcha({ text: 'test', noise: 'extreme' as 'low' }), /noise/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('accepts custom colors', () => {
|
|
54
|
+
const result = captcha({ text: 'test', bg: 'f0f0f0', color: '333333' });
|
|
55
|
+
assert.ok(Buffer.isBuffer(result.body));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('throws on invalid color', () => {
|
|
59
|
+
assert.throws(() => captcha({ text: 'test', bg: 'xyz' }), /color/i);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import chat from '../../src/modules/v4/chat.js';
|
|
4
|
+
import type { ChatStorage } from '../../src/types/storage.js';
|
|
5
|
+
|
|
6
|
+
function makeStorage(): ChatStorage {
|
|
7
|
+
return { messages: [], privateChats: {}, sessions: {}, rateLimits: {} };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('chat', () => {
|
|
11
|
+
test('sends a public message', () => {
|
|
12
|
+
const storage = makeStorage();
|
|
13
|
+
const result = chat('message', { username: 'alice', message: 'hello', session: 'a1', storage });
|
|
14
|
+
assert.ok((result as { message: string }).message.includes('sent'));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('fetches public messages', () => {
|
|
18
|
+
const storage = makeStorage();
|
|
19
|
+
chat('message', { username: 'alice', message: 'test', session: 'a1', storage });
|
|
20
|
+
const result = chat('fetch', { username: 'bob', storage });
|
|
21
|
+
assert.ok(Array.isArray(result));
|
|
22
|
+
assert.equal((result as { message: string }[])[0]!.message, 'test');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('sends and retrieves private messages', () => {
|
|
26
|
+
const storage = makeStorage();
|
|
27
|
+
chat('message', { username: 'alice', message: 'secret', session: 'a1', token: 'tok1', storage });
|
|
28
|
+
const result = chat('private', { username: 'bob', token: 'tok1', storage });
|
|
29
|
+
assert.ok(Array.isArray(result));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('clears a private chat', () => {
|
|
33
|
+
const storage = makeStorage();
|
|
34
|
+
chat('message', { username: 'alice', message: 'secret', session: 'a1', token: 'tok2', storage });
|
|
35
|
+
const result = chat('clear', { username: 'alice', session: 'a1', token: 'tok2', storage });
|
|
36
|
+
assert.ok((result as { message: string }).message.includes('cleared'));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('throws on missing username', () => {
|
|
40
|
+
assert.throws(
|
|
41
|
+
() => chat('message', { username: '', message: 'hi', session: 's', storage: makeStorage() }),
|
|
42
|
+
/username/,
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('throws on invalid action', () => {
|
|
47
|
+
assert.throws(() => chat('delete', { username: 'alice', storage: makeStorage() }), /Invalid action/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('throws on fetch with no messages', () => {
|
|
51
|
+
assert.throws(() => chat('fetch', { username: 'alice', storage: makeStorage() }), /No messages/);
|
|
52
|
+
});
|
|
53
|
+
});
|
package/tests/unit/color.test.ts
CHANGED
|
@@ -15,19 +15,39 @@ describe('color', () => {
|
|
|
15
15
|
|
|
16
16
|
test('hex matches #RRGGBB', () => {
|
|
17
17
|
for (let i = 0; i < 20; i++) {
|
|
18
|
-
assert.match(color().hex
|
|
18
|
+
assert.match(color().hex, /^#[0-9a-f]{6}$/);
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
test('rgb format', () => {
|
|
23
|
-
assert.match(color().rgb
|
|
23
|
+
assert.match(color().rgb, /^rgb\(\d{1,3}, \d{1,3}, \d{1,3}\)$/);
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
test('hsl format', () => {
|
|
27
|
-
assert.match(color().hsl
|
|
27
|
+
assert.match(color().hsl, /^hsl\(\d+(\.\d+)?, \d+(\.\d+)?%, \d+(\.\d+)?%\)$/);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
test('cmyk format with 4 channels', () => {
|
|
31
|
-
assert.match(color().cmyk
|
|
31
|
+
assert.match(color().cmyk, /^cmyk\(/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('accepts hex input', () => {
|
|
35
|
+
const result = color('#ff6600');
|
|
36
|
+
assert.equal(result.hex, '#ff6600');
|
|
37
|
+
assert.equal(result.rgb, 'rgb(255, 102, 0)');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('accepts hex without #', () => {
|
|
41
|
+
const result = color('ff6600');
|
|
42
|
+
assert.equal(result.hex, '#ff6600');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('throws on invalid hex', () => {
|
|
46
|
+
assert.throws(() => color('xyz'), /Invalid HEX/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('HSV correct for equal channels', () => {
|
|
50
|
+
const result = color('#808000');
|
|
51
|
+
assert.match(result.hsv, /^hsv\(60\.0,/);
|
|
32
52
|
});
|
|
33
53
|
});
|
|
@@ -36,7 +36,32 @@ describe('convert', () => {
|
|
|
36
36
|
assert.throws(() => convert(-300, 'celsius', 'kelvin'), /absolute zero/);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
test('km to mi', () => {
|
|
40
|
+
const result = convert(1, 'km', 'mi');
|
|
41
|
+
assert.ok(Math.abs(result.result - 0.621371) < 0.001);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('kg to lb', () => {
|
|
45
|
+
const result = convert(1, 'kg', 'lb');
|
|
46
|
+
assert.ok(Math.abs(result.result - 2.20462) < 0.001);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('kb to mb', () => {
|
|
50
|
+
const result = convert(1024, 'kb', 'mb');
|
|
51
|
+
assert.equal(result.result, 1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('km/h to mph', () => {
|
|
55
|
+
const result = convert(100, 'km/h', 'mph');
|
|
56
|
+
assert.ok(Math.abs(result.result - 62.1371) < 0.01);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('negative values allowed for non-temperature', () => {
|
|
60
|
+
const result = convert(-5, 'km', 'mi');
|
|
61
|
+
assert.ok(result.result < 0);
|
|
62
|
+
});
|
|
63
|
+
|
|
39
64
|
test('throws on non-numeric value', () => {
|
|
40
|
-
assert.throws(() => convert(
|
|
65
|
+
assert.throws(() => convert(NaN, 'celsius', 'kelvin'), /must be a number/);
|
|
41
66
|
});
|
|
42
67
|
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import geo from '../../src/modules/v4/geo.js';
|
|
4
|
+
|
|
5
|
+
describe('geo', () => {
|
|
6
|
+
test('Paris to NYC distance ~5837 km', () => {
|
|
7
|
+
const result = geo('48.8566', '2.3522', '40.7128', '-74.006');
|
|
8
|
+
assert.ok(Math.abs(result.distance.km - 5837) < 50, `expected ~5837, got ${result.distance.km}`);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('same point returns 0 km', () => {
|
|
12
|
+
const result = geo('48.8566', '2.3522', '48.8566', '2.3522');
|
|
13
|
+
assert.equal(result.distance.km, 0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('miles and nautical miles match km conversion', () => {
|
|
17
|
+
const result = geo('0', '0', '0', '90');
|
|
18
|
+
assert.ok(Math.abs(result.distance.miles - result.distance.km * 0.621371) < 0.01);
|
|
19
|
+
assert.ok(Math.abs(result.distance.nauticalMiles - result.distance.km * 0.539957) < 0.01);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('bearing N for due north', () => {
|
|
23
|
+
const result = geo('0', '0', '10', '0');
|
|
24
|
+
assert.equal(result.bearing.degrees, 0);
|
|
25
|
+
assert.equal(result.bearing.cardinal, 'N');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('bearing E for due east', () => {
|
|
29
|
+
const result = geo('0', '0', '0', '10');
|
|
30
|
+
assert.equal(result.bearing.degrees, 90);
|
|
31
|
+
assert.equal(result.bearing.cardinal, 'E');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('throws on invalid latitude', () => {
|
|
35
|
+
assert.throws(() => geo('100', '0', '0', '0'), /lat1 must be between/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('throws on invalid longitude', () => {
|
|
39
|
+
assert.throws(() => geo('0', '200', '0', '0'), /lon1 must be between/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('throws on non-numeric coords', () => {
|
|
43
|
+
assert.throws(() => geo('abc', '0', '0', '0'), /must be a number/);
|
|
44
|
+
});
|
|
45
|
+
});
|
package/tests/unit/hash.test.ts
CHANGED
|
@@ -8,25 +8,32 @@ describe('hash', () => {
|
|
|
8
8
|
assert.deepEqual(result, {
|
|
9
9
|
method: 'sha256',
|
|
10
10
|
hash: '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
|
|
11
|
+
encoding: 'hex',
|
|
11
12
|
});
|
|
12
13
|
});
|
|
13
14
|
|
|
14
15
|
test('md5 of "hello"', () => {
|
|
15
16
|
const result = hash('hello', 'md5');
|
|
16
|
-
assert.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
assert.equal(result.hash, '5d41402abc4b2a76b9719d911017c592');
|
|
18
|
+
assert.equal(result.encoding, 'hex');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('base64 encoding', () => {
|
|
22
|
+
const result = hash('hello', 'sha256', 'base64');
|
|
23
|
+
assert.equal(result.encoding, 'base64');
|
|
24
|
+
assert.ok(result.hash.length > 0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('throws on empty text', () => {
|
|
28
|
+
assert.throws(() => hash('', 'sha256'), /Text is required/);
|
|
20
29
|
});
|
|
21
30
|
|
|
22
|
-
test('
|
|
23
|
-
|
|
24
|
-
assert.equal((result as { hash: string }).hash, 'da39a3ee5e6b4b0d3255bfef95601890afd80709');
|
|
31
|
+
test('throws on unsupported method', () => {
|
|
32
|
+
assert.throws(() => hash('hello', 'fakehash'), /Unsupported method/);
|
|
25
33
|
});
|
|
26
34
|
|
|
27
|
-
test('
|
|
28
|
-
|
|
29
|
-
assert.ok('error' in result);
|
|
35
|
+
test('throws on invalid encoding', () => {
|
|
36
|
+
assert.throws(() => hash('hello', 'sha256', 'utf8'), /Encoding must be/);
|
|
30
37
|
});
|
|
31
38
|
|
|
32
39
|
test('same input + method = same hash (deterministic)', () => {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import palette from '../../src/modules/v4/palette.js';
|
|
4
|
+
|
|
5
|
+
describe('palette', () => {
|
|
6
|
+
test('complementary returns 2 colors', () => {
|
|
7
|
+
const result = palette('#ff6600', 'complementary');
|
|
8
|
+
assert.equal(result.colors.length, 2);
|
|
9
|
+
assert.equal(result.type, 'complementary');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('triadic returns 3 colors', () => {
|
|
13
|
+
const result = palette('#ff6600', 'triadic');
|
|
14
|
+
assert.equal(result.colors.length, 3);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('analogous returns 5 colors', () => {
|
|
18
|
+
const result = palette('#ff6600', 'analogous');
|
|
19
|
+
assert.equal(result.colors.length, 5);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('tetradic returns 4 colors', () => {
|
|
23
|
+
const result = palette('#0066ff', 'tetradic');
|
|
24
|
+
assert.equal(result.colors.length, 4);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('split-complementary returns 3 colors', () => {
|
|
28
|
+
const result = palette('#0066ff', 'split-complementary');
|
|
29
|
+
assert.equal(result.colors.length, 3);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('each color has hex/rgb/hsl', () => {
|
|
33
|
+
const result = palette('#ff6600', 'triadic');
|
|
34
|
+
for (const c of result.colors) {
|
|
35
|
+
assert.match(c.hex, /^#[0-9a-f]{6}$/);
|
|
36
|
+
assert.match(c.rgb, /^rgb\(/);
|
|
37
|
+
assert.match(c.hsl, /^hsl\(/);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('base color is included', () => {
|
|
42
|
+
const result = palette('#ff6600', 'complementary');
|
|
43
|
+
assert.equal(result.base.hex, '#ff6600');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('throws on invalid hex', () => {
|
|
47
|
+
assert.throws(() => palette('#zzz', 'triadic'), /Invalid HEX/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('throws on unknown type', () => {
|
|
51
|
+
assert.throws(() => palette('#ff6600', 'rainbow'), /must be one of/);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import placeholder from '../../src/modules/v4/placeholder.js';
|
|
4
|
+
|
|
5
|
+
describe('placeholder', () => {
|
|
6
|
+
test('image returns SVG with text', () => {
|
|
7
|
+
const result = placeholder('image', { width: '200', height: '100', text: 'Hello' });
|
|
8
|
+
assert.equal(result.type, 'image');
|
|
9
|
+
assert.equal(result.contentType, 'image/svg+xml');
|
|
10
|
+
assert.match(result.body, /<svg/);
|
|
11
|
+
assert.match(result.body, /Hello/);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('skeleton returns SVG with animation', () => {
|
|
15
|
+
const result = placeholder('skeleton', { width: '300', height: '200' });
|
|
16
|
+
assert.equal(result.type, 'skeleton');
|
|
17
|
+
assert.equal(result.contentType, 'image/svg+xml');
|
|
18
|
+
assert.match(result.body, /<svg/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('default dimensions 800x600', () => {
|
|
22
|
+
const result = placeholder('image', {});
|
|
23
|
+
assert.match(result.body, /width="800"/);
|
|
24
|
+
assert.match(result.body, /height="600"/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('skeleton with avatar', () => {
|
|
28
|
+
const result = placeholder('skeleton', { avatar: 'circle' });
|
|
29
|
+
assert.match(result.body, /<circle/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('skeleton with rows', () => {
|
|
33
|
+
const result = placeholder('skeleton', { rows: '5' });
|
|
34
|
+
assert.match(result.body, /<rect/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('animate none removes animation', () => {
|
|
38
|
+
const result = placeholder('skeleton', { animate: 'none' });
|
|
39
|
+
assert.ok(!result.body.includes('<animate'));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('throws on invalid type', () => {
|
|
43
|
+
assert.throws(() => placeholder('video', {}), /Type must be one of/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('throws on invalid width', () => {
|
|
47
|
+
assert.throws(() => placeholder('image', { width: '5000' }), /width/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('throws on invalid avatar shape', () => {
|
|
51
|
+
assert.throws(() => placeholder('skeleton', { avatar: 'hexagon' }), /avatar/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('throws on invalid speed', () => {
|
|
55
|
+
assert.throws(() => placeholder('skeleton', { speed: '99' }), /speed/);
|
|
56
|
+
});
|
|
57
|
+
});
|