mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-29 13:05:18 +02:00
107 lines
3.9 KiB
JavaScript
107 lines
3.9 KiB
JavaScript
'use strict';
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { createApp } = require('../src/app');
|
|
|
|
process.env.DATA_FILE = require('node:path').join(require('node:os').tmpdir(),
|
|
`shortlink-test-${process.pid}.json`);
|
|
|
|
let server;
|
|
let port;
|
|
test.before(async () => {
|
|
server = createApp();
|
|
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
port = server.address().port;
|
|
});
|
|
test.after(() => server.close());
|
|
|
|
const post = body => fetch(`http://127.0.0.1:${port}/links`, {
|
|
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
|
const get = p => fetch(`http://127.0.0.1:${port}${p}`, { redirect: 'manual' });
|
|
|
|
test('creates a link with default expiry', async () => {
|
|
const res = await post({ url: 'https://example.com/a' });
|
|
assert.equal(res.status, 201);
|
|
const body = await res.json();
|
|
assert.match(body.code, /^[A-Za-z0-9]{6,10}$/);
|
|
assert.ok(Date.parse(body.expiresAt) > Date.now());
|
|
});
|
|
|
|
test('redirects with 302 and location', async () => {
|
|
const { code } = await (await post({ url: 'https://example.com/b' })).json();
|
|
const res = await get(`/${code}`);
|
|
assert.equal(res.status, 302);
|
|
assert.equal(res.headers.get('location'), 'https://example.com/b');
|
|
});
|
|
|
|
test('unknown code is a 404 envelope', async () => {
|
|
const res = await get('/zzzzzz');
|
|
assert.equal(res.status, 404);
|
|
assert.equal((await res.json()).error.code, 'NOT_FOUND');
|
|
});
|
|
|
|
test('invalid url is a 400 envelope', async () => {
|
|
const res = await post({ url: 'notaurl' });
|
|
assert.equal(res.status, 400);
|
|
assert.equal((await res.json()).error.code, 'INVALID_URL');
|
|
});
|
|
|
|
test('javascript scheme rejected', async () => {
|
|
const res = await post({ url: 'javascript:alert(1)' });
|
|
assert.equal(res.status, 400);
|
|
});
|
|
|
|
test('ttl bounds enforced', async () => {
|
|
const res = await post({ url: 'https://example.com', ttlSeconds: 99999999 });
|
|
assert.equal(res.status, 400);
|
|
assert.equal((await res.json()).error.code, 'INVALID_TTL');
|
|
});
|
|
|
|
test('delete flow', async () => {
|
|
const { code } = await (await post({ url: 'https://example.com/c' })).json();
|
|
const del = await fetch(`http://127.0.0.1:${port}/links/${code}`, { method: 'DELETE' });
|
|
assert.equal(del.status, 204);
|
|
assert.equal((await get(`/${code}`)).status, 404);
|
|
});
|
|
|
|
test('stats start at zero and count redirects', async () => {
|
|
const { code } = await (await post({ url: 'https://example.com/d' })).json();
|
|
const zero = await (await fetch(`http://127.0.0.1:${port}/links/${code}/stats`)).json();
|
|
assert.equal(zero.hits, 0);
|
|
await get(`/${code}`);
|
|
await get(`/${code}`);
|
|
const two = await (await fetch(`http://127.0.0.1:${port}/links/${code}/stats`)).json();
|
|
assert.equal(two.hits, 2);
|
|
});
|
|
|
|
test('stats for unknown code are a 404 envelope', async () => {
|
|
const res = await fetch(`http://127.0.0.1:${port}/links/zzzzzz/stats`);
|
|
assert.equal(res.status, 404);
|
|
assert.equal((await res.json()).error.code, 'NOT_FOUND');
|
|
});
|
|
|
|
test('expired links are 410', async () => {
|
|
const { code } = await (await post({ url: 'https://example.com/e', ttlSeconds: 1 })).json();
|
|
await new Promise(resolve => setTimeout(resolve, 1200));
|
|
assert.equal((await get(`/${code}`)).status, 410);
|
|
});
|
|
|
|
test('malformed json is a 400 envelope', async () => {
|
|
const res = await fetch(`http://127.0.0.1:${port}/links`, {
|
|
method: 'POST', headers: { 'content-type': 'application/json' }, body: '{nope' });
|
|
assert.equal(res.status, 400);
|
|
assert.equal((await res.json()).error.code, 'INVALID_JSON');
|
|
});
|
|
|
|
test('error responses never leak html', async () => {
|
|
const res = await get('/zzzzzz');
|
|
assert.match(res.headers.get('content-type'), /application\/json/);
|
|
});
|
|
|
|
// Last: the flood exhausts the per-client rate-limit bucket.
|
|
test('rate limiting kicks in under a flood', async () => {
|
|
const responses = await Promise.all(Array.from({ length: 30 }, (_, i) =>
|
|
post({ url: `https://example.com/flood-${i}` })));
|
|
assert.ok(responses.some(r => r.status === 429));
|
|
});
|