collabSupport/tests/paloalto.sites.test.js
jmcqueen b802383441 Add Prisma SD-WAN voice-quality enrichment for /phonestatus + /voicediag
Introduces a full Palo Alto Prisma SD-WAN integration (dual-mode SASE
OAuth 2.0 / legacy CloudGenix auth, pagination, 429 backoff, session
priming) that surfaces per-path latency/jitter/loss/MOS, site
healthscore, link state, and alarm data for a store. Wired into the
/phonestatus WAN follow-up and eight new /voicediag WAN checks graded
against ITU-T G.114 / RFC 3550 defaults (env-overridable via
WAN_STANDARD_*).

Also adds a shape-aware detail renderer for /voicediag (per-link
tables with verdict icons instead of a stringified JSON dump) and a
--window flag (15m / 1h / 6h / 24h / 1d, env default via
WAN_STANDARD_WINDOW_MINUTES) so operators can widen the look-back
without redeploying. scripts/prismaProbe.js is bundled as a CLI for
schema iteration against a live tenant.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 09:45:29 -04:00

666 lines
23 KiB
JavaScript

// tests/paloalto.sites.test.js
//
// Unit + integration coverage for integrations/paloalto/sites.js.
//
// The `siteNameForStore()` function is pure — most cases are covered
// by inline assertions. The `findSdwanSiteForStore()` +
// `getElementsForSite()` functions hit HTTP; we stand up a tiny
// fake Prisma server on an ephemeral port and point the module at
// it via PRISMA_SASE_BASE_URL. Same pattern as dectRelayHub.test.js.
//
// Env cleanup runs after every test that touches process.env so
// one failing test can't poison the rest of the file.
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import {
siteNameForStore,
findSdwanSiteForStore,
getAllSites,
getElementsForSite,
getWanInterfacesForSite,
_resetSitesCache,
} from '../integrations/paloalto/sites.js';
import { _resetPrismaAuthCache } from '../integrations/paloalto/client.js';
const FAKE_TOKEN = 'test-token-abc123';
// ─── Fake Prisma server ─────────────────────────────────────────────
//
// Handles just enough of the two endpoints the sites module talks
// to. Each test passes a `handlers` map so it can control what
// each endpoint returns — a missing handler responds 404. All
// requests get authenticated against FAKE_TOKEN so the client's
// interceptor + refresh flow gets exercised.
async function makeFakePrisma(handlers = {}) {
const requests = [];
const server = http.createServer((req, res) => {
let body = '';
req.on('data', (c) => (body += c));
req.on('end', () => {
requests.push({ method: req.method, url: req.url, body, headers: req.headers });
// Auth token endpoint (SASE). The auth URL is the whole URL
// (not a path on baseURL) — but we override it too so
// the auth POST comes here.
if (req.url === '/oauth2/access_token' && req.method === 'POST') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ access_token: FAKE_TOKEN, expires_in: 900 }));
return;
}
// Mandatory SASE unified SD-WAN session priming call. Every
// token acquisition should be followed by exactly one hit
// against this URL — we respond with a stub profile.
if (req.url === '/sdwan/v2.1/api/profile' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: 'stub-profile', tenant_id: 'stub-tenant' }));
return;
}
// Match on method + path (ignoring query string) so tests
// don't have to encode every ?limit=1000&cursor=... permutation.
// Query params are still preserved on `req.query` and the raw
// `req.url` for handlers that need them.
const pathOnly = (req.url || '').split('?')[0];
const rawQuery = (req.url || '').includes('?') ? req.url.split('?')[1] : '';
const query = Object.fromEntries(new URLSearchParams(rawQuery));
const handler =
handlers[`${req.method} ${req.url}`] || // exact match wins
handlers[`${req.method} ${pathOnly}`]; // path-only fallback
if (handler) {
const parsed = body ? JSON.parse(body) : null;
const result = handler({ req, body: parsed, query });
res.writeHead(result.status || 200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result.body || {}));
return;
}
res.writeHead(404);
res.end();
});
});
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const { port } = server.address();
return {
port,
baseUrl: `http://127.0.0.1:${port}`,
requests,
close: () => new Promise((r) => server.close(r)),
};
}
function setSaseEnv(baseUrl) {
process.env.PRISMA_AUTH_MODE = 'sase';
process.env.PRISMA_SASE_BASE_URL = baseUrl;
process.env.PRISMA_AUTH_URL = `${baseUrl}/oauth2/access_token`;
process.env.PRISMA_CLIENT_ID = 'test-client';
process.env.PRISMA_CLIENT_SECRET = 'test-secret';
process.env.PRISMA_TSG_ID = 'test-tsg';
}
function clearPrismaEnv() {
delete process.env.PRISMA_AUTH_MODE;
delete process.env.PRISMA_SASE_BASE_URL;
delete process.env.PRISMA_AUTH_URL;
delete process.env.PRISMA_CLIENT_ID;
delete process.env.PRISMA_CLIENT_SECRET;
delete process.env.PRISMA_TSG_ID;
delete process.env.PRISMA_LEGACY_BASE_URL;
delete process.env.PRISMA_EMAIL;
delete process.env.PRISMA_PASSWORD;
}
// ─── siteNameForStore (pure) ────────────────────────────────────────
test('siteNameForStore: pads 3-digit store to 5 with CG prefix', () => {
assert.equal(siteNameForStore(782), 'CG00782');
assert.equal(siteNameForStore('782'), 'CG00782');
});
test('siteNameForStore: pads 5-digit store correctly', () => {
assert.equal(siteNameForStore(2477), 'CG02477');
assert.equal(siteNameForStore(305), 'CG00305');
});
test('siteNameForStore: handles 4-digit stores', () => {
assert.equal(siteNameForStore(1234), 'CG01234');
});
test('siteNameForStore: strips non-digits before padding', () => {
assert.equal(siteNameForStore('store-782'), 'CG00782');
});
test('siteNameForStore: throws on empty / non-numeric input', () => {
assert.throws(() => siteNameForStore(''), /no digits/i);
assert.throws(() => siteNameForStore('abc'), /no digits/i);
assert.throws(() => siteNameForStore(null), /no digits/i);
});
// ─── findSdwanSiteForStore + cache ──────────────────────────────────
test('findSdwanSiteForStore: exact match on CG00782', async () => {
_resetSitesCache();
_resetPrismaAuthCache();
const fake = await makeFakePrisma({
'GET /sdwan/v4.13/api/sites': () => ({
status: 200,
body: {
items: [
{ id: 'site-1', name: 'CG00305', description: 'store 305' },
{ id: 'site-2', name: 'CG00782', description: 'store 782' },
{ id: 'site-3', name: 'CG02477', description: 'store 2477' },
],
},
}),
});
setSaseEnv(fake.baseUrl);
try {
const site = await findSdwanSiteForStore(782);
assert.ok(site, 'expected site to resolve');
assert.equal(site.name, 'CG00782');
assert.equal(site.id, 'site-2');
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('findSdwanSiteForStore: null when no match', async () => {
_resetSitesCache();
_resetPrismaAuthCache();
const fake = await makeFakePrisma({
'GET /sdwan/v4.13/api/sites': () => ({
status: 200,
body: { items: [{ id: 'site-x', name: 'CG99999', description: '' }] },
}),
});
setSaseEnv(fake.baseUrl);
try {
const site = await findSdwanSiteForStore(782);
assert.equal(site, null);
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('getAllSites: caches — second call does not re-fetch', async () => {
_resetSitesCache();
_resetPrismaAuthCache();
let fetchCount = 0;
const fake = await makeFakePrisma({
'GET /sdwan/v4.13/api/sites': () => {
fetchCount += 1;
return { status: 200, body: { items: [{ id: 's', name: 'CG00782' }] } };
},
});
setSaseEnv(fake.baseUrl);
try {
await getAllSites();
await getAllSites();
await getAllSites();
assert.equal(fetchCount, 1, 'only one refresh expected within TTL');
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('getAllSites: uses GET (not POST) so no request body → no schema games', async () => {
// Regression guard: the observed SASE tenant rejects POST
// /sites/query with a moving target of body-schema errors
// (getDeleted expected bool, total_count expected long, etc.)
// even when we send those fields correctly. The tenant's SASE
// proxy appears to auto-inject fields with `{}` defaults into
// the body before schema validation, making POST unusable.
// GET has no body, so no auto-injection, so no drift.
_resetSitesCache();
_resetPrismaAuthCache();
const seen = { method: null, path: null, query: null, bodyLen: 0 };
const fake = await makeFakePrisma({
'GET /sdwan/v4.13/api/sites': ({ req, body, query }) => {
seen.method = req.method;
seen.path = (req.url || '').split('?')[0];
seen.query = query;
seen.bodyLen = body ? JSON.stringify(body).length : 0;
return { status: 200, body: { items: [{ id: 's', name: 'CG00782' }] } };
},
});
setSaseEnv(fake.baseUrl);
try {
await getAllSites();
assert.equal(seen.method, 'GET', 'MUST be GET, not POST');
assert.equal(seen.path, '/sdwan/v4.13/api/sites');
assert.equal(seen.bodyLen, 0, 'no request body');
assert.equal(seen.query.limit, '1000', 'first-page limit maximised');
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('getAllSites: forceRefresh bypasses cache', async () => {
_resetSitesCache();
_resetPrismaAuthCache();
let fetchCount = 0;
const fake = await makeFakePrisma({
'GET /sdwan/v4.13/api/sites': () => {
fetchCount += 1;
return { status: 200, body: { items: [{ id: 's', name: 'CG00782' }] } };
},
});
setSaseEnv(fake.baseUrl);
try {
await getAllSites();
await getAllSites(true);
assert.equal(fetchCount, 2);
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('getAllSites: follows next_query cursor across multiple pages', async () => {
_resetSitesCache();
_resetPrismaAuthCache();
let pageCallCount = 0;
const pages = [
// Page 1: full page (1000 items) + a cursor that gets echoed
// back verbatim as the next request body.
{
items: Array.from({ length: 1000 }, (_, i) => ({ id: `s${i}`, name: `CG${String(i).padStart(5, '0')}` })),
next_query: { limit: 1000, getDeleted: false, cursor: 'page-2' },
},
// Page 2: full page + another cursor.
{
items: Array.from({ length: 1000 }, (_, i) => ({ id: `s${1000 + i}`, name: `CG${String(1000 + i).padStart(5, '0')}` })),
next_query: { limit: 1000, getDeleted: false, cursor: 'page-3' },
},
// Page 3: short page → terminates the loop by "fewer than limit".
{
items: Array.from({ length: 234 }, (_, i) => ({ id: `s${2000 + i}`, name: `CG${String(2000 + i).padStart(5, '0')}` })),
next_query: null,
},
];
const fake = await makeFakePrisma({
'GET /sdwan/v4.13/api/sites': () => {
const page = pages[pageCallCount] || { items: [], next_query: null };
pageCallCount += 1;
return { status: 200, body: page };
},
});
setSaseEnv(fake.baseUrl);
try {
const sites = await getAllSites();
assert.equal(pageCallCount, 3, 'should fetch all three pages');
assert.equal(sites.length, 1000 + 1000 + 234, 'should union every page');
assert.equal(sites[0].name, 'CG00000');
assert.equal(sites.at(-1).name, 'CG02233');
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('getAllSites: keeps paginating when Prisma caps pages below the requested limit', async () => {
// Regression guard for a live bug: Prisma silently caps some
// tenants at ~200 rows per page regardless of the requested
// `limit: 1000`. The original short-page termination heuristic
// cut the sweep off after the first page, hiding 800+ sites from
// the store-lookup resolver. `next_query` alone must decide.
_resetSitesCache();
_resetPrismaAuthCache();
let pageCallCount = 0;
const pages = [
{
// Page 1: 200 items (capped, WELL below the requested 1000)
// BUT a non-empty next_query says there's more.
items: Array.from({ length: 200 }, (_, i) => ({ id: `s${i}`, name: `CG${String(i).padStart(5, '0')}` })),
next_query: { cursor: 'p2', limit: 1000 },
},
{
// Page 2: another capped 200 + more.
items: Array.from({ length: 200 }, (_, i) => ({ id: `s${200 + i}`, name: `CG${String(200 + i).padStart(5, '0')}` })),
next_query: { cursor: 'p3', limit: 1000 },
},
{
// Page 3: final short page + empty cursor.
items: Array.from({ length: 42 }, (_, i) => ({ id: `s${400 + i}`, name: `CG${String(400 + i).padStart(5, '0')}` })),
next_query: null,
},
];
const fake = await makeFakePrisma({
'GET /sdwan/v4.13/api/sites': () => {
const page = pages[pageCallCount] || { items: [], next_query: null };
pageCallCount += 1;
return { status: 200, body: page };
},
});
setSaseEnv(fake.baseUrl);
try {
const sites = await getAllSites();
assert.equal(pageCallCount, 3, 'must not short-circuit on the capped first page');
assert.equal(sites.length, 442, 'all 3 pages unioned');
assert.equal(sites.at(-1).name, 'CG00441');
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('getAllSites: terminates on empty next_query even if page is full-sized', async () => {
_resetSitesCache();
_resetPrismaAuthCache();
let pageCallCount = 0;
const fake = await makeFakePrisma({
'GET /sdwan/v4.13/api/sites': () => {
pageCallCount += 1;
// Full page (== limit) but next_query is an empty object →
// terminate. Guards against a Prisma quirk where the last
// page happens to be exactly the page-size boundary.
return {
status: 200,
body: {
items: Array.from({ length: 1000 }, (_, i) => ({ id: `s${i}`, name: `CG${String(i).padStart(5, '0')}` })),
next_query: {},
},
};
},
});
setSaseEnv(fake.baseUrl);
try {
const sites = await getAllSites();
assert.equal(pageCallCount, 1);
assert.equal(sites.length, 1000);
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('getAllSites: findSdwanSiteForStore resolves a store on page 3 of a paginated tenant', async () => {
_resetSitesCache();
_resetPrismaAuthCache();
let pageCallCount = 0;
const pages = [
{ items: Array.from({ length: 1000 }, (_, i) => ({ id: `s${i}`, name: `CG${String(i).padStart(5, '0')}` })),
next_query: { cursor: 'p2' } },
{ items: Array.from({ length: 1000 }, (_, i) => ({ id: `s${1000 + i}`, name: `CG${String(1000 + i).padStart(5, '0')}` })),
next_query: { cursor: 'p3' } },
// Store 2200 lives on page 3.
{ items: [{ id: 'target', name: 'CG02200' }],
next_query: null },
];
const fake = await makeFakePrisma({
'GET /sdwan/v4.13/api/sites': () => {
const page = pages[pageCallCount] || { items: [], next_query: null };
pageCallCount += 1;
return { status: 200, body: page };
},
});
setSaseEnv(fake.baseUrl);
try {
const site = await findSdwanSiteForStore(2200);
assert.ok(site, 'expected store 2200 to resolve');
assert.equal(site.name, 'CG02200');
assert.equal(site.id, 'target');
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('getAllSites: returns empty array on network error with no cache', async () => {
_resetSitesCache();
_resetPrismaAuthCache();
const fake = await makeFakePrisma({
'GET /sdwan/v4.13/api/sites': () => ({
status: 500,
body: { error: 'boom' },
}),
});
setSaseEnv(fake.baseUrl);
try {
const sites = await getAllSites();
assert.deepEqual(sites, []);
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('getElementsForSite: single tenant-wide GET, indexed by site_id in-memory', async () => {
_resetSitesCache();
_resetPrismaAuthCache();
const seen = { count: 0, urls: [] };
const fake = await makeFakePrisma({
'GET /sdwan/v3.1/api/elements': ({ req }) => {
seen.count += 1;
seen.urls.push(req.url);
return {
status: 200,
body: {
items: [
{ id: 'el-1', name: 'ION-1000-A', model_name: 'ION 1000', serial_number: 'SN1', connected: true, site_id: 'site-abc' },
{ id: 'el-2', name: 'ION-1000-B', model_name: 'ION 1000', serial_number: 'SN2', connected: false, site_id: 'site-abc' },
{ id: 'el-3', name: 'ION-3000-X', model_name: 'ION 3000', serial_number: 'SN3', connected: true, site_id: 'site-xyz' },
],
},
};
},
});
setSaseEnv(fake.baseUrl);
try {
const abc = await getElementsForSite('site-abc');
assert.equal(abc.length, 2);
assert.equal(abc[0].id, 'el-1');
assert.equal(abc[0].model, 'ION 1000');
const xyz = await getElementsForSite('site-xyz');
assert.equal(xyz.length, 1);
assert.equal(xyz[0].id, 'el-3');
const unknown = await getElementsForSite('site-nope');
assert.deepEqual(unknown, [], 'unknown site returns empty (no extra fetch)');
// Critical: only ONE network call across three site lookups.
assert.equal(seen.count, 1, 'tenant-wide cache serves subsequent site lookups');
assert.equal(seen.urls[0], '/sdwan/v3.1/api/elements', 'no ?site_id= filter — full tenant fetch');
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('getElementsForSite: elements with no site_id are dropped', async () => {
_resetSitesCache();
_resetPrismaAuthCache();
const fake = await makeFakePrisma({
'GET /sdwan/v3.1/api/elements': () => ({
status: 200,
body: {
items: [
{ id: 'el-1', name: 'A', connected: true, site_id: 'site-abc' },
{ id: 'el-orphan', name: 'unclaimed', connected: false, site_id: null },
],
},
}),
});
setSaseEnv(fake.baseUrl);
try {
const els = await getElementsForSite('site-abc');
assert.equal(els.length, 1);
assert.equal(els[0].id, 'el-1');
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('getElementsForSite: caches tenant-wide (one fetch across many sites)', async () => {
_resetSitesCache();
_resetPrismaAuthCache();
let fetchCount = 0;
const fake = await makeFakePrisma({
'GET /sdwan/v3.1/api/elements': () => {
fetchCount += 1;
return { status: 200, body: { items: [
{ id: 'el-a', name: 'a', connected: true, site_id: 'site-1' },
{ id: 'el-b', name: 'b', connected: true, site_id: 'site-2' },
{ id: 'el-c', name: 'c', connected: true, site_id: 'site-3' },
] } };
},
});
setSaseEnv(fake.baseUrl);
try {
await getElementsForSite('site-1');
await getElementsForSite('site-2');
await getElementsForSite('site-3');
await getElementsForSite('site-1'); // re-hit, no extra fetch
assert.equal(fetchCount, 1);
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('getWanInterfacesForSite: GETs per-site waninterfaces, normalises to rows with admin state', async () => {
_resetSitesCache();
_resetPrismaAuthCache();
const fake = await makeFakePrisma({
'GET /sdwan/v2.10/api/sites/site-abc/waninterfaces': () => ({
status: 200,
body: {
items: [
{ id: 'wi-1', name: 'MPLS Circuit', admin_up: true, used_for: 'primary', wan_network_id: 'net-1', bw_config_mode: 'manual' },
{ id: 'wi-2', name: 'Broadband', admin_up: true, used_for: 'secondary', wan_network_id: 'net-2', bw_config_mode: 'manual' },
{ id: 'wi-3', name: 'LTE Backup', admin_up: false, used_for: 'backup', wan_network_id: 'net-3', bw_config_mode: 'manual' },
],
},
}),
});
setSaseEnv(fake.baseUrl);
try {
const wans = await getWanInterfacesForSite('site-abc');
assert.equal(wans.length, 3);
assert.equal(wans[0].id, 'wi-1');
assert.equal(wans[0].name, 'MPLS Circuit');
assert.equal(wans[0].adminUp, true);
assert.equal(wans[0].usedFor, 'primary');
assert.equal(wans[2].adminUp, false);
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('getWanInterfacesForSite: missing admin_up field → adminUp:null (not false)', async () => {
// Live regression: some tenant schema variants don't return an
// `admin_up` field on the waninterface config. Old code did
// `w.admin_up === true`, which falsy-coerced missing → false and
// then rendered every circuit as ❌ DOWN — a misleading false
// positive. The parser must distinguish missing (unknown) from
// an explicit `false` value.
_resetSitesCache();
_resetPrismaAuthCache();
const fake = await makeFakePrisma({
'GET /sdwan/v2.10/api/sites/site-x/waninterfaces': () => ({
status: 200,
body: {
items: [
{ id: 'wi-a', name: 'no admin_up field at all' },
{ id: 'wi-b', name: 'admin_up=null', admin_up: null },
{ id: 'wi-c', name: 'admin_up="true" string', admin_up: 'true' },
{ id: 'wi-d', name: 'admin_up=true bool', admin_up: true },
{ id: 'wi-e', name: 'admin_up=false bool', admin_up: false },
],
},
}),
});
setSaseEnv(fake.baseUrl);
try {
const wans = await getWanInterfacesForSite('site-x');
assert.equal(wans[0].adminUp, null, 'missing field → null (unknown)');
assert.equal(wans[1].adminUp, null, 'explicit null → null (unknown)');
assert.equal(wans[2].adminUp, null, 'stringy "true" is not a boolean → null');
assert.equal(wans[3].adminUp, true, 'explicit true stays true');
assert.equal(wans[4].adminUp, false, 'explicit false stays false (admin-disabled)');
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('getWanInterfacesForSite: caches per-site', async () => {
_resetSitesCache();
_resetPrismaAuthCache();
let fetchCount = 0;
const fake = await makeFakePrisma({
'GET /sdwan/v2.10/api/sites/site-x/waninterfaces': () => {
fetchCount += 1;
return { status: 200, body: { items: [{ id: 'wi', name: 'x', admin_up: true }] } };
},
});
setSaseEnv(fake.baseUrl);
try {
await getWanInterfacesForSite('site-x');
await getWanInterfacesForSite('site-x');
assert.equal(fetchCount, 1);
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});
test('getWanInterfacesForSite: returns empty list on 500 with no cache', async () => {
_resetSitesCache();
_resetPrismaAuthCache();
const fake = await makeFakePrisma({
'GET /sdwan/v2.10/api/sites/broken/waninterfaces': () => ({ status: 500, body: { error: 'nope' } }),
});
setSaseEnv(fake.baseUrl);
try {
const wans = await getWanInterfacesForSite('broken');
assert.deepEqual(wans, []);
} finally {
await fake.close();
_resetSitesCache();
_resetPrismaAuthCache();
clearPrismaEnv();
}
});