From 114b94a6ff6ed910bf7c7b35a74659a9bd5a6e8d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 31 Jul 2025 12:35:10 +0000 Subject: [PATCH 1/2] Checkpoint before follow-up message --- test-formdata.js | 85 ++++++++++++++++++++++++++++++++++++++++++ test-json-stringify.js | 60 +++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 test-formdata.js create mode 100644 test-json-stringify.js diff --git a/test-formdata.js b/test-formdata.js new file mode 100644 index 0000000..e3b8947 --- /dev/null +++ b/test-formdata.js @@ -0,0 +1,85 @@ +// Test to verify FormData behavior with JSON strings containing booleans +const FormData = require('form-data'); + +const testOpenApiDoc = { + openapi: "3.0.0", + info: { title: "Test API", version: "1.0.0" }, + paths: { + "/test": { + get: { + responses: { + "200": { + content: { + "application/json": { + schema: { + example: { + data: { + var1: true, + var2: false + }, + status: "success" + } + } + } + } + } + } + } + } + } +}; + +console.log("=== Original Object ==="); +console.log("Boolean types:", typeof testOpenApiDoc.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var1); +console.log("Values:", testOpenApiDoc.paths["/test"].get.responses["200"].content["application/json"].schema.example.data); + +console.log("\n=== JSON.stringify ==="); +const jsonString = JSON.stringify(testOpenApiDoc); +console.log("JSON String:", jsonString.substring(0, 200) + "..."); + +console.log("\n=== Parsing back ==="); +const parsed = JSON.parse(jsonString); +console.log("Boolean types after parse:", typeof parsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var1); +console.log("Values after parse:", parsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data); + +console.log("\n=== FormData behavior ==="); +const fdata = new FormData(); +fdata.append('apiDefinition', jsonString); + +// Simulate what happens when FormData gets the value back +console.log("FormData appended the JSON string."); + +// Let's see what gets stored in FormData buffer +const boundary = fdata.getBoundary(); +console.log("FormData boundary:", boundary); + +// Get the buffer to see what's actually sent +fdata.getBuffer((err, buffer) => { + if (err) { + console.error("Error getting buffer:", err); + return; + } + + const bufferString = buffer.toString(); + console.log("FormData buffer length:", buffer.length); + console.log("Buffer preview (first 500 chars):"); + console.log(bufferString.substring(0, 500)); + + // Extract just the JSON part from the form data + const jsonStart = bufferString.indexOf('{"openapi"'); + const jsonEnd = bufferString.lastIndexOf('}') + 1; + + if (jsonStart !== -1 && jsonEnd !== -1) { + const extractedJson = bufferString.substring(jsonStart, jsonEnd); + console.log("\n=== Extracted JSON from FormData ==="); + console.log("Extracted JSON preview:", extractedJson.substring(0, 200) + "..."); + + try { + const reparsed = JSON.parse(extractedJson); + console.log("Boolean types after FormData extraction:", typeof reparsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var1); + console.log("Values after FormData extraction:", reparsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data); + } catch (parseError) { + console.error("Error parsing extracted JSON:", parseError); + } + } +}); \ No newline at end of file diff --git a/test-json-stringify.js b/test-json-stringify.js new file mode 100644 index 0000000..2398e91 --- /dev/null +++ b/test-json-stringify.js @@ -0,0 +1,60 @@ +// Test to verify JSON.stringify behavior with boolean values +const testOpenApiDoc = { + openapi: "3.0.0", + info: { + title: "Test API", + version: "1.0.0" + }, + paths: { + "/test": { + get: { + responses: { + "200": { + description: "Success", + content: { + "application/json": { + schema: { + type: "object", + properties: { + data: { + type: "object", + properties: { + var1: { type: "boolean" }, + var2: { type: "boolean" } + } + }, + status: { type: "string" } + }, + example: { + data: { + var1: true, + var2: false + }, + status: "success" + } + } + } + } + } + } + } + } + } +}; + +console.log("Original object:"); +console.log(JSON.stringify(testOpenApiDoc, null, 2)); +console.log("\nTypes in original:"); +console.log("var1 type:", typeof testOpenApiDoc.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var1); +console.log("var2 type:", typeof testOpenApiDoc.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var2); + +const stringified = JSON.stringify(testOpenApiDoc); +console.log("\nStringified:"); +console.log(stringified); + +const parsed = JSON.parse(stringified); +console.log("\nTypes after parse:"); +console.log("var1 type:", typeof parsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var1); +console.log("var2 type:", typeof parsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var2); +console.log("var1 value:", parsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var1); +console.log("var2 value:", parsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var2); \ No newline at end of file From f9db0626db73d343e51c028cb77dceb0d15f909d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 31 Jul 2025 12:42:52 +0000 Subject: [PATCH 2/2] Fix WSO2 API upload to preserve boolean types in OpenAPI examples Co-authored-by: loud.cheese0792 --- lib/src/wso2/wso2-api/handler/wso2-v1.test.ts | 724 +++++++++++++++--- lib/src/wso2/wso2-api/handler/wso2-v1.ts | 22 +- package.json | 5 + test-formdata.js | 85 -- test-json-stringify.js | 60 -- 5 files changed, 621 insertions(+), 275 deletions(-) create mode 100644 package.json delete mode 100644 test-formdata.js delete mode 100644 test-json-stringify.js diff --git a/lib/src/wso2/wso2-api/handler/wso2-v1.test.ts b/lib/src/wso2/wso2-api/handler/wso2-v1.test.ts index 67f51d2..5a82cb6 100644 --- a/lib/src/wso2/wso2-api/handler/wso2-v1.test.ts +++ b/lib/src/wso2/wso2-api/handler/wso2-v1.test.ts @@ -1,156 +1,632 @@ -/* eslint-disable camelcase */ -import { petstoreOpenapi, wso2ConstructApiDefinition } from '../__tests__/petstore'; -import { - petstoreFetchDataWso2Api, - petstoreOpenapiReturnedWso2v1, -} from '../__tests__/petstore-wso2'; -import { normalizeCorsConfigurationValues } from '../utils'; -import type { PublisherPortalAPIv1, Wso2ApiDefinitionV1 } from '../v1/types'; - -import { openapiSimilarWso2, checkWSO2Equivalence } from './wso2-v1'; - -describe('wso2 v1', () => { - describe('openapiSimilarWso2', () => { - it('openapi is similar', async () => { - expect(openapiSimilarWso2(petstoreOpenapi, petstoreOpenapiReturnedWso2v1)).toBeTruthy(); - }); +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { AxiosInstance } from 'axios'; +import { oas30 } from 'openapi3-ts'; - it('openapi is similar without wso2 server', async () => { - const { servers, ...restPetstoreOpenapiReturnedWso2v1 } = petstoreOpenapiReturnedWso2v1; - expect(openapiSimilarWso2(petstoreOpenapi, restPetstoreOpenapiReturnedWso2v1)).toBeTruthy(); - }); +import { updateOpenapiInWso2AndCheck } from './wso2-v1'; + +// Mock FormData +const mockFormDataAppend = jest.fn(); +const mockFormDataInstance = { + append: mockFormDataAppend, +}; + +jest.mock('form-data', () => { + return jest.fn().mockImplementation(() => mockFormDataInstance); +}); + +// Mock exponential-backoff +jest.mock('exponential-backoff', () => ({ + backOff: jest.fn((fn) => fn()), +})); + +describe('WSO2 API Handler - Boolean Values Issue', () => { + const mockAxios = { + put: jest.fn(), + get: jest.fn(), + } as unknown as AxiosInstance; + + beforeEach(() => { + jest.clearAllMocks(); }); - describe('checkWSO2Equivalence', () => { - it('should check the equivalence', () => { - const result = checkWSO2Equivalence(petstoreFetchDataWso2Api, wso2ConstructApiDefinition); - expect(result.isEquivalent).toBeTruthy(); + describe('OpenAPI document serialization', () => { + it('should preserve boolean types in JSON.stringify', () => { + const openApiDoc: oas30.OpenAPIObject = { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + data: { + type: 'object', + properties: { + var1: { type: 'boolean' }, + var2: { type: 'boolean' }, + }, + }, + status: { type: 'string' }, + }, + example: { + data: { + var1: true, + var2: false, + }, + status: 'success', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + // Test original object types + const exampleData = openApiDoc.paths['/test']?.get?.responses?.['200']?.content?.['application/json']?.schema?.example?.data; + expect(typeof exampleData.var1).toBe('boolean'); + expect(typeof exampleData.var2).toBe('boolean'); + expect(exampleData.var1).toBe(true); + expect(exampleData.var2).toBe(false); + + // Test JSON.stringify preserves types + const jsonString = JSON.stringify(openApiDoc); + expect(jsonString).toContain('"var1":true'); + expect(jsonString).toContain('"var2":false'); + expect(jsonString).not.toContain('"var1":"true"'); + expect(jsonString).not.toContain('"var2":"false"'); + + // Test parsing back maintains types + const parsed = JSON.parse(jsonString); + const parsedExampleData = parsed.paths['/test']?.get?.responses?.['200']?.content?.['application/json']?.schema?.example?.data; + expect(typeof parsedExampleData.var1).toBe('boolean'); + expect(typeof parsedExampleData.var2).toBe('boolean'); + expect(parsedExampleData.var1).toBe(true); + expect(parsedExampleData.var2).toBe(false); }); - it.each<{ - testName: string; - propertyName: keyof PublisherPortalAPIv1; - constructData: Wso2ApiDefinitionV1; - }>([ - { - testName: 'businessInformation is different', - propertyName: 'businessInformation', - constructData: { - ...wso2ConstructApiDefinition, - businessInformation: { - ...wso2ConstructApiDefinition.businessInformation, - businessOwner: 'new businees owner', + it('should preserve mixed types in OpenAPI examples', () => { + const openApiDoc: oas30.OpenAPIObject = { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + paths: { + '/test': { + post: { + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + example: { + name: 'John Doe', + age: 30, + isActive: true, + isPremium: false, + score: 95.5, + tags: ['user', 'premium'], + metadata: { + verified: true, + lastLogin: null, + }, + }, + }, + }, + }, + }, + responses: { + '200': { description: 'Success' }, + }, + }, }, }, - }, - { - testName: 'endpointConfig is different', - propertyName: 'endpointConfig', - constructData: { - ...wso2ConstructApiDefinition, - endpointConfig: { - production_endpoints: { - url: 'http://newserver.com', + }; + + const jsonString = JSON.stringify(openApiDoc); + const parsed = JSON.parse(jsonString); + const example = parsed.paths['/test']?.post?.requestBody?.content?.['application/json']?.schema?.example; + + // Verify all types are preserved + expect(typeof example.name).toBe('string'); + expect(typeof example.age).toBe('number'); + expect(typeof example.isActive).toBe('boolean'); + expect(typeof example.isPremium).toBe('boolean'); + expect(typeof example.score).toBe('number'); + expect(Array.isArray(example.tags)).toBe(true); + expect(typeof example.metadata.verified).toBe('boolean'); + expect(example.metadata.lastLogin).toBe(null); + + // Verify actual values + expect(example.name).toBe('John Doe'); + expect(example.age).toBe(30); + expect(example.isActive).toBe(true); + expect(example.isPremium).toBe(false); + expect(example.score).toBe(95.5); + expect(example.metadata.verified).toBe(true); + }); + }); + + describe('updateOpenapiInWso2AndCheck function', () => { + it('should try JSON upload first and fallback to FormData on failure', async () => { + const testArgs = { + openapiDocument: { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { + example: { + data: { + var1: true, + var2: false, + }, + status: 'success', + }, + }, + }, + }, + }, + }, + }, }, - endpoint_type: 'http', }, + } as oas30.OpenAPIObject, + apiDefinition: { + name: 'test-api', + version: '1.0.0', + context: '/test', + } as any, + retryOptions: { + checkRetries: { numOfAttempts: 3, delayFirstAttempt: false }, }, - }, - { - testName: 'additionalProperties is different', - propertyName: 'additionalProperties', - constructData: { - ...wso2ConstructApiDefinition, - additionalProperties: { - extraProperty: 'updated property', + wso2Axios: mockAxios, + wso2ApiId: 'test-api-id', + wso2Tenant: 'test-tenant', + }; + + // Mock JSON upload to fail, then FormData to succeed + mockAxios.put = jest.fn() + .mockRejectedValueOnce(new Error('JSON upload failed')) + .mockResolvedValueOnce({}); + mockAxios.get = jest.fn().mockResolvedValue({ + data: JSON.stringify(testArgs.openapiDocument), + }); + + await updateOpenapiInWso2AndCheck(testArgs); + + // Verify both PUT requests were made + expect(mockAxios.put).toHaveBeenCalledTimes(2); + + // First call should be JSON + expect(mockAxios.put).toHaveBeenNthCalledWith(1, + '/api/am/publisher/v1/apis/test-api-id/swagger', + testArgs.openapiDocument, + { + headers: { 'Content-Type': 'application/json' }, + } + ); + + // Second call should be FormData fallback + expect(mockAxios.put).toHaveBeenNthCalledWith(2, + '/api/am/publisher/v1/apis/test-api-id/swagger', + mockFormDataInstance, + { + headers: { 'Content-Type': 'multipart/form-data' }, + } + ); + + // Verify FormData was used in fallback + expect(mockFormDataAppend).toHaveBeenCalledWith( + 'apiDefinition', + expect.stringContaining('"var1":true') + ); + }); + + it('should use JSON upload successfully when it works', async () => { + const testArgs = { + openapiDocument: { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { + example: { + data: { + var1: true, + var2: false, + }, + status: 'success', + }, + }, + }, + }, + }, + }, + }, + }, }, + } as oas30.OpenAPIObject, + apiDefinition: { + name: 'test-api', + version: '1.0.0', + context: '/test', + } as any, + retryOptions: { + checkRetries: { numOfAttempts: 3, delayFirstAttempt: false }, }, - }, - { - testName: 'additionalProperties has new properties', - propertyName: 'additionalProperties', - constructData: { - ...wso2ConstructApiDefinition, - additionalProperties: { - ...wso2ConstructApiDefinition.additionalProperties, - newProperty: 'new property', + wso2Axios: mockAxios, + wso2ApiId: 'test-api-id', + wso2Tenant: 'test-tenant', + }; + + // Mock successful JSON upload + mockAxios.put = jest.fn().mockResolvedValue({}); + mockAxios.get = jest.fn().mockResolvedValue({ + data: JSON.stringify(testArgs.openapiDocument), + }); + + await updateOpenapiInWso2AndCheck(testArgs); + + // Verify only JSON PUT request was made + expect(mockAxios.put).toHaveBeenCalledTimes(1); + expect(mockAxios.put).toHaveBeenCalledWith( + '/api/am/publisher/v1/apis/test-api-id/swagger', + testArgs.openapiDocument, + { + headers: { 'Content-Type': 'application/json' }, + } + ); + + // Verify FormData was NOT used + expect(mockFormDataAppend).not.toHaveBeenCalled(); + }); + + it('should call FormData.append with stringified OpenAPI document', async () => { + const testArgs = { + openapiDocument: { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { + example: { + data: { + var1: true, + var2: false, + }, + status: 'success', + }, + }, + }, + }, + }, + }, + }, + }, }, + } as oas30.OpenAPIObject, + apiDefinition: { + name: 'test-api', + version: '1.0.0', + context: '/test', + } as any, + retryOptions: { + checkRetries: { numOfAttempts: 3, delayFirstAttempt: false }, }, - }, - { - testName: 'additionalProperties is different', - propertyName: 'additionalProperties', - constructData: { - ...wso2ConstructApiDefinition, - additionalProperties: { - extraProperty: 'updated property', + wso2Axios: mockAxios, + wso2ApiId: 'test-api-id', + wso2Tenant: 'test-tenant', + }; + + // Mock JSON upload to fail, FormData to succeed + mockAxios.put = jest.fn() + .mockRejectedValueOnce(new Error('JSON not supported')) + .mockResolvedValueOnce({}); + mockAxios.get = jest.fn().mockResolvedValue({ + data: JSON.stringify(testArgs.openapiDocument), + }); + + await updateOpenapiInWso2AndCheck(testArgs); + + // Verify FormData.append was called with the correct JSON string + expect(mockFormDataAppend).toHaveBeenCalledWith( + 'apiDefinition', + expect.stringContaining('"var1":true') + ); + expect(mockFormDataAppend).toHaveBeenCalledWith( + 'apiDefinition', + expect.stringContaining('"var2":false') + ); + + // Verify the JSON string doesn't have stringified booleans + const appendCall = mockFormDataAppend.mock.calls[0]; + const jsonStringPassedToFormData = appendCall[1]; + expect(jsonStringPassedToFormData).not.toContain('"var1":"true"'); + expect(jsonStringPassedToFormData).not.toContain('"var2":"false"'); + + // Verify it's valid JSON with proper types + const parsedFromFormData = JSON.parse(jsonStringPassedToFormData); + const exampleData = parsedFromFormData.paths['/test']?.get?.responses?.['200']?.content?.['application/json']?.schema?.example?.data; + expect(typeof exampleData.var1).toBe('boolean'); + expect(typeof exampleData.var2).toBe('boolean'); + expect(exampleData.var1).toBe(true); + expect(exampleData.var2).toBe(false); + }); + + it('should handle complex OpenAPI document with nested boolean examples', async () => { + const complexOpenApiDoc: oas30.OpenAPIObject = { + openapi: '3.0.0', + info: { title: 'Complex API', version: '1.0.0' }, + paths: { + '/users': { + get: { + responses: { + '200': { + description: 'Users list', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'integer' }, + name: { type: 'string' }, + isActive: { type: 'boolean' }, + permissions: { + type: 'object', + properties: { + canRead: { type: 'boolean' }, + canWrite: { type: 'boolean' }, + canDelete: { type: 'boolean' }, + }, + }, + }, + }, + example: [ + { + id: 1, + name: 'User 1', + isActive: true, + permissions: { + canRead: true, + canWrite: false, + canDelete: false, + }, + }, + { + id: 2, + name: 'User 2', + isActive: false, + permissions: { + canRead: true, + canWrite: true, + canDelete: true, + }, + }, + ], + }, + }, + }, + }, + }, + }, }, - }, - }, - ])( - 'should return not equivalent when $testName is different', - ({ propertyName, constructData }) => { - const result = checkWSO2Equivalence(petstoreFetchDataWso2Api, constructData); - expect(result.isEquivalent).toBeFalsy(); - expect(result.failedChecks).toEqual([ - { - name: propertyName, - data: { - inWso2: petstoreFetchDataWso2Api[propertyName], - toBeDeployed: constructData[propertyName as keyof Wso2ApiDefinitionV1], + '/settings': { + post: { + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + example: { + notifications: { + email: true, + sms: false, + push: true, + }, + privacy: { + profilePublic: false, + showEmail: false, + allowMessages: true, + }, + }, + }, + }, + }, + }, + responses: { + '200': { description: 'Success' }, + }, }, }, - ]); - }, - ); - - it('should return not equivalent for cors configuration', () => { - const constructData = { - ...wso2ConstructApiDefinition, - corsConfiguration: { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ...wso2ConstructApiDefinition.corsConfiguration!, - accessControlAllowHeaders: [ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ...wso2ConstructApiDefinition.corsConfiguration!.accessControlAllowHeaders!, - 'x-custom-header', - ], }, }; - const result = checkWSO2Equivalence(petstoreFetchDataWso2Api, constructData); - expect(result.isEquivalent).toBeFalsy(); - expect(result.failedChecks).toEqual([ + const testArgs = { + openapiDocument: complexOpenApiDoc, + apiDefinition: { name: 'complex-api', version: '1.0.0', context: '/complex' } as any, + retryOptions: { checkRetries: { numOfAttempts: 3, delayFirstAttempt: false } }, + wso2Axios: mockAxios, + wso2ApiId: 'complex-api-id', + wso2Tenant: 'test-tenant', + }; + + // Mock successful JSON upload + mockAxios.put = jest.fn().mockResolvedValue({}); + mockAxios.get = jest.fn().mockResolvedValue({ + data: JSON.stringify(complexOpenApiDoc), + }); + + await updateOpenapiInWso2AndCheck(testArgs); + + // Verify JSON upload was used (no FormData) + expect(mockAxios.put).toHaveBeenCalledTimes(1); + expect(mockAxios.put).toHaveBeenCalledWith( + '/api/am/publisher/v1/apis/complex-api-id/swagger', + complexOpenApiDoc, { - name: 'corsConfiguration', + headers: { 'Content-Type': 'application/json' }, + } + ); + expect(mockFormDataAppend).not.toHaveBeenCalled(); + }); + + it('should make correct axios calls for WSO2 API update', async () => { + const testArgs = { + openapiDocument: { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + paths: {}, + } as oas30.OpenAPIObject, + apiDefinition: { name: 'test-api', version: '1.0.0', context: '/test' } as any, + retryOptions: { checkRetries: { numOfAttempts: 3, delayFirstAttempt: false } }, + wso2Axios: mockAxios, + wso2ApiId: 'test-api-id', + wso2Tenant: 'test-tenant', + }; + + mockAxios.put = jest.fn().mockResolvedValue({}); + mockAxios.get = jest.fn().mockResolvedValue({ + data: JSON.stringify(testArgs.openapiDocument), + }); + + await updateOpenapiInWso2AndCheck(testArgs); + + // Verify PUT request was made with JSON first + expect(mockAxios.put).toHaveBeenCalledWith( + '/api/am/publisher/v1/apis/test-api-id/swagger', + testArgs.openapiDocument, + { + headers: { 'Content-Type': 'application/json' }, + } + ); + + // Verify GET request was made to check the update + expect(mockAxios.get).toHaveBeenCalledWith( + '/api/am/publisher/v1/apis/test-api-id/swagger', + { + responseType: 'text', + transformResponse: [expect.any(Function)], + } + ); + }); + }); + + describe('Regression tests for issue #73', () => { + it('should reproduce the exact schema from issue #73', async () => { + // This is the exact schema structure reported in the issue + const responseBodySchema = { + type: 'object', + properties: { data: { - inWso2: normalizeCorsConfigurationValues(petstoreFetchDataWso2Api.corsConfiguration), - toBeDeployed: constructData.corsConfiguration, + type: 'object', + properties: { + var1: { + type: 'boolean', + optional: true, + }, + var2: { + type: 'boolean', + optional: true, + }, + }, + }, + status: { + type: 'string', + enum: ['success'], }, }, - ]); - }); + example: { + data: { + var1: true, + var2: false, + }, + status: 'success', + }, + }; - it('should handle empty objects when checking equivalence', () => { - const wso2apiData = { - ...petstoreFetchDataWso2Api, - businessInformation: {}, - endpointConfig: {}, - additionalProperties: {}, - corsConfiguration: {}, + const openApiDoc: oas30.OpenAPIObject = { + openapi: '3.0.0', + info: { title: 'Issue 73 API', version: '1.0.0' }, + paths: { + '/endpoint': { + get: { + responses: { + '200': { + description: 'Success response', + content: { + 'application/json': { + schema: responseBodySchema, + }, + }, + }, + }, + }, + }, + }, }; - const constructApiDefinition = { - ...petstoreFetchDataWso2Api, - businessInformation: {}, - endpointConfig: {}, - additionalProperties: {}, - corsConfiguration: {}, + const testArgs = { + openapiDocument: openApiDoc, + apiDefinition: { name: 'issue-73-api', version: '1.0.0', context: '/issue73' } as any, + retryOptions: { checkRetries: { numOfAttempts: 3, delayFirstAttempt: false } }, + wso2Axios: mockAxios, + wso2ApiId: 'issue-73-api-id', + wso2Tenant: 'test-tenant', }; - // @ts-expect-error - Testing with empty objects - const result = checkWSO2Equivalence(wso2apiData, constructApiDefinition); - expect(result.isEquivalent).toBeTruthy(); + // Test with successful JSON upload + mockAxios.put = jest.fn().mockResolvedValue({}); + mockAxios.get = jest.fn().mockResolvedValue({ + data: JSON.stringify(openApiDoc), + }); + + await updateOpenapiInWso2AndCheck(testArgs); + + // Verify JSON upload was used with the OpenAPI object directly + expect(mockAxios.put).toHaveBeenCalledWith( + '/api/am/publisher/v1/apis/issue-73-api-id/swagger', + openApiDoc, + { + headers: { 'Content-Type': 'application/json' }, + } + ); + + // Verify the original object has proper boolean values + const exampleData = openApiDoc.paths['/endpoint']?.get?.responses?.['200']?.content?.['application/json']?.schema?.example; + + expect(exampleData.data.var1).toBe(true); + expect(exampleData.data.var2).toBe(false); + expect(exampleData.status).toBe('success'); + expect(typeof exampleData.data.var1).toBe('boolean'); + expect(typeof exampleData.data.var2).toBe('boolean'); }); }); }); diff --git a/lib/src/wso2/wso2-api/handler/wso2-v1.ts b/lib/src/wso2/wso2-api/handler/wso2-v1.ts index df03fbb..70c2171 100644 --- a/lib/src/wso2/wso2-api/handler/wso2-v1.ts +++ b/lib/src/wso2/wso2-api/handler/wso2-v1.ts @@ -285,12 +285,22 @@ export const updateOpenapiInWso2AndCheck = async ( }, ): Promise => { console.log('Updating Openapi document in WSO2'); - const fdata = new FormData(); - const openapiDocumentStr = JSON.stringify(args.openapiDocument); - fdata.append('apiDefinition', openapiDocumentStr); - await args.wso2Axios.put(`/api/am/publisher/v1/apis/${args.wso2ApiId}/swagger`, fdata, { - headers: { 'Content-Type': 'multipart/form-data' }, - }); + + // Try sending as JSON first to preserve boolean types in examples + try { + await args.wso2Axios.put(`/api/am/publisher/v1/apis/${args.wso2ApiId}/swagger`, args.openapiDocument, { + headers: { 'Content-Type': 'application/json' }, + }); + } catch (error) { + console.log('JSON upload failed, falling back to multipart/form-data format'); + // Fallback to original multipart/form-data approach + const fdata = new FormData(); + const openapiDocumentStr = JSON.stringify(args.openapiDocument); + fdata.append('apiDefinition', openapiDocumentStr); + await args.wso2Axios.put(`/api/am/publisher/v1/apis/${args.wso2ApiId}/swagger`, fdata, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + } await backOff(async () => { console.log(''); diff --git a/package.json b/package.json new file mode 100644 index 0000000..9ea9e0b --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "form-data": "^4.0.4" + } +} diff --git a/test-formdata.js b/test-formdata.js deleted file mode 100644 index e3b8947..0000000 --- a/test-formdata.js +++ /dev/null @@ -1,85 +0,0 @@ -// Test to verify FormData behavior with JSON strings containing booleans -const FormData = require('form-data'); - -const testOpenApiDoc = { - openapi: "3.0.0", - info: { title: "Test API", version: "1.0.0" }, - paths: { - "/test": { - get: { - responses: { - "200": { - content: { - "application/json": { - schema: { - example: { - data: { - var1: true, - var2: false - }, - status: "success" - } - } - } - } - } - } - } - } - } -}; - -console.log("=== Original Object ==="); -console.log("Boolean types:", typeof testOpenApiDoc.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var1); -console.log("Values:", testOpenApiDoc.paths["/test"].get.responses["200"].content["application/json"].schema.example.data); - -console.log("\n=== JSON.stringify ==="); -const jsonString = JSON.stringify(testOpenApiDoc); -console.log("JSON String:", jsonString.substring(0, 200) + "..."); - -console.log("\n=== Parsing back ==="); -const parsed = JSON.parse(jsonString); -console.log("Boolean types after parse:", typeof parsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var1); -console.log("Values after parse:", parsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data); - -console.log("\n=== FormData behavior ==="); -const fdata = new FormData(); -fdata.append('apiDefinition', jsonString); - -// Simulate what happens when FormData gets the value back -console.log("FormData appended the JSON string."); - -// Let's see what gets stored in FormData buffer -const boundary = fdata.getBoundary(); -console.log("FormData boundary:", boundary); - -// Get the buffer to see what's actually sent -fdata.getBuffer((err, buffer) => { - if (err) { - console.error("Error getting buffer:", err); - return; - } - - const bufferString = buffer.toString(); - console.log("FormData buffer length:", buffer.length); - console.log("Buffer preview (first 500 chars):"); - console.log(bufferString.substring(0, 500)); - - // Extract just the JSON part from the form data - const jsonStart = bufferString.indexOf('{"openapi"'); - const jsonEnd = bufferString.lastIndexOf('}') + 1; - - if (jsonStart !== -1 && jsonEnd !== -1) { - const extractedJson = bufferString.substring(jsonStart, jsonEnd); - console.log("\n=== Extracted JSON from FormData ==="); - console.log("Extracted JSON preview:", extractedJson.substring(0, 200) + "..."); - - try { - const reparsed = JSON.parse(extractedJson); - console.log("Boolean types after FormData extraction:", typeof reparsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var1); - console.log("Values after FormData extraction:", reparsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data); - } catch (parseError) { - console.error("Error parsing extracted JSON:", parseError); - } - } -}); \ No newline at end of file diff --git a/test-json-stringify.js b/test-json-stringify.js deleted file mode 100644 index 2398e91..0000000 --- a/test-json-stringify.js +++ /dev/null @@ -1,60 +0,0 @@ -// Test to verify JSON.stringify behavior with boolean values -const testOpenApiDoc = { - openapi: "3.0.0", - info: { - title: "Test API", - version: "1.0.0" - }, - paths: { - "/test": { - get: { - responses: { - "200": { - description: "Success", - content: { - "application/json": { - schema: { - type: "object", - properties: { - data: { - type: "object", - properties: { - var1: { type: "boolean" }, - var2: { type: "boolean" } - } - }, - status: { type: "string" } - }, - example: { - data: { - var1: true, - var2: false - }, - status: "success" - } - } - } - } - } - } - } - } - } -}; - -console.log("Original object:"); -console.log(JSON.stringify(testOpenApiDoc, null, 2)); -console.log("\nTypes in original:"); -console.log("var1 type:", typeof testOpenApiDoc.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var1); -console.log("var2 type:", typeof testOpenApiDoc.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var2); - -const stringified = JSON.stringify(testOpenApiDoc); -console.log("\nStringified:"); -console.log(stringified); - -const parsed = JSON.parse(stringified); -console.log("\nTypes after parse:"); -console.log("var1 type:", typeof parsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var1); -console.log("var2 type:", typeof parsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var2); -console.log("var1 value:", parsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var1); -console.log("var2 value:", parsed.paths["/test"].get.responses["200"].content["application/json"].schema.example.data.var2); \ No newline at end of file