diff --git a/pages/myportal/viewall.js b/pages/myportal/viewall.js index 46aa4cbc..c6a4e656 100644 --- a/pages/myportal/viewall.js +++ b/pages/myportal/viewall.js @@ -14,10 +14,9 @@ import ServiceBanner from "../../components/myportal/servicebanner"; import { getSession } from "next-auth/react"; import pLimit from "p-limit"; -import { - getPersonalAccount, - getPortalLogin -} from "../../actions/services/accountService"; +import { getPortalLogin } from "../../actions/services/accountService"; +import { getCrmPersonalAccount } from "../../lib/server/getCrmPersonalAccount"; +import { consoleLogger } from "../../actions/core/logger"; import { getPortalModuleDetails } from "../../actions/services/caseService"; @@ -189,7 +188,25 @@ export const getServerSideProps = wrapper.getServerSideProps( }; } - const accountDetails = await getPersonalAccount(loggedInUser); + let accountDetails; + + try { + accountDetails = await getCrmPersonalAccount(loggedInUser); + } catch (error) { + consoleLogger({ + name: "MyPortalViewAllAccountLookup", + reasonCode: "UPSTREAM_FAILURE", + message: "myportal viewall personal account lookup failed", + error: error?.message + }); + + return { + redirect: { + destination: "/myportal/error", + permanent: false + } + }; + } if (!accountDetails || accountDetails.errorCode) { return { diff --git a/tests/phase22/myportal-viewall-loader-guards.test.cjs b/tests/phase22/myportal-viewall-loader-guards.test.cjs new file mode 100644 index 00000000..20b16af6 --- /dev/null +++ b/tests/phase22/myportal-viewall-loader-guards.test.cjs @@ -0,0 +1,314 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +const loadViewAllLoader = (overrides = {}) => { + const filePath = path.join( + __dirname, + "..", + "..", + "pages", + "myportal", + "viewall.js" + ); + + let source = fs.readFileSync(filePath, "utf8"); + + // Execute the actual SSR loader, excluding the React component. + const start = source.indexOf("export const getServerSideProps"); + const end = source.indexOf("const mapStateToProps", start); + + assert.ok(start >= 0, "SSR loader declaration not found"); + assert.ok(end > start, "SSR loader boundary not found"); + + source = source.slice(start, end); + source = source.replace( + /export\s+const\s+getServerSideProps\s*=/, + "const getServerSideProps =" + ); + source += "\nmodule.exports = { getServerSideProps };\n"; + + const dispatches = []; + const store = { + dispatch: (action) => dispatches.push(action) + }; + + const action = (type) => (value) => ({ type, value }); + + const context = { + module: { exports: {} }, + exports: {}, + console: { + log: () => {}, + error: () => {} + }, + wrapper: { + getServerSideProps: (factory) => async (ctx) => factory(store)(ctx) + }, + getSession: async () => ({ + user: { + id: "session-user-1", + email: "user@example.com" + } + }), + getPortalLogin: async () => ({ + value: [{ contactid: "crm-contact-1" }] + }), + getCrmPersonalAccount: async () => ({ + firstname: "Test", + pinswg_typeofinvolvement: 0 + }), + consoleLogger: () => {}, + pLimit: () => (fn) => fn(), + getMyCases: async () => ({ value: [] }), + getMyLPACases: async () => ({ value: [] }), + getWatchedCases: async () => ({ value: [] }), + getRepsFromBlob: async () => ({ value: [] }), + getAwaitingSubmissionFromBlob: async () => ({ value: [] }), + splitWatchedCasesBySubmissionState: () => ({ + watchedCases: [], + submittedRepresentations: [] + }), + getPortalModuleDetails: async () => ({}), + getFormCollectionByID: () => ({ + LogicalCollectionName: "pinswg_cases" + }), + getBasicSearch: async () => ({ value: [] }), + setAccountDetails: action("accountDetails"), + setLoggedInUserId: action("loggedInUserId"), + setContainerID: action("containerID"), + setCurrentView: action("currentView"), + setMyCases: action("myCases"), + setMyCasesDetails: action("myCasesDetails"), + setWatchedCases: action("watchedCases"), + setWatchedCasesDetails: action("watchedCasesDetails"), + setMySubmittedReps: action("mySubmittedReps"), + setMySubmittedRepsDetails: action("mySubmittedRepsDetails"), + setMyRepresentations: action("myRepresentations"), + setMyRepresentationsDetails: action("myRepresentationsDetails"), + setAwaitingSubmissionFromBlob: action("awaitingSubmission"), + setAwaitingSubmissionDetails: action("awaitingSubmissionDetails"), + setSearchResults: action("searchResults"), + setSearch: action("search"), + ...overrides + }; + + vm.runInNewContext(source, context, { filename: filePath }); + + return { + getServerSideProps: context.module.exports.getServerSideProps, + dispatches + }; +}; + +const createContext = (key = "myCases") => ({ + query: { key }, + req: { cookies: {} }, + locale: "en" +}); + +const getDispatch = (loader, type) => + loader.dispatches.find((item) => item.type === type); + +test("invalid view category redirects before account lookup", async () => { + let accountLookupCalled = false; + + const loader = loadViewAllLoader({ + getCrmPersonalAccount: async () => { + accountLookupCalled = true; + return {}; + } + }); + + const result = await loader.getServerSideProps(createContext("invalid")); + + assert.strictEqual(result.redirect.destination, "/myportal"); + assert.strictEqual(accountLookupCalled, false); +}); + +test("missing session redirects to signin", async () => { + const loader = loadViewAllLoader({ + getSession: async () => null + }); + + const result = await loader.getServerSideProps(createContext()); + + assert.strictEqual(result.redirect.destination, "/auth/signin"); +}); + +test("missing session email redirects to signin", async () => { + const loader = loadViewAllLoader({ + getSession: async () => ({ + user: { id: "session-user-1" } + }) + }); + + const result = await loader.getServerSideProps(createContext()); + + assert.strictEqual(result.redirect.destination, "/auth/signin"); +}); + +test("missing CRM contact redirects to signin", async () => { + const loader = loadViewAllLoader({ + getPortalLogin: async () => ({ value: [] }) + }); + + const result = await loader.getServerSideProps(createContext()); + + assert.strictEqual(result.redirect.destination, "/auth/signin"); +}); + +test("personal account exception redirects to error page", async () => { + const loader = loadViewAllLoader({ + getCrmPersonalAccount: async () => { + throw new Error("CRM unavailable"); + } + }); + + const result = await loader.getServerSideProps(createContext()); + + assert.strictEqual(result.redirect.destination, "/myportal/error"); +}); + +test("missing personal account redirects to error page", async () => { + const loader = loadViewAllLoader({ + getCrmPersonalAccount: async () => null + }); + + const result = await loader.getServerSideProps(createContext()); + + assert.strictEqual(result.redirect.destination, "/myportal/error"); +}); + +test("personal account error response redirects to error page", async () => { + const loader = loadViewAllLoader({ + getCrmPersonalAccount: async () => ({ + errorCode: "CRM_CONTACT_MISSING" + }) + }); + + const result = await loader.getServerSideProps(createContext()); + + assert.strictEqual(result.redirect.destination, "/myportal/error"); +}); + +test("successful lookup uses session-resolved CRM contact", async () => { + let capturedEmail; + let capturedContactId; + + const accountDetails = { + firstname: "Test", + pinswg_typeofinvolvement: 0 + }; + + const loader = loadViewAllLoader({ + getPortalLogin: async (email) => { + capturedEmail = email; + return { + value: [{ contactid: "crm-contact-123" }] + }; + }, + getCrmPersonalAccount: async (contactId) => { + capturedContactId = contactId; + return accountDetails; + } + }); + + const result = await loader.getServerSideProps(createContext()); + + assert.strictEqual(capturedEmail, "user@example.com"); + assert.strictEqual(capturedContactId, "crm-contact-123"); + assert.ok(result.props); + + assert.strictEqual( + getDispatch(loader, "accountDetails").value.firstname, + "Test" + ); + assert.strictEqual( + getDispatch(loader, "loggedInUserId").value, + "crm-contact-123" + ); + assert.strictEqual( + getDispatch(loader, "containerID").value, + "session-user-1" + ); + assert.strictEqual( + getDispatch(loader, "currentView").value.viewKey, + "myCases" + ); +}); + +test("all supported view categories load successfully", async () => { + const categories = [ + "myCases", + "watchedCases", + "mySubmittedReps", + "myRepresentations", + "awaitingSubmissionDetails" + ]; + + for (const category of categories) { + const loader = loadViewAllLoader(); + + const result = await loader.getServerSideProps(createContext(category)); + + assert.ok(result.props, `${category} should return props`); + + assert.strictEqual( + getDispatch(loader, "currentView").value.viewKey, + category + ); + } +}); + +test("LPA account uses associated LPA for case lookup", async () => { + let capturedLpaId; + let personalCaseLookupCalled = false; + + const loader = loadViewAllLoader({ + getCrmPersonalAccount: async () => ({ + pinswg_typeofinvolvement: 846040012, + "pinswg_contact_associatedlpa@OData.Community.Display.V1.FormattedValue": + "lpa-123" + }), + getMyLPACases: async (lpaId) => { + capturedLpaId = lpaId; + return { value: [] }; + }, + getMyCases: async () => { + personalCaseLookupCalled = true; + return { value: [] }; + } + }); + + await loader.getServerSideProps(createContext()); + + assert.strictEqual(capturedLpaId, "lpa-123"); + assert.strictEqual(personalCaseLookupCalled, false); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 22 myportal-viewall-loader-guards tests passed (${passed}/${tests.length}).` + ); +}; + +module.exports = run; + +if (require.main === module) { + run().catch((error) => { + console.error(error); + process.exit(1); + }); +}