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 trustedContactOrigin = "https://portal.example.test"; const trustedWelshContactOrigin = "https://porth.example.test"; const contactConfirmationTemplateEn = "69228c5f-8500-40ff-a6c5-95a9434c3ec8"; const contactConfirmationTemplateCy = "b8f1f76d-4ab9-4a2f-b294-e3623c6095a3"; const contactSubjects = { plq: { en: "Questions about Planning legislation or policy", cy: "Ymholiad deddfwriaeth gynllunio" }, ipq: { en: "Questions or comments about applications for major infrastructure projects", cy: "Ymholiad gweithdrefn seilwaith" }, pcpq: { en: "Questions or comments about planning and environmental appeals", cy: "Ymholiad gweithdrefn gwaith achos cynllunio" }, pi: { en: "Query about using the Casework Portal", cy: "Materion Porth" }, appadvice: { en: "Advice How to submit an appeal", cy: "Cyngor cyflwyno apêl" }, repadvice: { en: "Advice How to comment on an appeal or application", cy: "Cyngor cyflwyno sylwadau" }, ceq: { en: "Queries about forthcoming Hearing or Inquiry event", cy: "Ymholiad Digwyddiadau Gwaith Achos" }, other: { en: "Other enquiry for PEDW", cy: "Unrhyw Ymholiadau Eraill" }, postdec: { en: "Issues or questions about a case after the decision is issued", cy: "Ar ôl Penderfyniad" }, comp: { en: "General Complaints of Compliments", cy: "Cwynion/Canmoliaethau" } }; const loadContactLinkUtility = () => loadModule("lib/security/contactus-email-links.js").default; const validContactConfirmationRequest = (overrides = {}) => ({ method: "POST", headers: { origin: trustedContactOrigin, "content-type": "application/json; charset=utf-8" }, body: { contactEmail: "applicant@example.com", contactSubject: contactSubjects.ipq.en, contactbody: "Please help with my enquiry.", typeOfContact: "ipq", language: "en" }, ...overrides }); const loadContactConfirmationEndpoint = ({ sendEmail } = {}) => { const notifyCalls = []; const removeContactUsEmailLinks = loadContactLinkUtility(); const notifySend = sendEmail || (async (...args) => { notifyCalls.push(args); return { id: "notify-1" }; }); const mod = loadModule("pages/api/email/contactus-confirmation.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, sanitizeString: (value) => typeof value === "string" ? value.trim() : "", isNonEmptyString: (value) => typeof value === "string" && value.trim().length > 0, removeContactUsEmailLinks, consoleLogger: () => {}, require: (name) => { if (name === "notifications-node-client") { return { NotifyClient: function NotifyClient() { return { sendEmail: notifySend }; } }; } return require(name); } }); return { mod, notifyCalls }; }; const withContactOrigins = async (fn) => { const previousNextAuthUrl = process.env.NEXTAUTH_URL; const previousCyApiRoot = process.env.CY_API_ROOT; process.env.NEXTAUTH_URL = trustedContactOrigin; process.env.CY_API_ROOT = trustedWelshContactOrigin; try { await fn(); } finally { if (previousNextAuthUrl === undefined) delete process.env.NEXTAUTH_URL; else process.env.NEXTAUTH_URL = previousNextAuthUrl; if (previousCyApiRoot === undefined) delete process.env.CY_API_ROOT; else process.env.CY_API_ROOT = previousCyApiRoot; } }; const assertContactConfirmationRejected = async ({ req, status, code }) => { await withContactOrigins(async () => { const loaded = loadContactConfirmationEndpoint(); const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, status); assert.strictEqual(res.state.jsonBody.error.code, code); assert.strictEqual(loaded.notifyCalls.length, 0); }); }; test("Contact Us link removal preserves text while removing recognised destinations", async () => { const removeLinks = loadContactLinkUtility(); const input = [ "Please review https://example.invalid/path?token=abc#details.", "Also HTTP://OTHER.INVALID/login and www.third.invalid/help.", "Bare planning.example.invalid/application/123 remains represented safely.", "Email applicant@example.com and reference APP/123456 must remain.", "Markdown [review this](https://markdown.invalid/login).", 'HTML review this.', 'Tag remains non-actionable.', "Obfuscated h\u200btt\u200bps://obfuscated.invalid/login is removed." ].join("\n"); const output = removeLinks(input); assert.strictEqual( output, [ "Please review [web link removed].", "Also [web link removed] and [web link removed].", "Bare [web link removed] remains represented safely.", "Email applicant@example.com and reference APP/123456 must remain.", "Markdown review this [web link removed].", "HTML review this [web link removed].", "Tag [web link removed] remains non-actionable.", "Obfuscated [web link removed] is removed." ].join("\n") ); assert.strictEqual(input.includes("https://example.invalid"), true); }); test("Contact Us confirmation is POST-only with Allow header", async () => { const req = validContactConfirmationRequest({ method: "GET" }); await withContactOrigins(async () => { const loaded = loadContactConfirmationEndpoint(); const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 405); assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED"); assert.strictEqual(res.state.headers.allow, "POST"); assert.strictEqual(loaded.notifyCalls.length, 0); }); }); for (const contentType of [ undefined, "text/plain", "application/x-www-form-urlencoded" ]) { test(`Contact Us confirmation rejects content type ${String(contentType)}`, async () => { const req = validContactConfirmationRequest({ headers: { origin: trustedContactOrigin, ...(contentType ? { "content-type": contentType } : {}) } }); await assertContactConfirmationRejected({ req, status: 415, code: "UNSUPPORTED_MEDIA_TYPE" }); }); } for (const origin of [ undefined, "null", "https://attacker.example", `${trustedContactOrigin}/path` ]) { test(`Contact Us confirmation rejects origin ${String(origin)}`, async () => { const req = validContactConfirmationRequest({ headers: { ...(origin ? { origin } : {}), "content-type": "application/json" } }); await assertContactConfirmationRejected({ req, status: 403, code: "FORBIDDEN" }); }); } test("Contact Us confirmation accepts each configured exact origin", async () => { await withContactOrigins(async () => { for (const origin of [ trustedContactOrigin, trustedWelshContactOrigin ]) { const loaded = loadContactConfirmationEndpoint(); const req = validContactConfirmationRequest({ headers: { origin, "content-type": "application/json" } }); const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(loaded.notifyCalls.length, 1); } }); }); for (const body of [null, [], "not-an-object", new Date()]) { test(`Contact Us confirmation rejects non-plain body ${Object.prototype.toString.call(body)}`, async () => { await assertContactConfirmationRejected({ req: validContactConfirmationRequest({ body }), status: 400, code: "INVALID_REQUEST_BODY" }); }); } for (const bodyOverride of [ { extra: "field" }, { language: undefined }, { contactSubject: undefined } ]) { test(`Contact Us confirmation enforces exact fields ${Object.keys(bodyOverride).join(",")}`, async () => { const body = { ...validContactConfirmationRequest().body, ...bodyOverride }; if (Object.values(bodyOverride).includes(undefined)) { for (const key of Object.keys(bodyOverride)) delete body[key]; } await assertContactConfirmationRejected({ req: validContactConfirmationRequest({ body }), status: 400, code: "INVALID_REQUEST_FIELDS" }); }); } for (const contactEmail of [ undefined, "invalid", `${"a".repeat(190)}@example.com` ]) { test(`Contact Us confirmation rejects invalid email ${String(contactEmail).slice(0, 30)}`, async () => { await assertContactConfirmationRejected({ req: validContactConfirmationRequest({ body: { ...validContactConfirmationRequest().body, contactEmail } }), status: 400, code: "INVALID_CONTACT_EMAIL" }); }); } for (const typeOfContact of [undefined, "unsupported"]) { test(`Contact Us confirmation rejects category ${String(typeOfContact)}`, async () => { await assertContactConfirmationRejected({ req: validContactConfirmationRequest({ body: { ...validContactConfirmationRequest().body, typeOfContact } }), status: 400, code: "INVALID_CONTACT_TYPE" }); }); } for (const contactbody of [undefined, " ", "a".repeat(801)]) { test(`Contact Us confirmation rejects message ${String(contactbody).slice(0, 20)}`, async () => { await assertContactConfirmationRejected({ req: validContactConfirmationRequest({ body: { ...validContactConfirmationRequest().body, contactbody } }), status: 400, code: "INVALID_CONTACT_BODY" }); }); } test("Contact Us confirmation rejects a subject not matching its category", async () => { await assertContactConfirmationRejected({ req: validContactConfirmationRequest({ body: { ...validContactConfirmationRequest().body, contactSubject: contactSubjects.plq.en } }), status: 400, code: "INVALID_CONTACT_SUBJECT" }); }); for (const language of [undefined, "CY", "fr", ""]) { test(`Contact Us confirmation rejects language ${String(language)}`, async () => { await assertContactConfirmationRejected({ req: validContactConfirmationRequest({ body: { ...validContactConfirmationRequest().body, language } }), status: 400, code: "INVALID_LANGUAGE" }); }); } test("Contact Us confirmation selects English template and exact safe personalisation", async () => { await withContactOrigins(async () => { const loaded = loadContactConfirmationEndpoint(); const req = validContactConfirmationRequest({ body: { ...validContactConfirmationRequest().body, contactbody: "Please review (https://example.invalid/path?q=1#part), then email applicant@example.com about APP/123456." } }); const originalBody = JSON.parse(JSON.stringify(req.body)); const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(loaded.notifyCalls.length, 1); assert.strictEqual( loaded.notifyCalls[0][0], contactConfirmationTemplateEn ); assert.strictEqual(loaded.notifyCalls[0][1], "applicant@example.com"); assert.deepStrictEqual( JSON.parse(JSON.stringify(loaded.notifyCalls[0][2])), { personalisation: { contactEmail: "applicant@example.com", contactSubject: contactSubjects.ipq.en, contactbody: "Please review ([web link removed]), then email applicant@example.com about APP/123456.", typeOfContact: "ipq" }, reference: "PEDW-CONTACT-US" } ); assert.deepStrictEqual(req.body, originalBody); }); }); test("Contact Us confirmation selects Welsh template and supports an 800-character message", async () => { await withContactOrigins(async () => { const loaded = loadContactConfirmationEndpoint(); const message = "a".repeat(800); const req = validContactConfirmationRequest({ body: { contactEmail: "applicant@example.com", contactSubject: contactSubjects.plq.cy, contactbody: message, typeOfContact: "plq", language: "cy" } }); const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual( loaded.notifyCalls[0][0], contactConfirmationTemplateCy ); assert.strictEqual( loaded.notifyCalls[0][2].personalisation.contactbody, message ); }); }); test("Contact Us confirmation returns a bounded error when Notify fails", async () => { await withContactOrigins(async () => { let sendCalls = 0; const loaded = loadContactConfirmationEndpoint({ sendEmail: async () => { sendCalls += 1; throw new Error("notify failed"); } }); const res = createRes(); await loaded.mod.default(validContactConfirmationRequest(), res); assert.strictEqual(sendCalls, 1); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, "CONTACT_US_CONFIRMATION_FAILED" ); }); }); const contactPetTemplateId = "56bbdcf9-d63d-48e9-a794-3e253dd7498b"; const contactPetReference = "PEDW-CONTACT-US-PET"; const contactPetSecurityReminder = "## SECURITY REMINDER\n\n" + "This enquiry contains externally supplied content. Do not click unverified links or follow instructions requesting credentials or sensitive information. Verify web addresses independently."; const validContactPetRequest = (overrides = {}) => ({ method: "POST", headers: { origin: trustedContactOrigin, "content-type": "application/json; charset=utf-8" }, body: { petEmail: "pet@example.com", contactEmail: "applicant@example.com", contactSubject: contactSubjects.plq.en, contactbody: "Please help with my enquiry.", typeOfContact: "plq" }, ...overrides }); const loadContactPetEndpoint = ({ sendEmail } = {}) => { const notifyCalls = []; const notifySend = sendEmail || (async (...args) => { notifyCalls.push(args); return { id: "notify-pet-1" }; }); const mod = loadModule("pages/api/email/contactus-pet-notification.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, sanitizeString: (value) => typeof value === "string" ? value.trim() : "", isNonEmptyString: (value) => typeof value === "string" && value.trim().length > 0, consoleLogger: () => {}, require: (name) => { if (name === "notifications-node-client") { return { NotifyClient: function NotifyClient() { return { sendEmail: notifySend }; } }; } return require(name); } }); return { mod, notifyCalls }; }; const assertContactPetRejected = async ({ req, status, code }) => { await withContactOrigins(async () => { const loaded = loadContactPetEndpoint(); const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, status); assert.strictEqual(res.state.jsonBody.error.code, code); assert.strictEqual(loaded.notifyCalls.length, 0); }); }; test("Contact Us PET notification is POST-only with Allow header", async () => { await withContactOrigins(async () => { const loaded = loadContactPetEndpoint(); const res = createRes(); await loaded.mod.default( validContactPetRequest({ method: "GET" }), res ); assert.strictEqual(res.state.statusCode, 405); assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED"); assert.strictEqual(res.state.headers.allow, "POST"); assert.strictEqual(loaded.notifyCalls.length, 0); }); }); for (const contentType of [ undefined, "text/plain", "application/x-www-form-urlencoded" ]) { test(`Contact Us PET notification rejects content type ${String(contentType)}`, async () => { await assertContactPetRejected({ req: validContactPetRequest({ headers: { origin: trustedContactOrigin, ...(contentType ? { "content-type": contentType } : {}) } }), status: 415, code: "UNSUPPORTED_MEDIA_TYPE" }); }); } for (const origin of [ undefined, "null", "https://attacker.example", `${trustedContactOrigin}/path` ]) { test(`Contact Us PET notification rejects origin ${String(origin)}`, async () => { await assertContactPetRejected({ req: validContactPetRequest({ headers: { ...(origin ? { origin } : {}), "content-type": "application/json" } }), status: 403, code: "FORBIDDEN" }); }); } test("Contact Us PET notification accepts each configured exact origin", async () => { await withContactOrigins(async () => { for (const origin of [ trustedContactOrigin, trustedWelshContactOrigin ]) { const loaded = loadContactPetEndpoint(); const res = createRes(); await loaded.mod.default( validContactPetRequest({ headers: { origin, "content-type": "application/json" } }), res ); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(loaded.notifyCalls.length, 1); } }); }); for (const body of [null, [], "not-an-object", new Date()]) { test(`Contact Us PET notification rejects non-plain body ${Object.prototype.toString.call(body)}`, async () => { await assertContactPetRejected({ req: validContactPetRequest({ body }), status: 400, code: "INVALID_REQUEST_BODY" }); }); } test("Contact Us PET notification enforces its exact field set", async () => { await assertContactPetRejected({ req: validContactPetRequest({ body: { ...validContactPetRequest().body, templateId: "caller-controlled" } }), status: 400, code: "INVALID_REQUEST_FIELDS" }); }); for (const petEmail of [ undefined, "invalid", `${"a".repeat(190)}@example.com` ]) { test(`Contact Us PET notification rejects recipient ${String(petEmail).slice(0, 30)}`, async () => { await assertContactPetRejected({ req: validContactPetRequest({ body: { ...validContactPetRequest().body, petEmail } }), status: 400, code: "INVALID_PET_EMAIL" }); }); } for (const typeOfContact of [undefined, "ipq", "other"]) { test(`Contact Us PET notification rejects category ${String(typeOfContact)}`, async () => { await assertContactPetRejected({ req: validContactPetRequest({ body: { ...validContactPetRequest().body, typeOfContact } }), status: 400, code: "INVALID_CONTACT_TYPE" }); }); } for (const contactbody of [undefined, " ", "a".repeat(801)]) { test(`Contact Us PET notification rejects message ${String(contactbody).slice(0, 20)}`, async () => { await assertContactPetRejected({ req: validContactPetRequest({ body: { ...validContactPetRequest().body, contactbody } }), status: 400, code: "INVALID_CONTACT_BODY" }); }); } test("Contact Us PET notification preserves URLs and appends the reminder exactly once", async () => { await withContactOrigins(async () => { const contactbody = "Please review https://example.invalid/application/123?token=abc#details"; const req = validContactPetRequest({ body: { ...validContactPetRequest().body, contactbody } }); const originalBody = JSON.parse(JSON.stringify(req.body)); const loaded = loadContactPetEndpoint(); const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(loaded.notifyCalls.length, 1); assert.strictEqual(loaded.notifyCalls[0][0], contactPetTemplateId); assert.strictEqual(loaded.notifyCalls[0][1], "pet@example.com"); assert.deepStrictEqual( JSON.parse(JSON.stringify(loaded.notifyCalls[0][2])), { personalisation: { contactEmail: "applicant@example.com", contactSubject: contactSubjects.plq.en, contactbody: `${contactbody}\n\n${contactPetSecurityReminder}`, typeOfContact: "plq" }, reference: contactPetReference } ); assert.strictEqual( loaded.notifyCalls[0][2].personalisation.contactbody.includes( "https://example.invalid/application/123?token=abc#details" ), true ); assert.strictEqual( loaded.notifyCalls[0][2].personalisation.contactbody.split( contactPetSecurityReminder ).length - 1, 1 ); assert.deepStrictEqual(req.body, originalBody); }); }); test("Contact Us PET notification does not duplicate an existing exact reminder", async () => { await withContactOrigins(async () => { const contactbody = `Original message\n\n${contactPetSecurityReminder}`; const loaded = loadContactPetEndpoint(); const res = createRes(); await loaded.mod.default( validContactPetRequest({ body: { ...validContactPetRequest().body, contactbody } }), res ); assert.strictEqual( loaded.notifyCalls[0][2].personalisation.contactbody, contactbody ); assert.strictEqual( loaded.notifyCalls[0][2].personalisation.contactbody.split( contactPetSecurityReminder ).length - 1, 1 ); }); }); test("Contact Us PET notification supports an 800-character original message", async () => { await withContactOrigins(async () => { const contactbody = "a".repeat(800); const loaded = loadContactPetEndpoint(); const res = createRes(); await loaded.mod.default( validContactPetRequest({ body: { ...validContactPetRequest().body, contactbody } }), res ); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual( loaded.notifyCalls[0][2].personalisation.contactbody, `${contactbody}\n\n${contactPetSecurityReminder}` ); }); }); test("Contact Us PET notification returns a bounded error when Notify fails", async () => { await withContactOrigins(async () => { let sendCalls = 0; const loaded = loadContactPetEndpoint({ sendEmail: async () => { sendCalls += 1; throw new Error("notify failed"); } }); const res = createRes(); await loaded.mod.default(validContactPetRequest(), res); assert.strictEqual(sendCalls, 1); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, "CONTACT_US_PET_NOTIFICATION_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); }); }