From 58cc1b9e8f85769fab6632d54c3a69501e015d76 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 3 Feb 2026 12:45:20 +0530 Subject: [PATCH 1/3] SK-2536: fix different content-types in request and respones in invoke connection --- src/error/codes/index.ts | 1 + src/error/messages/index.ts | 1 + src/utils/index.ts | 42 + src/vault/controller/connections/index.ts | 362 +++++--- test/vault/controller/connection.test.js | 968 ++++++++++++++++++++-- test/vault/utils/utils.test.js | 273 +++++- 6 files changed, 1501 insertions(+), 146 deletions(-) diff --git a/src/error/codes/index.ts b/src/error/codes/index.ts index 1916fa01..889d10a8 100644 --- a/src/error/codes/index.ts +++ b/src/error/codes/index.ts @@ -231,6 +231,7 @@ const SKYFLOW_ERROR_CODE = { EMPTY_RUN_ID:{ http_code: 400, message: errorMessages.EMPTY_RUN_ID }, INVALID_RUN_ID:{ http_code: 400, message: errorMessages.INVALID_RUN_ID }, INTERNAL_SERVER_ERROR: { http_code: 500, message: errorMessages.INTERNAL_SERVER_ERROR }, + INVALID_XML_FORMAT: { http_code: 400, message: errorMessages.INVALID_XML_FORMAT }, }; export default SKYFLOW_ERROR_CODE; \ No newline at end of file diff --git a/src/error/messages/index.ts b/src/error/messages/index.ts index 4a2f54cf..32d4cf34 100644 --- a/src/error/messages/index.ts +++ b/src/error/messages/index.ts @@ -244,6 +244,7 @@ const errorMessages = { EMPTY_RUN_ID: `${errorPrefix} Validation error. Run id cannot be empty. Specify a valid run id.`, INVALID_RUN_ID: `${errorPrefix} Validation error. Invalid run id. Specify a valid run id as string.`, INTERNAL_SERVER_ERROR: `${errorPrefix}. Internal server error. %s1.`, + INVALID_XML_FORMAT: `${errorPrefix} Validation error. Invalid XML format. Specify a valid XML format as string.`, }; export default errorMessages; \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index 10b861cb..01f020dd 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -230,6 +230,10 @@ export const CONTENT_TYPE = { APPLICATION_JSON: 'application/json', APPLICATION_X_WWW_FORM_URLENCODED: 'application/x-www-form-urlencoded', TEXT_PLAIN: 'text/plain', + MULTIPART_FORM_DATA: 'multipart/form-data', + TEXT_XML: 'text/xml', + APPLICATION_XML: 'application/xml', + TEXT_HTML: 'text/html', } as const; // HTTP Headers @@ -582,3 +586,41 @@ export const isValidURL = (url: string) => { return false; } }; + + +export function objectToXML(obj: any, rootName: string = "root"): string { + function convertToXML(data: any, nodeName: string): string { + if (data === null || data === undefined) { + return `<${nodeName}/>`; + } + + if (typeof data === "object" && !Array.isArray(data)) { + let xml = `<${nodeName}>`; + for (const [key, value] of Object.entries(data)) { + xml += convertToXML(value, key); + } + xml += ``; + return xml; + } + + if (Array.isArray(data)) { + return data.map((item) => convertToXML(item, nodeName)).join(""); + } + + // Escape special XML characters + const escapedValue = String(data) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + + return `<${nodeName}>${escapedValue}`; + } + + const xmlContent = Object.entries(obj) + .map(([key, value]) => convertToXML(value, key)) + .join(""); + + return `<${rootName}>${xmlContent}`; +} \ No newline at end of file diff --git a/src/vault/controller/connections/index.ts b/src/vault/controller/connections/index.ts index b1f3552b..e63845b5 100644 --- a/src/vault/controller/connections/index.ts +++ b/src/vault/controller/connections/index.ts @@ -1,117 +1,291 @@ //imports -import { fillUrlWithPathAndQueryParams, generateSDKMetrics, getBearerToken, LogLevel, MessageType, RequestMethod, parameterizedString, printLog, SDK, SKYFLOW, REQUEST, TYPES, HTTP_HEADER, CONTENT_TYPE } from "../../../utils"; +import { + fillUrlWithPathAndQueryParams, + generateSDKMetrics, + getBearerToken, + LogLevel, + MessageType, + RequestMethod, + parameterizedString, + printLog, + SDK, + SKYFLOW, + REQUEST, + TYPES, + HTTP_HEADER, + CONTENT_TYPE, + objectToXML, +} from "../../../utils"; import InvokeConnectionRequest from "../../model/request/inkove"; import logs from "../../../utils/logs"; import { validateInvokeConnectionRequest } from "../../../utils/validations"; import VaultClient from "../../client"; import InvokeConnectionResponse from "../../model/response/invoke/invoke"; +import SkyflowError from "../../../error"; +import SKYFLOW_ERROR_CODE from "../../../error/codes"; class ConnectionController { + private client: VaultClient; - private client: VaultClient; + private logLevel: LogLevel; - private logLevel: LogLevel; + constructor(client: VaultClient) { + this.client = client; + this.logLevel = client.getLogLevel(); + } - constructor(client: VaultClient) { - this.client = client; - this.logLevel = client.getLogLevel(); + private buildInvokeConnectionBody(invokeRequest: InvokeConnectionRequest): { + body: any; + shouldRemoveContentType: boolean; + } { + let requestBody; + let shouldRemoveContentType: boolean = false; + const normalizedHeaders: Record = {}; + + if (invokeRequest.headers) { + Object.entries(invokeRequest.headers).forEach(([key, value]) => { + normalizedHeaders[key.toLowerCase()] = + typeof value === "string" ? value : JSON.stringify(value); + }); } - private buildInvokeConnectionBody(invokeRequest: InvokeConnectionRequest){ - let requestBody; - const contentType = invokeRequest.headers?.[HTTP_HEADER.CONTENT_TYPE] || CONTENT_TYPE.APPLICATION_JSON; - if (contentType === CONTENT_TYPE.APPLICATION_JSON) { - requestBody = JSON.stringify(invokeRequest.body); - } else if (contentType === CONTENT_TYPE.APPLICATION_X_WWW_FORM_URLENCODED) { - const urlSearchParams = new URLSearchParams(); - Object.entries(invokeRequest.body || {}).forEach(([key, value]) => { - if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([nestedKey, nestedValue]) => { - urlSearchParams.append(`${key}[${nestedKey}]`, nestedValue as string); - }); - } else { - urlSearchParams.append(key, value as string); - } - }); - requestBody = urlSearchParams.toString(); + const contentType = + normalizedHeaders[ + HTTP_HEADER.CONTENT_TYPE.toLowerCase() + ]?.toLowerCase() || ""; + + if ( + !contentType && + typeof invokeRequest.body === "object" && + invokeRequest.body !== null + ) { + requestBody = JSON.stringify(invokeRequest.body); + normalizedHeaders[HTTP_HEADER.CONTENT_TYPE.toLowerCase()] = + CONTENT_TYPE.APPLICATION_JSON; + } else if (contentType.includes(CONTENT_TYPE.APPLICATION_JSON)) { + requestBody = JSON.stringify(invokeRequest.body); + } else if ( + contentType.includes(CONTENT_TYPE.APPLICATION_X_WWW_FORM_URLENCODED) + ) { + const urlSearchParams = new URLSearchParams(); + Object.entries(invokeRequest.body || {}).forEach(([key, value]) => { + if ( + typeof value === "object" && + value !== null && + !Array.isArray(value) + ) { + Object.entries(value).forEach(([nestedKey, nestedValue]) => { + urlSearchParams.append(`${key}[${nestedKey}]`, String(nestedValue)); + }); + } else if (Array.isArray(value)) { + value.forEach((item) => { + urlSearchParams.append(key, String(item)); + }); } else { - requestBody = invokeRequest.body; + urlSearchParams.append(key, String(value)); } + }); + requestBody = urlSearchParams.toString(); + } else if (contentType.includes(CONTENT_TYPE.MULTIPART_FORM_DATA)) { + shouldRemoveContentType = true; - return requestBody; + if (invokeRequest.body instanceof FormData) { + requestBody = invokeRequest.body; + } else { + const formData = new FormData(); + Object.entries(invokeRequest.body || {}).forEach(([key, value]) => { + if (value instanceof File || value instanceof Blob) { + formData.append(key, value); + } else if (typeof value === "object" && value !== null) { + formData.append(key, JSON.stringify(value)); + } else { + formData.append(key, String(value)); + } + }); + requestBody = formData; + } + } else if ( + contentType.includes(CONTENT_TYPE.APPLICATION_XML) || + contentType.includes(CONTENT_TYPE.TEXT_XML) + ) { + if (typeof invokeRequest.body === "string") { + requestBody = invokeRequest.body; + } else if (typeof invokeRequest.body === "object") { + requestBody = objectToXML(invokeRequest.body, "request"); + } else { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_XML_FORMAT); + } + } else if (contentType.includes(CONTENT_TYPE.TEXT_PLAIN)) { + requestBody = + typeof invokeRequest.body === "string" + ? invokeRequest.body + : String(invokeRequest.body); + } else if (contentType.includes(CONTENT_TYPE.TEXT_HTML)) { + requestBody = + typeof invokeRequest.body === "string" + ? invokeRequest.body + : String(invokeRequest.body); + } else { + if (typeof invokeRequest.body === "string") { + requestBody = invokeRequest.body; + } else if ( + typeof invokeRequest.body === "object" && + invokeRequest.body !== null + ) { + requestBody = JSON.stringify(invokeRequest.body); + } else { + requestBody = invokeRequest.body; + } } - invoke(invokeRequest: InvokeConnectionRequest): Promise { - return new Promise((resolve, reject) => { - try { - printLog(logs.infoLogs.INVOKE_CONNECTION_TRIGGERED, MessageType.LOG, this.logLevel); - printLog(logs.infoLogs.VALIDATE_CONNECTION_CONFIG, MessageType.LOG, this.logLevel); - // validations checks - validateInvokeConnectionRequest(invokeRequest); - const filledUrl = fillUrlWithPathAndQueryParams(this.client.url, invokeRequest.pathParams, invokeRequest.queryParams); - getBearerToken(this.client.getCredentials(), this.logLevel).then((token) => { - printLog(parameterizedString(logs.infoLogs.EMIT_REQUEST, TYPES.INVOKE_CONNECTION), MessageType.LOG, this.logLevel); - const sdkHeaders = {}; - sdkHeaders[SKYFLOW.AUTH_HEADER_KEY] = token.key; - sdkHeaders[SDK.METRICS_HEADER_KEY] = JSON.stringify(generateSDKMetrics()); - - fetch(filledUrl, { - method: invokeRequest.method || RequestMethod.POST, - body: this.buildInvokeConnectionBody(invokeRequest), - headers: { ...invokeRequest.headers, ...sdkHeaders }, - }) - .then(async (response) => { - if (!response.ok) { - let errorBody:unknown = null ; - - try { - errorBody = await response.json(); - } catch { - try { - const text = await response.text(); - errorBody = text ? { message: text } : null; - } catch { - response.body?.cancel().catch(() => { }); - } - } - - throw { - body: errorBody, - statusCode: response.status, - message: response.statusText, - headers: response.headers - }; - } - - const headers = response.headers; - const body = await response.json(); - return { headers, body }; - }) - - .then(({headers, body}) => { - printLog(logs.infoLogs.INVOKE_CONNECTION_REQUEST_RESOLVED, MessageType.LOG, this.logLevel); - const requestId = headers?.get(REQUEST.ID_KEY) || ''; - const invokeConnectionResponse = new InvokeConnectionResponse({ - data:body, - metadata: { requestId }, - errors: null - }); - resolve(invokeConnectionResponse); - }).catch((err) => { - printLog(logs.errorLogs.INVOKE_CONNECTION_REQUEST_REJECTED, MessageType.LOG, this.logLevel); - this.client.failureResponse(err).catch((err) => reject(err)) - }); - }).catch(err => { - reject(err); - }) - } catch (e) { - if (e instanceof Error) - printLog(e.message, MessageType.ERROR, this.logLevel); - reject(e); - } - }); + return { body: requestBody, shouldRemoveContentType }; + } + + private async parseResponseBody(response: Response): Promise { + const contentType = + response.headers.get(HTTP_HEADER.CONTENT_TYPE)?.toLowerCase() || ""; + + try { + if (contentType.includes(CONTENT_TYPE.APPLICATION_JSON)) { + return await response.json(); + } else if ( + contentType.includes(CONTENT_TYPE.APPLICATION_XML) || + contentType.includes(CONTENT_TYPE.TEXT_XML) + ) { + return await response.text(); + } else if (contentType.includes(CONTENT_TYPE.TEXT_HTML)) { + return await response.text(); + } else if ( + contentType.includes(CONTENT_TYPE.APPLICATION_X_WWW_FORM_URLENCODED) + ) { + const text = await response.text(); + return Object.fromEntries(new URLSearchParams(text)); + } else if (contentType.includes(CONTENT_TYPE.MULTIPART_FORM_DATA)) { + return await response.text(); + } else if (contentType.includes(CONTENT_TYPE.TEXT_PLAIN)) { + return await response.text(); + } else { + try { + return await response.json(); + } catch { + return await response.text(); + } + } + } catch { + try { + const text = await response.text(); + return text ? { message: text } : null; + } catch { + response.body?.cancel().catch(() => {}); + return null; + } } + } + + invoke( + invokeRequest: InvokeConnectionRequest, + ): Promise { + return new Promise((resolve, reject) => { + try { + printLog( + logs.infoLogs.INVOKE_CONNECTION_TRIGGERED, + MessageType.LOG, + this.logLevel, + ); + printLog( + logs.infoLogs.VALIDATE_CONNECTION_CONFIG, + MessageType.LOG, + this.logLevel, + ); + validateInvokeConnectionRequest(invokeRequest); + const filledUrl = fillUrlWithPathAndQueryParams( + this.client.url, + invokeRequest.pathParams, + invokeRequest.queryParams, + ); + getBearerToken(this.client.getCredentials(), this.logLevel) + .then((token) => { + printLog( + parameterizedString( + logs.infoLogs.EMIT_REQUEST, + TYPES.INVOKE_CONNECTION, + ), + MessageType.LOG, + this.logLevel, + ); + + const { body, shouldRemoveContentType } = + this.buildInvokeConnectionBody(invokeRequest); + + const requestHeaders: Record = {}; + + if (invokeRequest.headers) { + Object.entries(invokeRequest.headers).forEach(([key, value]) => { + const lowerKey = key.toLowerCase(); + if (shouldRemoveContentType && lowerKey === "content-type") { + return; + } + requestHeaders[key] = + typeof value === "string" ? value : JSON.stringify(value); + }); + } + + requestHeaders[SKYFLOW.AUTH_HEADER_KEY] = token.key; + requestHeaders[SDK.METRICS_HEADER_KEY] = + JSON.stringify(generateSDKMetrics()); + + fetch(filledUrl, { + method: invokeRequest.method || RequestMethod.POST, + body: body, + headers: requestHeaders, + }) + .then(async (response) => { + const body = await this.parseResponseBody(response); + + if (!response.ok) { + throw { + body, + statusCode: response.status, + message: response.statusText, + headers: response.headers, + }; + } + return { headers: response.headers, body }; + }) + .then(({ headers, body }) => { + printLog( + logs.infoLogs.INVOKE_CONNECTION_REQUEST_RESOLVED, + MessageType.LOG, + this.logLevel, + ); + const requestId = headers?.get(REQUEST.ID_KEY) || ""; + const invokeConnectionResponse = new InvokeConnectionResponse({ + data: body, + metadata: { requestId }, + errors: null, + }); + resolve(invokeConnectionResponse); + }) + .catch((err) => { + printLog( + logs.errorLogs.INVOKE_CONNECTION_REQUEST_REJECTED, + MessageType.LOG, + this.logLevel, + ); + this.client.failureResponse(err).catch((err) => reject(err)); + }); + }) + .catch((err) => { + reject(err); + }); + } catch (e) { + if (e instanceof Error) + printLog(e.message, MessageType.ERROR, this.logLevel); + reject(e); + } + }); + } } export default ConnectionController; diff --git a/test/vault/controller/connection.test.js b/test/vault/controller/connection.test.js index 5ed4a3ca..d628e30d 100644 --- a/test/vault/controller/connection.test.js +++ b/test/vault/controller/connection.test.js @@ -6,10 +6,16 @@ import { RequestMethod, SDK, SKYFLOW, + REQUEST, + HTTP_HEADER, + CONTENT_TYPE, + objectToXML, } from "../../../src/utils"; import { validateInvokeConnectionRequest } from "../../../src/utils/validations"; import VaultClient from "../../../src/vault/client"; import ConnectionController from "../../../src/vault/controller/connections"; +import SkyflowError from "../../../src/error"; +import SKYFLOW_ERROR_CODE from "../../../src/error/codes"; jest.mock("../../../src/utils"); jest.mock("../../../src/utils/validations"); @@ -21,94 +27,578 @@ describe("ConnectionController Tests", () => { beforeEach(() => { jest.clearAllMocks(); jest.useRealTimers(); - mockClient = new VaultClient(); + mockClient = { + getLogLevel: jest.fn().mockReturnValue(LogLevel.ERROR), + getCredentials: jest.fn().mockReturnValue({ apiKey: "test-key" }), + url: "https://api.example.com", + failureResponse: jest.fn().mockRejectedValue(new Error("Failure")) + }; connectionController = new ConnectionController(mockClient); }); - it("should invoke a connection successfully", async () => { + // Test buildInvokeConnectionBody - JSON content type + it("should build request body for JSON content type", async () => { const token = { key: "bearer_token" }; - - // Mocking methods - mockClient.getLogLevel = jest.fn().mockReturnValue(LogLevel.INFO); - mockClient.getCredentials = jest.fn().mockReturnValue({ username: "user", password: "pass" }); - getBearerToken.mockImplementation(jest.fn().mockResolvedValue(token)); + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/json"; + return null; + }), + }, + json: jest.fn().mockResolvedValue({ success: true }), + }); + + const request = { + body: { data: "sample" }, + method: RequestMethod.POST, + headers: { "Content-Type": "application/json" }, + }; + + await connectionController.invoke(request); + + expect(fetch).toHaveBeenCalledWith( + "https://api.example.com/resource", + expect.objectContaining({ + method: RequestMethod.POST, + body: JSON.stringify(request.body), + }) + ); + }); + + // Test buildInvokeConnectionBody - URL encoded content type + it("should build request body for URL encoded content type", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); generateSDKMetrics.mockReturnValue({ metric: "value" }); validateInvokeConnectionRequest.mockImplementation(jest.fn()); + global.fetch = jest.fn().mockResolvedValue({ ok: true, status: 200, - statusText: "OK", - json: jest.fn().mockResolvedValue({ data: { success: true } }), headers: { get: jest.fn().mockImplementation((key) => { - if (key === "x-request-id") return "request_id"; + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/x-www-form-urlencoded"; + return null; + }), + }, + text: jest.fn().mockResolvedValue("key=value"), + }); + + const request = { + body: { key: "value", nested: { field: "data" } }, + method: RequestMethod.POST, + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }; + + await connectionController.invoke(request); + + const expectedBody = new URLSearchParams(); + expectedBody.append("key", "value"); + expectedBody.append("nested[field]", "data"); + + expect(fetch).toHaveBeenCalledWith( + "https://api.example.com/resource", + expect.objectContaining({ + method: RequestMethod.POST, + body: expectedBody.toString(), + }) + ); + }); + + // Test buildInvokeConnectionBody - multipart/form-data + it("should build request body for multipart/form-data and remove content-type header", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "multipart/form-data"; + return null; + }), + }, + text: jest.fn().mockResolvedValue("response"), + }); + + const request = { + body: { key: "value" }, + method: RequestMethod.POST, + headers: { "Content-Type": "multipart/form-data" }, + }; + + await connectionController.invoke(request); + + const callArgs = fetch.mock.calls[0][1]; + expect(callArgs.body).toBeInstanceOf(FormData); + // Content-Type should be removed for multipart + expect(callArgs.headers["Content-Type"]).toBeUndefined(); + }); + + // Test buildInvokeConnectionBody - XML content type + it("should build request body for XML content type from object", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + objectToXML.mockReturnValue('sample'); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/xml"; + return null; + }), + }, + text: jest.fn().mockResolvedValue(""), + }); + + const request = { + body: { data: "sample" }, + method: RequestMethod.POST, + headers: { "Content-Type": "application/xml" }, + }; + + await connectionController.invoke(request); + + expect(objectToXML).toHaveBeenCalledWith(request.body, "request"); + expect(fetch).toHaveBeenCalledWith( + "https://api.example.com/resource", + expect.objectContaining({ + body: 'sample', + }) + ); + }); + + // Test buildInvokeConnectionBody - XML as string + it("should build request body for XML content type from string", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/xml"; + return null; + }), + }, + text: jest.fn().mockResolvedValue(""), + }); + + const xmlString = 'sample'; + const request = { + body: xmlString, + method: RequestMethod.POST, + headers: { "Content-Type": "application/xml" }, + }; + + await connectionController.invoke(request); + + expect(fetch).toHaveBeenCalledWith( + "https://api.example.com/resource", + expect.objectContaining({ + body: xmlString, + }) + ); + }); + + // Test buildInvokeConnectionBody - text/plain + it("should build request body for text/plain content type", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "text/plain"; + return null; + }), + }, + text: jest.fn().mockResolvedValue("response"), + }); + + const request = { + body: "plain text content", + method: RequestMethod.POST, + headers: { "Content-Type": "text/plain" }, + }; + + await connectionController.invoke(request); + + expect(fetch).toHaveBeenCalledWith( + "https://api.example.com/resource", + expect.objectContaining({ + body: "plain text content", + }) + ); + }); + + // Test buildInvokeConnectionBody - no content type with object + it("should default to JSON when no content type is provided with object body", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/json"; + return null; + }), + }, + json: jest.fn().mockResolvedValue({ success: true }), + }); + + const request = { + body: { data: "sample" }, + method: RequestMethod.POST, + headers: {}, + }; + + await connectionController.invoke(request); + + expect(fetch).toHaveBeenCalledWith( + "https://api.example.com/resource", + expect.objectContaining({ + body: JSON.stringify(request.body), + }) + ); + }); + + // Test parseResponseBody - JSON response + it("should parse JSON response correctly", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/json"; + return null; + }), + }, + json: jest.fn().mockResolvedValue({ success: true }), + }); + + const request = { + body: { data: "sample" }, + method: RequestMethod.POST, + headers: { "Content-Type": "application/json" }, + }; + + const result = await connectionController.invoke(request); + + expect(result.data).toEqual({ success: true }); + }); + + // Test parseResponseBody - XML response + it("should parse XML response as text", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + const xmlResponse = 'true'; + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/xml"; + return null; + }), + }, + text: jest.fn().mockResolvedValue(xmlResponse), + }); + + const request = { + body: { data: "sample" }, + method: RequestMethod.POST, + headers: { "Content-Type": "application/json" }, + }; + + const result = await connectionController.invoke(request); + + expect(result.data).toEqual(xmlResponse); + }); + + // Test parseResponseBody - URL encoded response + it("should parse URL encoded response correctly", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + const responseText = "key1=value1&key2=value2"; + let textCalled = false; + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + const lowerKey = key?.toLowerCase(); + if (lowerKey === "x-request-id") return "request_id"; + if (lowerKey === "content-type") return "application/x-www-form-urlencoded"; + return null; + }), + }, + text: jest.fn().mockImplementation(() => { + if (textCalled) { + return Promise.reject(new Error("Body already read")); + } + textCalled = true; + return Promise.resolve(responseText); + }), + }); + + const request = { + body: { data: "sample" }, + method: RequestMethod.POST, + headers: { "Content-Type": "application/json" }, + }; + + const result = await connectionController.invoke(request); + + // URL encoded response is parsed as object + expect(result.data).toEqual({ key1: "value1", key2: "value2" }); + }); + + // Test parseResponseBody - HTML response + it("should parse HTML response as text", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + const htmlResponse = "Success"; + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "text/html"; + return null; + }), + }, + text: jest.fn().mockResolvedValue(htmlResponse), + }); + + const request = { + body: { data: "sample" }, + method: RequestMethod.POST, + headers: { "Content-Type": "application/json" }, + }; + + const result = await connectionController.invoke(request); + + expect(result.data).toEqual(htmlResponse); + }); + + // Test parseResponseBody - plain text response + it("should parse plain text response", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "text/plain"; + return null; + }), + }, + text: jest.fn().mockResolvedValue("Plain text response"), + }); + + const request = { + body: { data: "sample" }, + method: RequestMethod.POST, + headers: { "Content-Type": "application/json" }, + }; + + const result = await connectionController.invoke(request); + + expect(result.data).toEqual("Plain text response"); + }); + + // Test parseResponseBody - unknown content type fallback to JSON + it("should fallback to JSON parsing for unknown content type", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/custom"; + return null; + }), + }, + json: jest.fn().mockResolvedValue({ success: true }), + }); + + const request = { + body: { data: "sample" }, + method: RequestMethod.POST, + headers: { "Content-Type": "application/json" }, + }; + + const result = await connectionController.invoke(request); + + expect(result.data).toEqual({ success: true }); + }); + + // Test parseResponseBody - fallback to text when JSON fails + it("should fallback to text parsing when JSON parsing fails", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return ""; + return null; + }), + }, + json: jest.fn().mockRejectedValue(new Error("Invalid JSON")), + text: jest.fn().mockResolvedValue("Plain text"), + }); + + const request = { + body: { data: "sample" }, + method: RequestMethod.POST, + headers: { "Content-Type": "application/json" }, + }; + + const result = await connectionController.invoke(request); + + expect(result.data).toEqual("Plain text"); + }); + + // Test error handling with JSON error response + it("should handle errors with JSON error response", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + mockClient.failureResponse = jest.fn().mockRejectedValue(new Error("API Error")); + + global.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 500, + statusText: "Internal Server Error", + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "content-type") return "application/json"; return null; }), }, + json: jest.fn().mockResolvedValue({ error: "Something went wrong" }), }); - + const request = { - pathParams: { id: "123" }, - queryParams: { search: "test" }, body: { data: "sample" }, method: RequestMethod.POST, - headers: { "Content-Type": "application/json", "Custom-Header": "value" }, + headers: { "Content-Type": "application/json" }, }; - - const expectedResult = { - data: { success: true }, - metadata: { requestId: "request_id" }, - errors: undefined, - }; - - const result = await connectionController.invoke(request); - - expect(fetch).toHaveBeenCalledWith("https://api.example.com/resource", { - method: RequestMethod.POST, - body: JSON.stringify(request.body), - headers: { - ...request.headers, - [SKYFLOW.AUTH_HEADER_KEY]: token.key, - [SDK.METRICS_HEADER_KEY]: JSON.stringify(generateSDKMetrics()), - }, - }); + + await expect(connectionController.invoke(request)).rejects.toThrow(); }); - it("should handle errors in fetch call", async () => { + // Test error handling with HTML error response + it("should handle errors with HTML error response", async () => { const token = { key: "bearer_token" }; - - mockClient.getLogLevel = jest.fn().mockReturnValue(LogLevel.INFO); - mockClient.getCredentials = jest.fn().mockReturnValue({ username: "user", password: "pass" }); - getBearerToken.mockImplementation(jest.fn().mockResolvedValue(token)); + getBearerToken.mockResolvedValue(token); fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); generateSDKMetrics.mockReturnValue({ metric: "value" }); validateInvokeConnectionRequest.mockImplementation(jest.fn()); + mockClient.failureResponse = jest.fn().mockRejectedValue(new Error("API Error")); + global.fetch = jest.fn().mockResolvedValue({ ok: false, status: 500, statusText: "Internal Server Error", - json: jest.fn().mockResolvedValue({ error: "Something went wrong" }), headers: { - get: jest.fn().mockImplementation(() => null), + get: jest.fn().mockImplementation((key) => { + if (key === "content-type") return "text/html"; + return null; + }), }, + text: jest.fn().mockResolvedValue("Error"), }); - + const request = { - pathParams: { id: "123" }, - queryParams: { search: "test" }, body: { data: "sample" }, method: RequestMethod.POST, - headers: { "Content-Type": "application/json", "Custom-Header": "value" }, - }; - - const expectedError = { - body: { error: "Something went wrong" }, - statusCode: 500, - message: "Internal Server Error", - headers: expect.anything(), + headers: { "Content-Type": "application/json" }, }; - + await expect(connectionController.invoke(request)).rejects.toThrow(); }); @@ -155,4 +645,380 @@ describe("ConnectionController Tests", () => { await expect(connectionController.invoke(request)).rejects.toThrow(); }); + + // Test buildInvokeConnectionBody - XML request with object body + it("should convert object to XML when content-type is application/xml", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/json"; + return null; + }), + }, + json: jest.fn().mockResolvedValue({ success: true }), + }); + + const request = { + body: { root: { child: "value" } }, + method: RequestMethod.POST, + headers: { "Content-Type": "application/xml" }, + }; + + await connectionController.invoke(request); + + // Object is converted to XML with as root + expect(global.fetch).toHaveBeenCalledWith( + "https://api.example.com/resource", + expect.objectContaining({ + body: expect.stringContaining(""), + }) + ); + }); + + // Test buildInvokeConnectionBody - XML request with string body + it("should keep string body as-is when content-type is application/xml", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + const xmlString = 'value'; + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/json"; + return null; + }), + }, + json: jest.fn().mockResolvedValue({ success: true }), + }); + + const request = { + body: xmlString, + method: RequestMethod.POST, + headers: { "Content-Type": "text/xml" }, + }; + + await connectionController.invoke(request); + + expect(global.fetch).toHaveBeenCalledWith( + "https://api.example.com/resource", + expect.objectContaining({ + body: xmlString, + }) + ); + }); + + // Test buildInvokeConnectionBody - URL encoded request + it("should convert body to URLSearchParams for application/x-www-form-urlencoded", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/json"; + return null; + }), + }, + json: jest.fn().mockResolvedValue({ success: true }), + }); + + const request = { + body: { key1: "value1", key2: "value2" }, + method: RequestMethod.POST, + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }; + + await connectionController.invoke(request); + + // URLSearchParams is converted to string by fetch + expect(global.fetch).toHaveBeenCalledWith( + "https://api.example.com/resource", + expect.objectContaining({ + body: "key1=value1&key2=value2", + }) + ); + }); + + // Test buildInvokeConnectionBody - FormData request + it("should convert body to FormData for multipart/form-data and remove content-type", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/json"; + return null; + }), + }, + json: jest.fn().mockResolvedValue({ success: true }), + }); + + const request = { + body: { field1: "value1", field2: "value2" }, + method: RequestMethod.POST, + headers: { "Content-Type": "multipart/form-data" }, + }; + + await connectionController.invoke(request); + + const fetchCall = global.fetch.mock.calls[0][1]; + expect(fetchCall.body).toBeInstanceOf(FormData); + // Content-Type should be removed so fetch can set boundary + expect(fetchCall.headers["Content-Type"]).toBeUndefined(); + }); + + // Test buildInvokeConnectionBody - JSON request + it("should stringify body for application/json", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/json"; + return null; + }), + }, + json: jest.fn().mockResolvedValue({ success: true }), + }); + + const requestBody = { data: "sample", nested: { key: "value" } }; + const request = { + body: requestBody, + method: RequestMethod.POST, + headers: { "Content-Type": "application/json" }, + }; + + await connectionController.invoke(request); + + expect(global.fetch).toHaveBeenCalledWith( + "https://api.example.com/resource", + expect.objectContaining({ + body: JSON.stringify(requestBody), + }) + ); + }); + + // Test buildInvokeConnectionBody - plain text request + it("should keep body as-is for text/plain", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + const textBody = "Plain text content"; + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/json"; + return null; + }), + }, + json: jest.fn().mockResolvedValue({ success: true }), + }); + + const request = { + body: textBody, + method: RequestMethod.POST, + headers: { "Content-Type": "text/plain" }, + }; + + await connectionController.invoke(request); + + expect(global.fetch).toHaveBeenCalledWith( + "https://api.example.com/resource", + expect.objectContaining({ + body: textBody, + }) + ); + }); + + // Test buildInvokeConnectionBody - HTML request + it("should keep body as-is for text/html", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + const htmlBody = "Content"; + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/json"; + return null; + }), + }, + json: jest.fn().mockResolvedValue({ success: true }), + }); + + const request = { + body: htmlBody, + method: RequestMethod.POST, + headers: { "Content-Type": "text/html" }, + }; + + await connectionController.invoke(request); + + expect(global.fetch).toHaveBeenCalledWith( + "https://api.example.com/resource", + expect.objectContaining({ + body: htmlBody, + }) + ); + }); + + // Test buildInvokeConnectionBody - default to JSON + it("should default to JSON stringification for unknown content types", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/json"; + return null; + }), + }, + json: jest.fn().mockResolvedValue({ success: true }), + }); + + const requestBody = { data: "sample" }; + const request = { + body: requestBody, + method: RequestMethod.POST, + headers: { "Content-Type": "application/custom" }, + }; + + await connectionController.invoke(request); + + expect(global.fetch).toHaveBeenCalledWith( + "https://api.example.com/resource", + expect.objectContaining({ + body: JSON.stringify(requestBody), + }) + ); + }); + + // Test request without body + it("should handle requests without body (GET requests)", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/json"; + return null; + }), + }, + json: jest.fn().mockResolvedValue({ success: true }), + }); + + const request = { + method: RequestMethod.GET, + headers: {}, + }; + + await connectionController.invoke(request); + + expect(global.fetch).toHaveBeenCalledWith( + "https://api.example.com/resource", + expect.objectContaining({ + method: "GET", + }) + ); + }); + + // Test custom headers preservation + it("should preserve custom headers and override Authorization", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "application/json"; + return null; + }), + }, + json: jest.fn().mockResolvedValue({ success: true }), + }); + + const request = { + body: { data: "sample" }, + method: RequestMethod.POST, + headers: { + "Content-Type": "application/json", + "X-Custom-Header": "custom-value", + "x-skyflow-authorization": "should-be-overridden" + }, + }; + + await connectionController.invoke(request); + + const fetchCall = global.fetch.mock.calls[0][1]; + expect(fetchCall.headers["X-Custom-Header"]).toBe("custom-value"); + // The auth header is always set by the SDK + expect(fetchCall.headers["x-skyflow-authorization"]).toBe("bearer_token"); + }); }); diff --git a/test/vault/utils/utils.test.js b/test/vault/utils/utils.test.js index 974b32eb..a81d38d1 100644 --- a/test/vault/utils/utils.test.js +++ b/test/vault/utils/utils.test.js @@ -1,6 +1,6 @@ /* eslint-disable camelcase */ import errorMessages from "../../../src/error/messages"; -import { Env, getConnectionBaseURL, getVaultURL, validateToken, isValidURL, fillUrlWithPathAndQueryParams, generateSDKMetrics, printLog, getToken, getBearerToken, MessageType, LogLevel } from "../../../src/utils"; +import { Env, getConnectionBaseURL, getVaultURL, validateToken, isValidURL, fillUrlWithPathAndQueryParams, generateSDKMetrics, printLog, getToken, getBearerToken, MessageType, LogLevel, objectToXML } from "../../../src/utils"; import jwt_decode from 'jwt-decode'; import os from 'os'; import { generateBearerTokenFromCreds, generateBearerToken } from '../../../src/service-account'; @@ -530,3 +530,274 @@ describe('getBearerToken', () => { }); }); +describe('objectToXML', () => { + const { objectToXML } = require('../../../src/utils'); + + test('should convert simple object to XML with default root', () => { + const obj = { name: 'John', age: 30 }; + const result = objectToXML(obj); + + expect(result).toBe('John30'); + }); + + test('should convert simple object to XML with custom root name', () => { + const obj = { name: 'John', age: 30 }; + const result = objectToXML(obj, 'person'); + + expect(result).toBe('John30'); + }); + + test('should convert nested object to XML', () => { + const obj = { + user: { + name: 'John', + details: { + age: 30, + city: 'New York' + } + } + }; + const result = objectToXML(obj); + + expect(result).toContain(''); + expect(result).toContain('John'); + expect(result).toContain('
'); + expect(result).toContain('30'); + expect(result).toContain('New York'); + expect(result).toContain('
'); + expect(result).toContain('
'); + }); + + test('should convert array to XML with repeated elements', () => { + const obj = { + items: ['apple', 'banana', 'cherry'] + }; + const result = objectToXML(obj); + + expect(result).toContain('apple'); + expect(result).toContain('banana'); + expect(result).toContain('cherry'); + }); + + test('should handle null values', () => { + const obj = { name: 'John', middleName: null }; + const result = objectToXML(obj); + + expect(result).toContain('John'); + expect(result).toContain(''); + }); + + test('should handle undefined values', () => { + const obj = { name: 'John', middleName: undefined }; + const result = objectToXML(obj); + + expect(result).toContain('John'); + expect(result).toContain(''); + }); + + test('should escape special XML characters', () => { + const obj = { + text: 'This & that < > " \' are special' + }; + const result = objectToXML(obj); + + expect(result).toContain('&'); + expect(result).toContain('<'); + expect(result).toContain('>'); + expect(result).toContain('"'); + expect(result).toContain('''); + }); + + test('should handle ampersand character', () => { + const obj = { company: 'AT&T' }; + const result = objectToXML(obj); + + expect(result).toContain('AT&T'); + }); + + test('should handle less than and greater than characters', () => { + const obj = { expression: '5 < 10 > 3' }; + const result = objectToXML(obj); + + expect(result).toContain('5 < 10 > 3'); + }); + + test('should handle quotes and apostrophes', () => { + const obj = { text: 'He said "Hello" and it\'s true' }; + const result = objectToXML(obj); + + expect(result).toContain('"'); + expect(result).toContain('''); + }); + + test('should handle boolean values', () => { + const obj = { isActive: true, isDeleted: false }; + const result = objectToXML(obj); + + expect(result).toContain('true'); + expect(result).toContain('false'); + }); + + test('should handle numeric values', () => { + const obj = { age: 30, price: 99.99, negative: -5 }; + const result = objectToXML(obj); + + expect(result).toContain('30'); + expect(result).toContain('99.99'); + expect(result).toContain('-5'); + }); + + test('should handle empty object', () => { + const obj = {}; + const result = objectToXML(obj); + + expect(result).toBe(''); + }); + + test('should handle empty string values', () => { + const obj = { name: '' }; + const result = objectToXML(obj); + + expect(result).toContain(''); + }); + + test('should handle complex nested structure', () => { + const obj = { + order: { + id: 123, + customer: { + name: 'John Doe', + email: 'john@example.com' + }, + items: ['item1', 'item2'], + total: 99.99 + } + }; + const result = objectToXML(obj); + + expect(result).toContain(''); + expect(result).toContain('123'); + expect(result).toContain(''); + expect(result).toContain('John Doe'); + expect(result).toContain('john@example.com'); + expect(result).toContain(''); + expect(result).toContain('item1'); + expect(result).toContain('item2'); + expect(result).toContain('99.99'); + expect(result).toContain(''); + }); + + test('should handle array of objects', () => { + const obj = { + users: [ + { name: 'John', age: 30 }, + { name: 'Jane', age: 25 } + ] + }; + const result = objectToXML(obj); + + expect(result).toContain(''); + expect(result).toContain('John'); + expect(result).toContain('30'); + expect(result).toContain('Jane'); + expect(result).toContain('25'); + expect(result).toContain(''); + }); + + test('should handle mixed types in nested structure', () => { + const obj = { + data: { + string: 'text', + number: 42, + boolean: true, + null: null, + array: [1, 2, 3] + } + }; + const result = objectToXML(obj); + + expect(result).toContain('text'); + expect(result).toContain('42'); + expect(result).toContain('true'); + expect(result).toContain(''); + expect(result).toContain('1'); + expect(result).toContain('2'); + expect(result).toContain('3'); + }); + + test('should include XML declaration', () => { + const obj = { test: 'value' }; + const result = objectToXML(obj); + + expect(result.startsWith('')).toBe(true); + }); + + test('should handle objects with multiple root-level keys', () => { + const obj = { + firstName: 'John', + lastName: 'Doe', + age: 30 + }; + const result = objectToXML(obj, 'person'); + + expect(result).toContain(''); + expect(result).toContain('John'); + expect(result).toContain('Doe'); + expect(result).toContain('30'); + expect(result).toContain(''); + }); + + test('should handle deeply nested objects', () => { + const obj = { + level1: { + level2: { + level3: { + level4: { + value: 'deep' + } + } + } + } + }; + const result = objectToXML(obj); + + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain('deep'); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + }); + + test('should handle empty arrays', () => { + const obj = { items: [] }; + const result = objectToXML(obj); + + // Empty array should not produce any items elements + expect(result).toBe(''); + }); + + test('should convert numbers to strings', () => { + const obj = { zero: 0, negative: -100, float: 3.14159 }; + const result = objectToXML(obj); + + expect(result).toContain('0'); + expect(result).toContain('-100'); + expect(result).toContain('3.14159'); + }); + + test('should handle special characters in keys and values', () => { + const obj = { + 'data-id': 'test-123', + value: 'special & chars < >' + }; + const result = objectToXML(obj); + + expect(result).toContain('test-123'); + expect(result).toContain('special & chars < >'); + }); +}); + From 379b350862b1633002bdd02af6086857c2782a7b Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 3 Feb 2026 12:54:00 +0530 Subject: [PATCH 2/3] SK-2536: fix hard coded value --- src/vault/controller/connections/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vault/controller/connections/index.ts b/src/vault/controller/connections/index.ts index e63845b5..72b57548 100644 --- a/src/vault/controller/connections/index.ts +++ b/src/vault/controller/connections/index.ts @@ -222,7 +222,7 @@ class ConnectionController { if (invokeRequest.headers) { Object.entries(invokeRequest.headers).forEach(([key, value]) => { const lowerKey = key.toLowerCase(); - if (shouldRemoveContentType && lowerKey === "content-type") { + if (shouldRemoveContentType && lowerKey === HTTP_HEADER.CONTENT_TYPE.toLowerCase()) { return; } requestHeaders[key] = From 18bdcef9f975facf8c8ba47c1242e5a15ed9fcea Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Thu, 5 Feb 2026 17:42:59 +0530 Subject: [PATCH 3/3] SK-2536: add unit tests --- test/vault/controller/connection.test.js | 87 ++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/test/vault/controller/connection.test.js b/test/vault/controller/connection.test.js index d628e30d..e0ffd2da 100644 --- a/test/vault/controller/connection.test.js +++ b/test/vault/controller/connection.test.js @@ -116,6 +116,46 @@ describe("ConnectionController Tests", () => { ); }); + it("should handle arrays in URL encoded request body", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "text/plain"; + return null; + }), + }, + text: jest.fn().mockResolvedValue("success"), + }); + + const request = { + body: { + tags: ["tag1", "tag2", "tag3"], + name: "test" + }, + method: RequestMethod.POST, + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }; + + await connectionController.invoke(request); + + const fetchCall = global.fetch.mock.calls[0][1]; + const bodyString = fetchCall.body; + + expect(bodyString).toContain("tags=tag1"); + expect(bodyString).toContain("tags=tag2"); + expect(bodyString).toContain("tags=tag3"); + expect(bodyString).toContain("name=test"); + }); + // Test buildInvokeConnectionBody - multipart/form-data it("should build request body for multipart/form-data and remove content-type header", async () => { const token = { key: "bearer_token" }; @@ -151,6 +191,53 @@ describe("ConnectionController Tests", () => { expect(callArgs.headers["Content-Type"]).toBeUndefined(); }); + // Test buildInvokeConnectionBody - multipart/form-data with File/Blob + it("should handle File and Blob objects in multipart/form-data", async () => { + const token = { key: "bearer_token" }; + getBearerToken.mockResolvedValue(token); + fillUrlWithPathAndQueryParams.mockReturnValue("https://api.example.com/resource"); + generateSDKMetrics.mockReturnValue({ metric: "value" }); + validateInvokeConnectionRequest.mockImplementation(jest.fn()); + + // Create mock File and Blob + const mockFile = new File(["file content"], "test.txt", { type: "text/plain" }); + const mockBlob = new Blob(["blob content"], { type: "application/octet-stream" }); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { + get: jest.fn().mockImplementation((key) => { + if (key === "x-request-id") return "request_id"; + if (key === "content-type") return "text/plain"; + return null; + }), + }, + text: jest.fn().mockResolvedValue("success"), + }); + + const request = { + body: { + file: mockFile, + data: mockBlob, + name: "test" + }, + method: RequestMethod.POST, + headers: { "Content-Type": "multipart/form-data" }, + }; + + await connectionController.invoke(request); + + const callArgs = fetch.mock.calls[0][1]; + expect(callArgs.body).toBeInstanceOf(FormData); + + // Verify FormData was created and File/Blob were appended (covers lines 97-98) + const formData = callArgs.body; + expect(formData.has('file')).toBe(true); + expect(formData.has('data')).toBe(true); + expect(formData.has('name')).toBe(true); + }); + // Test buildInvokeConnectionBody - XML content type it("should build request body for XML content type from object", async () => { const token = { key: "bearer_token" };