diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 389679c..524718e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,16 +1,16 @@ - - - -### Expected Behaviour - -### Actual Behaviour - -### Reproduce Scenario (including but not limited to) - -#### Steps to Reproduce - -#### Platform and Version - -#### Sample Code that illustrates the problem - -#### Logs taken while reproducing problem + + + +### Expected Behaviour + +### Actual Behaviour + +### Reproduce Scenario (including but not limited to) + +#### Steps to Reproduce + +#### Platform and Version + +#### Sample Code that illustrates the problem + +#### Logs taken while reproducing problem diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9529d71..dd6a02a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,45 +1,45 @@ - - -## Description - - - -## Related Issue - - - - - - -## Motivation and Context - - - -## How Has This Been Tested? - - - - - -## Screenshots (if appropriate): - -## Types of changes - - - -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to change) - -## Checklist: - - - - -- [ ] I have signed the [Adobe Open Source CLA](http://opensource.adobe.com/cla.html). -- [ ] My code follows the code style of this project. -- [ ] My change requires a change to the documentation. -- [ ] I have updated the documentation accordingly. -- [ ] I have read the **CONTRIBUTING** document. -- [ ] I have added tests to cover my changes. -- [ ] All new and existing tests passed. + + +## Description + + + +## Related Issue + + + + + + +## Motivation and Context + + + +## How Has This Been Tested? + + + + + +## Screenshots (if appropriate): + +## Types of changes + + + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) + +## Checklist: + + + + +- [ ] I have signed the [Adobe Open Source CLA](http://opensource.adobe.com/cla.html). +- [ ] My code follows the code style of this project. +- [ ] My change requires a change to the documentation. +- [ ] I have updated the documentation accordingly. +- [ ] I have read the **CONTRIBUTING** document. +- [ ] I have added tests to cover my changes. +- [ ] All new and existing tests passed. diff --git a/src/SDKErrors.js b/src/SDKErrors.js index a1751e3..8513948 100644 --- a/src/SDKErrors.js +++ b/src/SDKErrors.js @@ -72,6 +72,7 @@ E('ERROR_GET_PROJECT_BY_WORKSPACE', '%s') E('ERROR_DELETE_CREDENTIAL', '%s') E('ERROR_CREATE_ORGANIZATION', '%s') E('ERROR_GET_ORGANIZATIONS', '%s') +E('ERROR_GET_ORGANIZATION_FEATURES', '%s') E('ERROR_GET_SERVICES_FOR_ORG', '%s') E('ERROR_GET_SERVICES_FOR_ORG_V2', '%s') E('ERROR_CREATE_RUNTIME_NAMESPACE', '%s') diff --git a/src/index.js b/src/index.js index 4100c48..3a4fd23 100644 --- a/src/index.js +++ b/src/index.js @@ -162,6 +162,14 @@ const API_HOST = { stage: 'developers-stage.adobe.io' } +// Console BFF host. Used by endpoints that are not exposed through the +// public Console API gateway (`API_HOST`) but only through the Console UI's +// backend-for-frontend (e.g. organization feature flags). +const CONSOLE_UI_HOST = { + prod: 'developer.adobe.com', + stage: 'developer-stage.adobe.com' +} + /** * Returns a Promise that resolves with a new CoreConsoleAPI object * @@ -788,6 +796,54 @@ class CoreConsoleAPI { } } + /** + * Get the enabled feature flags for an Organization. + * + * Note: this endpoint is served by the Console UI backend + * (`developer.adobe.com`), not the public Console API gateway, so it does + * not go through the swagger client. + * + * @param {string} organizationId Organization AMS ID + * @returns {Promise} the response, with `body` set to the feature + * array (e.g. `[{ name: 'RUNTIME', description: '...' }]`) + */ + async getOrganizationFeatures (organizationId) { + const parameters = { orgId: organizationId } + const sdkDetails = { parameters } + + // `this.env` is normalized by `init` to a known value (`prod` or `stage`), + // so a direct lookup is safe here. + const host = CONSOLE_UI_HOST[this.env] + const url = `https://${host}/console/api/organizations/${organizationId}/features` + + try { + const res = await fetchWithRetry(url, { + headers: { + accept: 'application/json', + authorization: `Bearer ${this.accessToken}`, + 'x-api-key': this.apiKey + } + }) + if (!res.ok) { + const body = await res.text() + const err = new Error(`${res.status} ${res.statusText}`) + err.response = { + status: res.status, + statusText: res.statusText, + body, + headers: Object.fromEntries(res.headers) + } + throw err + } + const body = await res.json() + // shape the result like the swagger-client responses do, so callers + // can treat this consistently with the rest of the SDK + return { ok: true, status: res.status, statusText: res.statusText, body, data: body } + } catch (err) { + throw new codes.ERROR_GET_ORGANIZATION_FEATURES({ sdkDetails, messageValues: reduceError(err) }) + } + } + /** * Get all Services available to an Organization * diff --git a/test/index_fetchmock.test.js b/test/index_fetchmock.test.js index 74f22d1..8ac3793 100644 --- a/test/index_fetchmock.test.js +++ b/test/index_fetchmock.test.js @@ -107,3 +107,77 @@ describe('getApplicationExtensions (xr api)', () => { expect(Array.isArray(res.data)).toBe(true) }) }) + +describe('getOrganizationFeatures', () => { + /** @private */ + function mockFeaturesResponse ({ ok = true, status = 200, statusText = 'OK', body = [] } = {}) { + mockFetch.mockReset() + mockFetch.mockResolvedValueOnce({ + ok, + status, + statusText, + headers: new Map(), + json: () => Promise.resolve(body), + text: () => Promise.resolve(typeof body === 'string' ? body : JSON.stringify(body)) + }) + } + + test('returns the features for an org (prod)', async () => { + const sdkClient = await sdk.init('accesstoken', 'apiKey') + mockFeaturesResponse({ body: [{ name: 'RUNTIME', description: 'OpenWhisk runtime' }] }) + + const res = await sdkClient.getOrganizationFeatures('304327') + expect(res.ok).toBe(true) + expect(res.body).toEqual([{ name: 'RUNTIME', description: 'OpenWhisk runtime' }]) + expect(mockFetch).toHaveBeenCalledWith( + 'https://developer.adobe.com/console/api/organizations/304327/features', + expect.objectContaining({ + headers: expect.objectContaining({ + accept: 'application/json', + authorization: 'Bearer accesstoken', + 'x-api-key': 'apiKey' + }) + }) + ) + }) + + test('uses the stage host when env=stage', async () => { + const { STAGE_ENV } = jest.requireActual('@adobe/aio-lib-env') + const sdkClient = await sdk.init('accesstoken', 'apiKey', STAGE_ENV) + mockFeaturesResponse({ body: [] }) + + await sdkClient.getOrganizationFeatures('304327') + expect(mockFetch).toHaveBeenCalledWith( + 'https://developer-stage.adobe.com/console/api/organizations/304327/features', + expect.any(Object) + ) + }) + + test('uses the prod host when env is unknown (init normalises it to prod)', async () => { + const sdkClient = await sdk.init('accesstoken', 'apiKey', 'gibberish') + mockFeaturesResponse({ body: [] }) + + await sdkClient.getOrganizationFeatures('304327') + expect(mockFetch).toHaveBeenCalledWith( + 'https://developer.adobe.com/console/api/organizations/304327/features', + expect.any(Object) + ) + }) + + test('throws ERROR_GET_ORGANIZATION_FEATURES on non-ok response', async () => { + const sdkClient = await sdk.init('accesstoken', 'apiKey') + mockFeaturesResponse({ ok: false, status: 500, statusText: 'Server Error', body: 'boom' }) + + await expect(sdkClient.getOrganizationFeatures('304327')) + .rejects.toThrow(/ERROR_GET_ORGANIZATION_FEATURES/) + }) + + test('throws ERROR_GET_ORGANIZATION_FEATURES on network error', async () => { + const sdkClient = await sdk.init('accesstoken', 'apiKey') + mockFetch.mockReset() + mockFetch.mockRejectedValueOnce(new Error('network down')) + + await expect(sdkClient.getOrganizationFeatures('304327')) + .rejects.toThrow(/ERROR_GET_ORGANIZATION_FEATURES/) + }) +})