From 6610e5c7f3e3cd4c64b73b3d1d26f96a34131810 Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 17 Mar 2026 14:23:42 +0000 Subject: [PATCH] TASK22109: email api contract cleanup and phase21 coverage --- pages/api/email/getall.js | 2 - pages/api/email/getdocuments.js | 3 - pages/api/email/getevents.js | 2 - tests/phase21/_shared.cjs | 25 ++- tests/phase21/api-contract-slice1.test.cjs | 2 + tests/phase21/email-handler-contract.test.cjs | 171 ++++++++++++++++++ 6 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 tests/phase21/email-handler-contract.test.cjs diff --git a/pages/api/email/getall.js b/pages/api/email/getall.js index e54812ac..9aaefd01 100644 --- a/pages/api/email/getall.js +++ b/pages/api/email/getall.js @@ -188,8 +188,6 @@ const buildNotifyPayloads = (watchlistByEmail) => { const prefLanguage = entries[0].pinswg_preferredlanguage; - console.log("Language to use for email tmeplace:", prefLanguage); - const personalisation = { contact_name: contactName, case_sections: filteredEntries diff --git a/pages/api/email/getdocuments.js b/pages/api/email/getdocuments.js index 3caa7ef7..0151110b 100644 --- a/pages/api/email/getdocuments.js +++ b/pages/api/email/getdocuments.js @@ -1,6 +1,5 @@ import axios from "axios"; import CryptoJS from "crypto-js"; -import _ from "lodash"; import { azureHeadersPaged } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; @@ -67,8 +66,6 @@ export default async function ApiProxy(req, res) { const fullUrl = `${WEBAPI_URL}${query}${hashAPIPath(query)}`; - console.log("Querying documents from:", fullUrl); - const { data } = await axios.get( fullUrl, azureHeadersPaged(token.access_token) diff --git a/pages/api/email/getevents.js b/pages/api/email/getevents.js index 79c9feda..92498f38 100644 --- a/pages/api/email/getevents.js +++ b/pages/api/email/getevents.js @@ -1,5 +1,4 @@ import axios from "axios"; -import CryptoJS from "crypto-js"; import _ from "lodash"; import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; @@ -7,7 +6,6 @@ import { getToken } from "../../../actions/core/token"; import { hashAPIPath } from "../../../actions/core/hash"; import { respondError, respondSuccess } from "../middleware/apiResponse"; -const WORDKEY = process.env.HASHKEY; const WEBAPI_URL = process.env.RELAY_ROOT || "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; diff --git a/tests/phase21/_shared.cjs b/tests/phase21/_shared.cjs index 9cf1aaab..ada96f29 100644 --- a/tests/phase21/_shared.cjs +++ b/tests/phase21/_shared.cjs @@ -7,13 +7,32 @@ const rootDir = path.resolve(__dirname, "..", ".."); const loadModule = (relativePath, injected = {}) => { const filePath = path.join(rootDir, relativePath); let source = fs.readFileSync(filePath, "utf8"); + const defaultExportNames = []; source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); - source = source.replace(/export const\s+/g, "const "); source = source.replace( - /export default\s+(\w+);/g, - "module.exports.default = $1;" + /export default async function\s+(\w+)\s*\(/g, + (match, name) => { + defaultExportNames.push(name); + return `async function ${name}(`; + } ); + source = source.replace( + /export default function\s+(\w+)\s*\(/g, + (match, name) => { + defaultExportNames.push(name); + return `function ${name}(`; + } + ); + source = source.replace(/export const\s+/g, "const "); + source = source.replace(/export default\s+(\w+);/g, (match, name) => { + defaultExportNames.push(name); + return `module.exports.default = ${name};`; + }); + + defaultExportNames.forEach((name) => { + source += `\nif (typeof ${name} !== "undefined" && !module.exports.default) module.exports.default = ${name};\n`; + }); source += '\nif (typeof respondSuccess !== "undefined") module.exports.respondSuccess = respondSuccess;\n'; diff --git a/tests/phase21/api-contract-slice1.test.cjs b/tests/phase21/api-contract-slice1.test.cjs index 5d493fb0..bc542f03 100644 --- a/tests/phase21/api-contract-slice1.test.cjs +++ b/tests/phase21/api-contract-slice1.test.cjs @@ -1,9 +1,11 @@ const runHelperTests = require("./api-response-helper.test.cjs"); const runHandlerTests = require("./file-handler-contract.test.cjs"); +const runEmailHandlerTests = require("./email-handler-contract.test.cjs"); const run = async () => { await runHelperTests(); await runHandlerTests(); + await runEmailHandlerTests(); console.log("Phase 21 combined suite passed."); }; diff --git a/tests/phase21/email-handler-contract.test.cjs b/tests/phase21/email-handler-contract.test.cjs new file mode 100644 index 00000000..95744a67 --- /dev/null +++ b/tests/phase21/email-handler-contract.test.cjs @@ -0,0 +1,171 @@ +const assert = require("assert"); +const { + loadModule, + createRes, + respondSuccessMock, + respondErrorMock +} = require("./_shared.cjs"); + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("getevents returns INCIDENT_ID_REQUIRED when incidentID missing", async () => { + const mod = loadModule("pages/api/email/getevents.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "token" }), + hashAPIPath: () => "&hash=expected", + azureHeaders: () => ({}), + axios: { get: async () => ({ data: { value: [] } }) }, + _: { get: (obj, key, fallback) => obj?.data?.value || fallback }, + consoleLogger: () => {} + }); + + const req = { query: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "INCIDENT_ID_REQUIRED"); +}); + +test("getevents returns SIPS_RECORD_NOT_FOUND when linked record is missing", async () => { + const mod = loadModule("pages/api/email/getevents.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "token" }), + hashAPIPath: () => "&hash=expected", + azureHeaders: () => ({}), + axios: { get: async () => ({ data: { value: [] } }) }, + _: { get: (obj, key, fallback) => obj?.data?.value || fallback }, + consoleLogger: () => {} + }); + + const req = { query: { incidentID: "123" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 404); + assert.strictEqual(res.state.jsonBody.error.code, "SIPS_RECORD_NOT_FOUND"); +}); + +test("getevents dependency failure returns EVENTS_FETCH_FAILED", async () => { + const mod = loadModule("pages/api/email/getevents.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "token" }), + hashAPIPath: () => "&hash=expected", + azureHeaders: () => ({}), + axios: { + get: async () => { + throw new Error("upstream failed"); + } + }, + _: { get: (obj, key, fallback) => obj?.data?.value || fallback }, + consoleLogger: () => {} + }); + + const req = { query: { incidentID: "123" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 500); + assert.strictEqual(res.state.jsonBody.error.code, "EVENTS_FETCH_FAILED"); +}); + +test("getdocuments returns INCIDENT_ID_REQUIRED when incidentid missing", async () => { + const mod = loadModule("pages/api/email/getdocuments.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "token" }), + hashAPIPath: () => "&hash=expected", + azureHeadersPaged: () => ({}), + axios: { get: async () => ({ data: { value: [] } }) }, + consoleLogger: () => {} + }); + + const req = { query: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "INCIDENT_ID_REQUIRED"); +}); + +test("getdocuments dependency failure returns DOCUMENTS_FETCH_FAILED", async () => { + const mod = loadModule("pages/api/email/getdocuments.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "token" }), + hashAPIPath: () => "&hash=expected", + azureHeadersPaged: () => ({}), + axios: { + get: async () => { + throw new Error("docs failed"); + } + }, + consoleLogger: () => {} + }); + + const req = { query: { incidentid: "123" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 500); + assert.strictEqual(res.state.jsonBody.error.code, "DOCUMENTS_FETCH_FAILED"); +}); + +test("getall top-level failure returns EMAIL_COMBINED_FETCH_FAILED", async () => { + const mod = loadModule("pages/api/email/getall.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => { + throw new Error("token failed"); + }, + consoleLogger: () => {}, + formatDates: () => "01/01/2026", + NotifyClient: function NotifyClient() { + return { sendEmail: async () => ({ id: "n1" }) }; + }, + require: (name) => { + if (name === "notifications-node-client") { + return { + NotifyClient: function NotifyClient() { + return { sendEmail: async () => ({ id: "n1" }) }; + } + }; + } + return require(name); + } + }); + + const req = { query: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 500); + assert.strictEqual( + res.state.jsonBody.error.code, + "EMAIL_COMBINED_FETCH_FAILED" + ); +}); + +const run = async () => { + let passed = 0; + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + console.log( + `Phase 21 email-handler contract tests passed (${passed}/${tests.length}).` + ); +}; + +module.exports = run; + +if (require.main === module) { + run().catch((error) => { + console.error(error); + process.exit(1); + }); +}