diff --git a/actions/clients/fileClient.js b/actions/clients/fileClient.js index c58c9748..856c7f04 100644 --- a/actions/clients/fileClient.js +++ b/actions/clients/fileClient.js @@ -1,8 +1,8 @@ import { getJson, requestJson } from "./endpointClient"; import { getSignedJson, postSignedJson } from "./signedRequestClient"; -export const getFileJson = (url) => { - return getJson(url); +export const getFileJson = (url, config) => { + return getJson(url, config); }; export const getSignedFileJson = async (queryUrl) => { diff --git a/actions/services/documentDirectService.js b/actions/services/documentDirectService.js index 436ee123..77ae6479 100644 --- a/actions/services/documentDirectService.js +++ b/actions/services/documentDirectService.js @@ -28,13 +28,48 @@ export const getAwaitingSubmissionFromBlob = (containerName) => { }); }; -export const getRepsFromBlob = (containerName) => { +export const getRepsFromBlob = (containerName, config) => { const route = buildFileQuery("/api/file/getrepsblob", { container: containerName }); + const url = withBaseUrl(BASE_URL, appendHashSuffix(route, hashAPIPath)); + + const hasForwardedCookie = + typeof config?.headers?.Cookie === "string" && + config.headers.Cookie.length > 0; + + if (hasForwardedCookie) { + let trustedDestination = false; + + try { + const destination = new URL(url); + const expectedOrigin = new URL(process.env.NEXTAUTH_URL); + + trustedDestination = + destination.origin === expectedOrigin.origin && + ["http:", "https:"].includes(destination.protocol) && + !destination.username && + !destination.password; + } catch { + trustedDestination = false; + } + + if (!trustedDestination) { + return Promise.reject( + new Error("Untrusted representation request destination") + ); + } + } + return getFileJson( - withBaseUrl(BASE_URL, appendHashSuffix(route, hashAPIPath)) + url, + hasForwardedCookie + ? { + ...config, + maxRedirects: 0 + } + : config ).catch((error) => { consoleLogger(error); }); diff --git a/lib/representation/pageLoaders.js b/lib/representation/pageLoaders.js index 60b5b4f4..853e568a 100644 --- a/lib/representation/pageLoaders.js +++ b/lib/representation/pageLoaders.js @@ -120,7 +120,9 @@ export const loadRepresentationBootstrap = async ({ ctx }) => { const [accountDetails, myRepresentations] = await Promise.all([ getPersonalAccount(loggedInUser), - getRepsFromBlob(thisSession.user.id) + getRepsFromBlob(thisSession.user.id, { + headers: { Cookie: ctx.req?.headers?.cookie || "" } + }) ]); const myRepresentationsDetails = await getDetails( diff --git a/pages/api/admin/getrepsblobproxy.js b/pages/api/admin/getrepsblobproxy.js index 129671d7..a8b1ba65 100644 --- a/pages/api/admin/getrepsblobproxy.js +++ b/pages/api/admin/getrepsblobproxy.js @@ -1,9 +1,11 @@ +import { BASE_URL } from "../../../actions/core/env"; import { getToken } from "../../../actions/core/token"; import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; import { hashAPIPath } from "../../../actions/core/hash"; import axios from "axios"; import { + ADMIN_SESSION_COOKIE, hasTrustedSameOrigin, readAdminSessionCookie, setAdminAuthNoStore, @@ -11,7 +13,16 @@ import { } from "../../../lib/admin/adminAuth"; import { respondError, respondSuccess } from "../middleware/apiResponse"; -const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`; +const isTrustedInternalDestination = () => { + try { + return ( + new URL(BASE_URL).origin === + new URL(process.env.NEXTAUTH_URL).origin + ); + } catch { + return false; + } +}; const forbidden = (res) => respondError(res, { @@ -39,10 +50,7 @@ export default async function handler(req, res) { } const adminSession = readAdminSessionCookie(req); - if ( - !validateAdminSession(adminSession) || - !hasTrustedSameOrigin(req) - ) { + if (!validateAdminSession(adminSession) || !hasTrustedSameOrigin(req)) { return forbidden(res); } @@ -55,14 +63,27 @@ export default async function handler(req, res) { }); } + if (!isTrustedInternalDestination()) { + return forbidden(res); + } + const token = await getToken(); const queryUrl = "/api/file/getrepsblob?container=" + encodeURIComponent(container); try { + const config = azureHeaders(token.access_token); + const { data } = await axios.get( BASE_URL + queryUrl + hashAPIPath(queryUrl), - azureHeaders(token.access_token) + { + ...config, + headers: { + ...config.headers, + Cookie: `${ADMIN_SESSION_COOKIE}=${encodeURIComponent(adminSession)}` + }, + maxRedirects: 0 + } ); return respondSuccess(res, data); } catch (error) { @@ -73,4 +94,4 @@ export default async function handler(req, res) { message: "Failed to fetch representation blobs" }); } -} \ No newline at end of file +} diff --git a/pages/api/file/getrepsblob.js b/pages/api/file/getrepsblob.js index 45dff296..ef58e15e 100644 --- a/pages/api/file/getrepsblob.js +++ b/pages/api/file/getrepsblob.js @@ -38,10 +38,23 @@ import middleware from "../middleware/middleware"; import { hashAPIPath } from "../../../actions/core/hash"; import { consoleLogger } from "../../../actions/core/logger"; import { respondError, respondSuccess } from "../middleware/apiResponse"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "../auth/[...nextauth]"; +import { + readAdminSessionCookie, + validateAdminSession +} from "../../../lib/admin/adminAuth"; const ApiProxy = nextConnect(); ApiProxy.use(middleware); +const forbidden = (res) => + respondError(res, { + status: 403, + code: "FORBIDDEN", + message: "Request not permitted" + }); + ApiProxy.get(async (req, res) => { const containerName = req.query.container; const checkHash = req.query.hash; @@ -59,6 +72,17 @@ ApiProxy.get(async (req, res) => { }); } + const session = await getServerSession(req, res, authOptions(req, res)); + const isPortalOwner = + typeof session?.user?.id === "string" && + session.user.id.length > 0 && + session.user.id === containerName; + const isAdministrator = validateAdminSession(readAdminSessionCookie(req)); + + if (!isPortalOwner && !isAdministrator) { + return forbidden(res); + } + const hashCandidatePaths = [ "/api/file/getrepsblob?container=" + containerName, "/api/file/getrepsblob?container=" + encodeURIComponent(containerName) diff --git a/pages/api/file/getrepsblobproxy.js b/pages/api/file/getrepsblobproxy.js index cf279da2..126237e6 100644 --- a/pages/api/file/getrepsblobproxy.js +++ b/pages/api/file/getrepsblobproxy.js @@ -1,3 +1,4 @@ +import { BASE_URL } from "../../../actions/core/env"; import { getToken } from "../../../actions/core/token"; import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; @@ -7,7 +8,16 @@ import { respondError, respondSuccess } from "../middleware/apiResponse"; import { getServerSession } from "next-auth/next"; import { authOptions } from "../auth/[...nextauth]"; -const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`; +const isTrustedInternalDestination = () => { + try { + return ( + new URL(BASE_URL).origin === + new URL(process.env.NEXTAUTH_URL).origin + ); + } catch { + return false; + } +}; const forbidden = (res) => respondError(res, { @@ -60,15 +70,28 @@ export default async function ApiProxy(req, res) { return forbidden(res); } + if (!isTrustedInternalDestination()) { + return forbidden(res); + } + const token = await getToken(); const queryUrl = "/api/file/getrepsblob?container=" + encodeURIComponent(containerName); try { + const config = azureHeaders(token.access_token); + const { data } = await axios.get( BASE_URL + queryUrl + hashAPIPath(queryUrl), - azureHeaders(token.access_token) + { + ...config, + headers: { + ...config.headers, + Cookie: req.headers.cookie || "" + }, + maxRedirects: 0 + } ); return respondSuccess(res, data); } catch (error) { diff --git a/pages/myportal/case/[ticketnumber].js b/pages/myportal/case/[ticketnumber].js index 04859115..9a28f691 100644 --- a/pages/myportal/case/[ticketnumber].js +++ b/pages/myportal/case/[ticketnumber].js @@ -304,7 +304,8 @@ export const getServerSideProps = wrapper.getServerSideProps( break; case "myRepresentations": const myRepresentations = await getRepsFromBlob( - thisSession.user.id + thisSession.user.id, + { headers: { Cookie: req.headers.cookie || "" } } ); const myRepresentationsDetails = await getDetails( diff --git a/pages/myportal/index.js b/pages/myportal/index.js index dc04b2ba..4c0195e0 100644 --- a/pages/myportal/index.js +++ b/pages/myportal/index.js @@ -293,7 +293,9 @@ export const getServerSideProps = wrapper.getServerSideProps( awaitingSubmissionFromBlob ] = await Promise.all([ isLPA ? getMyLPACases(lpaId) : getMyCases(loggedInUser), - getRepsFromBlob(thisSession.user.id), + getRepsFromBlob(thisSession.user.id, { + headers: { Cookie: req.headers.cookie || "" } + }), getWatchedCases(loggedInUser), getAwaitingSubmissionFromBlob(thisSession.user.id) ]); diff --git a/pages/myportal/viewall.js b/pages/myportal/viewall.js index c6a4e656..4db131b6 100644 --- a/pages/myportal/viewall.js +++ b/pages/myportal/viewall.js @@ -371,7 +371,8 @@ export const getServerSideProps = wrapper.getServerSideProps( case "myRepresentations": { const myRepresentations = await getRepsFromBlob( - thisSession.user.id + thisSession.user.id, + { headers: { Cookie: ctx.req?.headers?.cookie || "" } } ); // The existing View All component reads diff --git a/tests/phase21/_shared.cjs b/tests/phase21/_shared.cjs index 768aea02..2903a0a2 100644 --- a/tests/phase21/_shared.cjs +++ b/tests/phase21/_shared.cjs @@ -50,6 +50,7 @@ const loadModule = (relativePath, injected = {}) => { exports: {}, require, process, + URL, console: { log: () => {}, info: () => {}, diff --git a/tests/phase21/file-handler-contract.test.cjs b/tests/phase21/file-handler-contract.test.cjs index 3ecf8f68..53fcc5cc 100644 --- a/tests/phase21/file-handler-contract.test.cjs +++ b/tests/phase21/file-handler-contract.test.cjs @@ -1554,6 +1554,10 @@ test("getrepsblob handler success returns representations payload", async () => respondError: respondErrorMock, respondSuccess: respondSuccessMock, consoleLogger: () => {}, + getServerSession: async () => ({ user: { id: "c1" } }), + authOptions: () => ({}), + readAdminSessionCookie: () => null, + validateAdminSession: () => false, getRepsBlobs: async () => [ { path: "c/ref_rep.json", name: "ref_rep.json" } ], @@ -1612,6 +1616,10 @@ test("getrepsblob handler rejects invalid HMAC before Azure access", async () => respondError: respondErrorMock, respondSuccess: respondSuccessMock, consoleLogger: () => {}, + getServerSession: async () => ({ user: { id: "c1" } }), + authOptions: () => ({}), + readAdminSessionCookie: () => null, + validateAdminSession: () => false, getRepsBlobs: async () => { azureCalls += 1; return []; @@ -1817,6 +1825,10 @@ test("getrepsblob handler dependency failure returns GET_REPS_BLOB_FAILED", asyn respondError: respondErrorMock, respondSuccess: respondSuccessMock, consoleLogger: () => {}, + getServerSession: async () => ({ user: { id: "c1" } }), + authOptions: () => ({}), + readAdminSessionCookie: () => null, + validateAdminSession: () => false, getRepsBlobs: async () => { throw new Error("storage failure"); }, @@ -1844,6 +1856,10 @@ test("getrepsblob handler accepts encoded container hash variant", async () => { respondError: respondErrorMock, respondSuccess: respondSuccessMock, consoleLogger: () => {}, + getServerSession: async () => ({ user: { id: "cont/1" } }), + authOptions: () => ({}), + readAdminSessionCookie: () => null, + validateAdminSession: () => false, getRepsBlobs: async () => [{ path: "cont/rep1.json" }], downloadAllRepsFiles: async () => ({ value: [{ id: "r-encoded" }] }) }); @@ -2869,7 +2885,13 @@ const loadOrdinaryRepsProxy = (overrides = {}) => { return "&hash=abc"; }, consoleLogger: () => {}, - process: { env: { API_ROOT: "http://example.com" } }, + BASE_URL: "http://example.com", + process: { + env: { + API_ROOT: "http://example.com", + NEXTAUTH_URL: "http://example.com" + } + }, axios: { get: async (...args) => { calls.axios.push(args); @@ -2979,7 +3001,9 @@ for (const containerCase of [ test("getrepsblobproxy preserves signed path and response for owning user", async () => { const loaded = loadOrdinaryRepsProxy(); - const req = ordinaryRepsRequest(); + const req = ordinaryRepsRequest({ + headers: { cookie: "next-auth.session-token=test-session" } + }); const res = createRes(); await loaded.mod.default(req, res); @@ -2995,6 +3019,16 @@ test("getrepsblobproxy preserves signed path and response for owning user", asyn loaded.calls.axios[0][0], "http://example.com/api/file/getrepsblob?container=user-container&hash=abc" ); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(loaded.calls.axios[0][1])), + { + headers: { + Authorization: "Bearer tok", + Cookie: "next-auth.session-token=test-session" + }, + maxRedirects: 0 + } + ); assert.deepStrictEqual( JSON.parse(JSON.stringify(res.state.jsonBody)), loaded.payload @@ -3030,6 +3064,7 @@ const loadAdminRepsProxy = (overrides = {}) => { const mod = loadModule("pages/api/admin/getrepsblobproxy.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + ADMIN_SESSION_COOKIE: "pedw_admin_session", readAdminSessionCookie: () => "admin-token", validateAdminSession: () => true, hasTrustedSameOrigin: () => true, @@ -3044,7 +3079,13 @@ const loadAdminRepsProxy = (overrides = {}) => { return "&hash=abc"; }, consoleLogger: () => {}, - process: { env: { API_ROOT: "http://example.com" } }, + BASE_URL: "http://example.com", + process: { + env: { + API_ROOT: "http://example.com", + NEXTAUTH_URL: "http://example.com" + } + }, axios: { get: async (...args) => { calls.axios.push(args); @@ -3171,6 +3212,16 @@ test("admin getrepsblobproxy preserves cross-container signed path and response" loaded.calls.axios[0][0], "http://example.com/api/file/getrepsblob?container=selected-user-container&hash=abc" ); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(loaded.calls.axios[0][1])), + { + headers: { + Authorization: "Bearer tok", + Cookie: "pedw_admin_session=admin-token" + }, + maxRedirects: 0 + } + ); assert.deepStrictEqual( JSON.parse(JSON.stringify(res.state.jsonBody)), loaded.payload @@ -3202,6 +3253,19 @@ test("admin storage refresh uses dedicated POST proxy while ordinary callers rem assert.match(viewAllSource, /getRepsFromBlobProxy\(containerID\)/); }); +test("admin getrepsblobproxy rejects untrusted internal destination before proxy work", async () => { + const loaded = loadAdminRepsProxy({ + BASE_URL: "https://attacker.example" + }); + const res = createRes(); + + await loaded.mod.default(adminRepsRequest(), res); + + assert.strictEqual(res.state.statusCode, 403); + assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); + assertNoProxyWork(loaded.calls); +}); + test("admin storage completion uses dedicated POST route while ordinary completion remains unchanged", () => { const rootDir = path.resolve(__dirname, "..", ".."); const adminSource = fs.readFileSync( diff --git a/tests/phase22/api-grouping-facade-myportal-reads.test.cjs b/tests/phase22/api-grouping-facade-myportal-reads.test.cjs index a9464293..0f334d5b 100644 --- a/tests/phase22/api-grouping-facade-myportal-reads.test.cjs +++ b/tests/phase22/api-grouping-facade-myportal-reads.test.cjs @@ -129,7 +129,7 @@ test("excluded route families remain outside this myportal slice", async () => { const pageSource = read("pages/myportal/index.js"); assert.match(portalSource, /\/api\/subscriptions\/get-watched-cases/); - assert.match(pageSource, /getRepsFromBlob\(thisSession.user.id\)/); + assert.match(pageSource, /getRepsFromBlob\(thisSession.user.id,\s*\{/); assert.match( pageSource, /getAwaitingSubmissionFromBlob\(thisSession.user.id\)/ diff --git a/tests/phase22/representation-list-ownership.test.cjs b/tests/phase22/representation-list-ownership.test.cjs new file mode 100644 index 00000000..82eb7ce4 --- /dev/null +++ b/tests/phase22/representation-list-ownership.test.cjs @@ -0,0 +1,157 @@ +const assert = require("assert"); +const { + loadModule, + createRes, + createNextConnectMock, + respondSuccessMock, + respondErrorMock +} = require("../phase21/_shared.cjs"); + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +const loadRepresentationList = ({ session = null, administrator = false } = {}) => { + const calls = { list: [], download: [] }; + const mod = loadModule("pages/api/file/getrepsblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + getServerSession: async () => session, + authOptions: () => ({}), + readAdminSessionCookie: () => + administrator ? "valid-admin-session" : null, + validateAdminSession: () => administrator, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + consoleLogger: () => {}, + getRepsBlobs: async (...args) => { + calls.list.push(args); + return [{ path: "CASE/REP_rep.json" }]; + }, + downloadAllRepsFiles: async (...args) => { + calls.download.push(args); + return { value: [{ id: "rep-1" }] }; + } + }); + + return { mod, calls }; +}; + +const representationListRequest = (query = {}) => ({ + query: { + container: "organisation-container", + hash: "expected", + ...query + }, + headers: {} +}); + +test("representation listing rejects a missing session before Azure access", async () => { + const loaded = loadRepresentationList(); + const res = createRes(); + + await loaded.mod.default.handler(representationListRequest(), res); + + assert.strictEqual(res.state.statusCode, 403); + assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); + assert.strictEqual(loaded.calls.list.length, 0); + assert.strictEqual(loaded.calls.download.length, 0); +}); + +test("representation listing rejects cross-user container access before Azure access", async () => { + const loaded = loadRepresentationList({ + session: { user: { id: "different-container" } } + }); + const res = createRes(); + + await loaded.mod.default.handler(representationListRequest(), res); + + assert.strictEqual(res.state.statusCode, 403); + assert.strictEqual(loaded.calls.list.length, 0); + assert.strictEqual(loaded.calls.download.length, 0); +}); + +test("representation listing preserves authorised organisation-container access", async () => { + const loaded = loadRepresentationList({ + session: { user: { id: "organisation-container" } } + }); + const res = createRes(); + + await loaded.mod.default.handler(representationListRequest(), res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(loaded.calls.list, [["organisation-container"]]); + assert.strictEqual(loaded.calls.download.length, 1); +}); + +test("representation listing preserves existing administrative access", async () => { + const loaded = loadRepresentationList({ administrator: true }); + const res = createRes(); + + await loaded.mod.default.handler(representationListRequest(), res); + + assert.strictEqual(res.state.statusCode, 200); + assert.strictEqual(loaded.calls.list.length, 1); +}); + +test("representation listing rejects a missing identifier before authentication or Azure access", async () => { + let sessionCalls = 0; + const loaded = loadRepresentationList({ + session: { user: { id: "organisation-container" } } + }); + loaded.mod = loadModule("pages/api/file/getrepsblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + getServerSession: async () => { + sessionCalls += 1; + return { user: { id: "organisation-container" } }; + }, + authOptions: () => ({}), + readAdminSessionCookie: () => null, + validateAdminSession: () => false, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + consoleLogger: () => {}, + getRepsBlobs: async () => { + loaded.calls.list.push([]); + return []; + }, + downloadAllRepsFiles: async () => { + loaded.calls.download.push([]); + return {}; + } + }); + const res = createRes(); + + await loaded.mod.default.handler( + representationListRequest({ container: "" }), + res + ); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "MISSING_REQUIRED_QUERY"); + assert.strictEqual(sessionCalls, 0); + assert.strictEqual(loaded.calls.list.length, 0); + assert.strictEqual(loaded.calls.download.length, 0); +}); + +const run = async () => { + let passed = 0; + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + console.log( + `Representation listing ownership tests passed (${passed}/${tests.length}).` + ); +}; + +module.exports = run; + +if (require.main === module) { + run().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/tests/phase7/service-behaviour.test.cjs b/tests/phase7/service-behaviour.test.cjs index 05764a66..e53a3071 100644 --- a/tests/phase7/service-behaviour.test.cjs +++ b/tests/phase7/service-behaviour.test.cjs @@ -65,6 +65,93 @@ test("document/getAwaitingSubmissionFromBlob logs and returns undefined on failu assert.strictEqual(logger.calls.length, 1); }); +test("document/getRepsFromBlob forwards cookies only to trusted origin without redirects", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + axios.getHandler = async () => ({ + data: { value: ["representation"] } + }); + + const document = loadServiceModule("documentDirectService.js", { + axios, + BASE_URL: "http://example.local", + consoleLogger: logger.consoleLogger, + hashAPIPath: () => "&hash=abc123" + }); + + const result = await document.getRepsFromBlob("container-1", { + headers: { + Cookie: "next-auth.session-token=test-session" + } + }); + + assert.deepStrictEqual(normalize(result), { + value: ["representation"] + }); + + assert.strictEqual(axios.calls.length, 1); + assert.strictEqual( + axios.calls[0].url, + "http://example.local/api/file/getrepsblob?container=container-1&hash=abc123" + ); + assert.strictEqual( + axios.calls[0].config.headers.Cookie, + "next-auth.session-token=test-session" + ); + assert.strictEqual(axios.calls[0].config.maxRedirects, 0); + assert.strictEqual(logger.calls.length, 0); +}); + +test("document/getRepsFromBlob rejects untrusted origin before HTTP request", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + const document = loadServiceModule("documentDirectService.js", { + axios, + BASE_URL: "https://untrusted.example", + consoleLogger: logger.consoleLogger, + hashAPIPath: () => "&hash=abc123" + }); + + await assert.rejects( + document.getRepsFromBlob("container-1", { + headers: { + Cookie: "next-auth.session-token=test-session" + } + }), + /Untrusted representation request destination/ + ); + + assert.strictEqual(axios.calls.length, 0); + assert.strictEqual(logger.calls.length, 0); +}); + +test("document/getRepsFromBlob preserves requests without forwarded cookies", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + axios.getHandler = async () => ({ + data: { value: ["representation"] } + }); + + const document = loadServiceModule("documentDirectService.js", { + axios, + BASE_URL: "http://example.local", + consoleLogger: logger.consoleLogger, + hashAPIPath: () => "&hash=abc123" + }); + + const result = await document.getRepsFromBlob("container-1"); + + assert.deepStrictEqual(normalize(result), { + value: ["representation"] + }); + assert.strictEqual(axios.calls.length, 1); + assert.strictEqual(axios.calls[0].config, undefined); + assert.strictEqual(logger.calls.length, 0); +}); + test("portal/deleteWatchedCases uses signer hash and returns data", async () => { const axios = createAxiosMock(); const logger = createLoggerMock(); diff --git a/tests/serviceHarness.cjs b/tests/serviceHarness.cjs index aea3882b..a489b5ce 100644 --- a/tests/serviceHarness.cjs +++ b/tests/serviceHarness.cjs @@ -89,8 +89,8 @@ const loadServiceModule = (fileName, injected = {}) => { return injected.axios(config).then((response) => response.data); }; - const defaultGetFileJson = (url) => { - return defaultGetJson(url); + const defaultGetFileJson = (url, config) => { + return defaultGetJson(url, config); }; const defaultBuildHashedQueryUrl = async (queryUrl) => { @@ -207,8 +207,14 @@ const loadServiceModule = (fileName, injected = {}) => { module: { exports: {} }, exports: {}, require, + URL, URLSearchParams, encodeURIComponent, + process: injected.process || { + env: { + NEXTAUTH_URL: "http://example.local" + } + }, FormData: global.FormData, console: { log: () => {},