diff --git a/src/commands/runtime/api/list.js b/src/commands/runtime/api/list.js index 646c9f78..ba23ea1c 100644 --- a/src/commands/runtime/api/list.js +++ b/src/commands/runtime/api/list.js @@ -57,7 +57,23 @@ class ApiList extends RuntimeBaseCommand { const result = await ow.routes.list(options) if (shouldOutputJson) { - this.logJSON('', result.apis[0].value.apidoc) + const api = result.apis?.[0] + if (!api) { + this.logJSON('', {}) + return + } + const gwApiUrl = api.value?.gwApiUrl + const apidoc = structuredClone(api.value?.apidoc ?? {}) + Object.keys(apidoc.paths ?? {}).forEach(path => { + if (!path.startsWith('/')) return + Object.keys(apidoc.paths[path]).forEach(verb => { + const operation = apidoc.paths[path][verb] + if (operation && typeof operation === 'object' && operation['x-openwhisk']) { + operation['x-openwhisk'].url = `${gwApiUrl}${path}` + } + }) + }) + this.logJSON('', apidoc) return } diff --git a/test/commands/runtime/api/list.test.js b/test/commands/runtime/api/list.test.js index e716991b..15a7d461 100644 --- a/test/commands/runtime/api/list.test.js +++ b/test/commands/runtime/api/list.test.js @@ -104,7 +104,168 @@ describe('instance methods', () => { const output = stdout.output.trim() const jsonMatch = output.match(/\{[\s\S]*\}$/) const jsonOutput = jsonMatch ? jsonMatch[0] : output - expect(JSON.parse(jsonOutput)).toMatchObject(expectedJson.apis[0].value.apidoc) + const parsed = JSON.parse(jsonOutput) + const { apis } = expectedJson + const gwApiUrl = apis[0].value.gwApiUrl + // apidoc structure preserved + expect(parsed.basePath).toEqual(apis[0].value.apidoc.basePath) + expect(parsed.info).toMatchObject(apis[0].value.apidoc.info) + expect(parsed.swagger).toEqual(apis[0].value.apidoc.swagger) + // x-openwhisk.url updated with actual gateway URL (was "not-used") + expect(parsed.paths['/mypath'].get['x-openwhisk'].url).toEqual(`${gwApiUrl}/mypath`) + }) + .finally(() => { + stdout.stop() + }) + }) + + test('--json flag, operation without x-openwhisk leaves url unchanged', () => { + rtLib.mockResolved(rtAction, { + apis: [{ + value: { + gwApiUrl: 'https://example.com/api', + apidoc: { + basePath: '/test', + info: { title: 'test', version: '1.0.0' }, + swagger: '2.0', + paths: { + '/mypath': { + get: { + operationId: 'testOp', + responses: { default: { description: 'Default response' } } + // no x-openwhisk field + } + } + } + } + } + }] + }) + stdout.stop() + stdout.start() + const cmd = new TheCommand(['--json']) + return cmd.run() + .then(() => { + const output = stdout.output.trim() + const jsonMatch = output.match(/\{[\s\S]*\}$/) + const parsed = JSON.parse(jsonMatch ? jsonMatch[0] : output) + // operation without x-openwhisk should be left intact + expect(parsed.paths['/mypath'].get).not.toHaveProperty('x-openwhisk') + }) + .finally(() => { + stdout.stop() + }) + }) + + test('--json flag, result.apis is null returns {}', () => { + rtLib.mockResolved(rtAction, { apis: null }) + stdout.stop() + stdout.start() + const cmd = new TheCommand(['--json']) + return cmd.run() + .then(() => { + const output = stdout.output.trim() + const jsonMatch = output.match(/\{[\s\S]*\}$/) + const parsed = JSON.parse(jsonMatch ? jsonMatch[0] : output) + expect(parsed).toEqual({}) + }) + .finally(() => { + stdout.stop() + }) + }) + + test('--json flag, non-object operation value is skipped safely', () => { + rtLib.mockResolved(rtAction, { + apis: [{ + value: { + gwApiUrl: 'https://example.com/api', + apidoc: { + basePath: '/test', + paths: { + '/mypath': { + get: true + } + } + } + } + }] + }) + stdout.stop() + stdout.start() + const cmd = new TheCommand(['--json']) + return cmd.run() + .then(() => { + const output = stdout.output.trim() + const jsonMatch = output.match(/\{[\s\S]*\}$/) + const parsed = JSON.parse(jsonMatch ? jsonMatch[0] : output) + expect(parsed.paths['/mypath'].get).toEqual(true) + }) + .finally(() => { + stdout.stop() + }) + }) + + test('--json flag, api.value.apidoc is missing returns {}', () => { + rtLib.mockResolved(rtAction, { apis: [{ value: {} }] }) + stdout.stop() + stdout.start() + const cmd = new TheCommand(['--json']) + return cmd.run() + .then(() => { + const output = stdout.output.trim() + const jsonMatch = output.match(/\{[\s\S]*\}$/) + const parsed = JSON.parse(jsonMatch ? jsonMatch[0] : output) + expect(parsed).toEqual({}) + }) + .finally(() => { + stdout.stop() + }) + }) + + test('--json flag, api.value.apidoc.paths is missing returns apidoc without paths', () => { + rtLib.mockResolved(rtAction, { apis: [{ value: { gwApiUrl: 'https://example.com', apidoc: { basePath: '/test' } } }] }) + stdout.stop() + stdout.start() + const cmd = new TheCommand(['--json']) + return cmd.run() + .then(() => { + const output = stdout.output.trim() + const jsonMatch = output.match(/\{[\s\S]*\}$/) + const parsed = JSON.parse(jsonMatch ? jsonMatch[0] : output) + expect(parsed.basePath).toEqual('/test') + }) + .finally(() => { + stdout.stop() + }) + }) + + test('--json flag, empty result.apis returns {}', () => { + rtLib.mockResolved(rtAction, { apis: [] }) + stdout.stop() + stdout.start() + const cmd = new TheCommand(['--json']) + return cmd.run() + .then(() => { + const output = stdout.output.trim() + const jsonMatch = output.match(/\{[\s\S]*\}$/) + const parsed = JSON.parse(jsonMatch ? jsonMatch[0] : output) + expect(parsed).toEqual({}) + }) + .finally(() => { + stdout.stop() + }) + }) + + test('--json flag, does not mutate the SDK response object', () => { + const fixture = fixtureJson('api/list.json') + rtLib.mockResolved(rtAction, fixture) + const originalUrl = fixture.apis[0].value.apidoc.paths['/mypath'].get['x-openwhisk'].url + stdout.stop() + stdout.start() + const cmd = new TheCommand(['--json']) + return cmd.run() + .then(() => { + expect(fixture.apis[0].value.apidoc.paths['/mypath'].get['x-openwhisk'].url).toEqual(originalUrl) }) .finally(() => { stdout.stop()