From eca4acc9826400307dd997bcebbddcc1e45fd937 Mon Sep 17 00:00:00 2001 From: Moritz Raho Date: Tue, 13 Jul 2021 18:37:04 +0200 Subject: [PATCH 1/2] App helpers test + fix config-loader tests --- src/lib/app-helper.js | 45 +--- test/commands/lib/app-helper.test.js | 276 ++++++++++++------------ test/commands/lib/config-loader.test.js | 36 ++-- test/data-mocks/config-loader.js | 16 ++ 4 files changed, 178 insertions(+), 195 deletions(-) diff --git a/src/lib/app-helper.js b/src/lib/app-helper.js index 5ffd58f2..cb62e647 100644 --- a/src/lib/app-helper.js +++ b/src/lib/app-helper.js @@ -23,6 +23,7 @@ const { AIO_CONFIG_WORKSPACE_SERVICES, AIO_CONFIG_ORG_SERVICES } = require('./de const { EOL } = require('os') const { getCliEnv } = require('@adobe/aio-lib-env') const yaml = require('js-yaml') +const RuntimeLib = require('@adobe/aio-lib-runtime') /** @private */ function isNpmInstalled () { @@ -154,31 +155,6 @@ async function getCliInfo () { return { accessToken, env } } -/** @private */ -function getActionUrls (config, isRemoteDev = false, isLocalDev = false) { - // set action urls - // action urls {name: url}, if !LocalDev subdomain uses namespace - return Object.entries({ ...config.manifest.package.actions, ...(config.manifest.package.sequences || {}) }).reduce((obj, [name, action]) => { - const webArg = action['web-export'] || action.web - const webUri = (webArg && webArg !== 'no' && webArg !== 'false') ? 'web' : '' - if (isLocalDev) { - // http://localhost:3233/api/v1/web/// - obj[name] = urlJoin(config.ow.apihost, 'api', config.ow.apiversion, webUri, config.ow.namespace, config.ow.package, name) - } else if (isRemoteDev || !webUri || !config.app.hasFrontend) { - // - if remote dev we don't care about same domain as the UI runs on localhost - // - if action is non web it cannot be called from the UI and we can point directly to ApiHost domain - // - if action has no UI no need to use the CDN url - // NOTE this will not work for apihosts that do not support .apihost url - // https://.adobeioruntime.net/api/v1/web// - obj[name] = urlJoin('https://' + config.ow.namespace + '.' + removeProtocolFromURL(config.ow.apihost), 'api', config.ow.apiversion, webUri, config.ow.package, name) - } else { - // https://.adobe-static.net/api/v1/web// - obj[name] = urlJoin('https://' + config.ow.namespace + '.' + removeProtocolFromURL(config.app.hostname), 'api', config.ow.apiversion, webUri, config.ow.package, name) - } - return obj - }, {}) -} - /** * Joins url path parts * @@ -429,8 +405,7 @@ async function buildExcShellViewExtensionMetadata (libConsoleCLI, aioConfig) { */ function buildExtensionPointPayloadWoMetadata (extConfigs) { // Example input: - // application: {...} - // extensions: + // application: {...} // dx/excshell/1: // operations: // view: @@ -460,20 +435,17 @@ function buildExtensionPointPayloadWoMetadata (extConfigs) { .filter(([k, v]) => k !== 'application') .forEach(([extPointName, extPointConfig]) => { endpointsPayload[extPointName] = {} + const urls = RuntimeLib.utils.getActionUrls(extPointConfig, false, false) Object.entries(extPointConfig.operations) .forEach(([opName, opList]) => { // replace operations impl and type with a href, either for an action or for a UI endpointsPayload[extPointName][opName] = opList.map(op => { if (op.type === 'action') { - // todo modularize with getActionUrls from appHelper - const owPackage = op.impl.split('/')[0] - const owAction = op.impl.split('/')[1] - const manifestAction = extPointConfig.manifest.full.packages[owPackage].actions[owAction] - const webArg = manifestAction['web-export'] || manifestAction.web - const webUri = (webArg && webArg !== 'no' && webArg !== 'false') ? 'web' : '' - const packageWithAction = op.impl - // todo non runtime apihost do not support namespace as subdomain - const href = urlJoin('https://' + extPointConfig.ow.namespace + '.' + removeProtocolFromURL(extPointConfig.ow.apihost), 'api', extPointConfig.ow.apiversion, webUri, packageWithAction) + const actionAndPkgName = op.impl + const actionName = actionAndPkgName.split('/')[1] + // Note: if the package is the first package in the url getActionUrls will return actionName as key + // this should be fixed in runtime lib: https://github.com/adobe/aio-lib-runtime/issues/64 + const href = urls[actionName] || urls[actionAndPkgName] return { href, ...op.params } } else if (op.type === 'web') { // todo support for multi UI with a extname-opcode-subfolder @@ -516,7 +488,6 @@ module.exports = { runPackageScript, wrapError, getCliInfo, - getActionUrls, removeProtocolFromURL, urlJoin, checkFile, diff --git a/test/commands/lib/app-helper.test.js b/test/commands/lib/app-helper.test.js index 83691e06..f96b0251 100644 --- a/test/commands/lib/app-helper.test.js +++ b/test/commands/lib/app-helper.test.js @@ -9,12 +9,16 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +// unmock to test proper returned urls from getActionUrls +jest.unmock('@adobe/aio-lib-runtime') + const which = require('which') const fs = require('fs-extra') const execa = require('execa') const appHelper = require('../../../src/lib/app-helper') const fetch = require('node-fetch') -const config = require('@adobe/aio-lib-core-config') +const aioConfig = require('@adobe/aio-lib-core-config') jest.mock('@adobe/aio-lib-core-config') jest.mock('node-fetch') @@ -26,10 +30,12 @@ beforeEach(() => { execa.mockReset() execa.command.mockReset() fetch.mockReset() - config.get.mockReset() - config.set.mockReset() + aioConfig.get.mockReset() + aioConfig.set.mockReset() }) +const getMockConfig = require('../../data-mocks/config-loader') + test('isDockerRunning', async () => { let result @@ -120,15 +126,15 @@ test('isGitInstalled', () => { expect(appHelper.isGitInstalled()).toBeFalsy() }) -test('installPackage', async () => { - expect(appHelper.installPackage).toBeDefined() - expect(appHelper.installPackage).toBeInstanceOf(Function) +test('installPackages', async () => { + expect(appHelper.installPackages).toBeDefined() + expect(appHelper.installPackages).toBeInstanceOf(Function) // throws error if dir dne => // fs.statSync(dir).isDirectory() fs.statSync.mockReturnValue({ isDirectory: () => false }) - await expect(appHelper.installPackage('does-not-exist')) + await expect(appHelper.installPackages('does-not-exist')) .rejects.toThrow(/does-not-exist is not a directory/) // throws error if dir does not contain a package.json @@ -136,12 +142,12 @@ test('installPackage', async () => { isDirectory: () => true }) fs.readdirSync.mockReturnValue([]) - await expect(appHelper.installPackage('does-not-exist')) + await expect(appHelper.installPackages('does-not-exist')) .rejects.toThrow(/does-not-exist does not contain a package.json file./) // succeeds if npm install returns success fs.readdirSync.mockReturnValue(['package.json']) - appHelper.installPackage('does-not-exist') + appHelper.installPackages('does-not-exist') return expect(execa).toHaveBeenCalledWith('npm', ['install'], { cwd: 'does-not-exist' }) }) @@ -279,134 +285,6 @@ test('wrapError returns an a Error in any case', async () => { expect(error.stack).toBeDefined() }) -describe('getActionUrls', () => { - let fakeConfig - beforeEach(() => { - // base - fakeConfig = { - ow: { - namespace: 'dude', - apihost: 'https://fake.com', - package: 'thepackage', - apiversion: 'v0' - }, - app: { - hasFrontend: true, - hostname: 'https://cdn.com' - }, - manifest: { - package: { - actions: {} - } - } - } - }) - test('no actions in manifest config', () => { - expect(appHelper.getActionUrls(fakeConfig)).toEqual({}) - }) - test('6 actions, 2 web and 4 non-web and no sequence', () => { - fakeConfig.manifest.package.actions = { - one: { 'web-export': true }, - two: { web: true }, - three: { web: false }, - four: { web: 'no' }, - five: { web: 'false' }, - six: { other: 'something' } - } - expect(appHelper.getActionUrls(fakeConfig)).toEqual({ - five: 'https://dude.fake.com/api/v0/thepackage/five', - four: 'https://dude.fake.com/api/v0/thepackage/four', - one: 'https://dude.cdn.com/api/v0/web/thepackage/one', - six: 'https://dude.fake.com/api/v0/thepackage/six', - three: 'https://dude.fake.com/api/v0/thepackage/three', - two: 'https://dude.cdn.com/api/v0/web/thepackage/two' - }) - }) - test('6 sequences, 2 web and 4 non-web and no actions', () => { - fakeConfig.manifest.package.sequences = { - one: { 'web-export': true }, - two: { web: true }, - three: { web: false }, - four: { web: 'no' }, - five: { web: 'false' }, - six: { other: 'something' } - } - expect(appHelper.getActionUrls(fakeConfig)).toEqual({ - five: 'https://dude.fake.com/api/v0/thepackage/five', - four: 'https://dude.fake.com/api/v0/thepackage/four', - one: 'https://dude.cdn.com/api/v0/web/thepackage/one', - six: 'https://dude.fake.com/api/v0/thepackage/six', - three: 'https://dude.fake.com/api/v0/thepackage/three', - two: 'https://dude.cdn.com/api/v0/web/thepackage/two' - }) - }) - test('2 actions and 2 sequences, one web one non-web', () => { - fakeConfig.manifest.package.actions = { - aone: { 'web-export': true }, - atwo: {} - } - fakeConfig.manifest.package.sequences = { - sone: { 'web-export': true }, - stwo: {} - } - expect(appHelper.getActionUrls(fakeConfig)).toEqual({ - aone: 'https://dude.cdn.com/api/v0/web/thepackage/aone', - atwo: 'https://dude.fake.com/api/v0/thepackage/atwo', - sone: 'https://dude.cdn.com/api/v0/web/thepackage/sone', - stwo: 'https://dude.fake.com/api/v0/thepackage/stwo' - }) - }) - test('2 actions and 2 sequences, one web one non-web, app has no frontend', () => { - fakeConfig.manifest.package.actions = { - aone: { 'web-export': true }, - atwo: {} - } - fakeConfig.manifest.package.sequences = { - sone: { 'web-export': true }, - stwo: {} - } - fakeConfig.app.hasFrontend = false - expect(appHelper.getActionUrls(fakeConfig)).toEqual({ - aone: 'https://dude.fake.com/api/v0/web/thepackage/aone', - atwo: 'https://dude.fake.com/api/v0/thepackage/atwo', - sone: 'https://dude.fake.com/api/v0/web/thepackage/sone', - stwo: 'https://dude.fake.com/api/v0/thepackage/stwo' - }) - }) - test('2 actions and 2 sequences, one web one non-web, isRemoteDev=true', () => { - fakeConfig.manifest.package.actions = { - aone: { 'web-export': true }, - atwo: {} - } - fakeConfig.manifest.package.sequences = { - sone: { 'web-export': true }, - stwo: {} - } - expect(appHelper.getActionUrls(fakeConfig, true)).toEqual({ - aone: 'https://dude.fake.com/api/v0/web/thepackage/aone', - atwo: 'https://dude.fake.com/api/v0/thepackage/atwo', - sone: 'https://dude.fake.com/api/v0/web/thepackage/sone', - stwo: 'https://dude.fake.com/api/v0/thepackage/stwo' - }) - }) - test('2 actions and 2 sequences, one web one non-web, isLocalDev=true', () => { - fakeConfig.manifest.package.actions = { - aone: { 'web-export': true }, - atwo: {} - } - fakeConfig.manifest.package.sequences = { - sone: { 'web-export': true }, - stwo: {} - } - expect(appHelper.getActionUrls(fakeConfig, false, true)).toEqual({ - aone: 'https://fake.com/api/v0/web/dude/thepackage/aone', - atwo: 'https://fake.com/api/v0/dude/thepackage/atwo', - sone: 'https://fake.com/api/v0/web/dude/thepackage/sone', - stwo: 'https://fake.com/api/v0/dude/thepackage/stwo' - }) - }) -}) - test('removeProtocol', () => { let res = appHelper.removeProtocolFromURL('https://some-url') expect(res).toBe('some-url') @@ -558,7 +436,7 @@ test('runOpenWhiskJar with AIO_OW_JVM_ARGS env var is passed to execa', async () fetch.mockReturnValue({ ok: true }) execa.mockReturnValue({ stdout: jest.fn() }) - config.get.mockReturnValueOnce('arg1 arg2') + aioConfig.get.mockReturnValueOnce('arg1 arg2') const result = appHelper.runOpenWhiskJar('jar', 'conf') @@ -628,7 +506,7 @@ test('setWorkspaceServicesConfig', () => { { name: 'sec', sdkCode: 'secs', code: 'no such field', b: 'hello', type: 'no such field' } ] appHelper.setWorkspaceServicesConfig(fakeServiceProps) - expect(config.set).toHaveBeenCalledWith( + expect(aioConfig.set).toHaveBeenCalledWith( 'project.workspace.details.services', [ { name: 'first', code: 'firsts' }, { name: 'sec', code: 'secs' } @@ -644,7 +522,7 @@ test('setOrgServicesConfig', () => { { name: 'third', code: 'thirds', sdkCode: 'no such field', type: 'entp' } ] appHelper.setOrgServicesConfig(fakeOrgServices) - expect(config.set).toHaveBeenCalledWith( + expect(aioConfig.set).toHaveBeenCalledWith( 'project.org.details.services', [ { name: 'first', code: 'firsts', type: 'entp' }, { name: 'sec', code: 'secs', type: 'entp' }, @@ -653,3 +531,121 @@ test('setOrgServicesConfig', () => { true ) }) + +describe('buildExcShellViewExtensionMetadata', () => { + test('with service properties', async () => { + const mockConsoleCLIInstance = { + getServicePropertiesFromWorkspace: jest.fn() + } + const mockAIOConfig = { + project: { + org: { + id: 'hola' + }, + workspace: { + id: 'yay', + name: 'yo' + }, + id: 'bonjour' + } + } + mockConsoleCLIInstance.getServicePropertiesFromWorkspace.mockResolvedValue([ + { name: 'service1', sdkCode: 'service1code', other: 'field' }, + { name: 'service2', sdkCode: 'service2code', other: 'field2' } + ]) + const res = await appHelper.buildExcShellViewExtensionMetadata(mockConsoleCLIInstance, mockAIOConfig) + expect(res).toEqual({ + services: [ + { name: 'service1', code: 'service1code' }, + { name: 'service2', code: 'service2code' } + ], + profile: { + client_id: 'firefly-app', + scope: 'ab.manage,additional_info.job_function,additional_info.projectedProductContext,additional_info.roles,additional_info,AdobeID,adobeio_api,adobeio.appregistry.read,audiencemanager_api,creative_cloud,mps,openid,read_organizations,read_pc.acp,read_pc.dma_tartan,read_pc,session' + } + }) + expect(mockConsoleCLIInstance.getServicePropertiesFromWorkspace).toHaveBeenCalledWith('hola', 'bonjour', { id: 'yay', name: 'yo' }) + }) +}) + +describe('buildExtensionPointPayloadWoMetadata', () => { + test('app config', () => { + // application config has no ext reg payload + const mockConfig = getMockConfig('app', {}) + expect(appHelper.buildExtensionPointPayloadWoMetadata(mockConfig.all)) + .toEqual({ endpoints: {} }) + }) + + test('exc config', () => { + const mockConfig = getMockConfig('exc', { runtime: { namespace: 'hola' } }) + expect(appHelper.buildExtensionPointPayloadWoMetadata(mockConfig.all)) + .toEqual({ + endpoints: { + 'dx/excshell/1': { view: [{ href: 'https://hola.adobeio-static.net/index.html' }] } + } + }) + }) + + test('app-exc-nui config', () => { + const mockConfig = getMockConfig('app-exc-nui', { runtime: { namespace: 'hola' } }) + expect(appHelper.buildExtensionPointPayloadWoMetadata(mockConfig.all)) + .toEqual({ + endpoints: { + 'dx/asset-compute/worker/1': { apply: [{ href: 'https://hola.adobeioruntime.net/api/v1/web/my-nui-package/action' }] }, + 'dx/excshell/1': { view: [{ href: 'https://hola.adobeio-static.net/index.html' }] } + } + }) + }) + + test('fake headless extension with multi package actions', () => { + const fakeConfig = { + all: { + fake: { + operations: { + one: [{ + type: 'action', + impl: 'pkg1/action1' + }], + two: [{ + type: 'action', + impl: 'pkg2/action2' + }] + }, + ow: { + apihost: 'https://some.com', + defaultApihost: 'https://adobeioruntime.com', + package: 'bla-1', + namespace: 'hola', + apiversion: 'v1' + }, + app: { + hostname: 'fake.com', + defaultHostname: 'another' + }, + manifest: { + full: { + packages: { + pkg1: { + actions: { + action1: { + web: 'yes' + } + } + }, + pkg2: { + actions: { + action2: { + web: 'false' + } + } + } + } + } + } + } + } + } + expect(appHelper.buildExtensionPointPayloadWoMetadata(fakeConfig.all)) + .toEqual({ endpoints: { fake: { one: [{ href: 'https://hola.fake.com/api/v1/web/pkg1/action1' }], two: [{ href: 'https://some.com/api/v1/hola/pkg2/action2' }] } } }) + }) +}) diff --git a/test/commands/lib/config-loader.test.js b/test/commands/lib/config-loader.test.js index d52abd2b..259ec7ec 100644 --- a/test/commands/lib/config-loader.test.js +++ b/test/commands/lib/config-loader.test.js @@ -95,16 +95,16 @@ describe('load config', () => { test('exc with custom dist folder', async () => { global.loadFixtureApp('exc') // rewrite configuration - const userConfig = yaml.load(global.fixtureFile('exc/app.config.yaml')) - userConfig.extensions['dx/excshell/1'].dist = 'new/dist/for/excshell' - global.fakeFileSystem.addJson({ '/app.config.yaml': yaml.dump(userConfig) }) + const userConfig = yaml.load(global.fixtureFile('exc/src/dx-excshell-1/ext.config.yaml')) + userConfig.dist = 'new/dist/for/excshell' + global.fakeFileSystem.addJson({ '/src/dx-excshell-1/ext.config.yaml': yaml.dump(userConfig) }) config = loadConfig({}) expect(config).toEqual(getMockConfig('exc', global.fakeConfig.tvm, { - 'all.dx/excshell/1.app.dist': path.resolve('/new/dist/for/excshell'), - 'all.dx/excshell/1.actions.dist': path.resolve('/new/dist/for/excshell/actions'), - 'all.dx/excshell/1.web.distDev': path.resolve('/new/dist/for/excshell/web-dev'), - 'all.dx/excshell/1.web.distProd': path.resolve('/new/dist/for/excshell/web-prod'), + 'all.dx/excshell/1.app.dist': path.resolve('/src/dx-excshell-1/new/dist/for/excshell'), + 'all.dx/excshell/1.actions.dist': path.resolve('/src/dx-excshell-1/new/dist/for/excshell/actions'), + 'all.dx/excshell/1.web.distDev': path.resolve('/src/dx-excshell-1/new/dist/for/excshell/web-dev'), + 'all.dx/excshell/1.web.distProd': path.resolve('/src/dx-excshell-1/new/dist/for/excshell/web-prod'), includeIndex: expect.any(Object) })) }) @@ -112,11 +112,11 @@ describe('load config', () => { test('exc with byo aws credentials', async () => { global.loadFixtureApp('exc') // rewrite configuration - const userConfig = yaml.load(global.fixtureFile('exc/app.config.yaml')) - userConfig.extensions['dx/excshell/1'].awsaccesskeyid = 'fakeid' - userConfig.extensions['dx/excshell/1'].awssecretaccesskey = 'fakesecret' - userConfig.extensions['dx/excshell/1'].s3bucket = 'fakebucket' - global.fakeFileSystem.addJson({ '/app.config.yaml': yaml.dump(userConfig) }) + const userConfig = yaml.load(global.fixtureFile('exc/src/dx-excshell-1/ext.config.yaml')) + userConfig.awsaccesskeyid = 'fakeid' + userConfig.awssecretaccesskey = 'fakesecret' + userConfig.s3bucket = 'fakebucket' + global.fakeFileSystem.addJson({ '/src/dx-excshell-1/ext.config.yaml': yaml.dump(userConfig) }) config = loadConfig({}) expect(config).toEqual(getMockConfig('exc', global.fakeConfig.tvm, { @@ -132,9 +132,9 @@ describe('load config', () => { test('exc with custom tvm url', async () => { global.loadFixtureApp('exc') // rewrite configuration - const userConfig = yaml.load(global.fixtureFile('exc/app.config.yaml')) - userConfig.extensions['dx/excshell/1'].tvmurl = 'customurl' - global.fakeFileSystem.addJson({ '/app.config.yaml': yaml.dump(userConfig) }) + const userConfig = yaml.load(global.fixtureFile('exc/src/dx-excshell-1/ext.config.yaml')) + userConfig.tvmurl = 'customurl' + global.fakeFileSystem.addJson({ '/src/dx-excshell-1/ext.config.yaml': yaml.dump(userConfig) }) config = loadConfig({}) expect(config).toEqual(getMockConfig('exc', global.fakeConfig.tvm, { @@ -146,9 +146,9 @@ describe('load config', () => { test('exc with default tvm url', async () => { global.loadFixtureApp('exc') // rewrite configuration - const userConfig = yaml.load(global.fixtureFile('exc/app.config.yaml')) - userConfig.extensions['dx/excshell/1'].tvmurl = 'https://firefly-tvm.adobe.io' - global.fakeFileSystem.addJson({ '/app.config.yaml': yaml.dump(userConfig) }) + const userConfig = yaml.load(global.fixtureFile('exc/src/dx-excshell-1/ext.config.yaml')) + userConfig.tvmurl = 'https://firefly-tvm.adobe.io' + global.fakeFileSystem.addJson({ '/src/dx-excshell-1/ext.config.yaml': yaml.dump(userConfig) }) config = loadConfig({}) expect(config).toEqual(getMockConfig('exc', global.fakeConfig.tvm, { diff --git a/test/data-mocks/config-loader.js b/test/data-mocks/config-loader.js index 21c14712..fcdb9f40 100644 --- a/test/data-mocks/config-loader.js +++ b/test/data-mocks/config-loader.js @@ -165,6 +165,10 @@ const excSingleConfig = { src: excActionsFolder, dist: winCompat(`${root}dist/dx-excshell-1/actions`) }, + tests: { + e2e: winCompat(`${root}/src/dx-excshell-1/e2e`), + unit: winCompat(`${root}/src/dx-excshell-1/test`) + }, root: `${root}`, name: 'dx/excshell/1', hooks: {}, @@ -207,6 +211,10 @@ const nuiSingleConfig = { src: nuiActionsFolder, dist: winCompat(`${root}dist/dx-asset-compute-worker-1/actions`) }, + tests: { + e2e: winCompat(`${root}/src/dx-asset-compute-worker-1/e2e`), + unit: winCompat(`${root}/src/dx-asset-compute-worker-1/test`) + }, root: `${root}`, name: 'dx/asset-compute/worker/1', hooks: { @@ -257,6 +265,10 @@ const applicationSingleConfig = { src: appActionsFolder, dist: winCompat(`${root}dist/application/actions`) }, + tests: { + e2e: winCompat(`${root}e2e`), + unit: winCompat(`${root}test`) + }, root: `${root}`, name: 'application', hooks: { @@ -304,6 +316,10 @@ const applicationNoActionsSingleConfig = { actions: { src: winCompat(`${root}actions`) }, + tests: { + e2e: winCompat(`${root}e2e`), + unit: winCompat(`${root}test`) + }, root: `${root}`, name: 'application', hooks: { From 78be846798a51eeaae7d8292987b57d17bc4cc32 Mon Sep 17 00:00:00 2001 From: Moritz Raho Date: Wed, 14 Jul 2021 11:49:04 +0200 Subject: [PATCH 2/2] 100% cov on app helper --- src/lib/app-helper.js | 9 +- test/commands/lib/app-helper.test.js | 143 +++++++++++++++++++++++++-- 2 files changed, 142 insertions(+), 10 deletions(-) diff --git a/src/lib/app-helper.js b/src/lib/app-helper.js index cb62e647..ae6095d6 100644 --- a/src/lib/app-helper.js +++ b/src/lib/app-helper.js @@ -435,7 +435,10 @@ function buildExtensionPointPayloadWoMetadata (extConfigs) { .filter(([k, v]) => k !== 'application') .forEach(([extPointName, extPointConfig]) => { endpointsPayload[extPointName] = {} - const urls = RuntimeLib.utils.getActionUrls(extPointConfig, false, false) + let actionUrls = {} + if (extPointConfig.app.hasBackend) { + actionUrls = RuntimeLib.utils.getActionUrls(extPointConfig, false, false) + } Object.entries(extPointConfig.operations) .forEach(([opName, opList]) => { // replace operations impl and type with a href, either for an action or for a UI @@ -445,7 +448,7 @@ function buildExtensionPointPayloadWoMetadata (extConfigs) { const actionName = actionAndPkgName.split('/')[1] // Note: if the package is the first package in the url getActionUrls will return actionName as key // this should be fixed in runtime lib: https://github.com/adobe/aio-lib-runtime/issues/64 - const href = urls[actionName] || urls[actionAndPkgName] + const href = actionUrls[actionName] || actionUrls[actionAndPkgName] return { href, ...op.params } } else if (op.type === 'web') { // todo support for multi UI with a extname-opcode-subfolder @@ -454,7 +457,7 @@ function buildExtensionPointPayloadWoMetadata (extConfigs) { ...op.params } } else { - throw new Error(`unexpected op.type encountered => ${op.type}`) + throw new Error(`unexpected op.type encountered => '${op.type}'`) } }) }) diff --git a/test/commands/lib/app-helper.test.js b/test/commands/lib/app-helper.test.js index f96b0251..0b6537f1 100644 --- a/test/commands/lib/app-helper.test.js +++ b/test/commands/lib/app-helper.test.js @@ -13,18 +13,23 @@ governing permissions and limitations under the License. // unmock to test proper returned urls from getActionUrls jest.unmock('@adobe/aio-lib-runtime') +jest.mock('@adobe/aio-lib-core-config') +jest.mock('node-fetch') +jest.mock('execa') +jest.mock('process') +jest.mock('fs-extra') // do not touch the real fs +jest.mock('@adobe/aio-lib-env') +jest.mock('@adobe/aio-lib-ims') + const which = require('which') const fs = require('fs-extra') const execa = require('execa') const appHelper = require('../../../src/lib/app-helper') const fetch = require('node-fetch') const aioConfig = require('@adobe/aio-lib-core-config') +const libEnv = require('@adobe/aio-lib-env') +const libIms = require('@adobe/aio-lib-ims') -jest.mock('@adobe/aio-lib-core-config') -jest.mock('node-fetch') -jest.mock('execa') -jest.mock('process') -jest.mock('fs-extra') // do not touch the real fs beforeEach(() => { Object.defineProperty(process, 'platform', { value: 'linux' }) execa.mockReset() @@ -32,6 +37,8 @@ beforeEach(() => { fetch.mockReset() aioConfig.get.mockReset() aioConfig.set.mockReset() + libEnv.getCliEnv.mockReset() + libIms.getToken.mockReset() }) const getMockConfig = require('../../data-mocks/config-loader') @@ -146,9 +153,23 @@ test('installPackages', async () => { .rejects.toThrow(/does-not-exist does not contain a package.json file./) // succeeds if npm install returns success + execa.mockReset() fs.readdirSync.mockReturnValue(['package.json']) - appHelper.installPackages('does-not-exist') - return expect(execa).toHaveBeenCalledWith('npm', ['install'], { cwd: 'does-not-exist' }) + await appHelper.installPackages('does-not-exist') + expect(execa).toHaveBeenCalledWith('npm', ['install'], { cwd: 'does-not-exist' }) + + // verbose option + execa.mockReset() + await appHelper.installPackages('somedir', { verbose: true }) + expect(execa).toHaveBeenCalledWith('npm', ['install'], { cwd: 'somedir', stderr: 'inherit', stdout: 'inherit' }) + + // spinner option + execa.mockReset() + const spinner = { start: jest.fn(), stop: jest.fn() } + await appHelper.installPackages('somedir', { spinner, verbose: false }) + expect(execa).toHaveBeenCalledWith('npm', ['install'], { cwd: 'somedir' }) + expect(spinner.start).toHaveBeenCalled() + expect(spinner.stop).toHaveBeenCalled() }) test('runPackageScript', async () => { @@ -260,6 +281,23 @@ test('runPackageScript logs if package.json does not have matching script', asyn .resolves.toBeUndefined() }) +test('runScript with empty command', async () => { + await appHelper.runScript(undefined, 'dir') + expect(execa.command).not.toHaveBeenCalled() +}) + +test('runScript with defined dir', async () => { + execa.command.mockReturnValue({ on: () => {} }) + await appHelper.runScript('somecommand', 'somedir') + expect(execa.command).toHaveBeenCalledWith('somecommand', expect.objectContaining({ cwd: 'somedir' })) +}) + +test('runScript with empty dir => process.cwd', async () => { + execa.command.mockReturnValue({ on: () => {} }) + await appHelper.runScript('somecommand', undefined) + expect(execa.command).toHaveBeenCalledWith('somecommand', expect.objectContaining({ cwd: process.cwd() })) +}) + test('wrapError returns an a Error in any case', async () => { expect(appHelper.wrapError).toBeDefined() expect(appHelper.wrapError).toBeInstanceOf(Function) @@ -619,6 +657,7 @@ describe('buildExtensionPointPayloadWoMetadata', () => { apiversion: 'v1' }, app: { + hasBackend: true, hostname: 'fake.com', defaultHostname: 'another' }, @@ -648,4 +687,94 @@ describe('buildExtensionPointPayloadWoMetadata', () => { expect(appHelper.buildExtensionPointPayloadWoMetadata(fakeConfig.all)) .toEqual({ endpoints: { fake: { one: [{ href: 'https://hola.fake.com/api/v1/web/pkg1/action1' }], two: [{ href: 'https://some.com/api/v1/hola/pkg2/action2' }] } } }) }) + + test('fake extension with bad operation type', () => { + const fakeConfig = { + all: { + fake: { + operations: { + one: [{ + type: 'notsupported', + impl: 'pkg1/action1' + }] + }, + manifest: {}, + app: {}, + ow: {} + } + } + } + expect(() => appHelper.buildExtensionPointPayloadWoMetadata(fakeConfig.all)) + .toThrow('unexpected op.type encountered => \'notsupported\'') + }) +}) + +describe('atLeastOne', () => { + test('no input', () => { + expect(appHelper.atLeastOne([])).toEqual('please choose at least one option') + }) + test('some input', () => { + expect(appHelper.atLeastOne(['some', 'input'])).toEqual(true) + }) +}) + +describe('deleteUserConfig', () => { + beforeEach(() => { + fs.writeFileSync.mockReset() + fs.readFileSync.mockReset() + }) + + test('rewrite config', () => { + fs.readFileSync.mockReturnValue(Buffer.from(` +some: + config: + in: 'a yaml file' +`)) + appHelper.deleteUserConfig({ file: 'fake.file', key: 'some.config.in' }) + expect(fs.readFileSync).toHaveBeenLastCalledWith('fake.file') + expect(fs.writeFileSync).toHaveBeenCalledWith('fake.file', `some: + config: {} +`) + }) +}) + +describe('serviceToGeneratorInput', () => { + test('list with empty codes', () => { + expect(appHelper.servicesToGeneratorInput( + [{ name: 'hello', code: 'hellocode' }, + { name: 'bonjour', code: 'bonjourcode' }, + { name: 'nocode' }] + )).toEqual('hellocode,bonjourcode') + }) +}) + +describe('writeConfig', () => { + beforeEach(() => { + fs.writeFileSync.mockReset() + fs.ensureDirSync.mockReset() + }) + test('write a json to a file', () => { + appHelper.writeConfig('the/dir/some.file', { some: 'config' }) + expect(fs.ensureDirSync).toHaveBeenCalledWith('the/dir') + expect(fs.writeFileSync).toHaveBeenCalledWith('the/dir/some.file', '{"some":"config"}', { encoding: 'utf-8' }) + }) +}) + +describe('getCliInfo', () => { + test('prod', async () => { + libEnv.getCliEnv.mockReturnValue('prod') + libIms.getToken.mockResolvedValue('token') + const res = await appHelper.getCliInfo() + expect(res).toEqual( + { accessToken: 'token', env: 'prod' } + ) + }) + test('stage', async () => { + libEnv.getCliEnv.mockReturnValue('stage') + libIms.getToken.mockResolvedValue('stoken') + const res = await appHelper.getCliInfo() + expect(res).toEqual( + { accessToken: 'stoken', env: 'stage' } + ) + }) })