Proof: npm test (212/212) and npm run operator-dashboard:build cover non-JSON auth failures and rebuilt the dashboard bundle. Assumptions: browser auth failures may return plain text before a session cookie is established; API callers should receive JSON errors. Still fake: dashboard quote outcomes still depend on inventory-delta attribution instead of venue-native terminal fill events.
This commit is contained in:
parent
92aa636dc0
commit
fd899a3788
4 changed files with 63 additions and 2 deletions
|
|
@ -730,6 +730,9 @@ function authenticateHttpRequest(req, res) {
|
||||||
res.setHeader('WWW-Authenticate', buildDashboardAuthChallengeHeader({
|
res.setHeader('WWW-Authenticate', buildDashboardAuthChallengeHeader({
|
||||||
realm: config.operatorDashboardAuthRealm,
|
realm: config.operatorDashboardAuthRealm,
|
||||||
}));
|
}));
|
||||||
|
if ((req.url || '').startsWith('/api/')) {
|
||||||
|
return sendJson(res, 401, { error: 'authentication_required' });
|
||||||
|
}
|
||||||
res.end('authentication required\n');
|
res.end('authentication required\n');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,13 @@ export async function fetchJson(url, options = {}) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, fetchOptions);
|
const response = await fetch(url, fetchOptions);
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
const data = text ? JSON.parse(text) : null;
|
const data = parseJsonResponse(text, response);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(data?.error || `HTTP ${response.status}`);
|
const message = data?.error || text.trim() || `HTTP ${response.status}`;
|
||||||
|
throw new Error(message.startsWith(`HTTP ${response.status}`)
|
||||||
|
? message
|
||||||
|
: `HTTP ${response.status}: ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|
@ -28,3 +31,20 @@ export async function fetchJson(url, options = {}) {
|
||||||
if (timeout) globalThis.clearTimeout(timeout);
|
if (timeout) globalThis.clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseJsonResponse(text, response) {
|
||||||
|
if (!text) return null;
|
||||||
|
const trimmed = text.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
|
||||||
|
const contentType = response.headers.get('content-type') || '';
|
||||||
|
const looksJson = contentType.includes('json') || /^[{[]/.test(trimmed);
|
||||||
|
if (!looksJson) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch (error) {
|
||||||
|
if (!response.ok) return null;
|
||||||
|
throw new Error(`Invalid JSON response from ${response.url || 'dashboard API'}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,39 @@ test('dashboard fetch helper reports empty upstream 500 responses', async (t) =>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('dashboard fetch helper reports plain-text auth failures without leaking JSON parse errors', async (t) => {
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
t.after(() => {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
globalThis.fetch = async () => new Response('authentication required\n', { status: 401 });
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
fetchJson('/api/session', { timeoutMs: 0 }),
|
||||||
|
/HTTP 401: authentication required/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dashboard fetch helper reports invalid successful JSON responses explicitly', async (t) => {
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
t.after(() => {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
globalThis.fetch = async () => new Response('<html></html>', {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
fetchJson('/api/session', { timeoutMs: 0 }),
|
||||||
|
/Invalid JSON response/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('dashboard fetch helper times out stale port-forward requests', async (t) => {
|
test('dashboard fetch helper times out stale port-forward requests', async (t) => {
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
t.after(() => {
|
t.after(() => {
|
||||||
|
|
|
||||||
|
|
@ -28,3 +28,8 @@ test('operator dashboard exposes DB-backed pair activation and pause controls',
|
||||||
assert.match(source, /edgeBps: body\.edge_bps/);
|
assert.match(source, /edgeBps: body\.edge_bps/);
|
||||||
assert.match(source, /maxNotional: body\.max_notional/);
|
assert.match(source, /maxNotional: body\.max_notional/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('operator dashboard API auth failures are JSON for frontend fetches', () => {
|
||||||
|
assert.match(source, /req\.url \|\| ''\)\.startsWith\('\/api\/'\)/);
|
||||||
|
assert.match(source, /sendJson\(res, 401, \{ error: 'authentication_required' \}\)/);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue