diff --git a/pages/api/email/getall.js b/pages/api/email/getall.js index e54812ac..d906dc34 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 @@ -395,15 +393,9 @@ export default async function CombinedApiProxy(req, res) { }; }) ); - // res.status(200).json(results); // Step 3: Group results by contact_email const groupedByEmail = _.groupBy(results, "contact_email"); - //Convert to array format - // const groupedArray = Object.entries(groupedByEmail).map(([email, entries]) => ({ contact_email: email, entries })); - - //console.log(buildNotifyPayloads(groupedByEmail)); - const payloads = buildNotifyPayloads(groupedByEmail); // watchlistByEmail should be defined // Iterate and send @@ -412,7 +404,6 @@ export default async function CombinedApiProxy(req, res) { for (const payload of payloads) { if (!payload) continue; // skip null entries - //console.log(payload); const result = { email: payload.email_address, reference: payload.reference, diff --git a/pages/api/email/getcaseref.js b/pages/api/email/getcaseref.js index b4aa179d..ddc59198 100644 --- a/pages/api/email/getcaseref.js +++ b/pages/api/email/getcaseref.js @@ -1,6 +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"; import { getToken } from "../../../actions/core/token"; @@ -35,8 +33,6 @@ export default async function ApiProxy(req, res) { azureHeaders(token.access_token) ) .then(({ data }) => { - var dataStr; - const flattenedResults = data.value.map(flattenWatchlistEntry); return respondSuccess(res, flattenedResults); 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/pages/api/email/getmailinglist.js b/pages/api/email/getmailinglist.js index 44ff7297..3a980c57 100644 --- a/pages/api/email/getmailinglist.js +++ b/pages/api/email/getmailinglist.js @@ -1,14 +1,10 @@ import axios from "axios"; -import CryptoJS from "crypto-js"; -import _ from "lodash"; import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; 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/"; @@ -37,8 +33,6 @@ export default async function ApiProxy(req, res) { azureHeaders(token.access_token) ) .then(({ data }) => { - var dataStr; - const flattenedResults = data.value.map(flattenWatchlistEntry); return respondSuccess(res, flattenedResults); diff --git a/pages/api/email/notify.js b/pages/api/email/notify.js index a9cef216..edf8a7d0 100644 --- a/pages/api/email/notify.js +++ b/pages/api/email/notify.js @@ -63,19 +63,17 @@ export default async function ApiProxy(req, res) { const notifyClient = new NotifyClient(process.env.NOTIFY_API_KEY); //const emailReplyToId = process.env.EMAIL_REPLY_TO_ID; - console.log( - "///////////////\n Sending email \ns//////////////", - redactSensitive(data) - ); + consoleLogger({ + event: "notify_send_attempt", + payload: redactSensitive(data) + }); notifyClient .sendEmail(data.templateId, data.emailAddress, { personalisation: data.personalisation, reference: data.reference - // emailReplyToId: emailReplyToId, }) - .then((response) => { - //console.log("thisis the response", response); + .then(() => { return respondSuccess(res, data); }) .catch((error) => { @@ -86,5 +84,4 @@ export default async function ApiProxy(req, res) { message: "Failed to send notify email" }); }); - //return res.status(200).json(data); } 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..c648325c --- /dev/null +++ b/tests/phase21/email-handler-contract.test.cjs @@ -0,0 +1,358 @@ +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" + ); +}); + +test("getmailinglist success returns flattened mailing list results", async () => { + const mod = loadModule("pages/api/email/getmailinglist.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "token" }), + hashAPIPath: () => "&hash=expected", + azureHeaders: () => ({}), + axios: { + get: async () => ({ + data: { + value: [ + { + pinswg_emailnotifications: true, + pinswg_Contact: { + contactid: "c1", + emailaddress1: "a@test.local" + } + } + ] + } + }) + }, + consoleLogger: () => {} + }); + + const req = { query: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), [ + { + pinswg_emailnotifications: true, + contactid: "c1", + contact_email: "a@test.local" + } + ]); +}); + +test("getmailinglist failure returns MAILING_LIST_FETCH_FAILED", async () => { + const mod = loadModule("pages/api/email/getmailinglist.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "token" }), + hashAPIPath: () => "&hash=expected", + azureHeaders: () => ({}), + axios: { + get: async () => { + throw new Error("mailing list failed"); + } + }, + 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, + "MAILING_LIST_FETCH_FAILED" + ); +}); + +test("getcaseref success returns flattened case ref results", async () => { + const mod = loadModule("pages/api/email/getcaseref.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "token" }), + hashAPIPath: () => "&hash=expected", + azureHeaders: () => ({}), + axios: { + get: async () => ({ + data: { + value: [ + { + pinswg_emailnotifications: true, + pinswg_Contact: { + contactid: "c2", + emailaddress1: "b@test.local" + } + } + ] + } + }) + }, + consoleLogger: () => {} + }); + + const req = { query: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), [ + { + pinswg_emailnotifications: true, + contactid: "c2", + contact_email: "b@test.local" + } + ]); +}); + +test("getcaseref failure returns CASE_REF_FETCH_FAILED", async () => { + const mod = loadModule("pages/api/email/getcaseref.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "token" }), + hashAPIPath: () => "&hash=expected", + azureHeaders: () => ({}), + axios: { + get: async () => { + throw new Error("case ref failed"); + } + }, + 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, "CASE_REF_FETCH_FAILED"); +}); + +test("notify returns EMAIL_ADDRESS_REQUIRED when emailAddress is missing", async () => { + const mod = loadModule("pages/api/email/notify.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + consoleLogger: () => {}, + redactSensitive: (value) => value, + sanitizeString: () => "", + isNonEmptyString: () => false, + getPreferredLanguage: async () => ({ value: [] }) + }); + + const req = { body: { templateId: "t1", reference: "OTHER" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "EMAIL_ADDRESS_REQUIRED"); +}); + +test("notify send failure returns EMAIL_NOTIFY_FAILED", async () => { + const mod = loadModule("pages/api/email/notify.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + consoleLogger: () => {}, + redactSensitive: (value) => value, + sanitizeString: (value) => value, + isNonEmptyString: () => true, + getPreferredLanguage: async () => ({ value: [] }), + require: (name) => { + if (name === "notifications-node-client") { + return { + NotifyClient: function NotifyClient() { + return { + sendEmail: async () => { + throw new Error("notify failed"); + } + }; + } + }; + } + + return require(name); + } + }); + + const req = { + body: { + templateId: "t1", + emailAddress: "user@test.local", + personalisation: {}, + reference: "OTHER" + } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "EMAIL_NOTIFY_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); + }); +}