From e39c9203e30524ac9dccd529f1872055f0c4ded6 Mon Sep 17 00:00:00 2001 From: robbond Date: Fri, 13 Mar 2026 10:58:26 +0000 Subject: [PATCH] TASK21988a: Phase 7 search SSR fix and service behaviour expansion --- pages/searchresults.js | 2 +- tests/phase7/service-behaviour.test.cjs | 369 ++++++++++++++++++++++++ 2 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 tests/phase7/service-behaviour.test.cjs diff --git a/pages/searchresults.js b/pages/searchresults.js index 82a10fd2..35ff09ed 100644 --- a/pages/searchresults.js +++ b/pages/searchresults.js @@ -160,7 +160,7 @@ export const getServerSideProps = wrapper.getServerSideProps( // store.dispatch(setSearch(query.q)); // } - store.dispatch(setSearch(query.q)); + store.dispatch(setSearch(query?.q || "")); return { props: { isLinkedCase: isLinked, diff --git a/tests/phase7/service-behaviour.test.cjs b/tests/phase7/service-behaviour.test.cjs new file mode 100644 index 00000000..381cc31f --- /dev/null +++ b/tests/phase7/service-behaviour.test.cjs @@ -0,0 +1,369 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); +const assert = require("assert"); + +const rootDir = path.resolve(__dirname, "..", ".."); +const servicesDir = path.join(rootDir, "actions", "services"); + +const createAxiosMock = () => { + const axios = (config) => axios.request(config); + + axios.calls = []; + axios.requestHandler = async () => { + throw new Error("No axios.request handler configured"); + }; + axios.getHandler = async () => { + throw new Error("No axios.get handler configured"); + }; + axios.postHandler = async () => { + throw new Error("No axios.post handler configured"); + }; + + axios.request = (config) => { + axios.calls.push({ type: "request", config }); + return axios.requestHandler(config); + }; + + axios.get = (url, config) => { + axios.calls.push({ type: "get", url, config }); + return axios.getHandler(url, config); + }; + + axios.post = (url, data, config) => { + axios.calls.push({ type: "post", url, data, config }); + return axios.postHandler(url, data, config); + }; + + return axios; +}; + +const createLoggerMock = () => { + const calls = []; + const consoleLogger = (error) => { + calls.push(error); + }; + return { consoleLogger, calls }; +}; + +const createAxiosError = (status = 500, statusText = "Server Error") => { + return { + response: { + status, + statusText, + data: {} + }, + config: { + url: "/mock-url" + } + }; +}; + +const loadServiceModule = (fileName, injected = {}) => { + const filePath = path.join(servicesDir, fileName); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + + const exportNames = Array.from( + source.matchAll(/export const\s+(\w+)\s*=/g) + ).map((match) => match[1]); + + source = source.replace(/export const\s+/g, "const "); + source += `\nmodule.exports = { ${exportNames.join(", ")} };\n`; + + const context = { + module: { exports: {} }, + exports: {}, + require, + URLSearchParams, + encodeURIComponent, + FormData: global.FormData, + console: { + log: () => {}, + info: () => {}, + warn: () => {}, + error: () => {} + }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const tests = []; + +const test = (name, fn) => { + tests.push({ name, fn }); +}; + +const normalize = (value) => JSON.parse(JSON.stringify(value)); + +test("document/getAwaitingSubmissionFromBlob includes hash token and returns data", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + const hashCalls = []; + const hashAPIPath = (queryPath) => { + hashCalls.push(queryPath); + return "&hash=abc123"; + }; + + axios.getHandler = async () => ({ data: { value: ["blob-item"] } }); + + const document = loadServiceModule("documentDirectService.js", { + axios, + BASE_URL: "http://example.local", + consoleLogger: logger.consoleLogger, + hashAPIPath + }); + + const result = await document.getAwaitingSubmissionFromBlob("container-1"); + + assert.deepStrictEqual(normalize(result), { value: ["blob-item"] }); + assert.strictEqual(hashCalls.length, 1); + assert.strictEqual( + hashCalls[0], + "/api/file/getawaitingsubmissionfromblob?container=container-1" + ); + assert.strictEqual( + axios.calls[0].url, + "http://example.local/api/file/getawaitingsubmissionfromblob?container=container-1&hash=abc123" + ); +}); + +test("document/getAwaitingSubmissionFromBlob logs and returns undefined on failure", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + axios.getHandler = async () => + Promise.reject(createAxiosError(403, "Forbidden")); + + const document = loadServiceModule("documentDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + hashAPIPath: () => "&hash=xyz" + }); + + const result = await document.getAwaitingSubmissionFromBlob("container-2"); + + assert.strictEqual(result, undefined); + assert.strictEqual(logger.calls.length, 1); +}); + +test("portal/deleteMyRepresentations appends hash and returns data", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + const hashCalls = []; + const hashAPIPath = (queryPath) => { + hashCalls.push(queryPath); + return "&hash=portal123"; + }; + + axios.requestHandler = async () => ({ data: { removed: true } }); + + const portal = loadServiceModule("portalDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + hashAPIPath + }); + + const result = await portal.deleteMyRepresentations("rep-1"); + + assert.deepStrictEqual(normalize(result), { removed: true }); + assert.strictEqual(hashCalls.length, 1); + assert.strictEqual( + hashCalls[0], + "/api/endpoint/deletemyrepresentations_api?myRepresentationsID=rep-1" + ); + assert.strictEqual( + axios.calls[0].config.url, + "/api/endpoint/deletemyrepresentations_api?myRepresentationsID=rep-1&hash=portal123" + ); + assert.strictEqual(axios.calls[0].config.method, "delete"); +}); + +test("portal/deleteMyRepresentations logs and returns undefined on failure", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + axios.requestHandler = async () => + Promise.reject(createAxiosError(401, "Unauthorized")); + + const portal = loadServiceModule("portalDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + hashAPIPath: () => "&hash=portal-fail" + }); + + const result = await portal.deleteMyRepresentations("rep-2"); + + assert.strictEqual(result, undefined); + assert.strictEqual(logger.calls.length, 1); +}); + +test("account/getPortalLogin appends hash and returns res.data", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + const hashCalls = []; + const hashAPIPath = (queryPath) => { + hashCalls.push(queryPath); + return "&hash=login123"; + }; + + axios.getHandler = async () => ({ data: { value: [{ id: "user-1" }] } }); + + const account = loadServiceModule("accountDirectService.js", { + axios, + BASE_URL: "http://example.local", + consoleLogger: logger.consoleLogger, + hashAPIPath + }); + + const result = await account.getPortalLogin("person@example.com"); + + assert.deepStrictEqual(normalize(result), { value: [{ id: "user-1" }] }); + assert.strictEqual(hashCalls.length, 1); + assert.strictEqual( + hashCalls[0], + "/api/endpoint/getportallogin_api?emailAddress=person@example.com" + ); + assert.strictEqual( + axios.calls[0].url, + "http://example.local/api/endpoint/getportallogin_api?emailAddress=person@example.com&hash=login123" + ); +}); + +test("account/getPortalLogin returns JSON stringified error on failure", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + const error = createAxiosError(500, "Broken"); + + axios.getHandler = async () => Promise.reject(error); + + const account = loadServiceModule("accountDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + hashAPIPath: () => "&hash=err" + }); + + const result = await account.getPortalLogin("person@example.com"); + + assert.strictEqual(result, JSON.stringify(error)); + assert.strictEqual(logger.calls.length, 1); +}); + +test("notify/sendEmail posts payload and returns response data", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + axios.postHandler = async () => ({ data: { id: "msg-1" } }); + + const notify = loadServiceModule("notifyDirectService.js", { + axios, + consoleLogger: logger.consoleLogger + }); + + const result = await notify.sendEmail( + "template-1", + "person@example.com", + { name: "Person" }, + "ref-123" + ); + + assert.deepStrictEqual(normalize(result), { id: "msg-1" }); + assert.strictEqual(axios.calls[0].url, "/api/email/notify"); + assert.deepStrictEqual(normalize(axios.calls[0].data), { + templateId: "template-1", + emailAddress: "person@example.com", + reference: "ref-123", + personalisation: { name: "Person" } + }); +}); + +test("notify/sendEmail logs and rethrows on failure", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + const error = createAxiosError(429, "Too Many Requests"); + + axios.postHandler = async () => Promise.reject(error); + + const notify = loadServiceModule("notifyDirectService.js", { + axios, + consoleLogger: logger.consoleLogger + }); + + let thrown; + + try { + await notify.sendEmail("template-1", "person@example.com", {}, "ref"); + } catch (err) { + thrown = err; + } + + assert.strictEqual(thrown, error); + assert.strictEqual(logger.calls.length, 1); +}); + +test("integration/createCRMTask posts payload and returns data", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + axios.requestHandler = async () => ({ data: { taskId: "crm-1" } }); + + const integration = loadServiceModule("integrationDirectService.js", { + axios, + consoleLogger: logger.consoleLogger + }); + + const payload = { subject: "hello" }; + const result = await integration.createCRMTask(payload); + + assert.deepStrictEqual(normalize(result), { taskId: "crm-1" }); + assert.strictEqual( + axios.calls[0].config.url, + "/api/endpoint/createcrmtask_api" + ); + assert.strictEqual(axios.calls[0].config.method, "post"); + assert.deepStrictEqual(normalize(axios.calls[0].config.data), payload); +}); + +test("integration/createCRMTask logs and returns undefined on failure", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + axios.requestHandler = async () => + Promise.reject(createAxiosError(400, "Bad Request")); + + const integration = loadServiceModule("integrationDirectService.js", { + axios, + consoleLogger: logger.consoleLogger + }); + + const result = await integration.createCRMTask({ subject: "x" }); + + assert.strictEqual(result, undefined); + assert.strictEqual(logger.calls.length, 1); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 7 behavioural tests passed (${passed}/${tests.length}).` + ); +}; + +run().catch((error) => { + console.error(error); + process.exit(1); +});