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>
86 lines
3.5 KiB
JavaScript
86 lines
3.5 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { addProviderCard, providerSavedCard, specFromCardInputs } from '../src/cards.js';
|
|
|
|
test('addProviderCard has the required inputs and a submit action', () => {
|
|
const card = addProviderCard({ publicBaseUrl: 'https://status.example.com' });
|
|
assert.equal(card.contentType, 'application/vnd.microsoft.card.adaptive');
|
|
const ids = card.content.body.filter(b => b.type.startsWith('Input.')).map(b => b.id);
|
|
assert.deepEqual(ids.sort(), ['key', 'name', 'rssUrl', 'siteUrl', 'sourceType', 'webhookFormat'].sort());
|
|
assert.equal(card.content.actions[0].type, 'Action.Submit');
|
|
assert.equal(card.content.actions[0].data.action, 'addProvider');
|
|
// The URL hint should include the public base.
|
|
const hint = card.content.body.find(b => b.type === 'TextBlock' && b.text && b.text.includes('Webhook URL'));
|
|
assert.ok(hint.text.includes('status.example.com'));
|
|
});
|
|
|
|
test('addProviderCard falls back to a friendly warning when PUBLIC_BASE_URL is unset', () => {
|
|
const card = addProviderCard({});
|
|
const hint = card.content.body.find(b => b.type === 'TextBlock' && b.text && b.text.includes('PUBLIC_BASE_URL'));
|
|
assert.ok(hint, 'expected a PUBLIC_BASE_URL hint when the base URL is unset');
|
|
});
|
|
|
|
test('specFromCardInputs builds an RSS-only spec', () => {
|
|
const { key, spec } = specFromCardInputs({
|
|
key: 'acme',
|
|
name: 'Acme',
|
|
sourceType: 'rss',
|
|
rssUrl: 'https://acme.example/rss',
|
|
}, 'room-123');
|
|
assert.equal(key, 'acme');
|
|
assert.equal(spec.name, 'Acme');
|
|
assert.equal(spec.roomId, 'room-123');
|
|
assert.equal(spec.rss.url, 'https://acme.example/rss');
|
|
assert.equal(spec.webhook, undefined);
|
|
});
|
|
|
|
test('specFromCardInputs builds a webhook-only spec', () => {
|
|
const { spec } = specFromCardInputs({
|
|
key: 'acme',
|
|
name: 'Acme',
|
|
sourceType: 'webhook',
|
|
webhookFormat: 'uptimeKuma',
|
|
}, 'room-123');
|
|
assert.equal(spec.webhook.format, 'uptimeKuma');
|
|
assert.equal(spec.rss, undefined);
|
|
});
|
|
|
|
test('specFromCardInputs builds a both spec and copies siteUrl', () => {
|
|
const { spec } = specFromCardInputs({
|
|
key: 'acme',
|
|
name: 'Acme',
|
|
siteUrl: 'https://status.acme.com',
|
|
sourceType: 'both',
|
|
rssUrl: 'https://acme.example/rss',
|
|
webhookFormat: 'statusPage',
|
|
}, 'room-123');
|
|
assert.equal(spec.rss.url, 'https://acme.example/rss');
|
|
assert.equal(spec.webhook.format, 'statusPage');
|
|
assert.equal(spec.siteUrl, 'https://status.acme.com');
|
|
});
|
|
|
|
test('specFromCardInputs errors when RSS is selected but no URL was provided', () => {
|
|
assert.throws(() =>
|
|
specFromCardInputs({ key: 'acme', name: 'Acme', sourceType: 'rss', rssUrl: '' }, 'r'),
|
|
/RSS URL is required/);
|
|
});
|
|
|
|
test('providerSavedCard shows the webhook URL when applicable', () => {
|
|
const card = providerSavedCard({
|
|
key: 'acme',
|
|
provider: { name: 'Acme', webhook: { format: 'statusPage' } },
|
|
publicBaseUrl: 'https://status.example.com/',
|
|
});
|
|
const monospace = card.content.body.find(b => b.fontType === 'Monospace');
|
|
assert.equal(monospace.text, 'https://status.example.com/acme');
|
|
});
|
|
|
|
test('providerSavedCard omits webhook block for RSS-only providers', () => {
|
|
const card = providerSavedCard({
|
|
key: 'acme',
|
|
provider: { name: 'Acme', rss: { url: 'https://a/rss' } },
|
|
publicBaseUrl: 'https://x',
|
|
});
|
|
const monospace = card.content.body.find(b => b.fontType === 'Monospace');
|
|
assert.equal(monospace, undefined);
|
|
});
|