141 lines
4.9 KiB
JavaScript
141 lines
4.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import ts from 'typescript';
|
|
import { compileFunction } from 'node:vm';
|
|
const source = fs.readFileSync(
|
|
new URL('../../resources/js/lib/profile-json.ts', import.meta.url),
|
|
'utf8',
|
|
);
|
|
const output = ts.transpileModule(source, {
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
target: ts.ScriptTarget.ES2022,
|
|
},
|
|
}).outputText;
|
|
const module = { exports: {} };
|
|
compileFunction(output, ['exports'])(module.exports);
|
|
const { profileJson, ProfileValidationError } = module.exports;
|
|
async function transport(response, run) {
|
|
const fetch = globalThis.fetch,
|
|
document = globalThis.document;
|
|
let count = 0;
|
|
globalThis.document = { cookie: 'XSRF-TOKEN=synthetic' };
|
|
globalThis.fetch = async () => {
|
|
count++;
|
|
return response;
|
|
};
|
|
try {
|
|
await run();
|
|
assert.equal(count, 1);
|
|
} finally {
|
|
globalThis.fetch = fetch;
|
|
globalThis.document = document;
|
|
}
|
|
}
|
|
await test('profile HTML failures retain reauthentication without JSON parser errors or automatic retry', async () => {
|
|
await transport(
|
|
new Response('<html>authentication required</html>', {
|
|
status: 403,
|
|
headers: { 'X-Portal-Reauthentication': 'required' },
|
|
}),
|
|
async () => {
|
|
await assert.rejects(
|
|
profileJson('/synthetic'),
|
|
(e) =>
|
|
e instanceof ProfileValidationError &&
|
|
e.reauthenticate &&
|
|
Object.keys(e.fields).length === 0,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
await test('profile validation preserves field errors and server message', async () => {
|
|
await transport(
|
|
new Response(
|
|
JSON.stringify({
|
|
message: 'Invalid synthetic field',
|
|
errors: { name: ['Required', 'Too short'] },
|
|
}),
|
|
{ status: 422 },
|
|
),
|
|
async () => {
|
|
await assert.rejects(
|
|
profileJson('/synthetic'),
|
|
(e) =>
|
|
e instanceof ProfileValidationError &&
|
|
e.message === 'Invalid synthetic field' &&
|
|
e.fields.name === 'Required Too short',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
await test('profile rejects invalid successful response and explains expired sessions', async () => {
|
|
for (const status of [200, 419])
|
|
await transport(
|
|
new Response('<html>unexpected</html>', { status }),
|
|
async () => {
|
|
await assert.rejects(
|
|
profileJson('/synthetic'),
|
|
(e) =>
|
|
e instanceof ProfileValidationError &&
|
|
(status !== 419 || /expirou/.test(e.message)),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
await test('profile accepts no-content success', async () => {
|
|
await transport(new Response(null, { status: 204 }), async () =>
|
|
assert.equal(await profileJson('/synthetic'), null),
|
|
);
|
|
});
|
|
|
|
await test('profile reads ignore malformed CSRF while writes fail before transport with a useful error', async () => {
|
|
const oldFetch = globalThis.fetch,
|
|
oldDocument = globalThis.document;
|
|
let calls = 0;
|
|
try {
|
|
globalThis.document = { cookie: 'other=1;XSRF-TOKEN=%broken' };
|
|
globalThis.fetch = async (_url, options) => {
|
|
calls++;
|
|
assert.equal(options.method, 'GET');
|
|
assert.equal(options.headers['X-XSRF-TOKEN'], undefined);
|
|
assert.equal(options.credentials, 'same-origin');
|
|
return new Response('{"ok":true}');
|
|
};
|
|
assert.deepEqual(await profileJson('/synthetic', {}, 'GET'), {
|
|
ok: true,
|
|
});
|
|
await assert.rejects(
|
|
profileJson('/synthetic', {}),
|
|
(e) =>
|
|
e instanceof ProfileValidationError &&
|
|
/validada/.test(e.message),
|
|
);
|
|
assert.equal(calls, 1);
|
|
} finally {
|
|
globalThis.fetch = oldFetch;
|
|
globalThis.document = oldDocument;
|
|
}
|
|
});
|
|
await test('profile writes decode CSRF cookies regardless of separator spacing', async () => {
|
|
const oldFetch = globalThis.fetch,
|
|
oldDocument = globalThis.document;
|
|
try {
|
|
globalThis.document = { cookie: 'other=1;XSRF-TOKEN=synthetic%3D' };
|
|
globalThis.fetch = async (_url, options) => {
|
|
assert.equal(options.headers['X-XSRF-TOKEN'], 'synthetic=');
|
|
assert.equal(options.credentials, 'same-origin');
|
|
assert.equal(options.body, JSON.stringify({ name: 'synthetic' }));
|
|
return new Response('{"saved":true}');
|
|
};
|
|
assert.deepEqual(
|
|
await profileJson('/synthetic', { name: 'synthetic' }),
|
|
{ saved: true },
|
|
);
|
|
} finally {
|
|
globalThis.fetch = oldFetch;
|
|
globalThis.document = oldDocument;
|
|
}
|
|
});
|