124 lines
4.1 KiB
JavaScript
124 lines
4.1 KiB
JavaScript
/** @jest-environment jsdom */
|
|
|
|
const path = require('path');
|
|
|
|
// Install a fetch mock BEFORE loading the fetch-wrapper so it captures our mock
|
|
const fetchMock = jest.fn();
|
|
global.fetch = fetchMock;
|
|
|
|
// Load sanitizer and alerts (IIFE attaches to window)
|
|
require(path.join(__dirname, '..', 'sanitizer.js'));
|
|
require(path.join(__dirname, '..', 'alerts.js'));
|
|
// Load fetch wrapper (captures current global fetch as originalFetch)
|
|
require(path.join(__dirname, '..', 'fetch-wrapper.js'));
|
|
|
|
// Polyfill requestAnimationFrame for jsdom animations in alerts
|
|
beforeAll(() => {
|
|
if (!global.requestAnimationFrame) {
|
|
global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
|
|
}
|
|
});
|
|
|
|
beforeEach(() => {
|
|
document.body.innerHTML = '';
|
|
fetchMock.mockReset();
|
|
});
|
|
|
|
// Minimal UI helper that simulates the upload UI error handling
|
|
async function uploadFileUI() {
|
|
// Attempt an upload; on failure, surface the envelope via alerts.error with a sanitized HTML message
|
|
const resp = await window.http.wrappedFetch('/api/documents/upload/FILE-123', {
|
|
method: 'POST',
|
|
body: new FormData(),
|
|
});
|
|
if (!resp.ok) {
|
|
const err = await window.http.toError(resp, 'Upload failed');
|
|
const msg = window.http.formatAlert(err, 'Upload failed');
|
|
// html: true to allow basic markup from server but sanitized; duration 0 so it stays for assertions
|
|
return window.alerts.error(msg, { html: true, duration: 0, id: 'upload-error' });
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function makeErrorResponse({ status = 400, envelope, headerCid = null }) {
|
|
return {
|
|
ok: false,
|
|
status,
|
|
headers: {
|
|
get: (name) => {
|
|
if (name && name.toLowerCase() === 'x-correlation-id') return headerCid || null;
|
|
return null;
|
|
},
|
|
},
|
|
clone() {
|
|
return this;
|
|
},
|
|
async json() {
|
|
return envelope;
|
|
},
|
|
async text() {
|
|
try { return JSON.stringify(envelope); } catch (_) { return ''; }
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('Upload UI error handling', () => {
|
|
it('displays server error via alerts.error, sanitizes HTML, and includes correlation ID', async () => {
|
|
const cid = 'cid-abc123';
|
|
const envelope = {
|
|
success: false,
|
|
error: { status: 400, code: 'http_error', message: 'Invalid <b>file</b> <script>evil()</script><img src=x onerror="alert(1)">' },
|
|
correlation_id: cid,
|
|
};
|
|
fetchMock.mockResolvedValueOnce(makeErrorResponse({ status: 400, envelope, headerCid: cid }));
|
|
|
|
const wrapper = await uploadFileUI();
|
|
expect(wrapper).toBeTruthy();
|
|
expect(wrapper.id).toBe('upload-error');
|
|
|
|
const content = wrapper.querySelector('.text-sm.mt-1.font-semibold');
|
|
expect(content).toBeTruthy();
|
|
const html = content.innerHTML;
|
|
// Preserves safe markup
|
|
expect(html).toContain('<b>file</b>');
|
|
// Scripts and event handlers removed
|
|
expect(html).not.toMatch(/<script/i);
|
|
expect(html).not.toMatch(/onerror=/i);
|
|
// Correlation reference present
|
|
expect(html).toMatch(/Ref: cid-abc123/);
|
|
});
|
|
|
|
it('uses correlation ID from header when both header and envelope provide values', async () => {
|
|
const headerCid = 'cid-header';
|
|
const bodyCid = 'cid-body';
|
|
const envelope = {
|
|
success: false,
|
|
error: { status: 400, code: 'http_error', message: 'Invalid type' },
|
|
correlation_id: bodyCid,
|
|
};
|
|
fetchMock.mockResolvedValueOnce(makeErrorResponse({ status: 400, envelope, headerCid }));
|
|
|
|
const wrapper = await uploadFileUI();
|
|
const html = wrapper.querySelector('.text-sm.mt-1.font-semibold').innerHTML;
|
|
expect(html).toMatch(/Ref: cid-header/);
|
|
expect(html).not.toMatch(/cid-body/);
|
|
});
|
|
|
|
it('falls back to basic text if alerts module is missing but our alerts is present; ensures container exists', async () => {
|
|
const cid = 'cid-xyz';
|
|
const envelope = {
|
|
success: false,
|
|
error: { status: 400, code: 'http_error', message: 'Bad <em>upload</em>' },
|
|
correlation_id: cid,
|
|
};
|
|
fetchMock.mockResolvedValueOnce(makeErrorResponse({ status: 400, envelope, headerCid: cid }));
|
|
|
|
const wrapper = await uploadFileUI();
|
|
const container = document.getElementById('notification-container');
|
|
expect(container).toBeTruthy();
|
|
expect(container.contains(wrapper)).toBe(true);
|
|
});
|
|
});
|
|
|
|
|