@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.
- package/README.md +93 -55
- package/dist/config/versions.js +16 -0
- package/dist/config/versions.js.map +1 -1
- package/dist/constants.js +5 -0
- package/dist/constants.js.map +1 -1
- package/dist/modules/v5/chart.js +379 -0
- package/dist/modules/v5/chart.js.map +1 -0
- package/dist/modules/v5/evaluate.js +281 -0
- package/dist/modules/v5/evaluate.js.map +1 -0
- package/dist/modules/v5/matrix.js +238 -0
- package/dist/modules/v5/matrix.js.map +1 -0
- package/dist/modules/v5.js +5 -0
- package/dist/modules/v5.js.map +1 -0
- package/dist/routes/get.js +191 -162
- package/dist/routes/get.js.map +1 -1
- package/dist/routes/post.js +69 -0
- package/dist/routes/post.js.map +1 -1
- package/package.json +4 -2
- package/recap.md +98 -0
- package/.github/FUNDING.yml +0 -1
- package/.prettierrc +0 -7
- package/eslint.config.js +0 -24
- package/robots.txt +0 -74
- package/src/app.ts +0 -53
- package/src/config/env.ts +0 -87
- package/src/config/plans.ts +0 -61
- package/src/config/versions.ts +0 -121
- package/src/constants.ts +0 -61
- package/src/favicon.ico +0 -0
- package/src/middleware/cors.ts +0 -23
- package/src/middleware/error.ts +0 -16
- package/src/middleware/json.ts +0 -9
- package/src/middleware/logger.ts +0 -32
- package/src/middleware/ratelimit.ts +0 -89
- package/src/middleware/version.ts +0 -40
- package/src/modules/v3/algorithms.js +0 -218
- package/src/modules/v3/captcha.js +0 -60
- package/src/modules/v3/chat.js +0 -111
- package/src/modules/v3/color.js +0 -60
- package/src/modules/v3/convert.js +0 -39
- package/src/modules/v3/domain.js +0 -86
- package/src/modules/v3/hash.js +0 -16
- package/src/modules/v3/hyperplanning.js +0 -56
- package/src/modules/v3/levenshtein.js +0 -46
- package/src/modules/v3/personal.js +0 -139
- package/src/modules/v3/qrcode.js +0 -20
- package/src/modules/v3/tic_tac_toe.js +0 -242
- package/src/modules/v3/time.js +0 -98
- package/src/modules/v3/token.js +0 -54
- package/src/modules/v3/username.js +0 -94
- package/src/modules/v3/utils.js +0 -77
- package/src/modules/v3.js +0 -15
- package/src/modules/v4/address.ts +0 -412
- package/src/modules/v4/agent.ts +0 -104
- package/src/modules/v4/algorithms.ts +0 -276
- package/src/modules/v4/captcha.ts +0 -114
- package/src/modules/v4/chat.ts +0 -130
- package/src/modules/v4/color.ts +0 -75
- package/src/modules/v4/convert.ts +0 -127
- package/src/modules/v4/cron.ts +0 -221
- package/src/modules/v4/dice.ts +0 -46
- package/src/modules/v4/domain.ts +0 -86
- package/src/modules/v4/encode.ts +0 -241
- package/src/modules/v4/geo.ts +0 -64
- package/src/modules/v4/hash.ts +0 -32
- package/src/modules/v4/hyperplanning.ts +0 -93
- package/src/modules/v4/ip.ts +0 -134
- package/src/modules/v4/levenshtein.ts +0 -46
- package/src/modules/v4/palette.ts +0 -71
- package/src/modules/v4/password.ts +0 -379
- package/src/modules/v4/personal.ts +0 -140
- package/src/modules/v4/placeholder.ts +0 -197
- package/src/modules/v4/qrcode.ts +0 -121
- package/src/modules/v4/regex.ts +0 -67
- package/src/modules/v4/statistics.ts +0 -62
- package/src/modules/v4/text.ts +0 -330
- package/src/modules/v4/tic_tac_toe.ts +0 -264
- package/src/modules/v4/time.ts +0 -149
- package/src/modules/v4/token.ts +0 -53
- package/src/modules/v4/username.ts +0 -94
- package/src/modules/v4/validate.ts +0 -76
- package/src/modules/v4.ts +0 -29
- package/src/routes/delete.ts +0 -72
- package/src/routes/get.ts +0 -856
- package/src/routes/index.ts +0 -59
- package/src/routes/patch.ts +0 -45
- package/src/routes/post.ts +0 -209
- package/src/storage/index.ts +0 -16
- package/src/types/express.d.ts +0 -17
- package/src/types/storage.ts +0 -48
- package/src/utils/colors.ts +0 -91
- package/src/utils/helpers.ts +0 -90
- package/src/utils/response.ts +0 -28
- package/tests/integration/api.test.ts +0 -772
- package/tests/tsconfig.json +0 -3
- package/tests/unit/address.test.ts +0 -84
- package/tests/unit/agent.test.ts +0 -113
- package/tests/unit/algorithms.test.ts +0 -121
- package/tests/unit/captcha.test.ts +0 -62
- package/tests/unit/chat.test.ts +0 -54
- package/tests/unit/color.test.ts +0 -54
- package/tests/unit/convert.test.ts +0 -68
- package/tests/unit/cron.test.ts +0 -110
- package/tests/unit/dice.test.ts +0 -58
- package/tests/unit/domain.test.ts +0 -48
- package/tests/unit/encode.test.ts +0 -109
- package/tests/unit/geo.test.ts +0 -46
- package/tests/unit/hash.test.ts +0 -45
- package/tests/unit/hyperplanning.test.ts +0 -97
- package/tests/unit/ip.test.ts +0 -140
- package/tests/unit/levenshtein.test.ts +0 -35
- package/tests/unit/palette.test.ts +0 -54
- package/tests/unit/password.test.ts +0 -86
- package/tests/unit/personal.test.ts +0 -66
- package/tests/unit/placeholder.test.ts +0 -58
- package/tests/unit/qrcode.test.ts +0 -83
- package/tests/unit/regex.test.ts +0 -95
- package/tests/unit/statistics.test.ts +0 -58
- package/tests/unit/text.test.ts +0 -108
- package/tests/unit/tic_tac_toe.test.ts +0 -56
- package/tests/unit/time.test.ts +0 -115
- package/tests/unit/token.test.ts +0 -62
- package/tests/unit/username.test.ts +0 -35
- package/tests/unit/validate.test.ts +0 -72
- package/tsconfig.json +0 -17
- package/tsconfig.test.json +0 -9
|
@@ -1,772 +0,0 @@
|
|
|
1
|
-
import { strict as assert } from 'node:assert';
|
|
2
|
-
import type { Server } from 'node:http';
|
|
3
|
-
import type { AddressInfo } from 'node:net';
|
|
4
|
-
import { after, before, describe, test } from 'node:test';
|
|
5
|
-
|
|
6
|
-
import app from '../../src/app.js';
|
|
7
|
-
|
|
8
|
-
let server: Server;
|
|
9
|
-
let baseUrl: string;
|
|
10
|
-
|
|
11
|
-
before(() => {
|
|
12
|
-
return new Promise<void>((resolve) => {
|
|
13
|
-
server = app.listen(0, () => {
|
|
14
|
-
const port = (server.address() as AddressInfo).port;
|
|
15
|
-
baseUrl = `http://127.0.0.1:${port}`;
|
|
16
|
-
resolve();
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
after(() => {
|
|
22
|
-
return new Promise<void>((resolve) => {
|
|
23
|
-
server.close(() => resolve());
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
async function getJson(path: string): Promise<{ status: number; body: Record<string, unknown> }> {
|
|
28
|
-
const res = await fetch(`${baseUrl}${path}`);
|
|
29
|
-
const body = (await res.json()) as Record<string, unknown>;
|
|
30
|
-
return { status: res.status, body };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function sendJson(
|
|
34
|
-
method: string,
|
|
35
|
-
path: string,
|
|
36
|
-
body: Record<string, unknown>,
|
|
37
|
-
): Promise<{ status: number; body: Record<string, unknown> }> {
|
|
38
|
-
const res = await fetch(`${baseUrl}${path}`, {
|
|
39
|
-
method,
|
|
40
|
-
headers: { 'Content-Type': 'application/json' },
|
|
41
|
-
body: JSON.stringify(body),
|
|
42
|
-
});
|
|
43
|
-
const data = (await res.json()) as Record<string, unknown>;
|
|
44
|
-
return { status: res.status, body: data };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// --- GET endpoints ---
|
|
48
|
-
|
|
49
|
-
describe('GET / (version listing)', () => {
|
|
50
|
-
test('v4 endpoints listed', async () => {
|
|
51
|
-
const { body } = await getJson('/v4');
|
|
52
|
-
assert.equal(body.version, 'v4');
|
|
53
|
-
const endpoints = body.endpoints as Record<string, Record<string, string>>;
|
|
54
|
-
assert.ok('dice' in endpoints.get!);
|
|
55
|
-
assert.ok('encode' in endpoints.get!);
|
|
56
|
-
assert.ok('geo' in endpoints.get!);
|
|
57
|
-
assert.ok('palette' in endpoints.get!);
|
|
58
|
-
assert.ok('placeholder' in endpoints.get!);
|
|
59
|
-
assert.ok('statistics' in endpoints.get!);
|
|
60
|
-
assert.ok('text' in endpoints.get!);
|
|
61
|
-
assert.ok('validate' in endpoints.get!);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('invalid version returns 404', async () => {
|
|
65
|
-
const { status } = await getJson('/v99');
|
|
66
|
-
assert.equal(status, 404);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe('GET /v4/algorithms', () => {
|
|
71
|
-
test('fibonacci', async () => {
|
|
72
|
-
const { status, body } = await getJson('/v4/algorithms?method=fibonacci&value=10');
|
|
73
|
-
assert.equal(status, 200);
|
|
74
|
-
assert.deepEqual(body.answer, [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('isprime', async () => {
|
|
78
|
-
const { body } = await getJson('/v4/algorithms?method=isprime&value=17');
|
|
79
|
-
assert.equal(body.answer, true);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('roman 2024 to MMXXIV', async () => {
|
|
83
|
-
const { body } = await getJson('/v4/algorithms?method=roman&value=2024');
|
|
84
|
-
assert.equal(body.answer, 'MMXXIV');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('roman MMXXIV to 2024', async () => {
|
|
88
|
-
const { body } = await getJson('/v4/algorithms?method=roman&value=MMXXIV');
|
|
89
|
-
assert.equal(body.answer, 2024);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe('GET /v4/captcha', () => {
|
|
94
|
-
test('returns PNG with provided text', async () => {
|
|
95
|
-
const res = await fetch(`${baseUrl}/v4/captcha?text=hello`);
|
|
96
|
-
assert.equal(res.status, 200);
|
|
97
|
-
assert.match(res.headers.get('content-type') ?? '', /image\/png/);
|
|
98
|
-
assert.equal(res.headers.get('x-captcha-text'), 'hello');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('auto-generates text when none provided', async () => {
|
|
102
|
-
const res = await fetch(`${baseUrl}/v4/captcha`);
|
|
103
|
-
assert.equal(res.status, 200);
|
|
104
|
-
assert.match(res.headers.get('content-type') ?? '', /image\/png/);
|
|
105
|
-
assert.ok(res.headers.get('x-captcha-text')!.length > 0);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test('accepts noise and dimensions', async () => {
|
|
109
|
-
const res = await fetch(`${baseUrl}/v4/captcha?text=AB&noise=high&width=200&height=80`);
|
|
110
|
-
assert.equal(res.status, 200);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe('GET /v4/chat', () => {
|
|
115
|
-
test('fetches public messages', async () => {
|
|
116
|
-
await sendJson('POST', '/v4/chat', {
|
|
117
|
-
username: 'get-chat-user',
|
|
118
|
-
message: 'ping',
|
|
119
|
-
session: `get-chat-${Date.now()}`,
|
|
120
|
-
});
|
|
121
|
-
const res = await fetch(`${baseUrl}/v4/chat`);
|
|
122
|
-
assert.equal(res.status, 200);
|
|
123
|
-
const body = (await res.json()) as { username: string; message: string }[];
|
|
124
|
-
assert.ok(Array.isArray(body));
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe('GET /v4/color', () => {
|
|
129
|
-
test('returns random color formats', async () => {
|
|
130
|
-
const { status, body } = await getJson('/v4/color');
|
|
131
|
-
assert.equal(status, 200);
|
|
132
|
-
assert.match(body.hex as string, /^#[0-9a-f]{6}$/);
|
|
133
|
-
assert.ok('rgb' in body);
|
|
134
|
-
assert.ok('hsl' in body);
|
|
135
|
-
assert.ok('cmyk' in body);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test('accepts hex input', async () => {
|
|
139
|
-
const { status, body } = await getJson('/v4/color?hex=ff6600');
|
|
140
|
-
assert.equal(status, 200);
|
|
141
|
-
assert.equal(body.hex, '#ff6600');
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe('GET /v4/convert', () => {
|
|
146
|
-
test('celsius to fahrenheit', async () => {
|
|
147
|
-
const { status, body } = await getJson('/v4/convert?value=100&from=celsius&to=fahrenheit');
|
|
148
|
-
assert.equal(status, 200);
|
|
149
|
-
assert.equal(body.result, 212);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test('km to mi', async () => {
|
|
153
|
-
const { status, body } = await getJson('/v4/convert?value=1&from=km&to=mi');
|
|
154
|
-
assert.equal(status, 200);
|
|
155
|
-
assert.ok(Math.abs((body.result as number) - 0.621371) < 0.001);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test('missing value returns 400', async () => {
|
|
159
|
-
const { status } = await getJson('/v4/convert?from=celsius&to=fahrenheit');
|
|
160
|
-
assert.equal(status, 400);
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe('GET /v4/dice', () => {
|
|
165
|
-
test('rolls 2d6+3', async () => {
|
|
166
|
-
const { body } = await getJson('/v4/dice?roll=2d6%2B3');
|
|
167
|
-
assert.equal(body.count, 2);
|
|
168
|
-
assert.equal(body.sides, 6);
|
|
169
|
-
assert.equal(body.modifier, 3);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test('rejects invalid notation', async () => {
|
|
173
|
-
const { status } = await getJson('/v4/dice?roll=foo');
|
|
174
|
-
assert.equal(status, 400);
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
describe('GET /v4/domain', () => {
|
|
179
|
-
test('has TLD', async () => {
|
|
180
|
-
const { status, body } = await getJson('/v4/domain');
|
|
181
|
-
assert.equal(status, 200);
|
|
182
|
-
assert.match(body.domain as string, /\./);
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
describe('GET /v4/encode', () => {
|
|
187
|
-
test('base64 encode', async () => {
|
|
188
|
-
const { status, body } = await getJson('/v4/encode?method=base64encode&text=hello');
|
|
189
|
-
assert.equal(status, 200);
|
|
190
|
-
assert.equal(body.result, 'aGVsbG8=');
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
test('rot13', async () => {
|
|
194
|
-
const { body } = await getJson('/v4/encode?method=rot13&text=Hello');
|
|
195
|
-
assert.equal(body.result, 'Uryyb');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test('missing method returns 400', async () => {
|
|
199
|
-
const { status } = await getJson('/v4/encode?text=hello');
|
|
200
|
-
assert.equal(status, 400);
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
describe('GET /v4/geo', () => {
|
|
205
|
-
test('Paris-NYC distance', async () => {
|
|
206
|
-
const { body } = await getJson('/v4/geo?lat1=48.8566&lon1=2.3522&lat2=40.7128&lon2=-74.006');
|
|
207
|
-
const distance = body.distance as { km: number };
|
|
208
|
-
assert.ok(Math.abs(distance.km - 5837) < 50);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test('missing param returns 400', async () => {
|
|
212
|
-
const { status } = await getJson('/v4/geo?lat1=0&lon1=0');
|
|
213
|
-
assert.equal(status, 400);
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
describe('GET /v4/infos', () => {
|
|
218
|
-
test('returns API infos', async () => {
|
|
219
|
-
const { status, body } = await getJson('/v4/infos');
|
|
220
|
-
assert.equal(status, 200);
|
|
221
|
-
assert.ok('endpoints' in body);
|
|
222
|
-
assert.ok('documentation' in body);
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
describe('GET /v4/levenshtein', () => {
|
|
227
|
-
test('computes distance', async () => {
|
|
228
|
-
const { status, body } = await getJson('/v4/levenshtein?str1=kitten&str2=sitting');
|
|
229
|
-
assert.equal(status, 200);
|
|
230
|
-
assert.equal(body.distance, 3);
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
describe('GET /v4/palette', () => {
|
|
235
|
-
test('triadic returns 3 colors', async () => {
|
|
236
|
-
const { body } = await getJson('/v4/palette?color=%23ff6600&type=triadic');
|
|
237
|
-
assert.equal((body.colors as unknown[]).length, 3);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
test('invalid type returns 400', async () => {
|
|
241
|
-
const { status } = await getJson('/v4/palette?color=%23ff6600&type=rainbow');
|
|
242
|
-
assert.equal(status, 400);
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
describe('GET /v4/personal', () => {
|
|
247
|
-
test('returns generated identity', async () => {
|
|
248
|
-
const { status, body } = await getJson('/v4/personal');
|
|
249
|
-
assert.equal(status, 200);
|
|
250
|
-
assert.ok('name' in body);
|
|
251
|
-
assert.ok('email' in body);
|
|
252
|
-
assert.ok('card' in body);
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
describe('GET /v4/placeholder', () => {
|
|
257
|
-
test('image returns SVG', async () => {
|
|
258
|
-
const res = await fetch(`${baseUrl}/v4/placeholder?type=image&width=100&height=50`);
|
|
259
|
-
assert.equal(res.status, 200);
|
|
260
|
-
assert.match(res.headers.get('content-type') ?? '', /image\/svg\+xml/);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test('skeleton returns SVG', async () => {
|
|
264
|
-
const res = await fetch(`${baseUrl}/v4/placeholder?type=skeleton&width=200&height=100`);
|
|
265
|
-
assert.equal(res.status, 200);
|
|
266
|
-
assert.match(res.headers.get('content-type') ?? '', /image\/svg\+xml/);
|
|
267
|
-
const body = await res.text();
|
|
268
|
-
assert.match(body, /<svg/);
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
describe('GET /v4/qrcode', () => {
|
|
273
|
-
test('returns PNG image by default', async () => {
|
|
274
|
-
const res = await fetch(`${baseUrl}/v4/qrcode?url=https%3A%2F%2Fexample.com`);
|
|
275
|
-
assert.equal(res.status, 200);
|
|
276
|
-
assert.match(res.headers.get('content-type') ?? '', /image\/png/);
|
|
277
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
278
|
-
assert.ok(buf.subarray(0, 4).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47])));
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
test('returns data URL with format=base64', async () => {
|
|
282
|
-
const { status, body } = await getJson('/v4/qrcode?url=https%3A%2F%2Fexample.com&format=base64');
|
|
283
|
-
assert.equal(status, 200);
|
|
284
|
-
assert.match(body as unknown as string, /^data:image\/png;base64,/);
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
describe('GET /v4/statistics', () => {
|
|
289
|
-
test('basic stats', async () => {
|
|
290
|
-
const { body } = await getJson('/v4/statistics?values=1,2,3,4,5');
|
|
291
|
-
assert.equal(body.mean, 3);
|
|
292
|
-
assert.equal(body.median, 3);
|
|
293
|
-
assert.equal(body.count, 5);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
test('missing values returns 400', async () => {
|
|
297
|
-
const { status } = await getJson('/v4/statistics');
|
|
298
|
-
assert.equal(status, 400);
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
describe('GET /v4/text', () => {
|
|
303
|
-
test('slug', async () => {
|
|
304
|
-
const { body } = await getJson('/v4/text?method=slug&value=Hello%20World');
|
|
305
|
-
assert.equal(body.result, 'hello-world');
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
test('number en', async () => {
|
|
309
|
-
const { body } = await getJson('/v4/text?method=number&value=42&lang=en');
|
|
310
|
-
assert.equal(body.result, 'forty-two');
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
test('lorem words', async () => {
|
|
314
|
-
const { body } = await getJson('/v4/text?method=lorem&type=words&count=5');
|
|
315
|
-
assert.equal((body.result as string).split(' ').length, 5);
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
describe('GET /v4/time', () => {
|
|
320
|
-
test('live', async () => {
|
|
321
|
-
const { status, body } = await getJson('/v4/time');
|
|
322
|
-
assert.equal(status, 200);
|
|
323
|
-
assert.ok('iso' in body);
|
|
324
|
-
assert.ok('timestamp' in body);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
test('random in range', async () => {
|
|
328
|
-
const { status, body } = await getJson('/v4/time?type=random&start=2020-01-01&end=2020-12-31');
|
|
329
|
-
assert.equal(status, 200);
|
|
330
|
-
const ts = body.timestamp as number;
|
|
331
|
-
assert.ok(ts >= new Date('2020-01-01').getTime());
|
|
332
|
-
assert.ok(ts <= new Date('2020-12-31').getTime());
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
test('countdown future returns direction and remaining', async () => {
|
|
336
|
-
const future = new Date(Date.now() + 10 * 24 * 3600 * 1000).toISOString();
|
|
337
|
-
const { status, body } = await getJson(`/v4/time?type=countdown&target=${encodeURIComponent(future)}`);
|
|
338
|
-
assert.equal(status, 200);
|
|
339
|
-
assert.equal(body.direction, 'future');
|
|
340
|
-
assert.ok(typeof body.human === 'string');
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
test('countdown missing target returns 400', async () => {
|
|
344
|
-
const { status } = await getJson('/v4/time?type=countdown');
|
|
345
|
-
assert.equal(status, 400);
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
describe('GET /v4/username', () => {
|
|
350
|
-
test('has fields', async () => {
|
|
351
|
-
const { status, body } = await getJson('/v4/username');
|
|
352
|
-
assert.equal(status, 200);
|
|
353
|
-
assert.ok('username' in body);
|
|
354
|
-
assert.ok('adjective' in body);
|
|
355
|
-
assert.ok('animal' in body);
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
describe('GET /v4/validate', () => {
|
|
360
|
-
test('valid luhn', async () => {
|
|
361
|
-
const { body } = await getJson('/v4/validate?type=luhn&value=4111111111111111');
|
|
362
|
-
assert.equal(body.valid, true);
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
test('valid email', async () => {
|
|
366
|
-
const { body } = await getJson('/v4/validate?type=email&value=hello%40example.com');
|
|
367
|
-
assert.equal(body.valid, true);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
test('invalid type returns 400', async () => {
|
|
371
|
-
const { status } = await getJson('/v4/validate?type=foo&value=bar');
|
|
372
|
-
assert.equal(status, 400);
|
|
373
|
-
});
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
describe('GET /v4/website', () => {
|
|
377
|
-
const originalFetch = globalThis.fetch;
|
|
378
|
-
|
|
379
|
-
after(() => {
|
|
380
|
-
globalThis.fetch = originalFetch;
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
test('returns aggregated payload (with mocked GitHub API)', async () => {
|
|
384
|
-
globalThis.fetch = (async (input: string | URL | Request) => {
|
|
385
|
-
const url = typeof input === 'string' ? input : input.toString();
|
|
386
|
-
if (url.includes('api.github.com')) {
|
|
387
|
-
return new Response(
|
|
388
|
-
JSON.stringify({
|
|
389
|
-
data: {
|
|
390
|
-
user: {
|
|
391
|
-
contributionsCollection: {
|
|
392
|
-
contributionCalendar: {
|
|
393
|
-
weeks: [
|
|
394
|
-
{
|
|
395
|
-
firstDay: '2026-01-01',
|
|
396
|
-
contributionDays: [{ date: '2026-01-01', contributionCount: 5 }],
|
|
397
|
-
},
|
|
398
|
-
],
|
|
399
|
-
},
|
|
400
|
-
},
|
|
401
|
-
},
|
|
402
|
-
},
|
|
403
|
-
}),
|
|
404
|
-
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
return originalFetch(input);
|
|
408
|
-
}) as typeof fetch;
|
|
409
|
-
|
|
410
|
-
const { status, body } = await getJson('/v4/website');
|
|
411
|
-
assert.equal(status, 200);
|
|
412
|
-
assert.ok('versions' in body);
|
|
413
|
-
assert.ok('stats' in body);
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
test('?key=active returns sub-key', async () => {
|
|
417
|
-
const { status, body } = await getJson('/v4/website?key=active');
|
|
418
|
-
assert.equal(status, 200);
|
|
419
|
-
assert.ok('active' in body);
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
test('?key=invalid.path returns 404', async () => {
|
|
423
|
-
const { status } = await getJson('/v4/website?key=does.not.exist');
|
|
424
|
-
assert.equal(status, 404);
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
test('rejects prototype pollution keys', async () => {
|
|
428
|
-
const { status } = await getJson('/v4/website?key=__proto__');
|
|
429
|
-
assert.equal(status, 400);
|
|
430
|
-
});
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
// --- POST endpoints ---
|
|
434
|
-
|
|
435
|
-
describe('POST /v4/chat', () => {
|
|
436
|
-
const session = `chat-${Date.now()}`;
|
|
437
|
-
|
|
438
|
-
test('sends a public message', async () => {
|
|
439
|
-
const { status, body } = await sendJson('POST', '/v4/chat', {
|
|
440
|
-
username: 'alice',
|
|
441
|
-
message: 'public hello',
|
|
442
|
-
session,
|
|
443
|
-
});
|
|
444
|
-
assert.equal(status, 200);
|
|
445
|
-
assert.match(body.message as string, /sent/);
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
test('fetches private chat (legacy)', async () => {
|
|
449
|
-
const token = `tok-${Date.now()}`;
|
|
450
|
-
await sendJson('POST', '/v4/chat', {
|
|
451
|
-
username: 'bob',
|
|
452
|
-
message: 'secret',
|
|
453
|
-
session: `chat-bob-${Date.now()}`,
|
|
454
|
-
token,
|
|
455
|
-
});
|
|
456
|
-
const { status, body } = await sendJson('POST', '/v4/chat/private', {
|
|
457
|
-
username: 'bob',
|
|
458
|
-
token,
|
|
459
|
-
});
|
|
460
|
-
assert.equal(status, 200);
|
|
461
|
-
assert.ok(Array.isArray(body));
|
|
462
|
-
});
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
describe('POST /v4/hash', () => {
|
|
466
|
-
test('sha256', async () => {
|
|
467
|
-
const { status, body } = await sendJson('POST', '/v4/hash', { text: 'hello', method: 'sha256' });
|
|
468
|
-
assert.equal(status, 200);
|
|
469
|
-
assert.equal(body.hash, '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
|
|
470
|
-
assert.equal(body.encoding, 'hex');
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
test('md5', async () => {
|
|
474
|
-
const { body } = await sendJson('POST', '/v4/hash', { text: 'hello', method: 'md5' });
|
|
475
|
-
assert.equal(body.hash, '5d41402abc4b2a76b9719d911017c592');
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
test('base64 encoding', async () => {
|
|
479
|
-
const { status, body } = await sendJson('POST', '/v4/hash', {
|
|
480
|
-
text: 'hello',
|
|
481
|
-
method: 'sha256',
|
|
482
|
-
encoding: 'base64',
|
|
483
|
-
});
|
|
484
|
-
assert.equal(status, 200);
|
|
485
|
-
assert.equal(body.encoding, 'base64');
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
test('unsupported method returns 400', async () => {
|
|
489
|
-
const { status } = await sendJson('POST', '/v4/hash', { text: 'hello', method: 'fakehash' });
|
|
490
|
-
assert.equal(status, 400);
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
test('missing text returns 400', async () => {
|
|
494
|
-
const { status } = await sendJson('POST', '/v4/hash', { method: 'sha256' });
|
|
495
|
-
assert.equal(status, 400);
|
|
496
|
-
});
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
describe('POST /v4/tic-tac-toe', () => {
|
|
500
|
-
const game = 'TTT' + Date.now().toString(36).slice(-3).toUpperCase();
|
|
501
|
-
const session = `ttt-${Date.now()}`;
|
|
502
|
-
|
|
503
|
-
test('list returns games', async () => {
|
|
504
|
-
const { status, body } = await sendJson('POST', '/v4/tic-tac-toe/list', {});
|
|
505
|
-
assert.equal(status, 200);
|
|
506
|
-
assert.ok(Array.isArray(body.games));
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
test('plays a move', async () => {
|
|
510
|
-
const { status, body } = await sendJson('POST', '/v4/tic-tac-toe', {
|
|
511
|
-
username: 'alice',
|
|
512
|
-
move: '1-1',
|
|
513
|
-
session,
|
|
514
|
-
game,
|
|
515
|
-
});
|
|
516
|
-
assert.equal(status, 200);
|
|
517
|
-
assert.match(body.message as string, /Move sent/);
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
test('fetch returns game state', async () => {
|
|
521
|
-
const { status, body } = await sendJson('POST', '/v4/tic-tac-toe/fetch', {
|
|
522
|
-
username: 'alice',
|
|
523
|
-
game,
|
|
524
|
-
});
|
|
525
|
-
assert.equal(status, 200);
|
|
526
|
-
assert.equal(body.id, game);
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
test('missing move returns 400', async () => {
|
|
530
|
-
const { status } = await sendJson('POST', '/v4/tic-tac-toe', {
|
|
531
|
-
username: 'alice',
|
|
532
|
-
session,
|
|
533
|
-
game,
|
|
534
|
-
});
|
|
535
|
-
assert.equal(status, 400);
|
|
536
|
-
});
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
describe('POST /v4/token', () => {
|
|
540
|
-
test('alpha 24', async () => {
|
|
541
|
-
const { status, body } = await sendJson('POST', '/v4/token', { len: 24, type: 'alpha' });
|
|
542
|
-
assert.equal(status, 200);
|
|
543
|
-
assert.equal(typeof body.token, 'string');
|
|
544
|
-
assert.equal((body.token as string).length, 24);
|
|
545
|
-
assert.match(body.token as string, /^[a-zA-Z]+$/);
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
test('below min length returns 400', async () => {
|
|
549
|
-
const { status } = await sendJson('POST', '/v4/token', { len: 5, type: 'alpha' });
|
|
550
|
-
assert.equal(status, 400);
|
|
551
|
-
});
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
// --- Security ---
|
|
555
|
-
|
|
556
|
-
describe('Security headers', () => {
|
|
557
|
-
test('returns X-Content-Type-Options and X-Frame-Options', async () => {
|
|
558
|
-
const res = await fetch(`${baseUrl}/v4/color`);
|
|
559
|
-
assert.equal(res.headers.get('x-content-type-options'), 'nosniff');
|
|
560
|
-
assert.equal(res.headers.get('x-frame-options'), 'DENY');
|
|
561
|
-
});
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
describe('Payload size limit', () => {
|
|
565
|
-
test('body > 10kb returns 413', async () => {
|
|
566
|
-
const res = await fetch(`${baseUrl}/v4/hash`, {
|
|
567
|
-
method: 'POST',
|
|
568
|
-
headers: { 'Content-Type': 'application/json' },
|
|
569
|
-
body: JSON.stringify({ text: 'A'.repeat(20000), method: 'sha256' }),
|
|
570
|
-
});
|
|
571
|
-
assert.equal(res.status, 413);
|
|
572
|
-
});
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
describe('GET /v4/headers', () => {
|
|
576
|
-
test('returns headers object with count, ip, method, url', async () => {
|
|
577
|
-
const { status, body } = await getJson('/v4/headers');
|
|
578
|
-
assert.equal(status, 200);
|
|
579
|
-
assert.ok(typeof body.count === 'number');
|
|
580
|
-
assert.ok(typeof body.headers === 'object');
|
|
581
|
-
assert.ok('method' in body);
|
|
582
|
-
assert.equal(body.method, 'GET');
|
|
583
|
-
assert.ok((body.url as string).includes('/v4/headers'));
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
test('filter param returns only matching headers', async () => {
|
|
587
|
-
const { body } = await getJson('/v4/headers?filter=host');
|
|
588
|
-
const headers = body.headers as Record<string, unknown>;
|
|
589
|
-
assert.ok('host' in headers);
|
|
590
|
-
assert.equal(body.count, 1);
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
test('cookie header is redacted', async () => {
|
|
594
|
-
const res = await fetch(`${baseUrl}/v4/headers`, {
|
|
595
|
-
headers: { Cookie: 'session=secret123' },
|
|
596
|
-
});
|
|
597
|
-
const body = (await res.json()) as Record<string, unknown>;
|
|
598
|
-
const headers = body.headers as Record<string, unknown>;
|
|
599
|
-
assert.equal(headers['cookie'], '[redacted]');
|
|
600
|
-
});
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
describe('GET /v4/ip', () => {
|
|
604
|
-
test('returns valid ip result with no address param', async () => {
|
|
605
|
-
const { status, body } = await getJson('/v4/ip');
|
|
606
|
-
assert.equal(status, 200);
|
|
607
|
-
assert.ok(['IPv4', 'IPv6'].includes(body.version as string));
|
|
608
|
-
assert.ok(typeof body.binary === 'string');
|
|
609
|
-
assert.ok(typeof body.reverse === 'string');
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
test('loopback 127.0.0.1', async () => {
|
|
613
|
-
const { status, body } = await getJson('/v4/ip?address=127.0.0.1');
|
|
614
|
-
assert.equal(status, 200);
|
|
615
|
-
assert.equal(body.type, 'loopback');
|
|
616
|
-
assert.equal(body.version, 'IPv4');
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
test('private 192.168.1.1', async () => {
|
|
620
|
-
const { body } = await getJson('/v4/ip?address=192.168.1.1');
|
|
621
|
-
assert.equal(body.type, 'private');
|
|
622
|
-
assert.equal(body.decimal, 3232235777);
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
test('invalid address returns 400', async () => {
|
|
626
|
-
const { status } = await getJson('/v4/ip?address=notanip');
|
|
627
|
-
assert.equal(status, 400);
|
|
628
|
-
});
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
describe('GET /v4/agent', () => {
|
|
632
|
-
test('returns parsed result with browser, os, device, engine, bot', async () => {
|
|
633
|
-
const { status, body } = await getJson('/v4/agent');
|
|
634
|
-
assert.equal(status, 200);
|
|
635
|
-
assert.ok(typeof body.browser === 'object');
|
|
636
|
-
assert.ok(typeof body.os === 'object');
|
|
637
|
-
assert.ok(typeof body.device === 'object');
|
|
638
|
-
assert.ok(typeof body.engine === 'object');
|
|
639
|
-
assert.ok(typeof body.bot === 'boolean');
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
test('custom ua param is parsed', async () => {
|
|
643
|
-
const ua = encodeURIComponent(
|
|
644
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
|
|
645
|
-
);
|
|
646
|
-
const { body } = await getJson(`/v4/agent?ua=${ua}`);
|
|
647
|
-
const browser = body.browser as Record<string, string>;
|
|
648
|
-
assert.equal(browser.name, 'Firefox');
|
|
649
|
-
assert.equal(browser.major, '121');
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
test('bot ua detected', async () => {
|
|
653
|
-
const ua = encodeURIComponent('Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)');
|
|
654
|
-
const { body } = await getJson(`/v4/agent?ua=${ua}`);
|
|
655
|
-
assert.equal(body.bot, true);
|
|
656
|
-
});
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
describe('GET /v4/address', () => {
|
|
660
|
-
test('returns one address by default', async () => {
|
|
661
|
-
const { status, body } = await getJson('/v4/address');
|
|
662
|
-
assert.equal(status, 200);
|
|
663
|
-
assert.equal((body.addresses as unknown[]).length, 1);
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
test('country=fr returns a French address', async () => {
|
|
667
|
-
const { body } = await getJson('/v4/address?country=fr');
|
|
668
|
-
const a = (body.addresses as Record<string, string>[])[0]!;
|
|
669
|
-
assert.equal(a.countryCode, 'FR');
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
test('count=3 returns 3 addresses', async () => {
|
|
673
|
-
const { body } = await getJson('/v4/address?count=3');
|
|
674
|
-
assert.equal((body.addresses as unknown[]).length, 3);
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
test('unknown country returns 400', async () => {
|
|
678
|
-
const { status } = await getJson('/v4/address?country=xx');
|
|
679
|
-
assert.equal(status, 400);
|
|
680
|
-
});
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
describe('GET /v4/password', () => {
|
|
684
|
-
test('returns one password by default', async () => {
|
|
685
|
-
const { status, body } = await getJson('/v4/password');
|
|
686
|
-
assert.equal(status, 200);
|
|
687
|
-
assert.equal((body.passwords as string[]).length, 1);
|
|
688
|
-
assert.equal((body.passwords as string[])[0]!.length, 16);
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
test('length param is respected', async () => {
|
|
692
|
-
const { body } = await getJson('/v4/password?length=32');
|
|
693
|
-
assert.equal((body.passwords as string[])[0]!.length, 32);
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
test('passphrase mode returns words', async () => {
|
|
697
|
-
const { body } = await getJson('/v4/password?type=passphrase&length=4');
|
|
698
|
-
assert.equal(body.type, 'passphrase');
|
|
699
|
-
assert.equal((body.passwords as string[])[0]!.split('-').length, 4);
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
test('count=5 returns 5 passwords', async () => {
|
|
703
|
-
const { body } = await getJson('/v4/password?count=5');
|
|
704
|
-
assert.equal((body.passwords as string[]).length, 5);
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
test('no charset active returns 400', async () => {
|
|
708
|
-
const { status } = await getJson('/v4/password?uppercase=false&lowercase=false&digits=false&symbols=false');
|
|
709
|
-
assert.equal(status, 400);
|
|
710
|
-
});
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
describe('GET /v4/cron', () => {
|
|
714
|
-
test('returns next dates for * * * * *', async () => {
|
|
715
|
-
const { status, body } = await getJson('/v4/cron?expr=*%20*%20*%20*%20*');
|
|
716
|
-
assert.equal(status, 200);
|
|
717
|
-
assert.equal((body.next as string[]).length, 5);
|
|
718
|
-
assert.ok(typeof body.description === 'string');
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
test('count param is respected', async () => {
|
|
722
|
-
const { body } = await getJson('/v4/cron?expr=*%20*%20*%20*%20*&count=3');
|
|
723
|
-
assert.equal((body.next as string[]).length, 3);
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
test('invalid expression returns 400', async () => {
|
|
727
|
-
const { status } = await getJson('/v4/cron?expr=invalid');
|
|
728
|
-
assert.equal(status, 400);
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
test('missing expr returns 400', async () => {
|
|
732
|
-
const { status } = await getJson('/v4/cron');
|
|
733
|
-
assert.equal(status, 400);
|
|
734
|
-
});
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
describe('GET /v4/regex', () => {
|
|
738
|
-
test('basic match returns count and matches', async () => {
|
|
739
|
-
const { status, body } = await getJson('/v4/regex?pattern=hello&text=hello%20world');
|
|
740
|
-
assert.equal(status, 200);
|
|
741
|
-
assert.equal(body.valid, true);
|
|
742
|
-
assert.ok((body.count as number) >= 1);
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
test('invalid pattern returns valid: false with 200', async () => {
|
|
746
|
-
const { status, body } = await getJson('/v4/regex?pattern=%5Binvalid&text=hello');
|
|
747
|
-
assert.equal(status, 200);
|
|
748
|
-
assert.equal(body.valid, false);
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
test('missing pattern returns 400', async () => {
|
|
752
|
-
const { status } = await getJson('/v4/regex?text=hello');
|
|
753
|
-
assert.equal(status, 400);
|
|
754
|
-
});
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
describe('Prototype access on dynamic endpoints', () => {
|
|
758
|
-
test('algorithms?method=toString returns 400', async () => {
|
|
759
|
-
const { status } = await getJson('/v4/algorithms?method=toString');
|
|
760
|
-
assert.equal(status, 400);
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
test('encode?method=constructor returns 400', async () => {
|
|
764
|
-
const { status } = await getJson('/v4/encode?method=constructor&text=hello');
|
|
765
|
-
assert.equal(status, 400);
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
test('validate?type=hasOwnProperty returns 400', async () => {
|
|
769
|
-
const { status } = await getJson('/v4/validate?type=hasOwnProperty&value=test');
|
|
770
|
-
assert.equal(status, 400);
|
|
771
|
-
});
|
|
772
|
-
});
|