diff --git a/actions/clients/fileClient.js b/actions/clients/fileClient.js index 595417b6..e639bb30 100644 --- a/actions/clients/fileClient.js +++ b/actions/clients/fileClient.js @@ -21,3 +21,14 @@ export const downloadFileBlob = (url) => { responseType: "blob" }); }; + +export const postSignedFileJson = async (queryUrl, data, config = {}) => { + const signedUrl = await buildHashedQueryUrl(queryUrl); + + return requestJson({ + method: "post", + url: signedUrl, + data, + ...config + }); +}; diff --git a/actions/services/documentDirectService.js b/actions/services/documentDirectService.js index f239ef41..e86819eb 100644 --- a/actions/services/documentDirectService.js +++ b/actions/services/documentDirectService.js @@ -6,7 +6,8 @@ import { getJson, requestJson } from "../clients/endpointClient"; import { getFileJson, getSignedFileJson, - downloadFileBlob + downloadFileBlob, + postSignedFileJson } from "../clients/fileClient"; export const getAwaitingSubmissionFromBlob = (containerName) => { @@ -117,17 +118,10 @@ export const uploadFiles = async ( var queryUrl = "/api/file/upload"; - const hashedUrl = await buildHashedQueryUrl(queryUrl); - - const config = { - method: "post", - url: hashedUrl, - data: formData, - headers: { "content-type": "multipart/form-data" } - }; - try { - return await requestJson(config); + return await postSignedFileJson(queryUrl, formData, { + headers: { "content-type": "multipart/form-data" } + }); } catch (error) { consoleLogger(error); } @@ -146,17 +140,10 @@ export const uploadSingleFile = async (filesObj, containerID, casefolderID) => { var queryUrl = "/api/file/uploadsinglefile"; - const hashedUrl = await buildHashedQueryUrl(queryUrl); - - const config = { - method: "post", - url: hashedUrl, - data: formData, - headers: { "content-type": "multipart/form-data" } - }; - try { - return await requestJson(config); + return await postSignedFileJson(queryUrl, formData, { + headers: { "content-type": "multipart/form-data" } + }); } catch (error) { consoleLogger(error); } @@ -176,17 +163,10 @@ export const uploadRepFiles = async ( var queryUrl = "/api/file/upload"; - const hashedUrl = await buildHashedQueryUrl(queryUrl); - - const config = { - method: "post", - url: hashedUrl, - data: formData, - headers: { "content-type": "multipart/form-data" } - }; - try { - return await requestJson(config); + return await postSignedFileJson(queryUrl, formData, { + headers: { "content-type": "multipart/form-data" } + }); } catch (error) { consoleLogger(error); } @@ -201,17 +181,10 @@ export const generateRepPDF = async ( var queryUrl = "/api/file/generatepdf" + (options.download ? "?download=true" : ""); - const hashedUrl = await buildHashedQueryUrl(queryUrl); - - const config = { - method: "post", - url: hashedUrl, - data: formValues, - ...(options.download ? { responseType: "blob" } : {}) - }; - try { - return await requestJson(config); + return await postSignedFileJson(queryUrl, formValues, { + ...(options.download ? { responseType: "blob" } : {}) + }); } catch (error) { consoleLogger(error); } @@ -234,17 +207,10 @@ export const generateAppealPDF = async ( appealType + (options.download ? "&download=true" : ""); - const hashedUrl = await buildHashedQueryUrl(queryUrl); - - const config = { - method: "post", - url: hashedUrl, - data: formValues, - ...(options.download ? { responseType: "blob" } : {}) - }; - try { - return await requestJson(config); + return await postSignedFileJson(queryUrl, formValues, { + ...(options.download ? { responseType: "blob" } : {}) + }); } catch (error) { consoleLogger(error); } diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 267cca9c..dff8f864 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -2383,3 +2383,49 @@ Validation: Follow-ups: - Optional next bounded slice: evaluate adding equivalent focused behavioural coverage in phase6 if download helper behavior becomes part of that suite’s scope. + +--- + +### CL-068: TASK22260 widened module-completion slice — documentDirectService signed POST flows via fileClient + +date: 2026-03-25 +author: Cline +scope: `actions/clients/fileClient.js`, `actions/services/documentDirectService.js`, `tests/phase22/file-client-behaviour.test.cjs` +type: change +rationale: Per user request to widen slices, complete a larger coherent module-level increment by moving the remaining signed POST file-route flows in document service onto `fileClient`. +impact: Further reduces request/signing boilerplate in document service and centralizes signed file-route behavior in client wrapper layer with added regression coverage. +status: completed + +Summary: + +- Extended `actions/clients/fileClient.js` with: + - `postSignedFileJson(queryUrl, data, config = {})` + - signs query URL via `buildHashedQueryUrl` and executes POST via `requestJson` +- Migrated all remaining signed POST helper paths in `actions/services/documentDirectService.js` to `postSignedFileJson`: + - `uploadFiles` + - `uploadSingleFile` + - `uploadRepFiles` + - `generateRepPDF` + - `generateAppealPDF` +- Preserved existing catch-path logging and request-option semantics: + - multipart headers for upload flows + - conditional `responseType: "blob"` for download variants +- Expanded phase22 file-client behavior suite with explicit POST-signed contract test: + - URL signing + POST method + - data passthrough + - config/header passthrough + +Validation: + +- `node tests/phase22/file-client-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - client-utils: 5/5 + - file-client: 4/4 + - phase22 combined: pass +- `node tests/phase7/service-behaviour.test.cjs` -> pass (13/13) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next widened slice: evaluate consolidating remaining direct `getJson` file-read flows in `documentDirectService` behind `fileClient` for full per-module client symmetry. diff --git a/tests/phase22/file-client-behaviour.test.cjs b/tests/phase22/file-client-behaviour.test.cjs index 31d8920f..767aad88 100644 --- a/tests/phase22/file-client-behaviour.test.cjs +++ b/tests/phase22/file-client-behaviour.test.cjs @@ -12,7 +12,7 @@ const loadFileClientModule = (injected = {}) => { source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); source = source.replace(/export const\s+/g, "const "); source += - "\nmodule.exports = { getFileJson, getSignedFileJson, downloadFileBlob };\n"; + "\nmodule.exports = { getFileJson, getSignedFileJson, downloadFileBlob, postSignedFileJson };\n"; const context = { module: { exports: {} }, @@ -104,6 +104,50 @@ test("clients/fileClient downloadFileBlob requests blob response", async () => { assert.strictEqual(requestCalls[0].url, "/api/file/downloadblob?x=1"); }); +test("clients/fileClient postSignedFileJson signs url and posts payload", async () => { + const signedCalls = []; + const requestCalls = []; + + const mod = loadFileClientModule({ + buildHashedQueryUrl: async (queryUrl) => { + signedCalls.push(queryUrl); + return queryUrl + "&hash=signed-post"; + }, + requestJson: async (config) => { + requestCalls.push(config); + return { uploaded: true }; + } + }); + + const payload = { name: "doc" }; + const result = await mod.postSignedFileJson( + "/api/file/uploadsinglefile", + payload, + { + headers: { "content-type": "multipart/form-data" } + } + ); + + assert.deepStrictEqual(JSON.parse(JSON.stringify(result)), { + uploaded: true + }); + assert.strictEqual(signedCalls.length, 1); + assert.strictEqual(requestCalls.length, 1); + assert.strictEqual(requestCalls[0].method, "post"); + assert.strictEqual( + requestCalls[0].url, + "/api/file/uploadsinglefile&hash=signed-post" + ); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(requestCalls[0].data)), + payload + ); + assert.strictEqual( + requestCalls[0].headers["content-type"], + "multipart/form-data" + ); +}); + const run = async () => { let passed = 0;