From c5b82a66bdeed3fe7b9e138f0a29ef139e8205aa Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Sun, 12 Oct 2025 14:07:23 +0300 Subject: [PATCH 01/21] feat: Log initial axios requests without data added from interceptors. --- packages/axios-extension/src/index.ts | 1 + .../src/utils/axios-traffic.ts | 95 +++++++++++++++++++ packages/generator-adp/src/app/index.ts | 3 +- 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 packages/axios-extension/src/utils/axios-traffic.ts diff --git a/packages/axios-extension/src/index.ts b/packages/axios-extension/src/index.ts index 12db2e58565..64df546bcbc 100644 --- a/packages/axios-extension/src/index.ts +++ b/packages/axios-extension/src/index.ts @@ -7,5 +7,6 @@ export * from './abap'; export * from './factory'; export * from './auth'; export * from './abap/message'; +export * from './utils/axios-traffic'; export { ServiceType } from './abap/catalog/base'; export { AxiosError, AxiosRequestConfig, isAxiosError }; diff --git a/packages/axios-extension/src/utils/axios-traffic.ts b/packages/axios-extension/src/utils/axios-traffic.ts new file mode 100644 index 00000000000..917c22d683f --- /dev/null +++ b/packages/axios-extension/src/utils/axios-traffic.ts @@ -0,0 +1,95 @@ +import { ToolsLogger } from '@sap-ux/logger'; +import { Axios, AxiosRequestConfig, AxiosResponse } from 'axios'; + +// We call the actual function with once because we want sort of idempotency. +// For example when called in yo generator, the generator could be started multiple times +// by the Application Wizard while changing pages, so we want to patch the Axios.request() +// method only once not for each page visit. +export const logAxiosTraffic = once(logAxiosTrafficInternal); + +const QUERY_PARAMS_PREFIX = '?'; +const QUERY_PARAMS_SEPARATOR = '&'; +const GET_REQUEST_METHOD = 'get'; + +type URLQueryParams = string | URLSearchParams | Record | string[][]; + +function logAxiosTrafficInternal(logger: ToolsLogger): void { + const prototype = Axios.prototype; + const originalRequest = prototype.request; + + prototype.request = async function patchRequest, D = any>( + this: Axios, + config: AxiosRequestConfig + ): Promise { + const mergedConfig = { + ...this.defaults, + ...config, + headers: { + ...(this.defaults?.headers || {}), + ...(config.headers || {}) + } + }; + const url = getFullUrlString(mergedConfig.baseURL ?? '', mergedConfig.url ?? '', mergedConfig.params); + // If the developer omits the request method when dooes a call to the .request() method + // internal axios interceptor sets a default method to GET so wee need to do the same here. + const method = (mergedConfig.method ?? GET_REQUEST_METHOD).toUpperCase(); + + logger.info(`[axios][=>][${method}] ${url}`); + if (mergedConfig.headers) { + logger.info(`[axios] headers: ${mergedConfig.headers}`); + } + if (mergedConfig.data) { + logger.info(`[axios] body: ${mergedConfig.data}`); + } + + try { + const response = await originalRequest.call(this, config); + + logger.info(`[axios][<=][${response.status}] ${url}`); + if (response.headers) { + logger.info(`[axios] headers: ${response.headers}`); + } + if (response.data) { + logger.info(`[axios] body: ${response.data}`); + } + + return response; + } catch (error) { + logger.error(`[axios][error] ${url} ${error.message}`); + if (error.response) { + logger.error(`[axios] status: ${error.response.status}`); + logger.error(`[axios] headers: ${error.response.headers}`); + logger.error(`[axios] body: ${error.response.data}`); + } + throw error; + } + }; +} + +function getFullUrlString(baseURL: string, relativeUrl: string, queryParams?: URLQueryParams): string { + try { + let fullUrl = new URL(relativeUrl, baseURL).toString(); + const paramsToString = queryParams ? new URLSearchParams(queryParams).toString() : ''; + if (paramsToString) { + const paramsPrefix = fullUrl.includes(QUERY_PARAMS_PREFIX) ? QUERY_PARAMS_SEPARATOR : QUERY_PARAMS_PREFIX; + const decodedParams = decodeURIComponent(paramsToString); + fullUrl += `${paramsPrefix}${decodedParams}`; + } + + return fullUrl; + } catch { + return `${baseURL}${relativeUrl}`; + } +} + +export function once any>(fn: T): (...args: Parameters) => ReturnType | undefined { + let isCalled = false; + let result: ReturnType; + + return function (this: ThisParameterType, ...args: Parameters): ReturnType | undefined { + if (isCalled) return result; + isCalled = true; + result = fn.apply(this, args); + return result; + }; +} diff --git a/packages/generator-adp/src/app/index.ts b/packages/generator-adp/src/app/index.ts index 32d2ee48802..3d177fb733d 100644 --- a/packages/generator-adp/src/app/index.ts +++ b/packages/generator-adp/src/app/index.ts @@ -16,7 +16,7 @@ import { type ConfigAnswers, type UI5Version } from '@sap-ux/adp-tooling'; -import type { AbapServiceProvider } from '@sap-ux/axios-extension'; +import { logAxiosTraffic, type AbapServiceProvider } from '@sap-ux/axios-extension'; import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import { TelemetryHelper, @@ -133,6 +133,7 @@ export default class extends Generator { this.options = opts; this._setupLogging(); + logAxiosTraffic(this.logger); const jsonInputString = getFirstArgAsString(args); this.jsonInput = parseJsonInput(jsonInputString, this.logger); From 3c4ad9b147b5ad2d9daa6fee72527ce706a00d92 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Sun, 12 Oct 2025 14:45:28 +0300 Subject: [PATCH 02/21] fix: For requests we log initial data without such added from interceptors. For responses we log all data --- .../axios-extension/src/utils/axios-traffic.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/axios-extension/src/utils/axios-traffic.ts b/packages/axios-extension/src/utils/axios-traffic.ts index 917c22d683f..46755a6c39c 100644 --- a/packages/axios-extension/src/utils/axios-traffic.ts +++ b/packages/axios-extension/src/utils/axios-traffic.ts @@ -21,6 +21,7 @@ function logAxiosTrafficInternal(logger: ToolsLogger): void { this: Axios, config: AxiosRequestConfig ): Promise { + // Thios config does not contain headers or query params added from interceptors. const mergedConfig = { ...this.defaults, ...config, @@ -29,12 +30,12 @@ function logAxiosTrafficInternal(logger: ToolsLogger): void { ...(config.headers || {}) } }; - const url = getFullUrlString(mergedConfig.baseURL ?? '', mergedConfig.url ?? '', mergedConfig.params); + const requestUrl = getFullUrlString(mergedConfig.baseURL ?? '', mergedConfig.url ?? '', mergedConfig.params); // If the developer omits the request method when dooes a call to the .request() method // internal axios interceptor sets a default method to GET so wee need to do the same here. const method = (mergedConfig.method ?? GET_REQUEST_METHOD).toUpperCase(); - logger.info(`[axios][=>][${method}] ${url}`); + logger.info(`[axios][=>][${method}] ${requestUrl}`); if (mergedConfig.headers) { logger.info(`[axios] headers: ${mergedConfig.headers}`); } @@ -44,8 +45,15 @@ function logAxiosTrafficInternal(logger: ToolsLogger): void { try { const response = await originalRequest.call(this, config); + // This config contains all data added from interceptors. + const responseConfig = response.config ?? {}; + const responseUrl = getFullUrlString( + responseConfig.baseURL ?? '', + responseConfig.url ?? '', + responseConfig.params + ); - logger.info(`[axios][<=][${response.status}] ${url}`); + logger.info(`[axios][<=][${response.status}] ${responseUrl}`); if (response.headers) { logger.info(`[axios] headers: ${response.headers}`); } @@ -55,7 +63,7 @@ function logAxiosTrafficInternal(logger: ToolsLogger): void { return response; } catch (error) { - logger.error(`[axios][error] ${url} ${error.message}`); + logger.error(`[axios][error] ${requestUrl} ${error.message}`); if (error.response) { logger.error(`[axios] status: ${error.response.status}`); logger.error(`[axios] headers: ${error.response.headers}`); From e6f6336184bc5d32fa1e955cf0ba5bbc913856f5 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Mon, 13 Oct 2025 13:59:37 +0300 Subject: [PATCH 03/21] feat: Add functionallity to save the moab config from the generator execution. Path the muab request url to exclude the sap-client query param. --- .../src/utils/axios-traffic.ts | 88 ++++++++++++++++--- packages/generator-adp/src/app/index.ts | 6 +- 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/packages/axios-extension/src/utils/axios-traffic.ts b/packages/axios-extension/src/utils/axios-traffic.ts index 46755a6c39c..58921ccee20 100644 --- a/packages/axios-extension/src/utils/axios-traffic.ts +++ b/packages/axios-extension/src/utils/axios-traffic.ts @@ -1,5 +1,12 @@ -import { ToolsLogger } from '@sap-ux/logger'; -import { Axios, AxiosRequestConfig, AxiosResponse } from 'axios'; +import type { ToolsLogger } from '@sap-ux/logger'; +import type { AxiosRequestConfig, AxiosResponse } from 'axios'; +import { Axios } from 'axios'; +import type { WriteStream } from 'fs'; +import fs from 'fs'; +import { rename } from 'fs/promises'; +import { once } from 'lodash'; +import os from 'os'; +import path from 'path'; // We call the actual function with once because we want sort of idempotency. // For example when called in yo generator, the generator could be started multiple times @@ -10,18 +17,30 @@ export const logAxiosTraffic = once(logAxiosTrafficInternal); const QUERY_PARAMS_PREFIX = '?'; const QUERY_PARAMS_SEPARATOR = '&'; const GET_REQUEST_METHOD = 'get'; +const SAP_CLIENT_QUERY_PARAM_NAME = 'sap-client'; +const TEMP_MUAB_CONFIG_PATH = path.join(os.tmpdir(), 'temp-muab-config.txt'); type URLQueryParams = string | URLSearchParams | Record | string[][]; -function logAxiosTrafficInternal(logger: ToolsLogger): void { +interface MuabResponse { + relativeUrl: string; + method: string; + statusCode: number; + body: any; +} + +function logAxiosTrafficInternal(logger: ToolsLogger) { const prototype = Axios.prototype; const originalRequest = prototype.request; + const muabConfigStream = fs.createWriteStream(TEMP_MUAB_CONFIG_PATH, { flags: 'w' }); + + appendNewLine(muabConfigStream, `## Automated muab config file.`); prototype.request = async function patchRequest, D = any>( this: Axios, config: AxiosRequestConfig ): Promise { - // Thios config does not contain headers or query params added from interceptors. + // This config does not contain headers or query params added from interceptors. const mergedConfig = { ...this.defaults, ...config, @@ -32,7 +51,8 @@ function logAxiosTrafficInternal(logger: ToolsLogger): void { }; const requestUrl = getFullUrlString(mergedConfig.baseURL ?? '', mergedConfig.url ?? '', mergedConfig.params); // If the developer omits the request method when dooes a call to the .request() method - // internal axios interceptor sets a default method to GET so wee need to do the same here. + // an internal axios interceptor sets the default method to a GET method, + // so wee need to do the same here. const method = (mergedConfig.method ?? GET_REQUEST_METHOD).toUpperCase(); logger.info(`[axios][=>][${method}] ${requestUrl}`); @@ -61,6 +81,13 @@ function logAxiosTrafficInternal(logger: ToolsLogger): void { logger.info(`[axios] body: ${response.data}`); } + appendMuabResponse(muabConfigStream, { + relativeUrl: response.request.path, + method, + statusCode: parseInt(response.status, 10), + body: response.data + }); + return response; } catch (error) { logger.error(`[axios][error] ${requestUrl} ${error.message}`); @@ -72,6 +99,12 @@ function logAxiosTrafficInternal(logger: ToolsLogger): void { throw error; } }; + + return { + saveMuabConfig: (muabConfigPath: string) => { + saveMuabConfig(muabConfigStream, muabConfigPath); + } + }; } function getFullUrlString(baseURL: string, relativeUrl: string, queryParams?: URLQueryParams): string { @@ -90,14 +123,41 @@ function getFullUrlString(baseURL: string, relativeUrl: string, queryParams?: UR } } -export function once any>(fn: T): (...args: Parameters) => ReturnType | undefined { - let isCalled = false; - let result: ReturnType; +function appendMuabResponse(stream: WriteStream, response: MuabResponse): void { + appendNewLine(stream, '# [axios] response'); + appendNewLine( + stream, + `${removeQueryParamFromPath(response.relativeUrl, SAP_CLIENT_QUERY_PARAM_NAME)};${response.method}|${ + response.statusCode + };body=${response.body}` + ); +} - return function (this: ThisParameterType, ...args: Parameters): ReturnType | undefined { - if (isCalled) return result; - isCalled = true; - result = fn.apply(this, args); - return result; - }; +function appendNewLine(stream: WriteStream, message: string): void { + stream.write(`${message}\n\n`); +} + +function saveMuabConfig(stream: WriteStream, muabConfigPath: string): void { + stream.end(async () => { + await rename(TEMP_MUAB_CONFIG_PATH, muabConfigPath); + }); +} + +/** + * Removes a query parameter from a URL path string. + * + * @param path - The URL path with optional query string + * @param paramName - The query parameter to remove + * @returns The path without the specified query parameter + */ +export function removeQueryParamFromPath(path: string, paramName: string): string { + // Use a dummy base so URL can parse relative paths + const url = new URL(path, 'http://dummy'); + + // Remove the parameter + url.searchParams.delete(paramName); + + // Return path + remaining query string + const newPath = url.pathname + decodeURIComponent(url.search); // exclude host and protocol + return newPath; } diff --git a/packages/generator-adp/src/app/index.ts b/packages/generator-adp/src/app/index.ts index 3d177fb733d..e540dc21b61 100644 --- a/packages/generator-adp/src/app/index.ts +++ b/packages/generator-adp/src/app/index.ts @@ -118,6 +118,8 @@ export default class extends Generator { */ private isCustomerBase: boolean; + private saveMuabConfig: (muabConfigPath: string) => void; + /** * Creates an instance of the generator. * @@ -133,7 +135,6 @@ export default class extends Generator { this.options = opts; this._setupLogging(); - logAxiosTraffic(this.logger); const jsonInputString = getFirstArgAsString(args); this.jsonInput = parseJsonInput(jsonInputString, this.logger); @@ -312,6 +313,7 @@ export default class extends Generator { } async end(): Promise { + this.saveMuabConfig(`${this._getProjectPath()}/muab-config.txt`); if (this.shouldCreateExtProject) { return; } @@ -385,6 +387,8 @@ export default class extends Generator { this.options.logWrapper ); this.logger = AdpGeneratorLogger.logger as unknown as ToolsLogger; + const { saveMuabConfig } = logAxiosTraffic(this.logger); + this.saveMuabConfig = saveMuabConfig; } /** From 64b1f8f3bcd6d66f78489a0a9d33ba7ce7ae504d Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Thu, 16 Oct 2025 16:03:43 +0300 Subject: [PATCH 04/21] feat: Merge .har file with final muab config --- .../src/utils/axios-traffic.ts | 67 +++++++--- .../src/utils/export-from-har.ts | 121 ++++++++++++++++++ .../preview-middleware/src/ui5/middleware.ts | 8 ++ 3 files changed, 177 insertions(+), 19 deletions(-) create mode 100644 packages/axios-extension/src/utils/export-from-har.ts diff --git a/packages/axios-extension/src/utils/axios-traffic.ts b/packages/axios-extension/src/utils/axios-traffic.ts index 58921ccee20..aa0ebc8ba5f 100644 --- a/packages/axios-extension/src/utils/axios-traffic.ts +++ b/packages/axios-extension/src/utils/axios-traffic.ts @@ -3,10 +3,10 @@ import type { AxiosRequestConfig, AxiosResponse } from 'axios'; import { Axios } from 'axios'; import type { WriteStream } from 'fs'; import fs from 'fs'; -import { rename } from 'fs/promises'; import { once } from 'lodash'; -import os from 'os'; -import path from 'path'; +import { rename, access, constants } from 'fs/promises'; +import { getResponseMapFromHar } from './export-from-har'; +import { once as Once } from 'events'; // We call the actual function with once because we want sort of idempotency. // For example when called in yo generator, the generator could be started multiple times @@ -18,7 +18,8 @@ const QUERY_PARAMS_PREFIX = '?'; const QUERY_PARAMS_SEPARATOR = '&'; const GET_REQUEST_METHOD = 'get'; const SAP_CLIENT_QUERY_PARAM_NAME = 'sap-client'; -const TEMP_MUAB_CONFIG_PATH = path.join(os.tmpdir(), 'temp-muab-config.txt'); +const TEMP_MUAB_CONFIG_PATH = '/Users/I762682/projects/temp-muab-config.txt'; +//path.join(os.tmpdir(), 'temp-muab-config.txt'); type URLQueryParams = string | URLSearchParams | Record | string[][]; @@ -29,10 +30,11 @@ interface MuabResponse { body: any; } -function logAxiosTrafficInternal(logger: ToolsLogger) { +function logAxiosTrafficInternal(logger: ToolsLogger, isGeneratorWorkflow: boolean = true) { const prototype = Axios.prototype; const originalRequest = prototype.request; const muabConfigStream = fs.createWriteStream(TEMP_MUAB_CONFIG_PATH, { flags: 'w' }); + logger.info('[axios] Start dump'); appendNewLine(muabConfigStream, `## Automated muab config file.`); @@ -81,12 +83,16 @@ function logAxiosTrafficInternal(logger: ToolsLogger) { logger.info(`[axios] body: ${response.data}`); } - appendMuabResponse(muabConfigStream, { - relativeUrl: response.request.path, - method, - statusCode: parseInt(response.status, 10), - body: response.data - }); + appendMuabResponse( + muabConfigStream, + { + relativeUrl: response.request.path, + method, + statusCode: parseInt(response.status, 10), + body: response.data + }, + isGeneratorWorkflow + ); return response; } catch (error) { @@ -101,9 +107,7 @@ function logAxiosTrafficInternal(logger: ToolsLogger) { }; return { - saveMuabConfig: (muabConfigPath: string) => { - saveMuabConfig(muabConfigStream, muabConfigPath); - } + saveMuabConfig: (muabConfigPath: string) => saveMuabConfig(muabConfigStream, muabConfigPath) }; } @@ -123,8 +127,8 @@ function getFullUrlString(baseURL: string, relativeUrl: string, queryParams?: UR } } -function appendMuabResponse(stream: WriteStream, response: MuabResponse): void { - appendNewLine(stream, '# [axios] response'); +function appendMuabResponse(stream: WriteStream, response: MuabResponse, isGeneratorWorkflow: boolean): void { + appendNewLine(stream, `# [${isGeneratorWorkflow ? 'generator' : 'editor'}][axios] response`); appendNewLine( stream, `${removeQueryParamFromPath(response.relativeUrl, SAP_CLIENT_QUERY_PARAM_NAME)};${response.method}|${ @@ -137,10 +141,35 @@ function appendNewLine(stream: WriteStream, message: string): void { stream.write(`${message}\n\n`); } -function saveMuabConfig(stream: WriteStream, muabConfigPath: string): void { - stream.end(async () => { - await rename(TEMP_MUAB_CONFIG_PATH, muabConfigPath); +async function saveMuabConfig(stream: WriteStream, muabConfigPath: string): Promise { + await mergeWithHarFile(stream, muabConfigPath); + await new Promise((resolve) => { + stream.end(() => resolve()); }); + + await access(TEMP_MUAB_CONFIG_PATH, constants.F_OK); + await rename(TEMP_MUAB_CONFIG_PATH, muabConfigPath + '/muab-config.txt'); +} + +async function mergeWithHarFile(stream: WriteStream, muabConfigPath: string): Promise { + const harMap = getResponseMapFromHar(`${muabConfigPath}/localhost.har`); + + for (const [url, response] of Object.entries(harMap)) { + appendMuabResponse( + stream, + { + relativeUrl: url.replace('http://localhost:8080', ''), + method: response.method, + statusCode: 200, + body: response.body + }, + false + ); + + // ✅ Wait for the stream buffer to drain if full + if (!stream.writableNeedDrain) continue; + await Once(stream, 'drain'); + } } /** diff --git a/packages/axios-extension/src/utils/export-from-har.ts b/packages/axios-extension/src/utils/export-from-har.ts new file mode 100644 index 00000000000..9605494c933 --- /dev/null +++ b/packages/axios-extension/src/utils/export-from-har.ts @@ -0,0 +1,121 @@ +/** + * har-to-map.ts + * + * Usage: + * npx ts-node har-to-map.ts [path/to/file.har] + * + * Produces: har-map.json in the current folder. + * + * Map format: + * { + * "http://example.com/path?query=1": { + * "method": "GET", + * "body": {...} // parsed JSON if JSON, otherwise string + * }, + * ... + * } + */ + +import * as fs from 'fs'; +import type { WriteStream } from 'fs'; + +interface HarEntry { + request: { + method: string; + url: string; + }; + response: { + content?: { + mimeType?: string; + text?: string; + encoding?: string; + }; + }; +} + +interface HarFile { + log?: { + entries?: HarEntry[]; + }; +} + +interface ResponseMapValue { + method: string; + body: unknown; +} + +type ResponseMap = Record; + +function safeParseJson(text: string): any { + try { + return JSON.parse(text); + } catch { + return text; + } +} + +function decodeIfBase64(text: string, encoding?: string): string { + if (!text) { + return ''; + } + if (encoding && encoding.toLowerCase() === 'base64') { + return Buffer.from(text, 'base64').toString('utf8'); + } + return text; +} + +function getResponseBody(entry: HarEntry): any { + const content = entry.response?.content; + if (!content?.text) { + return ''; + } + const decoded = decodeIfBase64(content.text, content.encoding); + return safeParseJson(decoded); +} + +export function getResponseMapFromHar(harPath: string): ResponseMap { + if (!fs.existsSync(harPath)) { + throw new Error(`HAR file not found: ${harPath}`); + } + + const raw = fs.readFileSync(harPath, 'utf8'); + let har: HarFile; + + let entries; + try { + har = JSON.parse(raw); + } catch (err) { + const entryRegex = /{"startedDateTime":.*?"response":\{.*?\}\s*}/gs; + const matches = raw.match(entryRegex); + entries = []; + if (matches) { + for (const match of matches) { + try { + const entry = JSON.parse(match); + entries.push(entry); + } catch { + // skip broken entries + } + } + } + } + + if (!entries) { + entries = har.log?.entries ?? []; + } + const map: ResponseMap = {}; + let skipped = 0; + + for (const entry of entries) { + const url = entry.request?.url; + if (!url) { + skipped++; + continue; + } + const method = entry.request?.method || 'GET'; + const body = getResponseBody(entry); + map[url] = { method, body }; + } + + return map; +} diff --git a/packages/preview-middleware/src/ui5/middleware.ts b/packages/preview-middleware/src/ui5/middleware.ts index de2cb42f366..c220ae93927 100644 --- a/packages/preview-middleware/src/ui5/middleware.ts +++ b/packages/preview-middleware/src/ui5/middleware.ts @@ -5,6 +5,7 @@ import { type EnhancedRouter, FlpSandbox, initAdp } from '../base/flp'; import type { MiddlewareConfig } from '../types'; import { getPreviewPaths, sanitizeConfig } from '../base/config'; import { logRemoteUrl, isRemoteConnectionsEnabled } from '../base/remote-url'; +import { logAxiosTraffic } from '@sap-ux/axios-extension'; /** * Create the router that is to be exposed as UI5 middleware. @@ -60,6 +61,13 @@ module.exports = async (params: MiddlewareParameters): Promise transports: [new UI5ToolingTransport({ moduleName: 'preview-middleware' })], logLevel: params.options.configuration?.debug ? LogLevel.Debug : LogLevel.Info }); + + const { saveMuabConfig } = logAxiosTraffic(logger, false); + process.on('SIGINT', async () => { + await saveMuabConfig(process.cwd()); + process.exit(); + }); + try { return await createRouter(params, logger); } catch (error) { From 055587023cbb22c5b18b3f52ed300db575774673 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Thu, 16 Oct 2025 16:56:13 +0300 Subject: [PATCH 05/21] docs: Add docs and todos. --- packages/axios-extension/src/utils/readme.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/axios-extension/src/utils/readme.md diff --git a/packages/axios-extension/src/utils/readme.md b/packages/axios-extension/src/utils/readme.md new file mode 100644 index 00000000000..de248b49fd5 --- /dev/null +++ b/packages/axios-extension/src/utils/readme.md @@ -0,0 +1,9 @@ +TODO +* This need to be put in a separate package, e.g. mock-server or muab ... +* Code quality needs improvement this `muab` branch is only for the demo. +* Check if .css or any static resources go through the muab if so how can we filter them we do nto want +to clutter the muab.config. + +* When we export .har, preserve logs in the checkbox need to be marked otherwise +the .har will contain ...show more for large response bodies being truncated and the .har json will be invalid. +This need to be verified. \ No newline at end of file From 7d66d851d45fc09d1c0430298c2e6392359a7820 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Sun, 2 Nov 2025 21:50:38 +0200 Subject: [PATCH 06/21] feat: Add mock server package in the monorepo. --- packages/adp-mock-server/README.md | 0 packages/adp-mock-server/jest.config.js | 6 ++++ packages/adp-mock-server/package.json | 35 +++++++++++++++++++ packages/adp-mock-server/src/index.ts | 1 + packages/adp-mock-server/src/main.ts | 8 +++++ .../adp-mock-server/test/unit/main.test.ts | 20 +++++++++++ packages/adp-mock-server/tsconfig.json | 18 ++++++++++ 7 files changed, 88 insertions(+) create mode 100644 packages/adp-mock-server/README.md create mode 100644 packages/adp-mock-server/jest.config.js create mode 100644 packages/adp-mock-server/package.json create mode 100644 packages/adp-mock-server/src/index.ts create mode 100644 packages/adp-mock-server/src/main.ts create mode 100644 packages/adp-mock-server/test/unit/main.test.ts create mode 100644 packages/adp-mock-server/tsconfig.json diff --git a/packages/adp-mock-server/README.md b/packages/adp-mock-server/README.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/adp-mock-server/jest.config.js b/packages/adp-mock-server/jest.config.js new file mode 100644 index 00000000000..2ceece4bdda --- /dev/null +++ b/packages/adp-mock-server/jest.config.js @@ -0,0 +1,6 @@ +const config = require('../../jest.base'); +config.displayName = 'adp-mock-server'; +config.rootDir = './'; +config.testMatch = ['/test/unit/**/*.test.(ts|js)']; +config.collectCoverageFrom = ['/src/**/*.ts', '!/src/index.ts']; +module.exports = config; diff --git a/packages/adp-mock-server/package.json b/packages/adp-mock-server/package.json new file mode 100644 index 00000000000..563286ec416 --- /dev/null +++ b/packages/adp-mock-server/package.json @@ -0,0 +1,35 @@ +{ + "name": "@sap-ux/adp-mock-server", + "version": "0.0.1", + "description": "Adaptation project mock server", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "README.md", + "!dist/*.map", + "!dist/**/*.map" + ], + "scripts": { + "build": "tsc --build", + "clean": "rimraf dist *.tsbuildinfo", + "test": "jest --ci --forceExit --detectOpenHandles --colors", + "watch": "tsc --build --watch" + }, + "repository": { + "type": "git", + "url": "https://github.com/SAP/open-ux-tools.git", + "directory": "packages/adp-mock-server" + }, + "license": "Apache-2.0", + "dependencies": { + "@sap-ux/logger": "workspace:*" + }, + "devDependencies": { + "@jest/types": "30.0.1", + "rimraf": "5.0.5" + }, + "engines": { + "node": ">=20.0" + } +} \ No newline at end of file diff --git a/packages/adp-mock-server/src/index.ts b/packages/adp-mock-server/src/index.ts new file mode 100644 index 00000000000..aad1ca831ef --- /dev/null +++ b/packages/adp-mock-server/src/index.ts @@ -0,0 +1 @@ +export * from './main'; diff --git a/packages/adp-mock-server/src/main.ts b/packages/adp-mock-server/src/main.ts new file mode 100644 index 00000000000..469f7d31573 --- /dev/null +++ b/packages/adp-mock-server/src/main.ts @@ -0,0 +1,8 @@ +import type { ToolsLogger } from '@sap-ux/logger'; + +/** + * Main function for your package + */ +export function doSomething(logger: ToolsLogger): void { + logger.info('Hello from my-new-package!'); +} diff --git a/packages/adp-mock-server/test/unit/main.test.ts b/packages/adp-mock-server/test/unit/main.test.ts new file mode 100644 index 00000000000..201bac068cc --- /dev/null +++ b/packages/adp-mock-server/test/unit/main.test.ts @@ -0,0 +1,20 @@ +import { doSomething } from '../../src/main'; +import type { ToolsLogger } from '@sap-ux/logger'; + +describe('main', () => { + let mockLogger: jest.Mocked; + + beforeEach(() => { + mockLogger = { + info: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + warn: jest.fn() + } as any; + }); + + test('should log hello message', () => { + doSomething(mockLogger); + expect(mockLogger.info).toHaveBeenCalledWith('Hello from my-new-package!'); + }); +}); diff --git a/packages/adp-mock-server/tsconfig.json b/packages/adp-mock-server/tsconfig.json new file mode 100644 index 00000000000..20094039944 --- /dev/null +++ b/packages/adp-mock-server/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "composite": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test"], + "references": [ + { + "path": "../logger" + } + ] +} From 2853301a3277537fcc03ceca160a81f105ae76c7 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Sun, 2 Nov 2025 21:55:09 +0200 Subject: [PATCH 07/21] chore: Update .lock file and root tsconfig to be compliant with the new package. --- pnpm-lock.yaml | 13 +++++++++++++ tsconfig.json | 3 +++ 2 files changed, 16 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29602276fb1..ae79f511d26 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -584,6 +584,19 @@ importers: specifier: 6.3.0 version: 6.3.0(mem-fs@2.1.0)(yeoman-environment@3.8.0)(yeoman-generator@5.10.0) + packages/adp-mock-server: + dependencies: + '@sap-ux/logger': + specifier: workspace:* + version: link:../logger + devDependencies: + '@jest/types': + specifier: 30.0.1 + version: 30.0.1 + rimraf: + specifier: 5.0.5 + version: 5.0.5 + packages/adp-tooling: dependencies: '@sap-devx/yeoman-ui-types': diff --git a/tsconfig.json b/tsconfig.json index 6b0579db022..dc74cda0736 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,6 +36,9 @@ { "path": "packages/adp-flp-config-sub-generator" }, + { + "path": "packages/adp-mock-server" + }, { "path": "packages/adp-tooling" }, From 0aad85777161d70020c32b94f1e8c880de970d72 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Sun, 2 Nov 2025 21:58:05 +0200 Subject: [PATCH 08/21] chore: Add mockserver-node as dependency. --- packages/adp-mock-server/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/adp-mock-server/package.json b/packages/adp-mock-server/package.json index 563286ec416..e6d16873bc4 100644 --- a/packages/adp-mock-server/package.json +++ b/packages/adp-mock-server/package.json @@ -27,6 +27,7 @@ }, "devDependencies": { "@jest/types": "30.0.1", + "mockserver-node": "5.15.0", "rimraf": "5.0.5" }, "engines": { From 1a1d6de6c6c05ab05fba3a7030a33770df2a606d Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Sun, 2 Nov 2025 21:59:19 +0200 Subject: [PATCH 09/21] chore: Update .lock file --- pnpm-lock.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae79f511d26..d362156f3be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -593,6 +593,9 @@ importers: '@jest/types': specifier: 30.0.1 version: 30.0.1 + mockserver-node: + specifier: 5.15.0 + version: 5.15.0 rimraf: specifier: 5.0.5 version: 5.0.5 @@ -19901,6 +19904,17 @@ packages: through: 2.3.8 dev: true + /mockserver-node@5.15.0: + resolution: {integrity: sha512-Po442dwW5GpTSgP2T2FbGFv8vp07llfTVJSZBZQZ2VoTFvkhRgCN1D1FqRFhTFZ/42aCTYsWxQnjp1Tt1nISxA==} + engines: {node: '>= 0.8.0'} + dependencies: + follow-redirects: 1.15.6(debug@4.4.1) + glob: 8.0.3 + q: 2.0.3 + transitivePeerDependencies: + - debug + dev: true + /module-details-from-path@1.0.3: resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} dev: false From f642ba1b8272b30c0fdcd1706c8b1b6a83036f03 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Tue, 4 Nov 2025 20:24:38 +0200 Subject: [PATCH 10/21] feat(incomplete): Add functionallity for recor/replay requests. --- packages/adp-mock-server/.eslintignore | 1 + packages/adp-mock-server/.eslintrc.js | 7 ++ packages/adp-mock-server/.gitignore | 1 + packages/adp-mock-server/package.json | 12 ++- packages/adp-mock-server/src/client.ts | 61 ++++++++++++++ packages/adp-mock-server/src/constants.ts | 14 ++++ packages/adp-mock-server/src/index.ts | 81 ++++++++++++++++++- packages/adp-mock-server/src/logger.ts | 4 + packages/adp-mock-server/src/main.ts | 8 -- packages/adp-mock-server/src/utils.ts | 49 +++++++++++ .../adp-mock-server/test/unit/index.test.ts | 16 ++++ .../adp-mock-server/test/unit/main.test.ts | 20 ----- packages/adp-mock-server/tsconfig.eslint.json | 4 + packages/adp-mock-server/tsconfig.json | 3 +- .../types/mockserver-node.d.ts | 16 ++++ 15 files changed, 265 insertions(+), 32 deletions(-) create mode 100644 packages/adp-mock-server/.eslintignore create mode 100644 packages/adp-mock-server/.eslintrc.js create mode 100644 packages/adp-mock-server/.gitignore create mode 100644 packages/adp-mock-server/src/client.ts create mode 100644 packages/adp-mock-server/src/constants.ts create mode 100644 packages/adp-mock-server/src/logger.ts delete mode 100644 packages/adp-mock-server/src/main.ts create mode 100644 packages/adp-mock-server/src/utils.ts create mode 100644 packages/adp-mock-server/test/unit/index.test.ts delete mode 100644 packages/adp-mock-server/test/unit/main.test.ts create mode 100644 packages/adp-mock-server/tsconfig.eslint.json create mode 100644 packages/adp-mock-server/types/mockserver-node.d.ts diff --git a/packages/adp-mock-server/.eslintignore b/packages/adp-mock-server/.eslintignore new file mode 100644 index 00000000000..1521c8b7652 --- /dev/null +++ b/packages/adp-mock-server/.eslintignore @@ -0,0 +1 @@ +dist diff --git a/packages/adp-mock-server/.eslintrc.js b/packages/adp-mock-server/.eslintrc.js new file mode 100644 index 00000000000..b717f83ae98 --- /dev/null +++ b/packages/adp-mock-server/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['../../.eslintrc'], + parserOptions: { + project: './tsconfig.eslint.json', + tsconfigRootDir: __dirname + } +}; diff --git a/packages/adp-mock-server/.gitignore b/packages/adp-mock-server/.gitignore new file mode 100644 index 00000000000..6d66c3e2551 --- /dev/null +++ b/packages/adp-mock-server/.gitignore @@ -0,0 +1 @@ +mock-data \ No newline at end of file diff --git a/packages/adp-mock-server/package.json b/packages/adp-mock-server/package.json index e6d16873bc4..902af280212 100644 --- a/packages/adp-mock-server/package.json +++ b/packages/adp-mock-server/package.json @@ -12,6 +12,8 @@ ], "scripts": { "build": "tsc --build", + "start": "ts-node ./src/index.ts --start", + "stop": "ts-node ./src/index.ts --stop", "clean": "rimraf dist *.tsbuildinfo", "test": "jest --ci --forceExit --detectOpenHandles --colors", "watch": "tsc --build --watch" @@ -23,12 +25,18 @@ }, "license": "Apache-2.0", "dependencies": { - "@sap-ux/logger": "workspace:*" + "@sap-ux/logger": "workspace:*", + "@types/lodash": "4.14.202", + "dotenv": "16.3.1", + "lodash": "4.17.21", + "mockserver-client": "5.15.0" }, "devDependencies": { "@jest/types": "30.0.1", "mockserver-node": "5.15.0", - "rimraf": "5.0.5" + "rimraf": "5.0.5", + "ts-node": "10.9.2", + "typescript": "5.9.3" }, "engines": { "node": ">=20.0" diff --git a/packages/adp-mock-server/src/client.ts b/packages/adp-mock-server/src/client.ts new file mode 100644 index 00000000000..8e0e3e4266a --- /dev/null +++ b/packages/adp-mock-server/src/client.ts @@ -0,0 +1,61 @@ +import fs from 'fs/promises'; +import { once } from 'lodash'; +import { mockServerClient } from 'mockserver-client'; +import type { HttpRequest, HttpResponse } from 'mockserver-client/mockServer'; +import type { MockServerClient } from 'mockserver-client/mockServerClient'; +import { MOCK_SERVER_PORT, RESPONSES_JSON_PATH } from './constants'; +import { logger } from './logger'; +import { getSapSystemPort } from './utils'; + +/** + * In type HttpRequestAndHttpResponse, httpRequest and httpResponse fields are not arrays but objects. + * TODO Add .d.ts in types folder to patch the type. + */ +interface RequestAndResponsePatched { + httpRequest?: HttpRequest; + httpResponse?: HttpResponse; + timestamp?: string; +} + +export const getClient = once(getClientInternal); + +export async function recordResponses(): Promise { + logger.info('Record responses.'); + const client = await getClient(); + let requestResponseList = (await client.retrieveRecordedRequestsAndResponses( + {} + )) as unknown as RequestAndResponsePatched[]; + requestResponseList = postProcessRequestAndResponses(requestResponseList); + await fs.writeFile(RESPONSES_JSON_PATH, JSON.stringify(requestResponseList, null, 2)); +} + +async function getClientInternal(): Promise { + logger.info('Init mock server client.'); + const client = mockServerClient('localhost', MOCK_SERVER_PORT); + await client.mockAnyResponse({ + httpRequest: { + secure: false + }, + httpForward: { + // Forwards https requests to the actual sap system. + scheme: 'HTTPS', + host: process.env.SAP_SYSTEM_HOST, + port: getSapSystemPort() + } + }); + return client; +} + +function postProcessRequestAndResponses(requestResponseList: RequestAndResponsePatched[]): RequestAndResponsePatched[] { + return requestResponseList.map(({ httpRequest, httpResponse }) => { + const body = httpRequest?.body as any; + return { + httpRequest: { + ...httpRequest, + body: body?.type === 'BINARY' ? body.base64Bytes : body, + secure: false + }, + httpResponse + }; + }); +} diff --git a/packages/adp-mock-server/src/constants.ts b/packages/adp-mock-server/src/constants.ts new file mode 100644 index 00000000000..79504f1cf05 --- /dev/null +++ b/packages/adp-mock-server/src/constants.ts @@ -0,0 +1,14 @@ +export const ADP_MOCK_SERVER_LOG_PREFIX = '[ADP][Mock server]'; + +export const MOCK_SERVER_PORT = 1080; +export const DEFAULT_SAP_SYSTEM_PORT = 44355; + +export const MOCK_DATA_FOLDER_PATH = './mock-data'; +export const EXPECTATIONS_JSON_PATH = `${MOCK_DATA_FOLDER_PATH}/expectations.json`; +export const RESPONSES_JSON_PATH = `${MOCK_DATA_FOLDER_PATH}/responses.json`; + +export const CLI_PARAM_START = 'start'; +export const CLI_PARAM_STOP = 'stop'; +export const CLI_PARAM_RECORD = 'record'; + +export const NOOP = new Promise(() => {}); diff --git a/packages/adp-mock-server/src/index.ts b/packages/adp-mock-server/src/index.ts index aad1ca831ef..950334f3e32 100644 --- a/packages/adp-mock-server/src/index.ts +++ b/packages/adp-mock-server/src/index.ts @@ -1 +1,80 @@ -export * from './main'; +import dotenv from 'dotenv'; +import { start_mockserver, stop_mockserver } from 'mockserver-node'; +import path from 'path'; +import { getClient, recordResponses } from './client'; +import { + CLI_PARAM_RECORD, + CLI_PARAM_START, + CLI_PARAM_STOP, + EXPECTATIONS_JSON_PATH, + MOCK_SERVER_PORT, + NOOP, + RESPONSES_JSON_PATH +} from './constants'; +import { logger } from './logger'; +import { createMockDataFolderIfNeeded, getCliParamValueByName, getSapSystemPort } from './utils'; + +dotenv.config({ path: path.join(__dirname, '../.env') }); + +async function start(isRecordOrReplayMode: boolean): Promise { + await createMockDataFolderIfNeeded(); + if (isRecordOrReplayMode) { + await startInRecordMode(); + } else { + await startInReplayMode(); + } +} + +async function startInRecordMode(): Promise { + await start_mockserver({ + serverPort: MOCK_SERVER_PORT, + // Does not support https forwarding without client https forwarding. + proxyRemoteHost: process.env.SAP_SYSTEM_HOST, + proxyRemotePort: getSapSystemPort(), + jvmOptions: [ + '-Dmockserver.watchInitializationJson=true', + `-Dmockserver.initializationJsonPath=${EXPECTATIONS_JSON_PATH}`, + '-Dmockserver.persistExpectations=true', + `-Dmockserver.persistedExpectationsPath=${EXPECTATIONS_JSON_PATH}` + ] + // verbose: true + }); + logger.info(`✅ Server running on port ${MOCK_SERVER_PORT} in record mode.`); + await getClient(); +} + +async function startInReplayMode(): Promise { + await start_mockserver({ + serverPort: MOCK_SERVER_PORT, + jvmOptions: [ + '-Dmockserver.watchInitializationJson=true', + `-Dmockserver.initializationJsonPath=${RESPONSES_JSON_PATH}`, + '-Dmockserver.persistExpectations=false' + ] + // verbose: true + }); + logger.info(`✅ Server running on port ${MOCK_SERVER_PORT} in replay mode.`); +} + +async function stop(): Promise { + if (getCliParamValueByName(CLI_PARAM_RECORD)) { + await recordResponses(); + } + await stop_mockserver({ serverPort: MOCK_SERVER_PORT }); + logger.info('Stop mock server.'); +} + +async function main(): Promise { + if (getCliParamValueByName(CLI_PARAM_START)) { + await start(getCliParamValueByName(CLI_PARAM_RECORD)); + // Keep alive. + await NOOP; + } else if (getCliParamValueByName(CLI_PARAM_STOP)) { + await stop(); + } +} + +main().catch((error) => { + logger.error(`Unexpected error: ${error}.`); + process.exit(1); +}); diff --git a/packages/adp-mock-server/src/logger.ts b/packages/adp-mock-server/src/logger.ts new file mode 100644 index 00000000000..592aeee645f --- /dev/null +++ b/packages/adp-mock-server/src/logger.ts @@ -0,0 +1,4 @@ +import { ConsoleTransport, ToolsLogger } from '@sap-ux/logger'; +import { ADP_MOCK_SERVER_LOG_PREFIX } from './constants'; + +export const logger = new ToolsLogger({ logPrefix: ADP_MOCK_SERVER_LOG_PREFIX, transports: [new ConsoleTransport()] }); diff --git a/packages/adp-mock-server/src/main.ts b/packages/adp-mock-server/src/main.ts deleted file mode 100644 index 469f7d31573..00000000000 --- a/packages/adp-mock-server/src/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ToolsLogger } from '@sap-ux/logger'; - -/** - * Main function for your package - */ -export function doSomething(logger: ToolsLogger): void { - logger.info('Hello from my-new-package!'); -} diff --git a/packages/adp-mock-server/src/utils.ts b/packages/adp-mock-server/src/utils.ts new file mode 100644 index 00000000000..f7eb4e7d58a --- /dev/null +++ b/packages/adp-mock-server/src/utils.ts @@ -0,0 +1,49 @@ +import { setTimeout } from 'node:timers/promises'; +import { DEFAULT_SAP_SYSTEM_PORT, MOCK_DATA_FOLDER_PATH } from './constants'; +import { promises as fs } from 'fs'; +import { HttpRequestAndHttpResponse } from 'mockserver-client/mockServer'; +import { request } from 'node:http'; + +type CliParamValue = string | number | boolean | undefined; + +export async function wait(seconds: number): Promise { + return new Promise((resolve) => setTimeout(seconds * 1000, resolve)); +} + +export function getSapSystemPort(): number { + return parseInt(process.env.SAP_SYSTEM_PORT ?? DEFAULT_SAP_SYSTEM_PORT.toString(), 10); +} + +export function createMockDataFolderIfNeeded(): Promise { + return fs.mkdir(MOCK_DATA_FOLDER_PATH, { recursive: true }); +} + +export function getCliParamValueByName(name: string): T { + const arg = process.argv.find((arg) => arg.startsWith(`--${name}`)); + + if (!arg) { + return undefined as T; + } + + const value = arg.split('=')[1]; + + if (!value) { + // If we have param without a value we return true, e.g. --record. + return true as T; + } + + if (value.toLowerCase() === 'true') { + return true as T; + } + + if (value.toLowerCase() === 'false') { + return false as T; + } + + const numericValue = Number(value); + if (!isNaN(numericValue)) { + return numericValue as T; + } + + return value as T; +} diff --git a/packages/adp-mock-server/test/unit/index.test.ts b/packages/adp-mock-server/test/unit/index.test.ts new file mode 100644 index 00000000000..5c9f9af2621 --- /dev/null +++ b/packages/adp-mock-server/test/unit/index.test.ts @@ -0,0 +1,16 @@ +import type { ToolsLogger } from '@sap-ux/logger'; + +describe('main', () => { + let loggerMock: jest.Mocked; + + beforeEach(() => { + loggerMock = { + info: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + warn: jest.fn() + } as any; + }); + + test('should ', () => {}); +}); diff --git a/packages/adp-mock-server/test/unit/main.test.ts b/packages/adp-mock-server/test/unit/main.test.ts deleted file mode 100644 index 201bac068cc..00000000000 --- a/packages/adp-mock-server/test/unit/main.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { doSomething } from '../../src/main'; -import type { ToolsLogger } from '@sap-ux/logger'; - -describe('main', () => { - let mockLogger: jest.Mocked; - - beforeEach(() => { - mockLogger = { - info: jest.fn(), - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn() - } as any; - }); - - test('should log hello message', () => { - doSomething(mockLogger); - expect(mockLogger.info).toHaveBeenCalledWith('Hello from my-new-package!'); - }); -}); diff --git a/packages/adp-mock-server/tsconfig.eslint.json b/packages/adp-mock-server/tsconfig.eslint.json new file mode 100644 index 00000000000..d5f1aa34747 --- /dev/null +++ b/packages/adp-mock-server/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src", "test", ".eslintrc.js"] +} diff --git a/packages/adp-mock-server/tsconfig.json b/packages/adp-mock-server/tsconfig.json index 20094039944..3552e9ff300 100644 --- a/packages/adp-mock-server/tsconfig.json +++ b/packages/adp-mock-server/tsconfig.json @@ -6,7 +6,8 @@ "composite": true, "declaration": true, "declarationMap": true, - "sourceMap": true + "sourceMap": true, + "typeRoots": ["./types", "./node_modules/@types"] }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "test"], diff --git a/packages/adp-mock-server/types/mockserver-node.d.ts b/packages/adp-mock-server/types/mockserver-node.d.ts new file mode 100644 index 00000000000..ff4c6863cc7 --- /dev/null +++ b/packages/adp-mock-server/types/mockserver-node.d.ts @@ -0,0 +1,16 @@ +declare module 'mockserver-node' { + interface MockServerOptions { + serverPort?: number; + /** + * Does not support https forwarding without client https forwarding. + */ + proxyRemotePort?: number; + proxyRemoteHost?: string; + verbose?: boolean; + trace?: boolean; + jvmOptions?: string[]; + } + + export function start_mockserver(options?: MockServerOptions): Promise; + export function stop_mockserver(options?: MockServerOptions): Promise; +} From ba2f3c8df9e826d76a1f1f4d0d1c693ca762e1c1 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Tue, 4 Nov 2025 20:33:15 +0200 Subject: [PATCH 11/21] chore: Disable axios traffic dump and .har file save --- packages/axios-extension/src/utils/axios-traffic.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/axios-extension/src/utils/axios-traffic.ts b/packages/axios-extension/src/utils/axios-traffic.ts index aa0ebc8ba5f..4545e207bf7 100644 --- a/packages/axios-extension/src/utils/axios-traffic.ts +++ b/packages/axios-extension/src/utils/axios-traffic.ts @@ -31,6 +31,7 @@ interface MuabResponse { } function logAxiosTrafficInternal(logger: ToolsLogger, isGeneratorWorkflow: boolean = true) { + return; const prototype = Axios.prototype; const originalRequest = prototype.request; const muabConfigStream = fs.createWriteStream(TEMP_MUAB_CONFIG_PATH, { flags: 'w' }); From d1b61237d6f191b696cb363b99c3fd78f78eabc3 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Thu, 6 Nov 2025 10:26:49 +0200 Subject: [PATCH 12/21] refactor: Reorganize files and partition logic into separate files. --- packages/adp-mock-server/src/client.ts | 61 ------------------- .../src/client/record-client.ts | 49 +++++++++++++++ .../src/client/replay-client.ts | 55 +++++++++++++++++ packages/adp-mock-server/src/index.ts | 13 ++-- packages/adp-mock-server/src/types.ts | 32 ++++++++++ .../src/{utils.ts => utils/cli-utils.ts} | 18 ------ .../adp-mock-server/src/utils/file-utils.ts | 6 ++ .../adp-mock-server/src/{ => utils}/logger.ts | 2 +- .../src/utils/sap-system-utils.ts | 5 ++ .../adp-mock-server/src/utils/type-guards.ts | 13 ++++ packages/adp-mock-server/tsconfig.json | 2 +- .../types/mockserver-node.d.ts | 2 +- .../src/utils/axios-traffic.ts | 1 - 13 files changed, 172 insertions(+), 87 deletions(-) delete mode 100644 packages/adp-mock-server/src/client.ts create mode 100644 packages/adp-mock-server/src/client/record-client.ts create mode 100644 packages/adp-mock-server/src/client/replay-client.ts create mode 100644 packages/adp-mock-server/src/types.ts rename packages/adp-mock-server/src/{utils.ts => utils/cli-utils.ts} (50%) create mode 100644 packages/adp-mock-server/src/utils/file-utils.ts rename packages/adp-mock-server/src/{ => utils}/logger.ts (75%) create mode 100644 packages/adp-mock-server/src/utils/sap-system-utils.ts create mode 100644 packages/adp-mock-server/src/utils/type-guards.ts diff --git a/packages/adp-mock-server/src/client.ts b/packages/adp-mock-server/src/client.ts deleted file mode 100644 index 8e0e3e4266a..00000000000 --- a/packages/adp-mock-server/src/client.ts +++ /dev/null @@ -1,61 +0,0 @@ -import fs from 'fs/promises'; -import { once } from 'lodash'; -import { mockServerClient } from 'mockserver-client'; -import type { HttpRequest, HttpResponse } from 'mockserver-client/mockServer'; -import type { MockServerClient } from 'mockserver-client/mockServerClient'; -import { MOCK_SERVER_PORT, RESPONSES_JSON_PATH } from './constants'; -import { logger } from './logger'; -import { getSapSystemPort } from './utils'; - -/** - * In type HttpRequestAndHttpResponse, httpRequest and httpResponse fields are not arrays but objects. - * TODO Add .d.ts in types folder to patch the type. - */ -interface RequestAndResponsePatched { - httpRequest?: HttpRequest; - httpResponse?: HttpResponse; - timestamp?: string; -} - -export const getClient = once(getClientInternal); - -export async function recordResponses(): Promise { - logger.info('Record responses.'); - const client = await getClient(); - let requestResponseList = (await client.retrieveRecordedRequestsAndResponses( - {} - )) as unknown as RequestAndResponsePatched[]; - requestResponseList = postProcessRequestAndResponses(requestResponseList); - await fs.writeFile(RESPONSES_JSON_PATH, JSON.stringify(requestResponseList, null, 2)); -} - -async function getClientInternal(): Promise { - logger.info('Init mock server client.'); - const client = mockServerClient('localhost', MOCK_SERVER_PORT); - await client.mockAnyResponse({ - httpRequest: { - secure: false - }, - httpForward: { - // Forwards https requests to the actual sap system. - scheme: 'HTTPS', - host: process.env.SAP_SYSTEM_HOST, - port: getSapSystemPort() - } - }); - return client; -} - -function postProcessRequestAndResponses(requestResponseList: RequestAndResponsePatched[]): RequestAndResponsePatched[] { - return requestResponseList.map(({ httpRequest, httpResponse }) => { - const body = httpRequest?.body as any; - return { - httpRequest: { - ...httpRequest, - body: body?.type === 'BINARY' ? body.base64Bytes : body, - secure: false - }, - httpResponse - }; - }); -} diff --git a/packages/adp-mock-server/src/client/record-client.ts b/packages/adp-mock-server/src/client/record-client.ts new file mode 100644 index 00000000000..6d8626a77bd --- /dev/null +++ b/packages/adp-mock-server/src/client/record-client.ts @@ -0,0 +1,49 @@ +import fs from 'fs/promises'; +import { once } from 'lodash'; +import { mockServerClient } from 'mockserver-client'; +import type { MockServerClient } from 'mockserver-client/mockServerClient'; +import { MOCK_SERVER_PORT, RESPONSES_JSON_PATH } from '../constants'; +import { HttpRequestAndHttpResponse } from '../types'; +import { logger } from '../utils/logger'; +import { getSapSystemPort } from '../utils/sap-system-utils'; + +export const getRecordClient = once(getRecordClientInternal); + +export async function recordResponses(): Promise { + logger.info('Record responses.'); + const client = await getRecordClient(); + let requestResponseList = await retrieveRecordedRequestsAndResponses(client); + requestResponseList = disableRequestsSecureFlag(requestResponseList); + await fs.writeFile(RESPONSES_JSON_PATH, JSON.stringify(requestResponseList, null, 2)); +} + +async function retrieveRecordedRequestsAndResponses(client: MockServerClient): Promise { + return client.retrieveRecordedRequestsAndResponses({}) as Promise; +} + +function disableRequestsSecureFlag(requestResponseList: HttpRequestAndHttpResponse[]): HttpRequestAndHttpResponse[] { + return requestResponseList.map(({ httpRequest, httpResponse }) => ({ + httpRequest: { + ...httpRequest, + secure: false + }, + httpResponse + })); +} + +async function getRecordClientInternal(): Promise { + logger.info('Init mock server client.'); + const client = mockServerClient('localhost', MOCK_SERVER_PORT); + await client.mockAnyResponse({ + httpRequest: { + secure: false + }, + httpForward: { + // Forwards https requests to the actual sap system. + scheme: 'HTTPS', + host: process.env.SAP_SYSTEM_HOST, + port: getSapSystemPort() + } + }); + return client; +} diff --git a/packages/adp-mock-server/src/client/replay-client.ts b/packages/adp-mock-server/src/client/replay-client.ts new file mode 100644 index 00000000000..10d144a4cb8 --- /dev/null +++ b/packages/adp-mock-server/src/client/replay-client.ts @@ -0,0 +1,55 @@ +import { once } from 'lodash'; +import { mockServerClient, MockServerClient } from 'mockserver-client/mockServerClient'; +import { MOCK_SERVER_PORT } from '../constants'; +import { Body, HttpRequestAndHttpResponse } from '../types'; +import { logger } from '../utils/logger'; +import { isBinaryBody, isBodyType, isXmlBody } from '../utils/type-guards'; +import { Expectation, HttpRequest } from 'mockserver-client'; + +export const getReplayClient = once(getReplayClientInternal); + +async function getReplayClientInternal(): Promise { + logger.info('Init mock server client.'); + const client = mockServerClient('localhost', MOCK_SERVER_PORT); + // await patchActiveExpectations(client); + return client; +} + +async function patchActiveExpectations(client: MockServerClient): Promise { + const requestResponseList = await retrieveActiveExpectations(client); + const mockPromises = requestResponseList + .filter(({ httpRequest, httpResponse }) => isBodyType(httpRequest?.body) || isBodyType(httpResponse?.body)) + .map(({ httpRequest, httpResponse }) => ({ + httpRequest: { + ...httpRequest, + body: parseBody(httpRequest?.body) + }, + httpResponse: { + ...httpResponse, + body: parseBody(httpResponse?.body) + } + })) + .map(({ httpRequest, httpResponse }) => client.clear(httpRequest as HttpRequest, 'EXPECTATIONS')); + await Promise.all(mockPromises); + logger.info(mockPromises); +} + +async function retrieveActiveExpectations(client: MockServerClient): Promise { + return client.retrieveActiveExpectations({}) as Promise; +} + +function parseBody(body: unknown): Body | string | unknown { + if (!isBodyType(body)) { + return body; + } + + if (isXmlBody(body)) { + return body.xml; + } + + if (isBinaryBody(body)) { + return body.base64Bytes; + } + + return body; +} diff --git a/packages/adp-mock-server/src/index.ts b/packages/adp-mock-server/src/index.ts index 950334f3e32..2ebc2bdc7dd 100644 --- a/packages/adp-mock-server/src/index.ts +++ b/packages/adp-mock-server/src/index.ts @@ -1,7 +1,8 @@ import dotenv from 'dotenv'; import { start_mockserver, stop_mockserver } from 'mockserver-node'; import path from 'path'; -import { getClient, recordResponses } from './client'; +import { getRecordClient, recordResponses } from './client/record-client'; +import { getReplayClient } from './client/replay-client'; import { CLI_PARAM_RECORD, CLI_PARAM_START, @@ -11,8 +12,10 @@ import { NOOP, RESPONSES_JSON_PATH } from './constants'; -import { logger } from './logger'; -import { createMockDataFolderIfNeeded, getCliParamValueByName, getSapSystemPort } from './utils'; +import { getCliParamValueByName } from './utils/cli-utils'; +import { createMockDataFolderIfNeeded } from './utils/file-utils'; +import { logger } from './utils/logger'; +import { getSapSystemPort } from './utils/sap-system-utils'; dotenv.config({ path: path.join(__dirname, '../.env') }); @@ -40,7 +43,7 @@ async function startInRecordMode(): Promise { // verbose: true }); logger.info(`✅ Server running on port ${MOCK_SERVER_PORT} in record mode.`); - await getClient(); + await getRecordClient(); } async function startInReplayMode(): Promise { @@ -48,12 +51,14 @@ async function startInReplayMode(): Promise { serverPort: MOCK_SERVER_PORT, jvmOptions: [ '-Dmockserver.watchInitializationJson=true', + // We load all request/responses as expectations for the replay mode. `-Dmockserver.initializationJsonPath=${RESPONSES_JSON_PATH}`, '-Dmockserver.persistExpectations=false' ] // verbose: true }); logger.info(`✅ Server running on port ${MOCK_SERVER_PORT} in replay mode.`); + await getReplayClient(); } async function stop(): Promise { diff --git a/packages/adp-mock-server/src/types.ts b/packages/adp-mock-server/src/types.ts new file mode 100644 index 00000000000..b569cfc6652 --- /dev/null +++ b/packages/adp-mock-server/src/types.ts @@ -0,0 +1,32 @@ +import { HttpRequest, HttpResponse } from 'mockserver-client'; + +export interface HttpRequestAndHttpResponse { + httpRequest?: HttpRequest; + httpResponse?: HttpResponse; + timestamp?: string; +} + +export type BodyType = + | 'BINARY' + | 'JSON' + | 'JSON_SCHEMA' + | 'JSON_PATH' + | 'PARAMETERS' + | 'REGEX' + | 'STRING' + | 'XML' + | 'XML_SCHEMA' + | 'XPATH'; + +export interface Body { + type: BodyType; + not?: boolean; +} + +export interface XmlBody extends Body { + xml: string; +} + +export interface BinaryBody extends Body { + base64Bytes: string; +} diff --git a/packages/adp-mock-server/src/utils.ts b/packages/adp-mock-server/src/utils/cli-utils.ts similarity index 50% rename from packages/adp-mock-server/src/utils.ts rename to packages/adp-mock-server/src/utils/cli-utils.ts index f7eb4e7d58a..8d89583ec12 100644 --- a/packages/adp-mock-server/src/utils.ts +++ b/packages/adp-mock-server/src/utils/cli-utils.ts @@ -1,23 +1,5 @@ -import { setTimeout } from 'node:timers/promises'; -import { DEFAULT_SAP_SYSTEM_PORT, MOCK_DATA_FOLDER_PATH } from './constants'; -import { promises as fs } from 'fs'; -import { HttpRequestAndHttpResponse } from 'mockserver-client/mockServer'; -import { request } from 'node:http'; - type CliParamValue = string | number | boolean | undefined; -export async function wait(seconds: number): Promise { - return new Promise((resolve) => setTimeout(seconds * 1000, resolve)); -} - -export function getSapSystemPort(): number { - return parseInt(process.env.SAP_SYSTEM_PORT ?? DEFAULT_SAP_SYSTEM_PORT.toString(), 10); -} - -export function createMockDataFolderIfNeeded(): Promise { - return fs.mkdir(MOCK_DATA_FOLDER_PATH, { recursive: true }); -} - export function getCliParamValueByName(name: string): T { const arg = process.argv.find((arg) => arg.startsWith(`--${name}`)); diff --git a/packages/adp-mock-server/src/utils/file-utils.ts b/packages/adp-mock-server/src/utils/file-utils.ts new file mode 100644 index 00000000000..5db6572ff5c --- /dev/null +++ b/packages/adp-mock-server/src/utils/file-utils.ts @@ -0,0 +1,6 @@ +import fs from 'fs/promises'; +import { MOCK_DATA_FOLDER_PATH } from '../constants'; + +export function createMockDataFolderIfNeeded(): Promise { + return fs.mkdir(MOCK_DATA_FOLDER_PATH, { recursive: true }); +} diff --git a/packages/adp-mock-server/src/logger.ts b/packages/adp-mock-server/src/utils/logger.ts similarity index 75% rename from packages/adp-mock-server/src/logger.ts rename to packages/adp-mock-server/src/utils/logger.ts index 592aeee645f..117f51ca466 100644 --- a/packages/adp-mock-server/src/logger.ts +++ b/packages/adp-mock-server/src/utils/logger.ts @@ -1,4 +1,4 @@ import { ConsoleTransport, ToolsLogger } from '@sap-ux/logger'; -import { ADP_MOCK_SERVER_LOG_PREFIX } from './constants'; +import { ADP_MOCK_SERVER_LOG_PREFIX } from '../constants'; export const logger = new ToolsLogger({ logPrefix: ADP_MOCK_SERVER_LOG_PREFIX, transports: [new ConsoleTransport()] }); diff --git a/packages/adp-mock-server/src/utils/sap-system-utils.ts b/packages/adp-mock-server/src/utils/sap-system-utils.ts new file mode 100644 index 00000000000..80b019e3aba --- /dev/null +++ b/packages/adp-mock-server/src/utils/sap-system-utils.ts @@ -0,0 +1,5 @@ +import { DEFAULT_SAP_SYSTEM_PORT } from '../constants'; + +export function getSapSystemPort(): number { + return parseInt(process.env.SAP_SYSTEM_PORT ?? DEFAULT_SAP_SYSTEM_PORT.toString(), 10); +} diff --git a/packages/adp-mock-server/src/utils/type-guards.ts b/packages/adp-mock-server/src/utils/type-guards.ts new file mode 100644 index 00000000000..f6eda763915 --- /dev/null +++ b/packages/adp-mock-server/src/utils/type-guards.ts @@ -0,0 +1,13 @@ +import { BinaryBody, Body, XmlBody } from '../types'; + +export function isBodyType(body: unknown): body is Body { + return typeof body === 'object' && body !== null && 'type' in body && typeof body.type === 'string'; +} + +export function isBinaryBody(body: unknown): body is BinaryBody { + return isBodyType(body) && body.type === 'BINARY' && typeof (body as any).base64Bytes === 'string'; +} + +export function isXmlBody(body: unknown): body is XmlBody { + return isBodyType(body) && body.type === 'XML' && typeof (body as any).xml === 'string'; +} diff --git a/packages/adp-mock-server/tsconfig.json b/packages/adp-mock-server/tsconfig.json index 3552e9ff300..c68dc3d7a07 100644 --- a/packages/adp-mock-server/tsconfig.json +++ b/packages/adp-mock-server/tsconfig.json @@ -7,7 +7,7 @@ "declaration": true, "declarationMap": true, "sourceMap": true, - "typeRoots": ["./types", "./node_modules/@types"] + "typeRoots": ["./node_modules/@types", "./types"] }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "test"], diff --git a/packages/adp-mock-server/types/mockserver-node.d.ts b/packages/adp-mock-server/types/mockserver-node.d.ts index ff4c6863cc7..ec51795e79f 100644 --- a/packages/adp-mock-server/types/mockserver-node.d.ts +++ b/packages/adp-mock-server/types/mockserver-node.d.ts @@ -13,4 +13,4 @@ declare module 'mockserver-node' { export function start_mockserver(options?: MockServerOptions): Promise; export function stop_mockserver(options?: MockServerOptions): Promise; -} +} \ No newline at end of file diff --git a/packages/axios-extension/src/utils/axios-traffic.ts b/packages/axios-extension/src/utils/axios-traffic.ts index 4545e207bf7..aa0ebc8ba5f 100644 --- a/packages/axios-extension/src/utils/axios-traffic.ts +++ b/packages/axios-extension/src/utils/axios-traffic.ts @@ -31,7 +31,6 @@ interface MuabResponse { } function logAxiosTrafficInternal(logger: ToolsLogger, isGeneratorWorkflow: boolean = true) { - return; const prototype = Axios.prototype; const originalRequest = prototype.request; const muabConfigStream = fs.createWriteStream(TEMP_MUAB_CONFIG_PATH, { flags: 'w' }); From 156ef49ae47bb0dededd65b9197a4c7c64aca626 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Thu, 6 Nov 2025 10:52:28 +0200 Subject: [PATCH 13/21] fix: Move types to dev dependency. --- packages/adp-mock-server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adp-mock-server/package.json b/packages/adp-mock-server/package.json index 902af280212..0223625e3f2 100644 --- a/packages/adp-mock-server/package.json +++ b/packages/adp-mock-server/package.json @@ -26,13 +26,13 @@ "license": "Apache-2.0", "dependencies": { "@sap-ux/logger": "workspace:*", - "@types/lodash": "4.14.202", "dotenv": "16.3.1", "lodash": "4.17.21", "mockserver-client": "5.15.0" }, "devDependencies": { "@jest/types": "30.0.1", + "@types/lodash": "4.14.202", "mockserver-node": "5.15.0", "rimraf": "5.0.5", "ts-node": "10.9.2", From 56331795a14c7373895e132ad318d5a3fb181aa5 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Thu, 6 Nov 2025 16:13:50 +0200 Subject: [PATCH 14/21] feat(unstable): Mock requests with binary payload manually with mock-server nodejs apis. This relates to the app descriptor sync requests which each time sent different byte array for the same files since the zipper includes some metadata like timestamps etc. --- packages/adp-mock-server/package.json | 2 + .../src/client/replay-client.ts | 87 +++++++++++-------- .../adp-mock-server/src/utils/file-utils.ts | 35 ++++++++ .../adp-mock-server/src/utils/type-guards.ts | 1 + 4 files changed, 90 insertions(+), 35 deletions(-) diff --git a/packages/adp-mock-server/package.json b/packages/adp-mock-server/package.json index 0223625e3f2..ed1aaa4afeb 100644 --- a/packages/adp-mock-server/package.json +++ b/packages/adp-mock-server/package.json @@ -26,12 +26,14 @@ "license": "Apache-2.0", "dependencies": { "@sap-ux/logger": "workspace:*", + "adm-zip": "0.5.10", "dotenv": "16.3.1", "lodash": "4.17.21", "mockserver-client": "5.15.0" }, "devDependencies": { "@jest/types": "30.0.1", + "@types/adm-zip": "0.5.5", "@types/lodash": "4.14.202", "mockserver-node": "5.15.0", "rimraf": "5.0.5", diff --git a/packages/adp-mock-server/src/client/replay-client.ts b/packages/adp-mock-server/src/client/replay-client.ts index 10d144a4cb8..7034c080089 100644 --- a/packages/adp-mock-server/src/client/replay-client.ts +++ b/packages/adp-mock-server/src/client/replay-client.ts @@ -1,55 +1,72 @@ import { once } from 'lodash'; +import { Expectation, HttpRequest, HttpResponse } from 'mockserver-client'; import { mockServerClient, MockServerClient } from 'mockserver-client/mockServerClient'; import { MOCK_SERVER_PORT } from '../constants'; -import { Body, HttpRequestAndHttpResponse } from '../types'; import { logger } from '../utils/logger'; -import { isBinaryBody, isBodyType, isXmlBody } from '../utils/type-guards'; -import { Expectation, HttpRequest } from 'mockserver-client'; +import { isBinaryBody } from '../utils/type-guards'; +import { normalizeZipBody } from '../utils/file-utils'; + +const NOT_FOUND_RESPONSE: HttpResponse = { + statusCode: 404, + reasonPhrase: '[ADP] Not found.' +}; export const getReplayClient = once(getReplayClientInternal); async function getReplayClientInternal(): Promise { logger.info('Init mock server client.'); const client = mockServerClient('localhost', MOCK_SERVER_PORT); - // await patchActiveExpectations(client); + await patchRequestsWithBinaryBody(client); return client; } -async function patchActiveExpectations(client: MockServerClient): Promise { +async function patchRequestsWithBinaryBody(client: MockServerClient): Promise { const requestResponseList = await retrieveActiveExpectations(client); - const mockPromises = requestResponseList - .filter(({ httpRequest, httpResponse }) => isBodyType(httpRequest?.body) || isBodyType(httpResponse?.body)) - .map(({ httpRequest, httpResponse }) => ({ - httpRequest: { - ...httpRequest, - body: parseBody(httpRequest?.body) - }, - httpResponse: { - ...httpResponse, - body: parseBody(httpResponse?.body) - } - })) - .map(({ httpRequest, httpResponse }) => client.clear(httpRequest as HttpRequest, 'EXPECTATIONS')); - await Promise.all(mockPromises); - logger.info(mockPromises); -} + const requestWithBinaryBodyResponseList = requestResponseList.filter(({ httpRequest }) => + isBinaryBody((httpRequest as HttpRequest).body) + ); + const uniqueRequestPaths = new Set( + requestWithBinaryBodyResponseList.map(({ httpRequest }) => (httpRequest as HttpRequest).path) + ); + await Promise.all( + Array.from(uniqueRequestPaths).map((path) => + client.mockWithCallback( + { path }, + (requst) => { + if (requst.path !== path || !isBinaryBody(requst.body)) { + return NOT_FOUND_RESPONSE; + } -async function retrieveActiveExpectations(client: MockServerClient): Promise { - return client.retrieveActiveExpectations({}) as Promise; -} + const expectation = requestWithBinaryBodyResponseList.find( + (expectation) => (expectation.httpRequest as HttpRequest).path === path + ); + + if (!expectation) { + return NOT_FOUND_RESPONSE; + } -function parseBody(body: unknown): Body | string | unknown { - if (!isBodyType(body)) { - return body; - } + const expectedRequest = expectation.httpRequest as HttpRequest; - if (isXmlBody(body)) { - return body.xml; - } + if (!isBinaryBody(expectedRequest.body)) { + return NOT_FOUND_RESPONSE; + } - if (isBinaryBody(body)) { - return body.base64Bytes; - } + const receivedRequestBuffer = Buffer.from(requst.body.base64Bytes, 'base64'); + const expectedRequestBuffer = Buffer.from(expectedRequest.body.base64Bytes, 'base64'); + + if (normalizeZipBody(expectedRequestBuffer) === normalizeZipBody(receivedRequestBuffer)) { + return expectation.httpResponse!; + } + + return NOT_FOUND_RESPONSE; + }, + { unlimited: true } // TODO a.vasilev: [optimization] add id param to the mockWithCallback to override + // the request from recorded file to reduce the failing attempts. + ) + ) + ); +} - return body; +async function retrieveActiveExpectations(client: MockServerClient): Promise { + return client.retrieveActiveExpectations({}); } diff --git a/packages/adp-mock-server/src/utils/file-utils.ts b/packages/adp-mock-server/src/utils/file-utils.ts index 5db6572ff5c..8d18a959339 100644 --- a/packages/adp-mock-server/src/utils/file-utils.ts +++ b/packages/adp-mock-server/src/utils/file-utils.ts @@ -1,6 +1,41 @@ +import AdmZip from 'adm-zip'; +import { createHash } from 'crypto'; import fs from 'fs/promises'; import { MOCK_DATA_FOLDER_PATH } from '../constants'; +interface FileInfo { + name: string; + hash: string; +} + +const SHA256_ALGORITHM = 'sha256'; +const alphabeticalOrder = (fileA: FileInfo, fileB: FileInfo) => fileA.name.localeCompare(fileB.name); + export function createMockDataFolderIfNeeded(): Promise { return fs.mkdir(MOCK_DATA_FOLDER_PATH, { recursive: true }); } + +export function normalizeZipBody(buffer: Buffer): string { + const zip = new AdmZip(buffer); + const entries = zip.getEntries(); + + // Create a deterministic stable representation. + const fileInfos = entries.map((entry) => { + const fileData = Uint8Array.from(entry.getData()); + return { + name: entry.entryName, + // Use a stable hash of file content. We use Uint8Array since Buffer inherits Uint8Array. + hash: createHash(SHA256_ALGORITHM).update(fileData).digest('hex') + }; + }); + + // Sort so order doesn’t matter. + fileInfos.sort(alphabeticalOrder); + + return JSON.stringify(fileInfos); +} + +function getFileContentStableHash(fileDataBuffer: Buffer): string { + const fileDataArray = Uint8Array.from(fileDataBuffer); + return createHash(SHA256_ALGORITHM).update(fileDataArray).digest('hex'); +} diff --git a/packages/adp-mock-server/src/utils/type-guards.ts b/packages/adp-mock-server/src/utils/type-guards.ts index f6eda763915..d9032173f2c 100644 --- a/packages/adp-mock-server/src/utils/type-guards.ts +++ b/packages/adp-mock-server/src/utils/type-guards.ts @@ -1,3 +1,4 @@ +import { HttpRequest } from 'mockserver-client'; import { BinaryBody, Body, XmlBody } from '../types'; export function isBodyType(body: unknown): body is Body { From 9b9b200e6a713dd904ad9cdc50aecf9c38d2a18d Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Sat, 8 Nov 2025 12:33:13 +0200 Subject: [PATCH 15/21] fix: * Patch some types exported from mock-server framework. * Do not record request headers. * Replay properly responses with zip file body. * Add needed data structures. --- packages/adp-mock-server/.gitignore | 3 +- .../src/client/record-client.ts | 48 ++++++--- .../src/client/replay-client.ts | 100 +++++++++++------- packages/adp-mock-server/src/types.ts | 10 +- .../adp-mock-server/src/utils/file-utils.ts | 20 ++-- .../adp-mock-server/src/utils/hash-map.ts | 66 ++++++++++++ .../adp-mock-server/src/utils/hash-set.ts | 22 ++++ 7 files changed, 200 insertions(+), 69 deletions(-) create mode 100644 packages/adp-mock-server/src/utils/hash-map.ts create mode 100644 packages/adp-mock-server/src/utils/hash-set.ts diff --git a/packages/adp-mock-server/.gitignore b/packages/adp-mock-server/.gitignore index 6d66c3e2551..68c8da22c2e 100644 --- a/packages/adp-mock-server/.gitignore +++ b/packages/adp-mock-server/.gitignore @@ -1 +1,2 @@ -mock-data \ No newline at end of file +mock-data +CertificateAuthorityCertificate.pem \ No newline at end of file diff --git a/packages/adp-mock-server/src/client/record-client.ts b/packages/adp-mock-server/src/client/record-client.ts index 6d8626a77bd..e5d36ea0cb8 100644 --- a/packages/adp-mock-server/src/client/record-client.ts +++ b/packages/adp-mock-server/src/client/record-client.ts @@ -1,5 +1,5 @@ import fs from 'fs/promises'; -import { once } from 'lodash'; +import { omit, once } from 'lodash'; import { mockServerClient } from 'mockserver-client'; import type { MockServerClient } from 'mockserver-client/mockServerClient'; import { MOCK_SERVER_PORT, RESPONSES_JSON_PATH } from '../constants'; @@ -12,19 +12,21 @@ export const getRecordClient = once(getRecordClientInternal); export async function recordResponses(): Promise { logger.info('Record responses.'); const client = await getRecordClient(); - let requestResponseList = await retrieveRecordedRequestsAndResponses(client); - requestResponseList = disableRequestsSecureFlag(requestResponseList); - await fs.writeFile(RESPONSES_JSON_PATH, JSON.stringify(requestResponseList, null, 2)); + let requestAndResponseList = await retrieveRecordedRequestsAndResponses(client); + requestAndResponseList = disableRequestsSecureFlag(requestAndResponseList); + await fs.writeFile(RESPONSES_JSON_PATH, JSON.stringify(requestAndResponseList, null, 2)); } async function retrieveRecordedRequestsAndResponses(client: MockServerClient): Promise { return client.retrieveRecordedRequestsAndResponses({}) as Promise; } -function disableRequestsSecureFlag(requestResponseList: HttpRequestAndHttpResponse[]): HttpRequestAndHttpResponse[] { - return requestResponseList.map(({ httpRequest, httpResponse }) => ({ +function disableRequestsSecureFlag(requestAndResponseList: HttpRequestAndHttpResponse[]): HttpRequestAndHttpResponse[] { + return requestAndResponseList.map(({ httpRequest, httpResponse }) => ({ httpRequest: { - ...httpRequest, + // TODO a.vasilev: Some times the settings request does not match + // the recordings due to a changed cookie. How is this possible, sb is changing the SAP_SESSION_ID cookie in replay mode??? + ...omit(httpRequest, ['headers', 'cookies']), secure: false }, httpResponse @@ -34,16 +36,38 @@ function disableRequestsSecureFlag(requestResponseList: HttpRequestAndHttpRespon async function getRecordClientInternal(): Promise { logger.info('Init mock server client.'); const client = mockServerClient('localhost', MOCK_SERVER_PORT); + const host = process.env.SAP_SYSTEM_HOST; + const port = getSapSystemPort(); await client.mockAnyResponse({ - httpRequest: { - secure: false - }, + // httpRequest: { + // secure: false + // }, httpForward: { // Forwards https requests to the actual sap system. scheme: 'HTTPS', - host: process.env.SAP_SYSTEM_HOST, - port: getSapSystemPort() + host, + port } }); + // await client.mockAnyResponse({ + // httpRequest: { + // path: '/.*\\.properties' + // }, + // httpForward: { + // scheme: 'HTTPS', + // host, + // port + // } + // }); + // await client.mockAnyResponse({ + // httpRequest: { + // path: '/.*\\.js' + // }, + // httpForward: { + // scheme: 'HTTPS', + // host, + // port + // } + // }); return client; } diff --git a/packages/adp-mock-server/src/client/replay-client.ts b/packages/adp-mock-server/src/client/replay-client.ts index 7034c080089..7988c15f8ef 100644 --- a/packages/adp-mock-server/src/client/replay-client.ts +++ b/packages/adp-mock-server/src/client/replay-client.ts @@ -1,72 +1,90 @@ -import { once } from 'lodash'; -import { Expectation, HttpRequest, HttpResponse } from 'mockserver-client'; +import { isEqual, once } from 'lodash'; +import { HttpRequest, HttpResponse } from 'mockserver-client'; import { mockServerClient, MockServerClient } from 'mockserver-client/mockServerClient'; import { MOCK_SERVER_PORT } from '../constants'; +import { BinaryBody, Expectation } from '../types'; +import { normalizeZipFileContent } from '../utils/file-utils'; +import { HashMap, HashMapKeyComparator } from '../utils/hash-map'; import { logger } from '../utils/logger'; import { isBinaryBody } from '../utils/type-guards'; -import { normalizeZipBody } from '../utils/file-utils'; +import { HashSet } from '../utils/hash-set'; +import { getSapSystemPort } from '../utils/sap-system-utils'; + +type RequestMatcher = Pick; const NOT_FOUND_RESPONSE: HttpResponse = { statusCode: 404, reasonPhrase: '[ADP] Not found.' }; +const zipRequestsComparator: HashMapKeyComparator = (requestA, requestB) => + requestA.path === requestB.path && + requestA.method === requestB.method && + isEqual(requestA.queryStringParameters, requestB.queryStringParameters) && + isBinaryBody(requestA.body) && + isBinaryBody(requestB.body) && + isBinaryBodyEqualWith(requestA.body, requestB.body); + +const isBinaryBodyEqualWith = (aBody: BinaryBody, bBody: BinaryBody) => { + const aBodyBuffer = Buffer.from(aBody.base64Bytes, 'base64'); + const bBodyBuffer = Buffer.from(bBody.base64Bytes, 'base64'); + return normalizeZipFileContent(aBodyBuffer) === normalizeZipFileContent(bBodyBuffer); +}; + +const requestMatcherComparator = (matcherA: RequestMatcher, matcherB: RequestMatcher) => + matcherA.path === matcherB.path && matcherA.method === matcherB.method; + export const getReplayClient = once(getReplayClientInternal); async function getReplayClientInternal(): Promise { logger.info('Init mock server client.'); const client = mockServerClient('localhost', MOCK_SERVER_PORT); - await patchRequestsWithBinaryBody(client); + await patchZipRequests(client); return client; } -async function patchRequestsWithBinaryBody(client: MockServerClient): Promise { - const requestResponseList = await retrieveActiveExpectations(client); - const requestWithBinaryBodyResponseList = requestResponseList.filter(({ httpRequest }) => - isBinaryBody((httpRequest as HttpRequest).body) - ); - const uniqueRequestPaths = new Set( - requestWithBinaryBodyResponseList.map(({ httpRequest }) => (httpRequest as HttpRequest).path) +async function patchZipRequests(client: MockServerClient): Promise { + const expectationsList = await retrieveActiveExpectations(client); + const zipExpectationsList = expectationsList.filter(({ httpRequest }) => isBinaryBody(httpRequest?.body)); + const zipResponsesByRequestMap = new HashMap(zipRequestsComparator); + zipExpectationsList.forEach(({ httpRequest, httpResponse }) => { + if (zipResponsesByRequestMap.has(httpRequest!)) { + zipResponsesByRequestMap.get(httpRequest!)?.push(httpResponse!); + } else { + zipResponsesByRequestMap.set(httpRequest!, [httpResponse!]); + } + }); + const zipRequestMatcherSet = new HashSet(requestMatcherComparator); + const zipRequestMatcherList = Array.from(zipResponsesByRequestMap.keys()).map(({ path, method }) => ({ + path, + method + })); + zipRequestMatcherList.forEach((matcher) => { + zipRequestMatcherSet.add(matcher); + }); + const zipResponseIteratorsByRequestMap = zipResponsesByRequestMap.map( + ([httpRequest, httpResponses]) => [httpRequest, httpResponses.values()], + zipRequestsComparator ); + await Promise.all( - Array.from(uniqueRequestPaths).map((path) => + zipRequestMatcherSet.values().map((matcher) => client.mockWithCallback( - { path }, - (requst) => { - if (requst.path !== path || !isBinaryBody(requst.body)) { + matcher, + (httpRequest) => { + if (!zipResponseIteratorsByRequestMap.has(httpRequest)) { return NOT_FOUND_RESPONSE; } - - const expectation = requestWithBinaryBodyResponseList.find( - (expectation) => (expectation.httpRequest as HttpRequest).path === path - ); - - if (!expectation) { - return NOT_FOUND_RESPONSE; - } - - const expectedRequest = expectation.httpRequest as HttpRequest; - - if (!isBinaryBody(expectedRequest.body)) { - return NOT_FOUND_RESPONSE; - } - - const receivedRequestBuffer = Buffer.from(requst.body.base64Bytes, 'base64'); - const expectedRequestBuffer = Buffer.from(expectedRequest.body.base64Bytes, 'base64'); - - if (normalizeZipBody(expectedRequestBuffer) === normalizeZipBody(receivedRequestBuffer)) { - return expectation.httpResponse!; - } - - return NOT_FOUND_RESPONSE; + const httpResponsesIterator = zipResponseIteratorsByRequestMap.get(httpRequest); + const nextResponse = httpResponsesIterator?.next().value; + return nextResponse ?? NOT_FOUND_RESPONSE; }, - { unlimited: true } // TODO a.vasilev: [optimization] add id param to the mockWithCallback to override - // the request from recorded file to reduce the failing attempts. + { unlimited: true } ) ) ); } async function retrieveActiveExpectations(client: MockServerClient): Promise { - return client.retrieveActiveExpectations({}); + return client.retrieveActiveExpectations({}) as Promise; } diff --git a/packages/adp-mock-server/src/types.ts b/packages/adp-mock-server/src/types.ts index b569cfc6652..4c31396b0f0 100644 --- a/packages/adp-mock-server/src/types.ts +++ b/packages/adp-mock-server/src/types.ts @@ -1,10 +1,12 @@ -import { HttpRequest, HttpResponse } from 'mockserver-client'; +import { HttpRequest, HttpResponse, Expectation as MockServerExpectation } from 'mockserver-client'; +import { HttpRequestAndHttpResponse as MockServerHttpRequestAndHttpResponse } from 'mockserver-client/mockServer'; -export interface HttpRequestAndHttpResponse { +export type HttpRequestAndHttpResponse = Omit & { httpRequest?: HttpRequest; httpResponse?: HttpResponse; - timestamp?: string; -} +}; + +export type Expectation = Omit & { httpRequest?: HttpRequest }; export type BodyType = | 'BINARY' diff --git a/packages/adp-mock-server/src/utils/file-utils.ts b/packages/adp-mock-server/src/utils/file-utils.ts index 8d18a959339..9522cb8f212 100644 --- a/packages/adp-mock-server/src/utils/file-utils.ts +++ b/packages/adp-mock-server/src/utils/file-utils.ts @@ -15,19 +15,16 @@ export function createMockDataFolderIfNeeded(): Promise { return fs.mkdir(MOCK_DATA_FOLDER_PATH, { recursive: true }); } -export function normalizeZipBody(buffer: Buffer): string { +export function normalizeZipFileContent(buffer: Buffer): string { const zip = new AdmZip(buffer); const entries = zip.getEntries(); // Create a deterministic stable representation. - const fileInfos = entries.map((entry) => { - const fileData = Uint8Array.from(entry.getData()); - return { - name: entry.entryName, - // Use a stable hash of file content. We use Uint8Array since Buffer inherits Uint8Array. - hash: createHash(SHA256_ALGORITHM).update(fileData).digest('hex') - }; - }); + const fileInfos = entries.map((entry) => ({ + name: entry.entryName, + // Use a stable hash of file content. + hash: getFileContentStableHash(entry.getData()) + })); // Sort so order doesn’t matter. fileInfos.sort(alphabeticalOrder); @@ -36,6 +33,7 @@ export function normalizeZipBody(buffer: Buffer): string { } function getFileContentStableHash(fileDataBuffer: Buffer): string { - const fileDataArray = Uint8Array.from(fileDataBuffer); - return createHash(SHA256_ALGORITHM).update(fileDataArray).digest('hex'); + // We use Uint8Array since Buffer inherits Uint8Array. + const fileDataBufferToArray = Uint8Array.from(fileDataBuffer); + return createHash(SHA256_ALGORITHM).update(fileDataBufferToArray).digest('hex'); } diff --git a/packages/adp-mock-server/src/utils/hash-map.ts b/packages/adp-mock-server/src/utils/hash-map.ts new file mode 100644 index 00000000000..4a473dde544 --- /dev/null +++ b/packages/adp-mock-server/src/utils/hash-map.ts @@ -0,0 +1,66 @@ +export type HashMapKeyComparator = (keyA: K, keyB: K) => boolean; + +export class HashMap { + private readonly keyValueMap: Map; + + constructor(private readonly keyComparator: HashMapKeyComparator) { + this.keyValueMap = new Map(); + } + + set(key: Key, value: Value): this { + const existingKey = this.findKey(key); + if (existingKey) { + this.keyValueMap.set(existingKey, value); + } else { + this.keyValueMap.set(key, value); + } + return this; + } + + has(key: Key): boolean { + return !!this.findKey(key); + } + + get(key: Key): Value | undefined { + const existingKey = this.findKey(key); + if (existingKey) { + return this.keyValueMap.get(existingKey); + } + return undefined; + } + + entries(): MapIterator<[Key, Value]> { + return this.keyValueMap.entries(); + } + + keys(): MapIterator { + return this.keyValueMap.keys(); + } + + map( + entryFactory: (entry: [Key, Value]) => [OutKey, OutValue], + keyComparator: HashMapKeyComparator + ): HashMap { + return HashMap.fromEntries(Array.from(this.entries()).map(entryFactory), keyComparator); + } + + private findKey(targetKey: Key): Key | undefined { + for (let [key] of this.keyValueMap) { + if (this.keyComparator(key, targetKey)) { + return key; + } + } + return undefined; + } + + static fromEntries( + entries: [Key, Value][], + keyComparator: HashMapKeyComparator + ): HashMap { + const hashMap = new HashMap(keyComparator); + for (const [key, value] of entries) { + hashMap.set(key, value); + } + return hashMap; + } +} diff --git a/packages/adp-mock-server/src/utils/hash-set.ts b/packages/adp-mock-server/src/utils/hash-set.ts new file mode 100644 index 00000000000..6fc7e5ace6c --- /dev/null +++ b/packages/adp-mock-server/src/utils/hash-set.ts @@ -0,0 +1,22 @@ +export type HashSetComparator = (valueA: V, valueB: V) => boolean; + +export class HashSet { + private valuesList: Value[] = []; + + constructor(private readonly comparator: HashSetComparator) {} + + add(value: Value): this { + if (!this.has(value)) { + this.valuesList.push(value); + } + return this; + } + + has(value: Value): boolean { + return this.valuesList.some((existingValue) => this.comparator(value, existingValue)); + } + + values(): Value[] { + return this.valuesList.concat(); + } +} From a93e9202dc9e60e60073352bd47af084c23a8e87 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Sat, 8 Nov 2025 13:23:04 +0200 Subject: [PATCH 16/21] refactor: Plish the code to be close to production ready. Some parametrization need to be done later, e.g. mock server port must be put in env variable. --- packages/adp-mock-server/package.json | 2 + .../src/client/record-client.ts | 37 ++++--------------- .../src/client/replay-client.ts | 37 ++++++++++--------- packages/adp-mock-server/src/index.ts | 19 ++++------ .../src/{constants.ts => server-constants.ts} | 8 ---- packages/adp-mock-server/src/types.ts | 5 +-- .../adp-mock-server/src/utils/cli-utils.ts | 5 ++- .../adp-mock-server/src/utils/file-utils.ts | 5 ++- packages/adp-mock-server/src/utils/logger.ts | 3 +- .../src/utils/sap-system-utils.ts | 2 +- .../adp-mock-server/src/utils/type-guards.ts | 14 +++++-- 11 files changed, 57 insertions(+), 80 deletions(-) rename packages/adp-mock-server/src/{constants.ts => server-constants.ts} (56%) diff --git a/packages/adp-mock-server/package.json b/packages/adp-mock-server/package.json index ed1aaa4afeb..e43834912db 100644 --- a/packages/adp-mock-server/package.json +++ b/packages/adp-mock-server/package.json @@ -14,6 +14,8 @@ "build": "tsc --build", "start": "ts-node ./src/index.ts --start", "stop": "ts-node ./src/index.ts --stop", + "start:record": "ts-node ./src/index.ts --start --record", + "stop:record": "ts-node ./src/index.ts --stop --record", "clean": "rimraf dist *.tsbuildinfo", "test": "jest --ci --forceExit --detectOpenHandles --colors", "watch": "tsc --build --watch" diff --git a/packages/adp-mock-server/src/client/record-client.ts b/packages/adp-mock-server/src/client/record-client.ts index e5d36ea0cb8..660d13d98ad 100644 --- a/packages/adp-mock-server/src/client/record-client.ts +++ b/packages/adp-mock-server/src/client/record-client.ts @@ -2,14 +2,14 @@ import fs from 'fs/promises'; import { omit, once } from 'lodash'; import { mockServerClient } from 'mockserver-client'; import type { MockServerClient } from 'mockserver-client/mockServerClient'; -import { MOCK_SERVER_PORT, RESPONSES_JSON_PATH } from '../constants'; +import { MOCK_SERVER_PORT, RESPONSES_JSON_PATH } from '../server-constants'; import { HttpRequestAndHttpResponse } from '../types'; import { logger } from '../utils/logger'; import { getSapSystemPort } from '../utils/sap-system-utils'; export const getRecordClient = once(getRecordClientInternal); -export async function recordResponses(): Promise { +export async function recordRequestsAndResponses(): Promise { logger.info('Record responses.'); const client = await getRecordClient(); let requestAndResponseList = await retrieveRecordedRequestsAndResponses(client); @@ -25,7 +25,9 @@ function disableRequestsSecureFlag(requestAndResponseList: HttpRequestAndHttpRes return requestAndResponseList.map(({ httpRequest, httpResponse }) => ({ httpRequest: { // TODO a.vasilev: Some times the settings request does not match - // the recordings due to a changed cookie. How is this possible, sb is changing the SAP_SESSION_ID cookie in replay mode??? + // the recordings due to a changed cookie. How is this possible, sb + // is changing the SAP_SESSIONID cookie in replay mode, how is that even + // possible??? ...omit(httpRequest, ['headers', 'cookies']), secure: false }, @@ -36,38 +38,13 @@ function disableRequestsSecureFlag(requestAndResponseList: HttpRequestAndHttpRes async function getRecordClientInternal(): Promise { logger.info('Init mock server client.'); const client = mockServerClient('localhost', MOCK_SERVER_PORT); - const host = process.env.SAP_SYSTEM_HOST; - const port = getSapSystemPort(); await client.mockAnyResponse({ - // httpRequest: { - // secure: false - // }, httpForward: { // Forwards https requests to the actual sap system. scheme: 'HTTPS', - host, - port + host: process.env.SAP_SYSTEM_HOST, + port: getSapSystemPort() } }); - // await client.mockAnyResponse({ - // httpRequest: { - // path: '/.*\\.properties' - // }, - // httpForward: { - // scheme: 'HTTPS', - // host, - // port - // } - // }); - // await client.mockAnyResponse({ - // httpRequest: { - // path: '/.*\\.js' - // }, - // httpForward: { - // scheme: 'HTTPS', - // host, - // port - // } - // }); return client; } diff --git a/packages/adp-mock-server/src/client/replay-client.ts b/packages/adp-mock-server/src/client/replay-client.ts index 7988c15f8ef..0d6b06115d9 100644 --- a/packages/adp-mock-server/src/client/replay-client.ts +++ b/packages/adp-mock-server/src/client/replay-client.ts @@ -1,31 +1,31 @@ import { isEqual, once } from 'lodash'; import { HttpRequest, HttpResponse } from 'mockserver-client'; import { mockServerClient, MockServerClient } from 'mockserver-client/mockServerClient'; -import { MOCK_SERVER_PORT } from '../constants'; -import { BinaryBody, Expectation } from '../types'; +import { MOCK_SERVER_PORT } from '../server-constants'; +import { Expectation } from '../types'; import { normalizeZipFileContent } from '../utils/file-utils'; import { HashMap, HashMapKeyComparator } from '../utils/hash-map'; -import { logger } from '../utils/logger'; -import { isBinaryBody } from '../utils/type-guards'; import { HashSet } from '../utils/hash-set'; -import { getSapSystemPort } from '../utils/sap-system-utils'; - -type RequestMatcher = Pick; +import { logger } from '../utils/logger'; +import { isZipBody } from '../utils/type-guards'; const NOT_FOUND_RESPONSE: HttpResponse = { statusCode: 404, reasonPhrase: '[ADP] Not found.' }; +type RequestMatcher = Pick; + const zipRequestsComparator: HashMapKeyComparator = (requestA, requestB) => requestA.path === requestB.path && requestA.method === requestB.method && isEqual(requestA.queryStringParameters, requestB.queryStringParameters) && - isBinaryBody(requestA.body) && - isBinaryBody(requestB.body) && - isBinaryBodyEqualWith(requestA.body, requestB.body); + isZipBodyEqualWith(requestA.body, requestB.body); -const isBinaryBodyEqualWith = (aBody: BinaryBody, bBody: BinaryBody) => { +const isZipBodyEqualWith = (aBody: unknown, bBody: unknown) => { + if (!isZipBody(aBody) || !isZipBody(bBody)) { + return false; + } const aBodyBuffer = Buffer.from(aBody.base64Bytes, 'base64'); const bBodyBuffer = Buffer.from(bBody.base64Bytes, 'base64'); return normalizeZipFileContent(aBodyBuffer) === normalizeZipFileContent(bBodyBuffer); @@ -39,19 +39,22 @@ export const getReplayClient = once(getReplayClientInternal); async function getReplayClientInternal(): Promise { logger.info('Init mock server client.'); const client = mockServerClient('localhost', MOCK_SERVER_PORT); - await patchZipRequests(client); + await patchZipRequestsAndResponses(client); return client; } -async function patchZipRequests(client: MockServerClient): Promise { +async function patchZipRequestsAndResponses(client: MockServerClient): Promise { const expectationsList = await retrieveActiveExpectations(client); - const zipExpectationsList = expectationsList.filter(({ httpRequest }) => isBinaryBody(httpRequest?.body)); + const zipExpectationsList = expectationsList.filter(({ httpRequest }) => isZipBody(httpRequest?.body)); const zipResponsesByRequestMap = new HashMap(zipRequestsComparator); zipExpectationsList.forEach(({ httpRequest, httpResponse }) => { - if (zipResponsesByRequestMap.has(httpRequest!)) { - zipResponsesByRequestMap.get(httpRequest!)?.push(httpResponse!); + if (!httpRequest || !httpResponse) { + return; + } + if (zipResponsesByRequestMap.has(httpRequest)) { + zipResponsesByRequestMap.get(httpRequest)?.push(httpResponse); } else { - zipResponsesByRequestMap.set(httpRequest!, [httpResponse!]); + zipResponsesByRequestMap.set(httpRequest, [httpResponse]); } }); const zipRequestMatcherSet = new HashSet(requestMatcherComparator); diff --git a/packages/adp-mock-server/src/index.ts b/packages/adp-mock-server/src/index.ts index 2ebc2bdc7dd..a49b931a211 100644 --- a/packages/adp-mock-server/src/index.ts +++ b/packages/adp-mock-server/src/index.ts @@ -1,22 +1,19 @@ import dotenv from 'dotenv'; import { start_mockserver, stop_mockserver } from 'mockserver-node'; import path from 'path'; -import { getRecordClient, recordResponses } from './client/record-client'; +import { getRecordClient, recordRequestsAndResponses } from './client/record-client'; import { getReplayClient } from './client/replay-client'; -import { - CLI_PARAM_RECORD, - CLI_PARAM_START, - CLI_PARAM_STOP, - EXPECTATIONS_JSON_PATH, - MOCK_SERVER_PORT, - NOOP, - RESPONSES_JSON_PATH -} from './constants'; +import { EXPECTATIONS_JSON_PATH, MOCK_SERVER_PORT, RESPONSES_JSON_PATH } from './server-constants'; import { getCliParamValueByName } from './utils/cli-utils'; import { createMockDataFolderIfNeeded } from './utils/file-utils'; import { logger } from './utils/logger'; import { getSapSystemPort } from './utils/sap-system-utils'; +const CLI_PARAM_START = 'start'; +const CLI_PARAM_STOP = 'stop'; +const CLI_PARAM_RECORD = 'record'; +const NOOP = new Promise(() => {}); + dotenv.config({ path: path.join(__dirname, '../.env') }); async function start(isRecordOrReplayMode: boolean): Promise { @@ -63,7 +60,7 @@ async function startInReplayMode(): Promise { async function stop(): Promise { if (getCliParamValueByName(CLI_PARAM_RECORD)) { - await recordResponses(); + await recordRequestsAndResponses(); } await stop_mockserver({ serverPort: MOCK_SERVER_PORT }); logger.info('Stop mock server.'); diff --git a/packages/adp-mock-server/src/constants.ts b/packages/adp-mock-server/src/server-constants.ts similarity index 56% rename from packages/adp-mock-server/src/constants.ts rename to packages/adp-mock-server/src/server-constants.ts index 79504f1cf05..fd7dc8122ae 100644 --- a/packages/adp-mock-server/src/constants.ts +++ b/packages/adp-mock-server/src/server-constants.ts @@ -1,14 +1,6 @@ -export const ADP_MOCK_SERVER_LOG_PREFIX = '[ADP][Mock server]'; - export const MOCK_SERVER_PORT = 1080; export const DEFAULT_SAP_SYSTEM_PORT = 44355; export const MOCK_DATA_FOLDER_PATH = './mock-data'; export const EXPECTATIONS_JSON_PATH = `${MOCK_DATA_FOLDER_PATH}/expectations.json`; export const RESPONSES_JSON_PATH = `${MOCK_DATA_FOLDER_PATH}/responses.json`; - -export const CLI_PARAM_START = 'start'; -export const CLI_PARAM_STOP = 'stop'; -export const CLI_PARAM_RECORD = 'record'; - -export const NOOP = new Promise(() => {}); diff --git a/packages/adp-mock-server/src/types.ts b/packages/adp-mock-server/src/types.ts index 4c31396b0f0..1c700a2043b 100644 --- a/packages/adp-mock-server/src/types.ts +++ b/packages/adp-mock-server/src/types.ts @@ -23,10 +23,7 @@ export type BodyType = export interface Body { type: BodyType; not?: boolean; -} - -export interface XmlBody extends Body { - xml: string; + contentType?: string; } export interface BinaryBody extends Body { diff --git a/packages/adp-mock-server/src/utils/cli-utils.ts b/packages/adp-mock-server/src/utils/cli-utils.ts index 8d89583ec12..6188af8a2dc 100644 --- a/packages/adp-mock-server/src/utils/cli-utils.ts +++ b/packages/adp-mock-server/src/utils/cli-utils.ts @@ -14,11 +14,12 @@ export function getCliParamValueByName(name: string): T return true as T; } - if (value.toLowerCase() === 'true') { + const valueToLowerCase = value.toLowerCase(); + if (valueToLowerCase === 'true') { return true as T; } - if (value.toLowerCase() === 'false') { + if (valueToLowerCase === 'false') { return false as T; } diff --git a/packages/adp-mock-server/src/utils/file-utils.ts b/packages/adp-mock-server/src/utils/file-utils.ts index 9522cb8f212..37c4e98a8d3 100644 --- a/packages/adp-mock-server/src/utils/file-utils.ts +++ b/packages/adp-mock-server/src/utils/file-utils.ts @@ -1,14 +1,15 @@ import AdmZip from 'adm-zip'; import { createHash } from 'crypto'; import fs from 'fs/promises'; -import { MOCK_DATA_FOLDER_PATH } from '../constants'; +import { MOCK_DATA_FOLDER_PATH } from '../server-constants'; + +const SHA256_ALGORITHM = 'sha256'; interface FileInfo { name: string; hash: string; } -const SHA256_ALGORITHM = 'sha256'; const alphabeticalOrder = (fileA: FileInfo, fileB: FileInfo) => fileA.name.localeCompare(fileB.name); export function createMockDataFolderIfNeeded(): Promise { diff --git a/packages/adp-mock-server/src/utils/logger.ts b/packages/adp-mock-server/src/utils/logger.ts index 117f51ca466..690e194c944 100644 --- a/packages/adp-mock-server/src/utils/logger.ts +++ b/packages/adp-mock-server/src/utils/logger.ts @@ -1,4 +1,5 @@ import { ConsoleTransport, ToolsLogger } from '@sap-ux/logger'; -import { ADP_MOCK_SERVER_LOG_PREFIX } from '../constants'; + +const ADP_MOCK_SERVER_LOG_PREFIX = '[ADP][Mock server]'; export const logger = new ToolsLogger({ logPrefix: ADP_MOCK_SERVER_LOG_PREFIX, transports: [new ConsoleTransport()] }); diff --git a/packages/adp-mock-server/src/utils/sap-system-utils.ts b/packages/adp-mock-server/src/utils/sap-system-utils.ts index 80b019e3aba..7da8a0fcc56 100644 --- a/packages/adp-mock-server/src/utils/sap-system-utils.ts +++ b/packages/adp-mock-server/src/utils/sap-system-utils.ts @@ -1,4 +1,4 @@ -import { DEFAULT_SAP_SYSTEM_PORT } from '../constants'; +import { DEFAULT_SAP_SYSTEM_PORT } from '../server-constants'; export function getSapSystemPort(): number { return parseInt(process.env.SAP_SYSTEM_PORT ?? DEFAULT_SAP_SYSTEM_PORT.toString(), 10); diff --git a/packages/adp-mock-server/src/utils/type-guards.ts b/packages/adp-mock-server/src/utils/type-guards.ts index d9032173f2c..0f0a5d528a6 100644 --- a/packages/adp-mock-server/src/utils/type-guards.ts +++ b/packages/adp-mock-server/src/utils/type-guards.ts @@ -1,5 +1,6 @@ -import { HttpRequest } from 'mockserver-client'; -import { BinaryBody, Body, XmlBody } from '../types'; +import { BinaryBody, Body } from '../types'; + +const ZIP_CONTENT_TYPE = 'application/zip'; export function isBodyType(body: unknown): body is Body { return typeof body === 'object' && body !== null && 'type' in body && typeof body.type === 'string'; @@ -9,6 +10,11 @@ export function isBinaryBody(body: unknown): body is BinaryBody { return isBodyType(body) && body.type === 'BINARY' && typeof (body as any).base64Bytes === 'string'; } -export function isXmlBody(body: unknown): body is XmlBody { - return isBodyType(body) && body.type === 'XML' && typeof (body as any).xml === 'string'; +export function isZipBody(body: unknown): body is BinaryBody { + return ( + isBodyType(body) && + body.type === 'BINARY' && + typeof (body as any).base64Bytes === 'string' && + (body as any).contentType === ZIP_CONTENT_TYPE + ); } From bf74c27f175d1e63f40bbe0a8b98898bbf85690c Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Tue, 11 Nov 2025 19:28:01 +0200 Subject: [PATCH 17/21] test: Remove jest dependency since iot is proided by the monorepo already. Add tests to the normalization logic related to appdescr_variant_preview put request which reponds with different zip base64 byte array for the same files - probaly ABAP specific. --- packages/adp-mock-server/package.json | 1 - .../test/fixtures/zipBase64Bytes1.txt | 1 + .../test/fixtures/zipBase64Bytes2.txt | 1 + .../test/unit/utils/file-utils.test.ts | 197 ++++++++++++++++++ .../test/utils/fixture-utils.ts | 6 + packages/adp-mock-server/tsconfig.json | 2 +- 6 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 packages/adp-mock-server/test/fixtures/zipBase64Bytes1.txt create mode 100644 packages/adp-mock-server/test/fixtures/zipBase64Bytes2.txt create mode 100644 packages/adp-mock-server/test/unit/utils/file-utils.test.ts create mode 100644 packages/adp-mock-server/test/utils/fixture-utils.ts diff --git a/packages/adp-mock-server/package.json b/packages/adp-mock-server/package.json index e43834912db..bfa0fe009d6 100644 --- a/packages/adp-mock-server/package.json +++ b/packages/adp-mock-server/package.json @@ -34,7 +34,6 @@ "mockserver-client": "5.15.0" }, "devDependencies": { - "@jest/types": "30.0.1", "@types/adm-zip": "0.5.5", "@types/lodash": "4.14.202", "mockserver-node": "5.15.0", diff --git a/packages/adp-mock-server/test/fixtures/zipBase64Bytes1.txt b/packages/adp-mock-server/test/fixtures/zipBase64Bytes1.txt new file mode 100644 index 00000000000..a0d1c5a9423 --- /dev/null +++ b/packages/adp-mock-server/test/fixtures/zipBase64Bytes1.txt @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/packages/adp-mock-server/test/fixtures/zipBase64Bytes2.txt b/packages/adp-mock-server/test/fixtures/zipBase64Bytes2.txt new file mode 100644 index 00000000000..ce2061c3b44 --- /dev/null +++ b/packages/adp-mock-server/test/fixtures/zipBase64Bytes2.txt @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/packages/adp-mock-server/test/unit/utils/file-utils.test.ts b/packages/adp-mock-server/test/unit/utils/file-utils.test.ts new file mode 100644 index 00000000000..79fe344fd62 --- /dev/null +++ b/packages/adp-mock-server/test/unit/utils/file-utils.test.ts @@ -0,0 +1,197 @@ +import AdmZip from 'adm-zip'; +import { createHash } from 'crypto'; +import fs from 'fs/promises'; +import { MOCK_DATA_FOLDER_PATH } from '../../../src/server-constants'; +import { createMockDataFolderIfNeeded, normalizeZipFileContent } from '../../../src/utils/file-utils'; +import { readFixture } from '../../utils/fixture-utils'; + +jest.mock('fs/promises'); +const mockFs = fs as jest.Mocked; + +describe('file-utils', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createMockDataFolderIfNeeded', () => { + test('should return the result from fs.mkdir', async () => { + const expectedResult = 'mock-result' as any; + mockFs.mkdir.mockResolvedValue(expectedResult); + + const result = await createMockDataFolderIfNeeded(); + + expect(result).toBe(expectedResult); + expect(mockFs.mkdir).toHaveBeenCalledWith(MOCK_DATA_FOLDER_PATH, { recursive: true }); + }); + }); + + describe('normalizeZipFileContent', () => { + test('should return deterministic output for same ZIP content', () => { + const files = [ + { name: 'file1.txt', content: 'content1' }, + { name: 'file2.txt', content: 'content2' } + ]; + const zipBuffer = createZipBuffer(files); + + const result1 = normalizeZipFileContent(zipBuffer); + const result2 = normalizeZipFileContent(zipBuffer); + + expect(result1).toBe(result2); + expect(zipBuffer); + }); + + test('should return deterministic output for same ZIP content when the zip buffer has different base64 byte representation', () => { + const zipBase64Bytes1 = readFixture('zipBase64Bytes1.txt'); + const zipBase64Bytes2 = readFixture('zipBase64Bytes2.txt'); + + const zipBuffer1 = Buffer.from(zipBase64Bytes1, 'base64'); + const zipBuffer2 = Buffer.from(zipBase64Bytes2, 'base64'); + + const result1 = normalizeZipFileContent(zipBuffer1); + const result2 = normalizeZipFileContent(zipBuffer2); + + expect(zipBase64Bytes1).not.toEqual(zipBase64Bytes2); + expect(result1).toEqual(result2); + }); + + test('should sort files alphabetically by name', () => { + const files = [ + { name: 'c-file.txt', content: 'content-z' }, + { name: 'a-file.txt', content: 'content-a' }, + { name: 'b-file.txt', content: 'content-m' } + ]; + const zipBuffer = createZipBuffer(files); + + const result = normalizeZipFileContent(zipBuffer); + const parsed = JSON.parse(result); + + expect(parsed).toHaveLength(3); + expect(parsed[0].name).toBe('a-file.txt'); + expect(parsed[1].name).toBe('b-file.txt'); + expect(parsed[2].name).toBe('c-file.txt'); + }); + + test('should generate correct file hashes', () => { + const files = [{ name: 'test.txt', content: 'test content' }]; + const zipBuffer = createZipBuffer(files); + + const result = normalizeZipFileContent(zipBuffer); + const parsed = JSON.parse(result); + + const expectedHash = getFileContentStableHash('test content'); + expect(parsed[0]).toEqual({ + name: 'test.txt', + hash: expectedHash + }); + expect(parsed).toHaveLength(1); + }); + + test('should handle empty ZIP files', () => { + const zip = new AdmZip(); + const emptyZipBuffer = zip.toBuffer(); + + const result = normalizeZipFileContent(emptyZipBuffer); + const parsed = JSON.parse(result); + + expect(parsed).toEqual([]); + }); + + test('should produce same result regardless of file addition order', () => { + // Create ZIP with files in different orders + const files1 = [ + { name: 'file1.txt', content: 'content1' }, + { name: 'file2.txt', content: 'content2' } + ]; + const files2 = [ + { name: 'file2.txt', content: 'content2' }, + { name: 'file1.txt', content: 'content1' } + ]; + + const zipBuffer1 = createZipBuffer(files1); + const zipBuffer2 = createZipBuffer(files2); + + const result1 = normalizeZipFileContent(zipBuffer1); + const result2 = normalizeZipFileContent(zipBuffer2); + + expect(result1).toBe(result2); + }); + + test('should handle files with same content but different names', () => { + const files = [ + { name: 'file1.txt', content: 'same content' }, + { name: 'file2.txt', content: 'same content' } + ]; + const zipBuffer = createZipBuffer(files); + + const result = normalizeZipFileContent(zipBuffer); + const parsed = JSON.parse(result); + + expect(parsed).toHaveLength(2); + expect(parsed[0].name).toBe('file1.txt'); + expect(parsed[1].name).toBe('file2.txt'); + // Both should have the same hash since content is identical + expect(parsed[0].hash).toBe(parsed[1].hash); + }); + + test('should handle files with different content', () => { + const files = [ + { name: 'file1.txt', content: 'content1' }, + { name: 'file2.txt', content: 'content2' } + ]; + const zipBuffer = createZipBuffer(files); + + const result = normalizeZipFileContent(zipBuffer); + const parsed = JSON.parse(result); + + expect(parsed).toHaveLength(2); + expect(parsed[0].hash).not.toBe(parsed[1].hash); + }); + + test('should handle binary content', () => { + const zip = new AdmZip(); + const binaryContent = Buffer.from([0x00, 0x01, 0x02, 0xff]); + zip.addFile('binary.bin', binaryContent); + const zipBuffer = zip.toBuffer(); + + const result = normalizeZipFileContent(zipBuffer); + const parsed = JSON.parse(result); + + expect(parsed).toHaveLength(1); + expect(parsed[0].name).toBe('binary.bin'); + expect(typeof parsed[0].hash).toBe('string'); + expect(parsed[0].hash).toHaveLength(64); // SHA256 hex length + }); + + test('should handle special characters in filenames', () => { + const files = [ + { name: 'файл.txt', content: 'unicode content' }, + { name: 'file with spaces.txt', content: 'space content' }, + { name: 'file-with-dashes.txt', content: 'dash content' } + ]; + const zipBuffer = createZipBuffer(files); + + const result = normalizeZipFileContent(zipBuffer); + const parsed = JSON.parse(result); + + expect(parsed).toHaveLength(3); + // Should be sorted alphabetically + const names = parsed.map((f: any) => f.name); + const sortedNames = [...names].sort(); + expect(names).toEqual(sortedNames); + }); + }); +}); + +function createZipBuffer(files: Array<{ name: string; content: string }>): Buffer { + const zip = new AdmZip(); + files.forEach(({ name, content }) => { + zip.addFile(name, Buffer.from(content, 'utf-8')); + }); + return zip.toBuffer(); +} + +function getFileContentStableHash(content: string): string { + const buffer = Buffer.from(content, 'utf-8'); + const uint8Array = Uint8Array.from(buffer); + return createHash('sha256').update(uint8Array).digest('hex'); +} diff --git a/packages/adp-mock-server/test/utils/fixture-utils.ts b/packages/adp-mock-server/test/utils/fixture-utils.ts new file mode 100644 index 00000000000..4b735338e1d --- /dev/null +++ b/packages/adp-mock-server/test/utils/fixture-utils.ts @@ -0,0 +1,6 @@ +import fs from 'fs'; +import path from 'path'; + +export function readFixture(name: string): string { + return fs.readFileSync(path.resolve(__dirname, '..', 'fixtures', name), 'utf8'); +} diff --git a/packages/adp-mock-server/tsconfig.json b/packages/adp-mock-server/tsconfig.json index c68dc3d7a07..bb98063b5ad 100644 --- a/packages/adp-mock-server/tsconfig.json +++ b/packages/adp-mock-server/tsconfig.json @@ -7,7 +7,7 @@ "declaration": true, "declarationMap": true, "sourceMap": true, - "typeRoots": ["./node_modules/@types", "./types"] + "typeRoots": ["./node_modules/@types", "./types", "../../node_modules/@types"] }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "test"], From 6c92121385336ee2a637143f93300ee24538e6b0 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Tue, 11 Nov 2025 19:30:46 +0200 Subject: [PATCH 18/21] chore: Add test case description related to the ABAP appdescr-variant_preview put api call. --- packages/adp-mock-server/test/unit/utils/file-utils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adp-mock-server/test/unit/utils/file-utils.test.ts b/packages/adp-mock-server/test/unit/utils/file-utils.test.ts index 79fe344fd62..e61efd453a0 100644 --- a/packages/adp-mock-server/test/unit/utils/file-utils.test.ts +++ b/packages/adp-mock-server/test/unit/utils/file-utils.test.ts @@ -40,7 +40,7 @@ describe('file-utils', () => { expect(zipBuffer); }); - test('should return deterministic output for same ZIP content when the zip buffer has different base64 byte representation', () => { + test('should return deterministic output for same ZIP content when the zip buffer has different base64 byte representation (ABAP behaviour)', () => { const zipBase64Bytes1 = readFixture('zipBase64Bytes1.txt'); const zipBase64Bytes2 = readFixture('zipBase64Bytes2.txt'); From 55e57827522194a20bc98ddae571421b1288ae18 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Tue, 11 Nov 2025 20:15:06 +0200 Subject: [PATCH 19/21] test: Fix test mocks. --- packages/adp-mock-server/test/unit/utils/file-utils.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/adp-mock-server/test/unit/utils/file-utils.test.ts b/packages/adp-mock-server/test/unit/utils/file-utils.test.ts index e61efd453a0..08f60d9affa 100644 --- a/packages/adp-mock-server/test/unit/utils/file-utils.test.ts +++ b/packages/adp-mock-server/test/unit/utils/file-utils.test.ts @@ -56,9 +56,9 @@ describe('file-utils', () => { test('should sort files alphabetically by name', () => { const files = [ - { name: 'c-file.txt', content: 'content-z' }, + { name: 'c-file.txt', content: 'content-c' }, { name: 'a-file.txt', content: 'content-a' }, - { name: 'b-file.txt', content: 'content-m' } + { name: 'b-file.txt', content: 'content-b' } ]; const zipBuffer = createZipBuffer(files); From 05ba208075651b692700261766ff9f23f3d058ea Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Wed, 12 Nov 2025 13:55:09 +0200 Subject: [PATCH 20/21] chore: Disable logging from axios traffic recorder + add logging for the appdescr_variant_preview api call, we log the buffer passed as request payload - its base 64 string representation and the zip file content. The zip content is always the same but the base64 string differs. --- .../src/base/abap/manifest-service.ts | 9 ++++++ .../adp-tooling/src/preview/adp-preview.ts | 10 ++++++- .../src/utils/axios-traffic.ts | 28 +++++++++---------- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/packages/adp-tooling/src/base/abap/manifest-service.ts b/packages/adp-tooling/src/base/abap/manifest-service.ts index 1cbe5d3d10d..d0635870a0e 100644 --- a/packages/adp-tooling/src/base/abap/manifest-service.ts +++ b/packages/adp-tooling/src/base/abap/manifest-service.ts @@ -148,6 +148,15 @@ export class ManifestService { const buffer = zip.toBuffer(); const lrep = this.provider.getLayeredRepository(); await lrep.getCsrfToken(); + const entries = zip.getEntries(); + // Create a deterministic stable representation. + const fileInfos = entries.map((entry) => ({ + name: entry.entryName, + // Use a stable hash of file content. + data: entry.getData().toString() + })); + this.logger.info('[avasilev][zip]' + JSON.stringify(fileInfos)); + this.logger.info('[buffer base64 string]' + buffer.toString('base64')); const response = await lrep.mergeAppDescriptorVariant(buffer); this.manifest = response[descriptorVariantId].manifest; } diff --git a/packages/adp-tooling/src/preview/adp-preview.ts b/packages/adp-tooling/src/preview/adp-preview.ts index 3aca81fc500..24846914c41 100644 --- a/packages/adp-tooling/src/preview/adp-preview.ts +++ b/packages/adp-tooling/src/preview/adp-preview.ts @@ -169,7 +169,15 @@ export class AdpPreview { zip.addFile(file.getPath().substring(1), await file.getBuffer()); } const buffer = zip.toBuffer(); - + const entries = zip.getEntries(); + // Create a deterministic stable representation. + const fileInfos = entries.map((entry) => ({ + name: entry.entryName, + // Use a stable hash of file content. + data: entry.getData().toString() + })); + this.logger.info('[avasilev][zip]' + JSON.stringify(fileInfos)); + this.logger.info('[buffer base64 string]' + buffer.toString('base64')); this.mergedDescriptor = (await this.lrep.mergeAppDescriptorVariant(buffer, '//'))[this.descriptorVariantId]; global.__SAP_UX_MANIFEST_SYNC_REQUIRED__ = false; } diff --git a/packages/axios-extension/src/utils/axios-traffic.ts b/packages/axios-extension/src/utils/axios-traffic.ts index aa0ebc8ba5f..59174b88811 100644 --- a/packages/axios-extension/src/utils/axios-traffic.ts +++ b/packages/axios-extension/src/utils/axios-traffic.ts @@ -57,13 +57,13 @@ function logAxiosTrafficInternal(logger: ToolsLogger, isGeneratorWorkflow: boole // so wee need to do the same here. const method = (mergedConfig.method ?? GET_REQUEST_METHOD).toUpperCase(); - logger.info(`[axios][=>][${method}] ${requestUrl}`); - if (mergedConfig.headers) { - logger.info(`[axios] headers: ${mergedConfig.headers}`); - } - if (mergedConfig.data) { - logger.info(`[axios] body: ${mergedConfig.data}`); - } + // logger.info(`[axios][=>][${method}] ${requestUrl}`); + // if (mergedConfig.headers) { + // logger.info(`[axios] headers: ${mergedConfig.headers}`); + // } + // if (mergedConfig.data) { + // logger.info(`[axios] body: ${mergedConfig.data}`); + // } try { const response = await originalRequest.call(this, config); @@ -75,13 +75,13 @@ function logAxiosTrafficInternal(logger: ToolsLogger, isGeneratorWorkflow: boole responseConfig.params ); - logger.info(`[axios][<=][${response.status}] ${responseUrl}`); - if (response.headers) { - logger.info(`[axios] headers: ${response.headers}`); - } - if (response.data) { - logger.info(`[axios] body: ${response.data}`); - } + // logger.info(`[axios][<=][${response.status}] ${responseUrl}`); + // if (response.headers) { + // logger.info(`[axios] headers: ${response.headers}`); + // } + // if (response.data) { + // logger.info(`[axios] body: ${response.data}`); + // } appendMuabResponse( muabConfigStream, From d0092ca080fd374773dc24ef74a150b70ff7f485 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Thu, 13 Nov 2025 20:48:02 +0200 Subject: [PATCH 21/21] chore: Add todos. --- packages/adp-mock-server/src/client/record-client.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/adp-mock-server/src/client/record-client.ts b/packages/adp-mock-server/src/client/record-client.ts index 660d13d98ad..d3a4c90a562 100644 --- a/packages/adp-mock-server/src/client/record-client.ts +++ b/packages/adp-mock-server/src/client/record-client.ts @@ -13,6 +13,8 @@ export async function recordRequestsAndResponses(): Promise { logger.info('Record responses.'); const client = await getRecordClient(); let requestAndResponseList = await retrieveRecordedRequestsAndResponses(client); + // TODO a.vasilev reponse.body = true does not work, mock server validation schema error is thrown when the server is started, + // need to be "true" what about false, maybe we can set the body to undefined for false values?! requestAndResponseList = disableRequestsSecureFlag(requestAndResponseList); await fs.writeFile(RESPONSES_JSON_PATH, JSON.stringify(requestAndResponseList, null, 2)); } @@ -24,7 +26,7 @@ async function retrieveRecordedRequestsAndResponses(client: MockServerClient): P function disableRequestsSecureFlag(requestAndResponseList: HttpRequestAndHttpResponse[]): HttpRequestAndHttpResponse[] { return requestAndResponseList.map(({ httpRequest, httpResponse }) => ({ httpRequest: { - // TODO a.vasilev: Some times the settings request does not match + // TODO a.vasilev: Sometimes the settings request does not match // the recordings due to a changed cookie. How is this possible, sb // is changing the SAP_SESSIONID cookie in replay mode, how is that even // possible???