diff --git a/.changeset/0188-customer-apps-list-encoding.md b/.changeset/0188-customer-apps-list-encoding.md new file mode 100644 index 0000000..eae6d20 --- /dev/null +++ b/.changeset/0188-customer-apps-list-encoding.md @@ -0,0 +1,5 @@ +--- +'@cdot65/prisma-airs-sdk': patch +--- + +Harden `customerApps.list()` by percent-encoding the TSG ID in the request path, so a TSG ID containing URL-reserved characters can no longer corrupt the URL. No behavior change for the numeric TSG IDs in normal use. diff --git a/src/management/customer-apps.ts b/src/management/customer-apps.ts index d4e17dc..e6c2f89 100644 --- a/src/management/customer-apps.ts +++ b/src/management/customer-apps.ts @@ -83,7 +83,7 @@ export class CustomerAppsClient { return request({ method: 'GET', baseUrl: this.baseUrl, - path: `${MGMT_CUSTOMER_APPS_TSG_PATH}/${this.tsgId}`, + path: `${MGMT_CUSTOMER_APPS_TSG_PATH}/${encodeURIComponent(this.tsgId)}`, params, responseSchema: CustomerAppListResponseSchema, auth: this.auth, diff --git a/test/management/customer-apps.spec.ts b/test/management/customer-apps.spec.ts index 9fe39c3..55768c6 100644 --- a/test/management/customer-apps.spec.ts +++ b/test/management/customer-apps.spec.ts @@ -53,6 +53,73 @@ describe('CustomerAppsClient', () => { const [url] = (globalThis.fetch as ReturnType).mock.calls[0]; expect(url).toContain('/v1/mgmt/customerapp/tsg/123'); }); + + it('sends default offset and limit query params', async () => { + mockFetch({ customer_apps: [], next_offset: 0 }); + await client.list(); + + const [url] = (globalThis.fetch as ReturnType).mock.calls[0]; + const params = new URL(url).searchParams; + expect(params.get('offset')).toBe('0'); + expect(params.get('limit')).toBe('100'); + }); + + it('sends explicit offset and limit query params', async () => { + mockFetch({ customer_apps: [], next_offset: 20 }); + await client.list({ offset: 20, limit: 5 }); + + const [url] = (globalThis.fetch as ReturnType).mock.calls[0]; + const params = new URL(url).searchParams; + expect(params.get('offset')).toBe('20'); + expect(params.get('limit')).toBe('5'); + }); + + it('round-trips a populated PaginatedCustomerAppObject', async () => { + mockFetch({ + customer_apps: [ + { + tsg_id: '123', + customer_appId: 'app-uuid-1', + app_name: 'myapp', + cloud_provider: 'aws', + environment: 'prod', + model_name: 'gpt-4', + ai_agent_framework: 'langchain', + api_keys_dp_info: [{ api_key_name: 'key1', dp_name: 'dp1', auth_code: 'abc' }], + }, + ], + next_offset: 10, + }); + const result = await client.list(); + + expect(result.next_offset).toBe(10); + expect(result.customer_apps).toHaveLength(1); + const app = result.customer_apps![0]; + expect(app.customer_appId).toBe('app-uuid-1'); + expect(app.cloud_provider).toBe('aws'); + expect(app.environment).toBe('prod'); + expect(app.api_keys_dp_info?.[0]).toEqual({ + api_key_name: 'key1', + dp_name: 'dp1', + auth_code: 'abc', + }); + }); + + it('percent-encodes a TSG ID with reserved characters in the path', async () => { + const encodingClient = new CustomerAppsClient({ + baseUrl: 'https://api.example.com', + auth: passthroughAuth(), + tsgId: 'tsg/with space', + numRetries: 0, + }); + mockFetch({ customer_apps: [], next_offset: 0 }); + await encodingClient.list(); + + const [url] = (globalThis.fetch as ReturnType).mock.calls[0]; + expect(url).toContain('/v1/mgmt/customerapp/tsg/tsg%2Fwith%20space'); + // The raw slash must not create an extra path segment. + expect(url).not.toContain('/tsg/tsg/with'); + }); }); describe('update', () => {