mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-13 06:42:23 +00:00
221 lines
6.8 KiB
JavaScript
221 lines
6.8 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { newsOrigin, newsProxy } from './news.js';
|
|
import { createKeyedNews } from './news/keyed.js';
|
|
|
|
/** Mount the proxy on a fake middleware stack and call it like Connect does. */
|
|
function mount(options) {
|
|
let handler = null;
|
|
newsProxy(options).configureServer({
|
|
middlewares: {
|
|
use(prefix, fn) {
|
|
assert.equal(prefix, '/api/news');
|
|
handler = fn;
|
|
},
|
|
},
|
|
});
|
|
return async (url, method = 'GET') => {
|
|
const res = {
|
|
destroyed: false,
|
|
status: 0,
|
|
headers: {},
|
|
body: '',
|
|
writeHead(status, headers) {
|
|
this.status = status;
|
|
this.headers = headers;
|
|
},
|
|
end(body) {
|
|
this.body = body;
|
|
},
|
|
};
|
|
await handler({ url, method }, res);
|
|
return { ...res, json: res.body ? JSON.parse(res.body) : null };
|
|
};
|
|
}
|
|
|
|
test('the origin must be a plain http(s) origin', () => {
|
|
assert.equal(
|
|
newsOrigin('http://news.example:8080'),
|
|
'http://news.example:8080',
|
|
);
|
|
assert.equal(
|
|
newsOrigin('https://news.example/base/'),
|
|
'https://news.example',
|
|
);
|
|
for (const bad of [
|
|
undefined,
|
|
'',
|
|
' ',
|
|
'ftp://x',
|
|
'http://u:p@x',
|
|
'http://x/?a=1',
|
|
'http://x/#f',
|
|
'not a url',
|
|
])
|
|
assert.equal(newsOrigin(bad), null, String(bad));
|
|
});
|
|
|
|
test('without GEV_NEWS_ORIGIN Core answers itself: Conflicts from GDELT, Local saying it needs its key', async () => {
|
|
const hosts = [];
|
|
const fetchImpl = async (url) => {
|
|
hosts.push(new URL(String(url)).host);
|
|
throw new Error('offline');
|
|
};
|
|
const call = mount({
|
|
origin: () => null,
|
|
fetchImpl,
|
|
keyed: createKeyedNews({ env: () => ({}), fetchImpl, log: () => {} }),
|
|
});
|
|
// GDELT's exports need no key; offline, the read fails like any upstream.
|
|
const hotspots = await call('/hotspots');
|
|
assert.equal(hotspots.status, 502);
|
|
assert.deepEqual(hotspots.json, { error: 'news_upstream_error' });
|
|
const local = await call('/feed?hotspot=all&at=31.95,35.93');
|
|
assert.equal(local.status, 503);
|
|
assert.deepEqual(local.json, { error: 'no_key', key: 'worldnewsapi' });
|
|
// The area is read from the bundled countries, with no key.
|
|
const area = await call('/outline?at=31.95,35.93&v=4');
|
|
assert.equal(area.status, 200);
|
|
assert.equal(area.json.area.name, 'Jordan');
|
|
// The client's allowlist still holds.
|
|
assert.equal((await call('/feed?side=UA')).status, 400);
|
|
assert.ok(hosts.length > 0);
|
|
assert.ok(hosts.every((host) => host === 'data.gdeltproject.org'));
|
|
});
|
|
|
|
test('forwards only the client routes, by GET, and relays the JSON', async () => {
|
|
const calls = [];
|
|
const call = mount({
|
|
origin: () => 'https://news.example',
|
|
fetchImpl: async (url, init) => {
|
|
calls.push({ url: String(url), init });
|
|
return new Response('{"reports":[],"nextBefore":null}', { status: 200 });
|
|
},
|
|
});
|
|
const ok = await call('/feed?hotspot=all&limit=60&today=1&events=1');
|
|
assert.equal(ok.status, 200);
|
|
assert.deepEqual(ok.json, { reports: [], nextBefore: null });
|
|
assert.equal(ok.headers['Cache-Control'], 'no-store');
|
|
assert.equal(
|
|
calls[0].url,
|
|
'https://news.example/api/news/feed?hotspot=all&limit=60&today=1&events=1',
|
|
);
|
|
assert.equal(calls[0].init.redirect, 'error');
|
|
assert.ok(calls[0].init.signal instanceof AbortSignal);
|
|
|
|
for (const url of [
|
|
'/air-alerts',
|
|
'/feed?side=UA',
|
|
'/feed?limit=9999',
|
|
'/../admin',
|
|
'/hotspots?x=1',
|
|
]) {
|
|
const refused = await call(url);
|
|
assert.equal(refused.status, 400, url);
|
|
}
|
|
const post = await call('/hotspots', 'POST');
|
|
assert.equal(post.status, 405);
|
|
assert.equal(post.headers.Allow, 'GET');
|
|
assert.equal(calls.length, 1);
|
|
});
|
|
|
|
test('upstream failures, bad JSON and timeouts become bounded errors', async () => {
|
|
const failing = mount({
|
|
origin: () => 'https://news.example',
|
|
fetchImpl: async () => new Response('down', { status: 500 }),
|
|
});
|
|
const down = await failing('/hotspots');
|
|
assert.equal(down.status, 502);
|
|
assert.deepEqual(down.json, { error: 'news_upstream_error', status: 500 });
|
|
|
|
const garbled = mount({
|
|
origin: () => 'https://news.example',
|
|
fetchImpl: async () => new Response('<html>', { status: 200 }),
|
|
});
|
|
assert.equal((await garbled('/hotspots')).status, 502);
|
|
|
|
const slow = mount({
|
|
origin: () => 'https://news.example',
|
|
timeoutMs: 10,
|
|
// AbortSignal.timeout does not hold the event loop open: the hanging
|
|
// fetch does, until its signal aborts, so the run cannot end before it.
|
|
fetchImpl: (_url, { signal }) =>
|
|
new Promise((_, reject) => {
|
|
const alive = setTimeout(() => {}, 60_000);
|
|
signal.addEventListener('abort', () => {
|
|
clearTimeout(alive);
|
|
reject(signal.reason);
|
|
});
|
|
}),
|
|
});
|
|
const timedOut = await slow('/hotspots');
|
|
assert.equal(timedOut.status, 504);
|
|
assert.deepEqual(timedOut.json, { error: 'news_unavailable' });
|
|
});
|
|
|
|
test('a GEV_NEWS_ORIGIN that is no plain origin is warned of once, and Core answers itself', async () => {
|
|
const logs = [];
|
|
const asked = [];
|
|
const call = mount({
|
|
env: () => ({ GEV_NEWS_ORIGIN: 'https://user:[email protected]' }),
|
|
log: (line) => logs.push(line),
|
|
fetchImpl: async () => assert.fail('nothing is forwarded'),
|
|
keyed: {
|
|
answer: async (path) => {
|
|
asked.push(path);
|
|
return { status: 200, body: { reports: [], nextBefore: null } };
|
|
},
|
|
},
|
|
});
|
|
assert.equal((await call('/feed?hotspot=all')).status, 200);
|
|
assert.equal((await call('/feed?hotspot=all')).status, 200);
|
|
assert.deepEqual(asked, ['/feed', '/feed']);
|
|
assert.equal(logs.length, 1);
|
|
assert.doesNotMatch(logs[0], /secret|news\.example/);
|
|
});
|
|
|
|
test('a built-in answer that throws is a 502, logged in one line without addresses', async () => {
|
|
const logs = [];
|
|
const call = mount({
|
|
env: () => ({}),
|
|
log: (line) => logs.push(line),
|
|
keyed: {
|
|
answer: async () => {
|
|
throw new TypeError('bad https://api.example/?key=abc\nmore');
|
|
},
|
|
},
|
|
});
|
|
const failed = await call('/hotspots');
|
|
assert.equal(failed.status, 502);
|
|
assert.deepEqual(failed.json, { error: 'news_unavailable' });
|
|
assert.equal(logs.length, 1);
|
|
assert.match(logs[0], /TypeError/);
|
|
assert.doesNotMatch(logs[0], /key=abc|\n/);
|
|
});
|
|
|
|
test('a host that serves news only from its own server turns the built-in backend off', async () => {
|
|
let fetches = 0;
|
|
const call = mount({
|
|
origin: () => null,
|
|
builtIn: false,
|
|
fetchImpl: async () => {
|
|
fetches++;
|
|
throw new Error('unexpected');
|
|
},
|
|
});
|
|
for (const url of [
|
|
'/hotspots',
|
|
'/feed?hotspot=all',
|
|
'/outline?at=31.95,35.93&v=4',
|
|
]) {
|
|
const answer = await call(url);
|
|
assert.equal(answer.status, 503, url);
|
|
assert.deepEqual(answer.json, {
|
|
error: 'news_not_configured',
|
|
unavailable: true,
|
|
retryable: false,
|
|
});
|
|
}
|
|
assert.equal(fetches, 0);
|
|
});
|