aeStatusPage/test/providerStore.test.js
jmcqueen 007086caf6 Initial commit: status-page bridge with Webex bot management
Bridges third-party status pages into Webex spaces via RSS polling and
inbound webhooks (Statuspage / Status.io / Uptime Kuma / generic).

Includes an interactive Webex bot (websocket transport) that lets space
members register sources with an Adaptive Card instead of hand-editing
config/feeds.json: help, add, list, webhook <key>, remove <key>.

Ships with an atomic JSON store (per-file mutex, tmp+rename), parallel
RSS polling, and unit tests via node:test. All secrets are sourced from
environment variables (see .env.example); no credentials in the repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 16:08:34 -04:00

102 lines
4.4 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { ProviderStore, assertValidKey, assertValidSpec } from '../src/providerStore.js';
function seed(initial) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'store-'));
const file = path.join(dir, 'feeds.json');
fs.writeFileSync(file, JSON.stringify(initial ?? { providers: {} }));
return file;
}
test('load + listByRoom returns only providers for that room', () => {
const file = seed({
providers: {
a: { name: 'A', roomId: 'r1', rss: { url: 'https://a.example/rss' } },
b: { name: 'B', roomId: 'r2', webhook: { format: 'statusPage' } },
c: { name: 'C', roomId: 'r1', webhook: { format: 'uptimeKuma' } },
},
});
const store = ProviderStore.load(file);
const inR1 = store.listByRoom('r1').map(p => p.key).sort();
assert.deepEqual(inR1, ['a', 'c']);
assert.deepEqual(store.listByRoom('r2').map(p => p.key), ['b']);
assert.deepEqual(store.listByRoom('missing'), []);
});
test('upsert adds a new provider and persists to disk', async () => {
const file = seed();
const store = ProviderStore.load(file);
await store.upsert('acme', {
name: 'Acme',
roomId: 'room-1',
rss: { url: 'https://acme.example/rss' },
});
const reloaded = JSON.parse(fs.readFileSync(file, 'utf8'));
assert.equal(reloaded.providers.acme.name, 'Acme');
assert.equal(reloaded.providers.acme.rss.url, 'https://acme.example/rss');
});
test('upsert preserves rss.lastCheck on edit', async () => {
const stamp = '2026-01-01T00:00:00.000Z';
const file = seed({
providers: {
acme: { name: 'Acme', roomId: 'r', rss: { url: 'https://a/rss', lastCheck: stamp } },
},
});
const store = ProviderStore.load(file);
await store.upsert('acme', {
name: 'Acme (renamed)',
roomId: 'r',
rss: { url: 'https://a/rss2' },
});
assert.equal(store.get('acme').rss.lastCheck, stamp);
assert.equal(store.get('acme').rss.url, 'https://a/rss2');
assert.equal(store.get('acme').name, 'Acme (renamed)');
});
test('remove deletes and persists', async () => {
const file = seed({ providers: { x: { name: 'X', roomId: 'r', rss: { url: 'https://x/rss' } } } });
const store = ProviderStore.load(file);
assert.equal(await store.remove('x'), true);
assert.equal(store.get('x'), undefined);
const reloaded = JSON.parse(fs.readFileSync(file, 'utf8'));
assert.equal(reloaded.providers.x, undefined);
assert.equal(await store.remove('x'), false);
});
test('reload picks up out-of-band edits without breaking existing refs', () => {
const file = seed({ providers: { a: { name: 'A', roomId: 'r', webhook: { format: 'statusPage' } } } });
const store = ProviderStore.load(file);
const rawRef = store.raw;
fs.writeFileSync(file, JSON.stringify({
providers: { b: { name: 'B', roomId: 'r', webhook: { format: 'statusIO' } } },
}));
store.reload();
assert.equal(store.get('a'), undefined);
assert.equal(store.get('b').name, 'B');
// The reference the caller took earlier must still point at live data.
assert.equal(rawRef.providers.b.name, 'B');
});
test('assertValidKey rejects garbage', () => {
assert.throws(() => assertValidKey(''), /Invalid provider key/);
assert.throws(() => assertValidKey('has spaces'), /Invalid provider key/);
assert.throws(() => assertValidKey('..'), /Invalid provider key/);
assert.doesNotThrow(() => assertValidKey('valid-key_1.a'));
});
test('assertValidSpec enforces required fields and formats', () => {
assert.throws(() => assertValidSpec({}), /name is required/);
assert.throws(() => assertValidSpec({ name: 'x' }), /roomId is required/);
assert.throws(() => assertValidSpec({ name: 'x', roomId: 'r' }), /at least one of rss or webhook/);
assert.throws(() => assertValidSpec({ name: 'x', roomId: 'r', rss: { url: 'not-a-url' } }), /http\(s\) URL/);
assert.throws(() => assertValidSpec({ name: 'x', roomId: 'r', webhook: { format: 'nope' } }), /webhook\.format must be one of/);
assert.doesNotThrow(() => assertValidSpec({ name: 'x', roomId: 'r', rss: { url: 'https://a/rss' } }));
assert.doesNotThrow(() => assertValidSpec({ name: 'x', roomId: 'r', webhook: { format: 'statusPage' } }));
});