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>
666 lines
23 KiB
JavaScript
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();
|
|
}
|
|
});
|