mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
295 lines
9.7 KiB
JavaScript
295 lines
9.7 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtemp, readdir, rm } from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import {
|
|
createGhostMapsWatcher,
|
|
ghostMapsPollMs,
|
|
ghostMapsProxy,
|
|
} from './ghostMaps.js';
|
|
|
|
const MASTER =
|
|
'ArcGIS Data for ATAK (KMZs)/Common Intelligence Picture/Master Database/';
|
|
const KINETIC =
|
|
'ArcGIS Data for ATAK (KMZs)/Common Intelligence Picture/30-Day Pulls/Kinetic_Activities_Last_30_Days.kmz';
|
|
const sha = (c) => c.repeat(40);
|
|
|
|
/** A fake GitHub: ref advertisement, tree, commit and raw files. */
|
|
function fakeGitHub() {
|
|
const repo = { head: sha('1'), trees: new Map(), calls: [] };
|
|
repo.trees.set(sha('1'), [
|
|
{
|
|
path: `${MASTER}S2U_Common_Intelligence_Picture_October_15_2026_Export_1.kmz`,
|
|
type: 'blob',
|
|
size: 4,
|
|
sha: sha('a'),
|
|
},
|
|
{ path: KINETIC, type: 'blob', size: 4, sha: sha('b') },
|
|
]);
|
|
const body = (text, status = 200) =>
|
|
new Response(text, {
|
|
status,
|
|
headers: { 'content-length': String(Buffer.byteLength(text)) },
|
|
});
|
|
const fetchImpl = async (url) => {
|
|
repo.calls.push(url);
|
|
if (url.endsWith('/info/refs?service=git-upload-pack'))
|
|
return body(
|
|
`001e# service=git-upload-pack\n0000\n003d${repo.head} refs/heads/main\n0000`,
|
|
);
|
|
const tree = /git\/trees\/([0-9a-f]{40})/.exec(url);
|
|
if (tree) return body(JSON.stringify({ tree: repo.trees.get(tree[1]) }));
|
|
if (/\/commits\//.test(url))
|
|
return body(
|
|
JSON.stringify({
|
|
commit: {
|
|
message: 'Oct 18 Update\nmore',
|
|
committer: { date: '2026-10-18T00:00:00Z' },
|
|
},
|
|
}),
|
|
);
|
|
if (url.startsWith('https://raw.githubusercontent.com/'))
|
|
return body('KMZ!');
|
|
return body('nope', 404);
|
|
};
|
|
return { repo, fetchImpl };
|
|
}
|
|
|
|
async function withCache(fn) {
|
|
const dir = await mkdtemp(path.join(os.tmpdir(), 'ghostmaps-'));
|
|
try {
|
|
await fn(dir);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
// checkNow announces the catalog before background warm-up finishes. Join the
|
|
// same file reads before changing editions or removing the temporary cache.
|
|
async function warmDefaults(watcher) {
|
|
await Promise.all(
|
|
watcher
|
|
.catalog()
|
|
.products.filter((product) => product.defaultOn)
|
|
.map((product) => watcher.file(product.id)),
|
|
);
|
|
}
|
|
|
|
test('a new commit is announced once, naming only the products that changed', () =>
|
|
withCache(async (cacheDir) => {
|
|
const { repo, fetchImpl } = fakeGitHub();
|
|
const watcher = createGhostMapsWatcher({
|
|
fetchImpl,
|
|
cacheDir,
|
|
log: () => {},
|
|
warn: () => {},
|
|
});
|
|
const events = [];
|
|
watcher.subscribe((catalog) => events.push(catalog));
|
|
|
|
assert.equal(await watcher.checkNow(), true);
|
|
await warmDefaults(watcher);
|
|
const first = watcher.catalog();
|
|
assert.equal(first.commit, sha('1'));
|
|
assert.equal(first.message, 'Oct 18 Update');
|
|
assert.deepEqual(
|
|
first.products.map((p) => p.id),
|
|
['cip', 'kinetic-30d'],
|
|
);
|
|
assert.equal(events.length, 1);
|
|
|
|
// Same head: one ref read, no REST calls, no event.
|
|
repo.calls.length = 0;
|
|
assert.equal(await watcher.checkNow(), false);
|
|
assert.equal(repo.calls.length, 1);
|
|
assert.equal(events.length, 1);
|
|
|
|
// A new upload: the CIP gets a newer edition, the 30-day pull is untouched.
|
|
repo.head = sha('2');
|
|
repo.trees.set(sha('2'), [
|
|
...repo.trees.get(sha('1')),
|
|
{
|
|
path: `${MASTER}S2U_Common_Intelligence_Picture_October_18_2026_Export_1.kmz`,
|
|
type: 'blob',
|
|
size: 4,
|
|
sha: sha('c'),
|
|
},
|
|
]);
|
|
assert.equal(await watcher.checkNow(), true);
|
|
await warmDefaults(watcher);
|
|
assert.equal(events.length, 2);
|
|
const cip = events[1].products.find((p) => p.id === 'cip');
|
|
assert.equal(cip.version, sha('c'));
|
|
assert.match(cip.fileName, /October_18/);
|
|
assert.equal(
|
|
events[1].products.find((p) => p.id === 'kinetic-30d').version,
|
|
sha('b'),
|
|
);
|
|
// Repository paths stay on the server.
|
|
assert.equal('path' in cip, false);
|
|
watcher.stop();
|
|
}));
|
|
|
|
test('files download by commit-pinned URL, cache by blob, and stale blobs are pruned', () =>
|
|
withCache(async (cacheDir) => {
|
|
const { repo, fetchImpl } = fakeGitHub();
|
|
const watcher = createGhostMapsWatcher({
|
|
fetchImpl,
|
|
cacheDir,
|
|
log: () => {},
|
|
warn: () => {},
|
|
});
|
|
await watcher.checkNow();
|
|
const found = await watcher.file('kinetic-30d');
|
|
assert.equal(found.bytes.toString(), 'KMZ!');
|
|
const raw = repo.calls.filter((url) =>
|
|
url.includes('raw.githubusercontent'),
|
|
);
|
|
assert.ok(raw.every((url) => url.includes(`/GhostMaps/${sha('1')}/`)));
|
|
assert.ok(
|
|
raw.some((url) => url.endsWith('/Kinetic_Activities_Last_30_Days.kmz')),
|
|
);
|
|
const before = repo.calls.length;
|
|
await watcher.file('kinetic-30d');
|
|
assert.equal(repo.calls.length, before, 'second read comes from disk');
|
|
assert.equal(await watcher.file('not-a-product'), null);
|
|
|
|
// When the edition changes, the old blob leaves the disk cache.
|
|
await watcher.file('cip');
|
|
repo.head = sha('2');
|
|
repo.trees.set(sha('2'), [
|
|
{
|
|
path: `${MASTER}S2U_Common_Intelligence_Picture_October_18_2026_Export_1.kmz`,
|
|
type: 'blob',
|
|
size: 4,
|
|
sha: sha('c'),
|
|
},
|
|
{ path: KINETIC, type: 'blob', size: 4, sha: sha('b') },
|
|
]);
|
|
await watcher.checkNow();
|
|
await watcher.file('cip');
|
|
const files = (await readdir(path.join(cacheDir, 'files'))).sort();
|
|
assert.deepEqual(files, [`${sha('b')}.kmz`, `${sha('c')}.kmz`]);
|
|
watcher.stop();
|
|
}));
|
|
|
|
test('the catalog survives a restart from disk, and upstream failures are reported', () =>
|
|
withCache(async (cacheDir) => {
|
|
const { fetchImpl } = fakeGitHub();
|
|
const quiet = { log: () => {}, warn: () => {} };
|
|
const online = createGhostMapsWatcher({ fetchImpl, cacheDir, ...quiet });
|
|
await online.checkNow();
|
|
await warmDefaults(online);
|
|
const offline = createGhostMapsWatcher({
|
|
fetchImpl: async () => new Response('down', { status: 503 }),
|
|
cacheDir,
|
|
...quiet,
|
|
});
|
|
assert.equal(await offline.checkNow(), false);
|
|
const catalog = offline.catalog();
|
|
assert.equal(catalog.commit, sha('1'));
|
|
assert.equal(catalog.products.length, 2);
|
|
assert.match(catalog.error, /HTTP 503/);
|
|
}));
|
|
|
|
test('an unchanged head clears an upstream outage and announces recovery once', () =>
|
|
withCache(async (cacheDir) => {
|
|
const { fetchImpl } = fakeGitHub();
|
|
let unavailable = false;
|
|
let checkedAt = 1000;
|
|
const requests = [];
|
|
const watcher = createGhostMapsWatcher({
|
|
cacheDir,
|
|
now: () => checkedAt,
|
|
fetchImpl: async (url, options) => {
|
|
requests.push(url);
|
|
if (unavailable) return new Response('down', { status: 503 });
|
|
return fetchImpl(url, options);
|
|
},
|
|
log: () => {},
|
|
warn: () => {},
|
|
});
|
|
const events = [];
|
|
watcher.subscribe((catalog) => events.push(catalog));
|
|
await watcher.checkNow();
|
|
await Promise.all([watcher.file('cip'), watcher.file('kinetic-30d')]);
|
|
const healthy = watcher.catalog();
|
|
assert.equal(events.length, 1);
|
|
|
|
unavailable = true;
|
|
checkedAt = 2000;
|
|
assert.equal(await watcher.checkNow(), false);
|
|
assert.match(watcher.catalog().error, /HTTP 503/);
|
|
assert.equal(events.length, 2);
|
|
|
|
unavailable = false;
|
|
checkedAt = 3000;
|
|
requests.length = 0;
|
|
assert.equal(await watcher.checkNow(), false, 'no new edition arrived');
|
|
const recovered = watcher.catalog();
|
|
assert.equal(recovered.error, null);
|
|
assert.equal(recovered.checkedAt, checkedAt);
|
|
assert.equal(recovered.commit, healthy.commit);
|
|
assert.equal(recovered.changedAt, healthy.changedAt);
|
|
assert.deepEqual(recovered.products, healthy.products);
|
|
assert.equal(requests.length, 1, 'recovery needs only the ref read');
|
|
assert.match(requests[0], /\/info\/refs\?service=git-upload-pack$/);
|
|
assert.equal(events.length, 3, 'subscribers see the recovered catalog');
|
|
assert.equal(events[2].error, null);
|
|
assert.equal(events[2].checkedAt, checkedAt);
|
|
|
|
checkedAt = 4000;
|
|
assert.equal(await watcher.checkNow(), false);
|
|
assert.equal(events.length, 3, 'healthy unchanged polls stay silent');
|
|
watcher.stop();
|
|
}));
|
|
|
|
test('poll interval is configurable with a 5 s floor', () => {
|
|
assert.equal(ghostMapsPollMs(undefined), 15000);
|
|
assert.equal(ghostMapsPollMs('30'), 30000);
|
|
assert.equal(ghostMapsPollMs('1'), 5000);
|
|
assert.equal(ghostMapsPollMs('soon'), 15000);
|
|
});
|
|
|
|
test('routes: unknown paths and methods are refused without contacting GitHub', async () => {
|
|
let started = 0;
|
|
const watcher = {
|
|
start: () => {
|
|
started += 1;
|
|
return Promise.resolve(false);
|
|
},
|
|
catalog: () => ({ commit: sha('1'), products: [] }),
|
|
file: async () => null,
|
|
subscribe: () => () => {},
|
|
subscribers: () => 0,
|
|
};
|
|
let handler;
|
|
ghostMapsProxy({ watcher }).configureServer({
|
|
middlewares: { use: (_route, fn) => (handler = fn) },
|
|
});
|
|
const call = async (method, url) => {
|
|
const res = {
|
|
destroyed: false,
|
|
writeHead(status, headers) {
|
|
this.status = status;
|
|
this.headers = headers;
|
|
},
|
|
end(body) {
|
|
this.body = String(body ?? '');
|
|
},
|
|
};
|
|
await handler({ method, url }, res);
|
|
return res;
|
|
};
|
|
assert.equal((await call('GET', '/nope')).status, 404);
|
|
assert.equal((await call('POST', '/catalog')).status, 405);
|
|
assert.equal((await call('GET', '/file/../../etc')).status, 404);
|
|
assert.equal(started, 0);
|
|
assert.equal((await call('GET', '/file/cip')).status, 404);
|
|
const catalog = await call('GET', '/catalog');
|
|
assert.equal(catalog.status, 200);
|
|
assert.equal(JSON.parse(catalog.body).commit, sha('1'));
|
|
assert.equal(started, 2);
|
|
});
|