From 24e0d017e43960c35b2a1262ee2c5e89e08c295f Mon Sep 17 00:00:00 2001 From: Danielle Maor Date: Mon, 7 Jul 2025 17:21:50 +0300 Subject: [PATCH 1/4] Empty post capabilities array --- .../server-tests/bad-request-body.test.ts | 100 +++++++++--------- 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/v2/api-validator/tests/server-tests/bad-request-body.test.ts b/v2/api-validator/tests/server-tests/bad-request-body.test.ts index a175092a..4fbe15ac 100644 --- a/v2/api-validator/tests/server-tests/bad-request-body.test.ts +++ b/v2/api-validator/tests/server-tests/bad-request-body.test.ts @@ -21,58 +21,60 @@ describe('Test request bodies missing one required property', () => { let accountId: string; - describe.each(postEndpoints)('$method $url', ({ operationId, url, schema }: EndpointSchema) => { - const [component] = schema.tags; - accountId = getCapableAccountId(component as keyof ApiComponents); - - // Test multiple times due to the randomness of the generated payloads - describe.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])('Attempt %d', () => { - const goodBody = JSONSchemaFaker.generate(schema.body as Schema); - if (!goodBody || typeof goodBody !== 'object') throw new Error('Unexpected body type'); - - const sendRequest = async (requestBody: JsonValue) => { - const operationFunction = client[schema.tags[0]]?.[operationId].bind(client); - - try { - await operationFunction({ requestBody, accountId }); - } catch (err) { - if (err instanceof ApiError) { - return err; - } - throw err; - } - throw new Error('Expected to throw'); - }; - - for (const propertyPath of getPropertyPaths(goodBody)) { - describe(`Body without ${propertyPath.join('.')}`, () => { - let apiError: ApiError; - - beforeAll(async () => { - const badBody = _.cloneDeep(goodBody); - deleteDeepProperty(badBody, propertyPath); - - if (Object.hasOwn(badBody, 'idempotencyKey')) { - badBody['idempotencyKey'] = randomUUID(); + if (postEndpoints.length > 0) { + describe.each(postEndpoints)('$method $url', ({ operationId, url, schema }: EndpointSchema) => { + const [component] = schema.tags; + accountId = getCapableAccountId(component as keyof ApiComponents); + + // Test multiple times due to the randomness of the generated payloads + describe.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])('Attempt %d', () => { + const goodBody = JSONSchemaFaker.generate(schema.body as Schema); + if (!goodBody || typeof goodBody !== 'object') throw new Error('Unexpected body type'); + + const sendRequest = async (requestBody: JsonValue) => { + const operationFunction = client[schema.tags[0]]?.[operationId].bind(client); + + try { + await operationFunction({ requestBody, accountId }); + } catch (err) { + if (err instanceof ApiError) { + return err; } - - apiError = await sendRequest(badBody); - }); - - it('should respond with HTTP response code 400 (Bad Request)', () => { - expect(apiError.status).toEqual(400); - }); - it('should properly describe the error in the response body', () => { - expect(apiError.body.requestPart).toEqual(RequestPart.BODY); - expect(getExpectedVariants(url, propertyPath)).toContain(apiError.body.propertyName); - expect(apiError.body.errorType).toEqual( - BadRequestError.errorType.SCHEMA_PROPERTY_ERROR - ); + throw err; + } + throw new Error('Expected to throw'); + }; + + for (const propertyPath of getPropertyPaths(goodBody)) { + describe(`Body without ${propertyPath.join('.')}`, () => { + let apiError: ApiError; + + beforeAll(async () => { + const badBody = _.cloneDeep(goodBody); + deleteDeepProperty(badBody, propertyPath); + + if (Object.hasOwn(badBody, 'idempotencyKey')) { + badBody['idempotencyKey'] = randomUUID(); + } + + apiError = await sendRequest(badBody); + }); + + it('should respond with HTTP response code 400 (Bad Request)', () => { + expect(apiError.status).toEqual(400); + }); + it('should properly describe the error in the response body', () => { + expect(apiError.body.requestPart).toEqual(RequestPart.BODY); + expect(getExpectedVariants(url, propertyPath)).toContain(apiError.body.propertyName); + expect(apiError.body.errorType).toEqual( + BadRequestError.errorType.SCHEMA_PROPERTY_ERROR + ); + }); }); - }); - } + } + }); }); - }); + } }); // When missing a properties appearing in oneOf, the server might report as if From cac3e036e21fc3c3c1a0e7a274ce07a98fc62fb2 Mon Sep 17 00:00:00 2001 From: Danielle Maor Date: Tue, 1 Sep 2026 19:24:28 +0300 Subject: [PATCH 2/4] Add custom capability --- v2/api-validator/src/client/generated/models/ApiComponents.ts | 1 + v2/openapi/fb-provider-api.yaml | 2 ++ v2/openapi/fb-unified-openapi.yaml | 2 ++ 3 files changed, 5 insertions(+) diff --git a/v2/api-validator/src/client/generated/models/ApiComponents.ts b/v2/api-validator/src/client/generated/models/ApiComponents.ts index b2b076f0..d5f41477 100644 --- a/v2/api-validator/src/client/generated/models/ApiComponents.ts +++ b/v2/api-validator/src/client/generated/models/ApiComponents.ts @@ -21,5 +21,6 @@ export type ApiComponents = { liquidity?: AccountsSet; collateral?: AccountsSet; ramps?: AccountsSet; + custom?: AccountsSet; }; diff --git a/v2/openapi/fb-provider-api.yaml b/v2/openapi/fb-provider-api.yaml index 7262309f..beb2fa0c 100644 --- a/v2/openapi/fb-provider-api.yaml +++ b/v2/openapi/fb-provider-api.yaml @@ -715,6 +715,8 @@ components: $ref: '#/components/schemas/AccountsSet' ramps: $ref: '#/components/schemas/AccountsSet' + custom: + $ref: '#/components/schemas/AccountsSet' AccountsSet: description: >- diff --git a/v2/openapi/fb-unified-openapi.yaml b/v2/openapi/fb-unified-openapi.yaml index 52d10c0b..bfeb3b92 100644 --- a/v2/openapi/fb-unified-openapi.yaml +++ b/v2/openapi/fb-unified-openapi.yaml @@ -2919,6 +2919,8 @@ components: $ref: '#/components/schemas/AccountsSet' ramps: $ref: '#/components/schemas/AccountsSet' + custom: + $ref: '#/components/schemas/AccountsSet' AccountsSet: description: Indicates to which sub-accounts an operation applies. The value could be either a list of sub-account IDs or "*". The value "*" indicates all the sub-accounts. oneOf: From 15a9013db7d04a128249deec9edae215219fdfb9 Mon Sep 17 00:00:00 2001 From: Danielle Maor Date: Wed, 2 Sep 2026 13:15:24 +0300 Subject: [PATCH 3/4] revert back to main --- .../server-tests/bad-request-body.test.ts | 100 +++++++++--------- 1 file changed, 49 insertions(+), 51 deletions(-) diff --git a/v2/api-validator/tests/server-tests/bad-request-body.test.ts b/v2/api-validator/tests/server-tests/bad-request-body.test.ts index 4fbe15ac..a175092a 100644 --- a/v2/api-validator/tests/server-tests/bad-request-body.test.ts +++ b/v2/api-validator/tests/server-tests/bad-request-body.test.ts @@ -21,60 +21,58 @@ describe('Test request bodies missing one required property', () => { let accountId: string; - if (postEndpoints.length > 0) { - describe.each(postEndpoints)('$method $url', ({ operationId, url, schema }: EndpointSchema) => { - const [component] = schema.tags; - accountId = getCapableAccountId(component as keyof ApiComponents); - - // Test multiple times due to the randomness of the generated payloads - describe.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])('Attempt %d', () => { - const goodBody = JSONSchemaFaker.generate(schema.body as Schema); - if (!goodBody || typeof goodBody !== 'object') throw new Error('Unexpected body type'); - - const sendRequest = async (requestBody: JsonValue) => { - const operationFunction = client[schema.tags[0]]?.[operationId].bind(client); - - try { - await operationFunction({ requestBody, accountId }); - } catch (err) { - if (err instanceof ApiError) { - return err; - } - throw err; + describe.each(postEndpoints)('$method $url', ({ operationId, url, schema }: EndpointSchema) => { + const [component] = schema.tags; + accountId = getCapableAccountId(component as keyof ApiComponents); + + // Test multiple times due to the randomness of the generated payloads + describe.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])('Attempt %d', () => { + const goodBody = JSONSchemaFaker.generate(schema.body as Schema); + if (!goodBody || typeof goodBody !== 'object') throw new Error('Unexpected body type'); + + const sendRequest = async (requestBody: JsonValue) => { + const operationFunction = client[schema.tags[0]]?.[operationId].bind(client); + + try { + await operationFunction({ requestBody, accountId }); + } catch (err) { + if (err instanceof ApiError) { + return err; } - throw new Error('Expected to throw'); - }; - - for (const propertyPath of getPropertyPaths(goodBody)) { - describe(`Body without ${propertyPath.join('.')}`, () => { - let apiError: ApiError; - - beforeAll(async () => { - const badBody = _.cloneDeep(goodBody); - deleteDeepProperty(badBody, propertyPath); - - if (Object.hasOwn(badBody, 'idempotencyKey')) { - badBody['idempotencyKey'] = randomUUID(); - } - - apiError = await sendRequest(badBody); - }); - - it('should respond with HTTP response code 400 (Bad Request)', () => { - expect(apiError.status).toEqual(400); - }); - it('should properly describe the error in the response body', () => { - expect(apiError.body.requestPart).toEqual(RequestPart.BODY); - expect(getExpectedVariants(url, propertyPath)).toContain(apiError.body.propertyName); - expect(apiError.body.errorType).toEqual( - BadRequestError.errorType.SCHEMA_PROPERTY_ERROR - ); - }); - }); + throw err; } - }); + throw new Error('Expected to throw'); + }; + + for (const propertyPath of getPropertyPaths(goodBody)) { + describe(`Body without ${propertyPath.join('.')}`, () => { + let apiError: ApiError; + + beforeAll(async () => { + const badBody = _.cloneDeep(goodBody); + deleteDeepProperty(badBody, propertyPath); + + if (Object.hasOwn(badBody, 'idempotencyKey')) { + badBody['idempotencyKey'] = randomUUID(); + } + + apiError = await sendRequest(badBody); + }); + + it('should respond with HTTP response code 400 (Bad Request)', () => { + expect(apiError.status).toEqual(400); + }); + it('should properly describe the error in the response body', () => { + expect(apiError.body.requestPart).toEqual(RequestPart.BODY); + expect(getExpectedVariants(url, propertyPath)).toContain(apiError.body.propertyName); + expect(apiError.body.errorType).toEqual( + BadRequestError.errorType.SCHEMA_PROPERTY_ERROR + ); + }); + }); + } }); - } + }); }); // When missing a properties appearing in oneOf, the server might report as if From 14580defedaeb3db5540e949ca5ed5ead20c6ba8 Mon Sep 17 00:00:00 2001 From: Danielle Maor Date: Wed, 2 Sep 2026 15:40:33 +0300 Subject: [PATCH 4/4] adding test --- v2/api-validator/src/server/handlers/capabilities-handlers.ts | 1 + v2/api-validator/tests/sanity-tests/capabilities.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/v2/api-validator/src/server/handlers/capabilities-handlers.ts b/v2/api-validator/src/server/handlers/capabilities-handlers.ts index 590a8f3b..f89523c1 100644 --- a/v2/api-validator/src/server/handlers/capabilities-handlers.ts +++ b/v2/api-validator/src/server/handlers/capabilities-handlers.ts @@ -18,6 +18,7 @@ const CAPABILITIES: Capabilities = { collateral: '*', ramps: '*', rates: '*', + custom: '*', }, }; diff --git a/v2/api-validator/tests/sanity-tests/capabilities.test.ts b/v2/api-validator/tests/sanity-tests/capabilities.test.ts index a1ad388f..51659acf 100644 --- a/v2/api-validator/tests/sanity-tests/capabilities.test.ts +++ b/v2/api-validator/tests/sanity-tests/capabilities.test.ts @@ -36,6 +36,7 @@ describe('Capabilities', () => { // 'trading', 'liquidity', 'ramps', + 'custom', ]; let accountIds: string[];