@20syldev/api 4.1.0 → 4.3.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/dist/app.js +4 -0
- package/dist/app.js.map +1 -1
- package/dist/config/versions.js +9 -0
- package/dist/config/versions.js.map +1 -1
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -1
- package/dist/modules/v4/placeholder.js +107 -46
- package/dist/modules/v4/placeholder.js.map +1 -1
- package/dist/modules/v4.js +6 -0
- package/dist/modules/v4.js.map +1 -1
- package/dist/routes/delete.js +4 -5
- package/dist/routes/delete.js.map +1 -1
- package/dist/routes/get.js +144 -0
- 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/package.json +1 -1
- package/src/app.ts +4 -0
- package/src/config/versions.ts +9 -0
- package/src/modules/v4/placeholder.ts +124 -54
- package/src/modules/v4.ts +6 -0
- package/src/routes/delete.ts +16 -17
- package/src/routes/get.ts +163 -0
- package/src/routes/patch.ts +2 -3
- package/tests/integration/api.test.ts +102 -3
- package/tests/unit/captcha.test.ts +23 -0
- package/tests/unit/chat.test.ts +50 -0
- package/tests/unit/placeholder.test.ts +57 -0
- package/tests/unit/qrcode.test.ts +14 -0
- package/tests/unit/tic_tac_toe.test.ts +52 -0
- package/docs/changelog.md +0 -333
- package/src/utils/version.ts +0 -5
package/src/routes/get.ts
CHANGED
|
@@ -173,6 +173,52 @@ router.get('/:version/dice', (req: Request, res: Response) => {
|
|
|
173
173
|
}
|
|
174
174
|
});
|
|
175
175
|
|
|
176
|
+
// Encode / decode text
|
|
177
|
+
router.get('/:version/encode', (req: Request, res: Response) => {
|
|
178
|
+
const { method, text, shift } = req.query;
|
|
179
|
+
const { version } = req.params;
|
|
180
|
+
|
|
181
|
+
const encode = (req.module as { encode?: Record<string, (v: string, v2?: string) => string> }).encode;
|
|
182
|
+
if (!encode) {
|
|
183
|
+
error(res, 404, `Endpoint not available in ${version}.`, `${version}/encode`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (!method || !encode[method as string]) {
|
|
187
|
+
error(res, 400, 'Please provide a valid method (?method={method})', `${version}/encode`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const result = encode[method as string]!(text as string, shift as string);
|
|
193
|
+
res.jsonResponse({ method, result });
|
|
194
|
+
} catch (err) {
|
|
195
|
+
error(res, 400, (err as Error).message, `${req.version}/encode`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Geographic distance and bearing between two coordinates
|
|
200
|
+
router.get('/:version/geo', (req: Request, res: Response) => {
|
|
201
|
+
const { lat1, lon1, lat2, lon2 } = req.query;
|
|
202
|
+
const { version } = req.params;
|
|
203
|
+
|
|
204
|
+
const geo = (req.module as { geo?: (a: string, b: string, c: string, d: string) => unknown }).geo;
|
|
205
|
+
if (!geo) {
|
|
206
|
+
error(res, 404, `Endpoint not available in ${version}.`, `${version}/geo`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (lat1 === undefined || lon1 === undefined || lat2 === undefined || lon2 === undefined) {
|
|
210
|
+
error(res, 400, 'Please provide lat1, lon1, lat2 and lon2', `${version}/geo`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const result = geo(lat1 as string, lon1 as string, lat2 as string, lon2 as string);
|
|
216
|
+
res.jsonResponse(result);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
error(res, 400, (err as Error).message, `${req.version}/geo`);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
176
222
|
// GET planning error
|
|
177
223
|
router.get('/:version/hyperplanning', (_req: Request, res: Response) => {
|
|
178
224
|
error(res, 405, 'This endpoint only supports POST requests.');
|
|
@@ -219,6 +265,33 @@ router.get('/:version/levenshtein', (req: Request, res: Response) => {
|
|
|
219
265
|
}
|
|
220
266
|
});
|
|
221
267
|
|
|
268
|
+
// Generate a color palette from a base color
|
|
269
|
+
router.get('/:version/palette', (req: Request, res: Response) => {
|
|
270
|
+
const { color, type } = req.query;
|
|
271
|
+
const { version } = req.params;
|
|
272
|
+
|
|
273
|
+
const palette = (req.module as { palette?: (c: string, t: string) => unknown }).palette;
|
|
274
|
+
if (!palette) {
|
|
275
|
+
error(res, 404, `Endpoint not available in ${version}.`, `${version}/palette`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (!color) {
|
|
279
|
+
error(res, 400, 'Please provide a base color (?color=#ff6600)', `${version}/palette`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (!type) {
|
|
283
|
+
error(res, 400, 'Please provide a palette type (&type=complementary)', `${version}/palette`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const result = palette(color as string, type as string);
|
|
289
|
+
res.jsonResponse(result);
|
|
290
|
+
} catch (err) {
|
|
291
|
+
error(res, 400, (err as Error).message, `${req.version}/palette`);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
222
295
|
// Generate personal data
|
|
223
296
|
router.get('/:version/personal', (req: Request, res: Response) => {
|
|
224
297
|
try {
|
|
@@ -229,6 +302,32 @@ router.get('/:version/personal', (req: Request, res: Response) => {
|
|
|
229
302
|
}
|
|
230
303
|
});
|
|
231
304
|
|
|
305
|
+
// Generate a placeholder image or skeleton
|
|
306
|
+
router.get('/:version/placeholder', (req: Request, res: Response) => {
|
|
307
|
+
const { type = 'image' } = req.query;
|
|
308
|
+
const { version } = req.params;
|
|
309
|
+
|
|
310
|
+
const placeholder = (
|
|
311
|
+
req.module as {
|
|
312
|
+
placeholder?: (
|
|
313
|
+
t: string,
|
|
314
|
+
q: Record<string, string | undefined>,
|
|
315
|
+
) => { type: string; contentType: string; body: Buffer | string };
|
|
316
|
+
}
|
|
317
|
+
).placeholder;
|
|
318
|
+
if (!placeholder) {
|
|
319
|
+
error(res, 404, `Endpoint not available in ${version}.`, `${version}/placeholder`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const result = placeholder(type as string, req.query as Record<string, string | undefined>);
|
|
325
|
+
res.type(result.contentType).send(result.body);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
error(res, 400, (err as Error).message, `${req.version}/placeholder`);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
232
331
|
// Generate QR Code
|
|
233
332
|
router.get('/:version/qrcode', async (req: Request, res: Response) => {
|
|
234
333
|
const { url } = req.query;
|
|
@@ -269,6 +368,70 @@ router.get('/:version/statistics', (req: Request, res: Response) => {
|
|
|
269
368
|
}
|
|
270
369
|
});
|
|
271
370
|
|
|
371
|
+
// Text utilities (slug, stats, lorem, number)
|
|
372
|
+
router.get('/:version/text', (req: Request, res: Response) => {
|
|
373
|
+
const { method, value, type, count, lang, text } = req.query;
|
|
374
|
+
const { version } = req.params;
|
|
375
|
+
|
|
376
|
+
const textMod = (req.module as { text?: Record<string, (...args: string[]) => unknown> }).text;
|
|
377
|
+
if (!textMod) {
|
|
378
|
+
error(res, 404, `Endpoint not available in ${version}.`, `${version}/text`);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (!method || !textMod[method as string]) {
|
|
382
|
+
error(res, 400, 'Please provide a valid method (?method={slug|stats|lorem|number})', `${version}/text`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
let result: unknown;
|
|
388
|
+
switch (method) {
|
|
389
|
+
case 'slug':
|
|
390
|
+
case 'stats':
|
|
391
|
+
result = textMod[method as string]!((value ?? text) as string);
|
|
392
|
+
break;
|
|
393
|
+
case 'lorem':
|
|
394
|
+
result = textMod.lorem!((type as string) || 'words', (count as string) || '5');
|
|
395
|
+
break;
|
|
396
|
+
case 'number':
|
|
397
|
+
result = textMod.number!(value as string, (lang as string) || 'en');
|
|
398
|
+
break;
|
|
399
|
+
default:
|
|
400
|
+
throw new Error('Unknown method');
|
|
401
|
+
}
|
|
402
|
+
res.jsonResponse({ method, result });
|
|
403
|
+
} catch (err) {
|
|
404
|
+
error(res, 400, (err as Error).message, `${req.version}/text`);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Validate data (luhn, iban, email)
|
|
409
|
+
router.get('/:version/validate', (req: Request, res: Response) => {
|
|
410
|
+
const { type, value } = req.query;
|
|
411
|
+
const { version } = req.params;
|
|
412
|
+
|
|
413
|
+
const validate = (req.module as { validate?: Record<string, (v: string) => unknown> }).validate;
|
|
414
|
+
if (!validate) {
|
|
415
|
+
error(res, 404, `Endpoint not available in ${version}.`, `${version}/validate`);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (!type || !validate[type as string]) {
|
|
419
|
+
error(res, 400, 'Please provide a valid type (?type={luhn|iban|email})', `${version}/validate`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (!value) {
|
|
423
|
+
error(res, 400, 'Please provide a value (&value={value})', `${version}/validate`);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
const result = validate[type as string]!(value as string);
|
|
429
|
+
res.jsonResponse(result);
|
|
430
|
+
} catch (err) {
|
|
431
|
+
error(res, 400, (err as Error).message, `${req.version}/validate`);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
272
435
|
// GET tic-tac-toe errors
|
|
273
436
|
router.get('/:version/tic-tac-toe', (_req: Request, res: Response) => {
|
|
274
437
|
error(res, 405, 'This endpoint only supports POST requests.');
|
package/src/routes/patch.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { Router, type Request, type Response } from 'express';
|
|
2
2
|
import { ticTacToeStorage } from '../storage/index.js';
|
|
3
3
|
import { error } from '../utils/response.js';
|
|
4
|
-
import { supportsRest } from '../utils/version.js';
|
|
5
4
|
|
|
6
5
|
const router = Router();
|
|
7
6
|
|
|
8
|
-
// Play a tic-tac-toe move
|
|
7
|
+
// Play a tic-tac-toe move
|
|
9
8
|
router.patch('/:version/tic-tac-toe/:game', (req: Request, res: Response) => {
|
|
10
|
-
if (
|
|
9
|
+
if (req.version !== 'v4') {
|
|
11
10
|
error(res, 405, 'PATCH is only supported in v4+.', `${req.version}/tic-tac-toe`);
|
|
12
11
|
return;
|
|
13
12
|
}
|
|
@@ -51,7 +51,13 @@ describe('GET / (version listing)', () => {
|
|
|
51
51
|
assert.equal(body.version, 'v4');
|
|
52
52
|
const endpoints = body.endpoints as Record<string, Record<string, string>>;
|
|
53
53
|
assert.ok('dice' in endpoints.get!);
|
|
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!);
|
|
54
58
|
assert.ok('statistics' in endpoints.get!);
|
|
59
|
+
assert.ok('text' in endpoints.get!);
|
|
60
|
+
assert.ok('validate' in endpoints.get!);
|
|
55
61
|
});
|
|
56
62
|
|
|
57
63
|
test('invalid version returns 404', async () => {
|
|
@@ -156,6 +162,37 @@ describe('GET /v4/domain', () => {
|
|
|
156
162
|
});
|
|
157
163
|
});
|
|
158
164
|
|
|
165
|
+
describe('GET /v4/encode', () => {
|
|
166
|
+
test('base64 encode', async () => {
|
|
167
|
+
const { status, body } = await getJson('/v4/encode?method=base64encode&text=hello');
|
|
168
|
+
assert.equal(status, 200);
|
|
169
|
+
assert.equal(body.result, 'aGVsbG8=');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('rot13', async () => {
|
|
173
|
+
const { body } = await getJson('/v4/encode?method=rot13&text=Hello');
|
|
174
|
+
assert.equal(body.result, 'Uryyb');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('missing method returns 400', async () => {
|
|
178
|
+
const { status } = await getJson('/v4/encode?text=hello');
|
|
179
|
+
assert.equal(status, 400);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('GET /v4/geo', () => {
|
|
184
|
+
test('Paris-NYC distance', async () => {
|
|
185
|
+
const { body } = await getJson('/v4/geo?lat1=48.8566&lon1=2.3522&lat2=40.7128&lon2=-74.006');
|
|
186
|
+
const distance = body.distance as { km: number };
|
|
187
|
+
assert.ok(Math.abs(distance.km - 5837) < 50);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('missing param returns 400', async () => {
|
|
191
|
+
const { status } = await getJson('/v4/geo?lat1=0&lon1=0');
|
|
192
|
+
assert.equal(status, 400);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
159
196
|
describe('GET /v4/infos', () => {
|
|
160
197
|
test('returns API infos', async () => {
|
|
161
198
|
const { status, body } = await getJson('/v4/infos');
|
|
@@ -173,6 +210,18 @@ describe('GET /v4/levenshtein', () => {
|
|
|
173
210
|
});
|
|
174
211
|
});
|
|
175
212
|
|
|
213
|
+
describe('GET /v4/palette', () => {
|
|
214
|
+
test('triadic returns 3 colors', async () => {
|
|
215
|
+
const { body } = await getJson('/v4/palette?color=%23ff6600&type=triadic');
|
|
216
|
+
assert.equal((body.colors as unknown[]).length, 3);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('invalid type returns 400', async () => {
|
|
220
|
+
const { status } = await getJson('/v4/palette?color=%23ff6600&type=rainbow');
|
|
221
|
+
assert.equal(status, 400);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
176
225
|
describe('GET /v4/personal', () => {
|
|
177
226
|
test('returns generated identity', async () => {
|
|
178
227
|
const { status, body } = await getJson('/v4/personal');
|
|
@@ -183,6 +232,22 @@ describe('GET /v4/personal', () => {
|
|
|
183
232
|
});
|
|
184
233
|
});
|
|
185
234
|
|
|
235
|
+
describe('GET /v4/placeholder', () => {
|
|
236
|
+
test('image returns SVG', async () => {
|
|
237
|
+
const res = await fetch(`${baseUrl}/v4/placeholder?type=image&width=100&height=50`);
|
|
238
|
+
assert.equal(res.status, 200);
|
|
239
|
+
assert.match(res.headers.get('content-type') ?? '', /image\/svg\+xml/);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('skeleton returns SVG', async () => {
|
|
243
|
+
const res = await fetch(`${baseUrl}/v4/placeholder?type=skeleton&width=200&height=100`);
|
|
244
|
+
assert.equal(res.status, 200);
|
|
245
|
+
assert.match(res.headers.get('content-type') ?? '', /image\/svg\+xml/);
|
|
246
|
+
const body = await res.text();
|
|
247
|
+
assert.match(body, /<svg/);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
186
251
|
describe('GET /v4/qrcode', () => {
|
|
187
252
|
test('returns data URL', async () => {
|
|
188
253
|
const { status, body } = await getJson('/v4/qrcode?url=https%3A%2F%2Fexample.com');
|
|
@@ -205,6 +270,23 @@ describe('GET /v4/statistics', () => {
|
|
|
205
270
|
});
|
|
206
271
|
});
|
|
207
272
|
|
|
273
|
+
describe('GET /v4/text', () => {
|
|
274
|
+
test('slug', async () => {
|
|
275
|
+
const { body } = await getJson('/v4/text?method=slug&value=Hello%20World');
|
|
276
|
+
assert.equal(body.result, 'hello-world');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('number en', async () => {
|
|
280
|
+
const { body } = await getJson('/v4/text?method=number&value=42&lang=en');
|
|
281
|
+
assert.equal(body.result, 'forty-two');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('lorem words', async () => {
|
|
285
|
+
const { body } = await getJson('/v4/text?method=lorem&type=words&count=5');
|
|
286
|
+
assert.equal((body.result as string).split(' ').length, 5);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
208
290
|
describe('GET /v4/time', () => {
|
|
209
291
|
test('live', async () => {
|
|
210
292
|
const { status, body } = await getJson('/v4/time');
|
|
@@ -232,6 +314,23 @@ describe('GET /v4/username', () => {
|
|
|
232
314
|
});
|
|
233
315
|
});
|
|
234
316
|
|
|
317
|
+
describe('GET /v4/validate', () => {
|
|
318
|
+
test('valid luhn', async () => {
|
|
319
|
+
const { body } = await getJson('/v4/validate?type=luhn&value=4111111111111111');
|
|
320
|
+
assert.equal(body.valid, true);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('valid email', async () => {
|
|
324
|
+
const { body } = await getJson('/v4/validate?type=email&value=hello%40example.com');
|
|
325
|
+
assert.equal(body.valid, true);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('invalid type returns 400', async () => {
|
|
329
|
+
const { status } = await getJson('/v4/validate?type=foo&value=bar');
|
|
330
|
+
assert.equal(status, 400);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
235
334
|
describe('GET /v4/website', () => {
|
|
236
335
|
const originalFetch = globalThis.fetch;
|
|
237
336
|
|
|
@@ -272,10 +371,10 @@ describe('GET /v4/website', () => {
|
|
|
272
371
|
assert.ok('stats' in body);
|
|
273
372
|
});
|
|
274
373
|
|
|
275
|
-
test('?key=
|
|
276
|
-
const { status, body } = await getJson('/v4/website?key=
|
|
374
|
+
test('?key=active returns sub-key', async () => {
|
|
375
|
+
const { status, body } = await getJson('/v4/website?key=active');
|
|
277
376
|
assert.equal(status, 200);
|
|
278
|
-
assert.ok('
|
|
377
|
+
assert.ok('active' in body);
|
|
279
378
|
});
|
|
280
379
|
|
|
281
380
|
test('?key=invalid.path returns 404', async () => {
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
describe('captcha', () => {
|
|
6
|
+
test('returns a Buffer for valid text', () => {
|
|
7
|
+
const result = captcha('hello');
|
|
8
|
+
assert.ok(Buffer.isBuffer(result));
|
|
9
|
+
assert.ok(result.length > 0);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('PNG starts with correct magic bytes', () => {
|
|
13
|
+
const result = captcha('test');
|
|
14
|
+
assert.equal(result[0], 0x89);
|
|
15
|
+
assert.equal(result[1], 0x50); // P
|
|
16
|
+
assert.equal(result[2], 0x4e); // N
|
|
17
|
+
assert.equal(result[3], 0x47); // G
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('throws on missing text', () => {
|
|
21
|
+
assert.throws(() => captcha(''));
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
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(() => chat('message', { username: '', message: 'hi', session: 's', storage: makeStorage() }), /username/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('throws on invalid action', () => {
|
|
44
|
+
assert.throws(() => chat('delete', { username: 'alice', storage: makeStorage() }), /Invalid action/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('throws on fetch with no messages', () => {
|
|
48
|
+
assert.throws(() => chat('fetch', { username: 'alice', storage: makeStorage() }), /No messages/);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import qrcode from '../../src/modules/v4/qrcode.js';
|
|
4
|
+
|
|
5
|
+
describe('qrcode', () => {
|
|
6
|
+
test('returns data URL for valid url', async () => {
|
|
7
|
+
const result = await qrcode('https://example.com');
|
|
8
|
+
assert.match(result, /^data:image\/png;base64,/);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('throws on missing url', async () => {
|
|
12
|
+
await assert.rejects(() => qrcode(''));
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import tic_tac_toe from '../../src/modules/v4/tic_tac_toe.js';
|
|
4
|
+
import type { TicTacToeStorage } from '../../src/types/storage.js';
|
|
5
|
+
|
|
6
|
+
function makeStorage(): TicTacToeStorage {
|
|
7
|
+
return { games: {}, sessions: {}, rateLimits: {} };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('tic_tac_toe', () => {
|
|
11
|
+
test('list returns empty games array', () => {
|
|
12
|
+
const result = tic_tac_toe('list', { storage: makeStorage() });
|
|
13
|
+
assert.ok(Array.isArray(result.games));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('play creates a game and makes a move', () => {
|
|
17
|
+
const storage = makeStorage();
|
|
18
|
+
const result = tic_tac_toe('play', { username: 'alice', move: '1-1', session: 'a1', game: 'TEST1', storage });
|
|
19
|
+
assert.ok((result.message as string).includes('Move sent'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('fetch returns game state', () => {
|
|
23
|
+
const storage = makeStorage();
|
|
24
|
+
tic_tac_toe('play', { username: 'alice', move: '1-1', session: 'a1', game: 'TEST2', storage });
|
|
25
|
+
const result = tic_tac_toe('fetch', { username: 'alice', game: 'TEST2', storage });
|
|
26
|
+
assert.equal(result.id, 'TEST2');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('forfeit throws on non-player', () => {
|
|
30
|
+
const storage = makeStorage();
|
|
31
|
+
tic_tac_toe('play', { username: 'alice', move: '1-1', session: 'a1', game: 'TEST3', storage });
|
|
32
|
+
assert.throws(
|
|
33
|
+
() => tic_tac_toe('forfeit', { username: 'charlie', session: 'c1', game: 'TEST3', storage }),
|
|
34
|
+
/not a player/,
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('throws on missing username', () => {
|
|
39
|
+
assert.throws(() => tic_tac_toe('play', { move: '1-1', session: 's', game: 'G', storage: makeStorage() }), /username/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('throws on missing move', () => {
|
|
43
|
+
assert.throws(
|
|
44
|
+
() => tic_tac_toe('play', { username: 'alice', session: 's', game: 'G', storage: makeStorage() }),
|
|
45
|
+
/move/i,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('throws on invalid action', () => {
|
|
50
|
+
assert.throws(() => tic_tac_toe('reset', { username: 'alice', storage: makeStorage() }), /Invalid action/);
|
|
51
|
+
});
|
|
52
|
+
});
|