242 lines
7.2 KiB
JavaScript
242 lines
7.2 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const vm = require("vm");
|
|
|
|
const rootDir = path.resolve(__dirname, "..", "..");
|
|
const tests = [];
|
|
const test = (name, fn) => tests.push({ name, fn });
|
|
|
|
const loadModule = (relativePath, injected = {}) => {
|
|
const filePath = path.join(rootDir, relativePath);
|
|
let source = fs.readFileSync(filePath, "utf8");
|
|
const exportNames = Array.from(
|
|
source.matchAll(/export const\s+(\w+)\s*=/g)
|
|
).map((match) => match[1]);
|
|
|
|
source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, "");
|
|
source = source.replace(/export const\s+/g, "const ");
|
|
source = source.replace(
|
|
/export default async function\s+(\w+)\s*\(/g,
|
|
"async function $1("
|
|
);
|
|
source += `\nmodule.exports = { ${exportNames.join(", ")} };\n`;
|
|
source +=
|
|
'\nif (typeof handler !== "undefined") module.exports.default = handler;\n';
|
|
|
|
const context = {
|
|
module: { exports: {} },
|
|
exports: {},
|
|
require,
|
|
process,
|
|
console: { log: () => {}, info: () => {}, warn: () => {}, error: () => {} },
|
|
...injected
|
|
};
|
|
|
|
vm.runInNewContext(source, context, { filename: filePath });
|
|
return context.module.exports;
|
|
};
|
|
|
|
const normalize = (value) => JSON.parse(JSON.stringify(value));
|
|
|
|
const createRes = () => {
|
|
const state = { statusCode: null, jsonBody: undefined };
|
|
|
|
return {
|
|
state,
|
|
status(code) {
|
|
state.statusCode = code;
|
|
return this;
|
|
},
|
|
json(body) {
|
|
state.jsonBody = body;
|
|
return body;
|
|
}
|
|
};
|
|
};
|
|
|
|
const loadLocaleResolver = ({ relayGetData }) =>
|
|
loadModule("lib/auth/resolveCrmLocale.js", {
|
|
azureHeadersPaged: (accessToken) => ({ accessToken }),
|
|
escapeODataString: (value) => value.trim().replace(/'/g, "''"),
|
|
consoleLogger: () => {},
|
|
relayGetData,
|
|
RELAY_POLICY_STRICT_LOGIN: {
|
|
method: "GET",
|
|
timeoutMs: 5000,
|
|
maxRetries: 0,
|
|
retryBaseDelayMs: 0,
|
|
retryMaxDelayMs: 0
|
|
}
|
|
});
|
|
|
|
test("pre-auth CRM locale lookup returns Welsh and selects only language", async () => {
|
|
let capturedRequest;
|
|
const mod = loadLocaleResolver({
|
|
relayGetData: async (request) => {
|
|
capturedRequest = request;
|
|
return {
|
|
data: {
|
|
value: [
|
|
{
|
|
pinswg_preferredlanguage: 846040000,
|
|
contactid: "must-not-be-returned",
|
|
firstname: "Must not be returned"
|
|
}
|
|
]
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
const result = await mod.resolveCrmLocale(" Person@Example.com ");
|
|
const selectClause = new URLSearchParams(
|
|
capturedRequest.queryUrl.split("?")[1]
|
|
).get("$select");
|
|
|
|
assert.strictEqual(result, "cy");
|
|
assert.match(capturedRequest.queryUrl, /\$filter=emailaddress1 eq /);
|
|
assert.match(capturedRequest.queryUrl, /statuscode eq 1/);
|
|
assert.strictEqual(selectClause, "pinswg_preferredlanguage");
|
|
assert.ok(result === "en" || result === "cy" || result === null);
|
|
for (const field of [
|
|
"contactid",
|
|
"emailaddress1",
|
|
"firstname",
|
|
"lastname",
|
|
"yomifullname"
|
|
]) {
|
|
assert.doesNotMatch(selectClause, new RegExp(field));
|
|
assert.doesNotMatch(result, new RegExp(field));
|
|
}
|
|
});
|
|
|
|
test("pre-auth CRM locale lookup returns English for a non-Welsh value", async () => {
|
|
const mod = loadLocaleResolver({
|
|
relayGetData: async () => ({
|
|
data: { value: [{ pinswg_preferredlanguage: 846040001 }] }
|
|
})
|
|
});
|
|
|
|
assert.strictEqual(await mod.resolveCrmLocale("person@example.com"), "en");
|
|
});
|
|
|
|
test("pre-auth CRM locale lookup returns null for no contact", async () => {
|
|
const mod = loadLocaleResolver({
|
|
relayGetData: async () => ({ data: { value: [] } })
|
|
});
|
|
|
|
assert.strictEqual(await mod.resolveCrmLocale("person@example.com"), null);
|
|
});
|
|
|
|
test("pre-auth CRM locale lookup preserves first-contact duplicate semantics", async () => {
|
|
const mod = loadLocaleResolver({
|
|
relayGetData: async () => ({
|
|
data: {
|
|
value: [
|
|
{ pinswg_preferredlanguage: 846040001 },
|
|
{ pinswg_preferredlanguage: 846040000 }
|
|
]
|
|
}
|
|
})
|
|
});
|
|
|
|
assert.strictEqual(await mod.resolveCrmLocale("person@example.com"), "en");
|
|
});
|
|
|
|
test("pre-auth CRM locale lookup rejects malformed email without CRM access", async () => {
|
|
let relayCalls = 0;
|
|
const mod = loadLocaleResolver({
|
|
relayGetData: async () => {
|
|
relayCalls += 1;
|
|
return { data: { value: [] } };
|
|
}
|
|
});
|
|
|
|
assert.strictEqual(await mod.resolveCrmLocale("not-an-email"), null);
|
|
assert.strictEqual(relayCalls, 0);
|
|
});
|
|
|
|
test("pre-auth CRM locale lookup fails closed without leaking CRM details", async () => {
|
|
const mod = loadLocaleResolver({
|
|
relayGetData: async () => {
|
|
throw new Error("CRM secret response details");
|
|
}
|
|
});
|
|
|
|
assert.strictEqual(await mod.resolveCrmLocale("person@example.com"), null);
|
|
});
|
|
|
|
test("pre-auth CRM locale lookup escapes OData apostrophes", async () => {
|
|
let queryUrl;
|
|
const mod = loadLocaleResolver({
|
|
relayGetData: async (request) => {
|
|
queryUrl = request.queryUrl;
|
|
return { data: { value: [] } };
|
|
}
|
|
});
|
|
|
|
await mod.resolveCrmLocale("o'connor@example.com");
|
|
|
|
assert.match(queryUrl, /o''connor@example\.com/);
|
|
});
|
|
|
|
test("public sign-in locale route preserves response without CRM lookup", async () => {
|
|
const source = fs.readFileSync(
|
|
path.join(rootDir, "pages/api/auth/resolve-locale.js"),
|
|
"utf8"
|
|
);
|
|
const mod = loadModule("pages/api/auth/resolve-locale.js");
|
|
const res = createRes();
|
|
|
|
await mod.default(
|
|
{
|
|
method: "POST",
|
|
body: { email: "other@example.com", locale: "cy" },
|
|
query: {},
|
|
cookies: {}
|
|
},
|
|
res
|
|
);
|
|
|
|
assert.strictEqual(res.state.statusCode, 200);
|
|
assert.deepStrictEqual(normalize(res.state.jsonBody), { locale: "cy" });
|
|
assert.doesNotMatch(source, /getPortalLogin|resolveCrmLocale|relayGetData/);
|
|
});
|
|
|
|
test("both pre-auth callers no longer depend on getPortalLogin", () => {
|
|
const nextAuthSource = fs.readFileSync(
|
|
path.join(rootDir, "pages/api/auth/[...nextauth].js"),
|
|
"utf8"
|
|
);
|
|
const routeSource = fs.readFileSync(
|
|
path.join(rootDir, "pages/api/auth/resolve-locale.js"),
|
|
"utf8"
|
|
);
|
|
|
|
assert.doesNotMatch(nextAuthSource, /getPortalLogin/);
|
|
assert.doesNotMatch(routeSource, /getPortalLogin/);
|
|
assert.match(nextAuthSource, /resolveCrmLocale/);
|
|
});
|
|
|
|
const run = async () => {
|
|
let passed = 0;
|
|
|
|
for (const currentTest of tests) {
|
|
await currentTest.fn();
|
|
passed += 1;
|
|
}
|
|
|
|
console.log(
|
|
`Phase 22 pre-auth locale tests passed (${passed}/${tests.length}).`
|
|
);
|
|
};
|
|
|
|
if (require.main === module) {
|
|
run().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = run; |