// tests/paloalto.client.test.js // // Coverage for integrations/paloalto/client.js — the dual-mode // auth wrapper. Uses a fake HTTP server to exercise: // - SASE OAuth token acquisition (form-encoded client_credentials // against a fake auth URL) // - Legacy CloudGenix login (JSON POST against /v2.0/api/login) // - Mutex behaviour under concurrent callers // - 401 → forced refresh + one-shot retry // - PRISMA_AUTH_MODE=unknown throws a clear error // // Uses the same fake-server pattern as paloalto.sites.test.js. import test from 'node:test'; import assert from 'node:assert/strict'; import http from 'node:http'; import { paloAltoAxios, getPrismaToken, _resetPrismaAuthCache, _resetPrismaConcurrency, _prismaInflightCount, } from '../integrations/paloalto/client.js'; const FAKE_TOKEN_1 = 'token-round-1'; const FAKE_TOKEN_2 = 'token-round-2'; async function makeFakeAuthServer(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 }); // Default SASE session-prime responder so tests that only care // about the token / retry path don't need to wire this in. // Individual tests can override by providing an explicit // handler for `GET /sdwan/v2.1/api/profile`. const explicitHandler = handlers[`${req.method} ${req.url}`]; if ( req.url === '/sdwan/v2.1/api/profile' && req.method === 'GET' && !explicitHandler ) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ id: 'stub-profile' })); return; } // Default OAuth token responder — same rationale as the prime // default above. Overridable by explicit handler. if ( req.url === '/oauth2/access_token' && req.method === 'POST' && !explicitHandler ) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ access_token: 'default-token', expires_in: 900 })); return; } const handler = explicitHandler; if (handler) { // Handlers may return a Promise so a test can pause the // response until an external gate resolves — used by the // concurrency-limiter test to hold in-flight requests open. Promise.resolve(handler({ req, body })).then((result) => { res.writeHead(result.status || 200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(result.body || {})); }).catch((err) => { res.writeHead(500); res.end(JSON.stringify({ err: err.message })); }); 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 clearAllEnv() { delete process.env.PRISMA_AUTH_MODE; delete process.env.PRISMA_SASE_BASE_URL; delete process.env.PRISMA_LEGACY_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_EMAIL; delete process.env.PRISMA_PASSWORD; } test('client: PRISMA_AUTH_MODE=unknown throws on first token request', async () => { _resetPrismaAuthCache(); clearAllEnv(); process.env.PRISMA_AUTH_MODE = 'nonsense'; try { await assert.rejects(() => getPrismaToken(true), /PRISMA_AUTH_MODE/); } finally { clearAllEnv(); _resetPrismaAuthCache(); } }); test('client: SASE mode fetches token via client_credentials + scopes to tsg_id', async () => { _resetPrismaAuthCache(); clearAllEnv(); const fake = await makeFakeAuthServer({ 'POST /oauth2/access_token': ({ body }) => { assert.match(body, /grant_type=client_credentials/); assert.match(body, /scope=tsg_id%3A/); return { status: 200, body: { access_token: FAKE_TOKEN_1, expires_in: 900 } }; }, }); process.env.PRISMA_AUTH_MODE = 'sase'; process.env.PRISMA_AUTH_URL = `${fake.baseUrl}/oauth2/access_token`; process.env.PRISMA_CLIENT_ID = 'id'; process.env.PRISMA_CLIENT_SECRET = 'secret'; process.env.PRISMA_TSG_ID = 'tsg'; try { const t = await getPrismaToken(true); assert.equal(t, FAKE_TOKEN_1); } finally { await fake.close(); clearAllEnv(); _resetPrismaAuthCache(); } }); test('client: SASE mode surfaces missing envs with a clear error', async () => { _resetPrismaAuthCache(); clearAllEnv(); process.env.PRISMA_AUTH_MODE = 'sase'; try { await assert.rejects(() => getPrismaToken(true), /PRISMA_CLIENT_ID/); } finally { clearAllEnv(); _resetPrismaAuthCache(); } }); test('client: legacy mode POSTs email+password to /v2.0/api/login', async () => { _resetPrismaAuthCache(); clearAllEnv(); const fake = await makeFakeAuthServer({ 'POST /v2.0/api/login': ({ body }) => { const parsed = JSON.parse(body); assert.equal(parsed.email, 'a@b.com'); assert.equal(parsed.password, 'secret'); return { status: 200, body: { x_auth_token: 'legacy-token-xyz' } }; }, }); process.env.PRISMA_AUTH_MODE = 'legacy'; process.env.PRISMA_LEGACY_BASE_URL = fake.baseUrl; process.env.PRISMA_EMAIL = 'a@b.com'; process.env.PRISMA_PASSWORD = 'secret'; try { const t = await getPrismaToken(true); assert.equal(t, 'legacy-token-xyz'); } finally { await fake.close(); clearAllEnv(); _resetPrismaAuthCache(); } }); test('client: legacy mode surfaces missing envs with a clear error', async () => { _resetPrismaAuthCache(); clearAllEnv(); process.env.PRISMA_AUTH_MODE = 'legacy'; try { await assert.rejects(() => getPrismaToken(true), /PRISMA_EMAIL/); } finally { clearAllEnv(); _resetPrismaAuthCache(); } }); test('client: concurrent callers coalesce onto a single token refresh', async () => { _resetPrismaAuthCache(); clearAllEnv(); let hits = 0; const fake = await makeFakeAuthServer({ 'POST /oauth2/access_token': () => { hits += 1; return { status: 200, body: { access_token: `token-${hits}`, expires_in: 900 } }; }, }); process.env.PRISMA_AUTH_MODE = 'sase'; process.env.PRISMA_AUTH_URL = `${fake.baseUrl}/oauth2/access_token`; process.env.PRISMA_CLIENT_ID = 'id'; process.env.PRISMA_CLIENT_SECRET = 'secret'; process.env.PRISMA_TSG_ID = 'tsg'; try { const tokens = await Promise.all([ getPrismaToken(true), getPrismaToken(false), getPrismaToken(false), getPrismaToken(false), ]); assert.equal(hits, 1, 'mutex should coalesce concurrent refresh requests'); assert.equal(tokens[0], 'token-1'); // Subsequent callers should get the same cached token as the // first (mutex holds them until it's cached). assert.equal(tokens[1], tokens[0]); assert.equal(tokens[2], tokens[0]); assert.equal(tokens[3], tokens[0]); } finally { await fake.close(); clearAllEnv(); _resetPrismaAuthCache(); } }); test('client: SASE mode primes session with GET /sdwan/v2.1/api/profile before the first SD-WAN call', async () => { _resetPrismaAuthCache(); clearAllEnv(); let profileHits = 0; let apiHits = 0; const fake = await makeFakeAuthServer({ 'GET /sdwan/v2.1/api/profile': ({ req }) => { profileHits += 1; assert.match(req.headers.authorization || '', /^Bearer /); return { status: 200, body: { id: 'stub-profile', tenant_id: 't' } }; }, 'GET /sdwan/v4.13/api/sites': () => { apiHits += 1; return { status: 200, body: { items: [] } }; }, }); process.env.PRISMA_AUTH_MODE = 'sase'; process.env.PRISMA_SASE_BASE_URL = fake.baseUrl; process.env.PRISMA_AUTH_URL = `${fake.baseUrl}/oauth2/access_token`; process.env.PRISMA_CLIENT_ID = 'id'; process.env.PRISMA_CLIENT_SECRET = 'secret'; process.env.PRISMA_TSG_ID = 'tsg'; try { await paloAltoAxios.get('/sdwan/v4.13/api/sites'); await paloAltoAxios.get('/sdwan/v4.13/api/sites'); await paloAltoAxios.get('/sdwan/v4.13/api/sites'); assert.equal(profileHits, 1, 'priming call should fire exactly once per token'); assert.equal(apiHits, 3, 'subsequent SD-WAN calls should all succeed'); // Ordering assertion: /profile happened before the first SD-WAN call. const seqUrls = fake.requests.map((r) => r.url).filter((u) => u.startsWith('/sdwan/')); assert.equal(seqUrls[0], '/sdwan/v2.1/api/profile'); } finally { await fake.close(); clearAllEnv(); _resetPrismaAuthCache(); } }); test('client: SASE priming re-runs after a forced token refresh', async () => { _resetPrismaAuthCache(); clearAllEnv(); let profileHits = 0; const fake = await makeFakeAuthServer({ 'GET /sdwan/v2.1/api/profile': () => { profileHits += 1; return { status: 200, body: { id: 'stub-profile' } }; }, 'GET /sdwan/v4.13/api/sites': () => ({ status: 200, body: { items: [] } }), }); process.env.PRISMA_AUTH_MODE = 'sase'; process.env.PRISMA_SASE_BASE_URL = fake.baseUrl; process.env.PRISMA_AUTH_URL = `${fake.baseUrl}/oauth2/access_token`; process.env.PRISMA_CLIENT_ID = 'id'; process.env.PRISMA_CLIENT_SECRET = 'secret'; process.env.PRISMA_TSG_ID = 'tsg'; try { await paloAltoAxios.get('/sdwan/v4.13/api/sites'); assert.equal(profileHits, 1); // Force a token refresh — priming should re-run on next call. await getPrismaToken(true); await paloAltoAxios.get('/sdwan/v4.13/api/sites'); assert.equal(profileHits, 2, 'new token → re-prime'); } finally { await fake.close(); clearAllEnv(); _resetPrismaAuthCache(); } }); test('client: axios instance retries once after 401 with forced refresh', async () => { _resetPrismaAuthCache(); clearAllEnv(); let tokenCallCount = 0; let apiCallCount = 0; const fake = await makeFakeAuthServer({ 'POST /oauth2/access_token': () => { tokenCallCount += 1; return { status: 200, body: { access_token: tokenCallCount === 1 ? FAKE_TOKEN_1 : FAKE_TOKEN_2, expires_in: 900, }, }; }, 'GET /some/api/endpoint': ({ req }) => { apiCallCount += 1; // First call → 401 to trigger refresh. Second call must // present the new token to succeed. if (apiCallCount === 1) { return { status: 401, body: { error: 'expired' } }; } const auth = req.headers.authorization || ''; if (auth === `Bearer ${FAKE_TOKEN_2}`) { return { status: 200, body: { ok: true } }; } return { status: 401, body: { error: 'still bad' } }; }, }); process.env.PRISMA_AUTH_MODE = 'sase'; process.env.PRISMA_SASE_BASE_URL = fake.baseUrl; process.env.PRISMA_AUTH_URL = `${fake.baseUrl}/oauth2/access_token`; process.env.PRISMA_CLIENT_ID = 'id'; process.env.PRISMA_CLIENT_SECRET = 'secret'; process.env.PRISMA_TSG_ID = 'tsg'; try { const res = await paloAltoAxios.get('/some/api/endpoint'); assert.equal(res.status, 200); assert.equal(res.data.ok, true); assert.equal(tokenCallCount, 2, 'token refresh happened after 401'); assert.equal(apiCallCount, 2, 'API call retried once'); } finally { await fake.close(); clearAllEnv(); _resetPrismaAuthCache(); } }); // ─── Concurrency limiter ─────────────────────────────────────────── // // Regression guard for the /voicediag rate-limit cascade: without // the semaphore, fanning out 10 metric calls in parallel would blow // past Prisma's per-second cap and 429 half of them. The cap has to // bound concurrent in-flight AXIOS calls at MAX_INFLIGHT (default 3) // AND has to release on both success + failure paths so 429 retries // don't stall queued callers. // // Testing strategy: the server introduces a small artificial delay // so we can observe the peak concurrent request count. All // responses complete before the test's finally block, so no socket // races on server-close. test('client: concurrency limiter caps peak concurrent server-side requests at MAX_INFLIGHT=3', async () => { _resetPrismaAuthCache(); _resetPrismaConcurrency(); clearAllEnv(); let peakConcurrent = 0; let concurrent = 0; const fake = await makeFakeAuthServer({ 'POST /oauth2/access_token': () => ({ status: 200, body: { access_token: FAKE_TOKEN_1, expires_in: 900 }, }), // ~80ms delay per response — long enough to let the client fill // its 3-permit window, short enough that all 8 requests finish // in ~250-400ms without complex gating logic. 'GET /slow': async () => { concurrent += 1; if (concurrent > peakConcurrent) peakConcurrent = concurrent; await new Promise((r) => setTimeout(r, 80)); concurrent -= 1; return { status: 200, body: { ok: true } }; }, }); process.env.PRISMA_AUTH_MODE = 'sase'; process.env.PRISMA_SASE_BASE_URL = fake.baseUrl; process.env.PRISMA_AUTH_URL = `${fake.baseUrl}/oauth2/access_token`; process.env.PRISMA_CLIENT_ID = 'id'; process.env.PRISMA_CLIENT_SECRET = 'secret'; process.env.PRISMA_TSG_ID = 'tsg'; try { const results = await Promise.all( Array.from({ length: 8 }, () => paloAltoAxios.get('/slow')), ); assert.equal(results.length, 8); assert.ok( peakConcurrent <= 3, `peak concurrent server-side requests should be <= 3 (semaphore cap), was ${peakConcurrent}`, ); // If the cap works, the first batch of 3 completes ~80ms in, // and the next batch fills — peak MUST equal MAX_INFLIGHT under // any realistic scheduling. If it's under 3, the semaphore is // too tight or requests are strictly serial (which would also // be a bug). assert.ok(peakConcurrent >= 2, `expected the client to actually parallelize (peak >= 2), was ${peakConcurrent}`); assert.equal(_prismaInflightCount(), 0, 'inflight counter must return to 0 after all requests finish'); } finally { await fake.close(); clearAllEnv(); _resetPrismaAuthCache(); _resetPrismaConcurrency(); } }); test('client: concurrency permit released on 500 (failure path drains cleanly, no permit leak)', async () => { _resetPrismaAuthCache(); _resetPrismaConcurrency(); clearAllEnv(); let apiCalls = 0; const fake = await makeFakeAuthServer({ 'POST /oauth2/access_token': () => ({ status: 200, body: { access_token: FAKE_TOKEN_1, expires_in: 900 }, }), 'GET /explode': () => { apiCalls += 1; return { status: 500, body: { err: 'boom' } }; }, }); process.env.PRISMA_AUTH_MODE = 'sase'; process.env.PRISMA_SASE_BASE_URL = fake.baseUrl; process.env.PRISMA_AUTH_URL = `${fake.baseUrl}/oauth2/access_token`; process.env.PRISMA_CLIENT_ID = 'id'; process.env.PRISMA_CLIENT_SECRET = 'secret'; process.env.PRISMA_TSG_ID = 'tsg'; try { // 6 requests that all 500. If the permit isn't released on the // error path, the 4th onwards would hang forever — the outer // 20s axios timeout would fire and the test would fail with // timeout rather than clean rejections. All should reject // cleanly and inflight must return to zero. const results = await Promise.allSettled( Array.from({ length: 6 }, () => paloAltoAxios.get('/explode')), ); assert.equal(results.filter((r) => r.status === 'rejected').length, 6, 'all 6 requests should have rejected — none hung'); assert.equal(apiCalls, 6, 'all 6 reached the server (queue drained)'); assert.equal(_prismaInflightCount(), 0, 'inflight counter should return to 0 after failure — no permit leak'); } finally { await fake.close(); clearAllEnv(); _resetPrismaAuthCache(); _resetPrismaConcurrency(); } });