diff --git a/.gitignore b/.gitignore index 1b277d4..c3f4958 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules package-lock.json coverage junit.xml +.env \ No newline at end of file diff --git a/e2e/e2e.js b/e2e/e2e.js index f4e52e7..631e896 100644 --- a/e2e/e2e.js +++ b/e2e/e2e.js @@ -13,6 +13,8 @@ const sdk = require('../src/index') const path = require('path') const deepClone = require('lodash.clonedeep') const fs = jest.requireActual('fs-extra') +const { createHttpsProxy } = require('@adobe/aio-lib-test-proxy') + jest.unmock('openwhisk') jest.unmock('archiver') jest.setTimeout(30000) @@ -20,17 +22,30 @@ jest.setTimeout(30000) // load .env values in the e2e folder, if any require('dotenv').config({ path: path.join(__dirname, '.env') }) +let proxyServer let sdkClient = {} let config = {} const apiKey = process.env['RuntimeAPI_API_KEY'] const apihost = process.env['RuntimeAPI_APIHOST'] || 'https://adobeioruntime.net' const namespace = process.env['RuntimeAPI_NAMESPACE'] -// console.log(apiKey) +const E2E_USE_PROXY = process.env.E2E_USE_PROXY +const HTTPS_PROXY = process.env.HTTPS_PROXY beforeAll(async () => { + if (E2E_USE_PROXY) { + proxyServer = await createHttpsProxy() + console.log(`Using test proxy at ${proxyServer.url}`) + } + sdkClient = await sdk.init({ api_key: apiKey, apihost }) }) +afterAll(() => { + if (proxyServer) { + proxyServer.stop() + } +}) + beforeEach(() => { config = deepClone(global.sampleAppConfig) config.ow.namespace = namespace @@ -41,6 +56,16 @@ beforeEach(() => { config.manifest.src = path.resolve(config.root + '/' + 'manifest.yml') }) +// eslint-disable-next-line jest/expect-expect +test('HTTPS_PROXY must be set if E2E_USE_PROXY is set', () => { + // jest wraps process.env, so libraries will not pick up an env change via code change, so it has to be set on the shell level + if (E2E_USE_PROXY) { + if (!HTTPS_PROXY) { + throw new Error(`If you set E2E_USE_PROXY, you must set the HTTPS_PROXY environment variable. Please set it to HTTPS_PROXY=${proxyServer.url}.`) + } + } +}) + describe('build-actions', () => { test('full config', async () => { expect(await sdk.buildActions(config)).toEqual(expect.arrayContaining([ diff --git a/e2e/.env b/e2e/env.sample similarity index 100% rename from e2e/.env rename to e2e/env.sample diff --git a/package.json b/package.json index 7aa7f38..5b3bfe0 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "dependencies": { "@adobe/aio-lib-core-errors": "^3.0.0", "@adobe/aio-lib-core-logging": "^1.1.2", + "@adobe/aio-lib-core-networking": "^2.0.0", "@adobe/aio-lib-env": "^1.0.0", "archiver": "^5.0.0", - "cross-fetch": "^3.0.4", "execa": "^4.0.3", "fs-extra": "^9.0.1", "globby": "^11.0.1", @@ -22,6 +22,7 @@ "lodash.clonedeep": "^4.5.0", "openwhisk": "^3.21.2", "openwhisk-fqn": "0.0.2", + "proxy-from-env": "^1.1.0", "semver": "^7.3.2", "sha1": "^1.1.1", "webpack": "^5.26.3" @@ -29,6 +30,7 @@ "deprecated": false, "description": "Adobe I/O Runtime Lib", "devDependencies": { + "@adobe/aio-lib-test-proxy": "^1.0.0", "@adobe/eslint-config-aio-lib-config": "^1.3.0", "@types/jest": "^26.0.4", "@types/node-fetch": "^2.5.4", @@ -57,7 +59,7 @@ "node": "^10 || ^12 || ^14" }, "scripts": { - "e2e": "jest --config e2e/jest.config.js", + "e2e": "jest --config e2e/jest.config.js --runInBand", "generate-docs": "npm run typings && npm run jsdoc", "jsdoc": "jsdoc2md -t ./docs/readme_template.md src/**/*.js > README.md", "lint": "eslint src test e2e", diff --git a/src/LogForwarding.js b/src/LogForwarding.js index c9d2908..ae971ef 100644 --- a/src/LogForwarding.js +++ b/src/LogForwarding.js @@ -9,7 +9,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const fetch = require('cross-fetch') +const { createFetch } = require('@adobe/aio-lib-core-networking') /** * Log Forwarding management API @@ -97,6 +97,8 @@ class LogForwarding { if (this.namespace === '_') { throw new Error("Namespace '_' is not supported by log forwarding management API") } + + const fetch = createFetch() return fetch( this.apiHost + '/runtime/namespaces/' + this.namespace + '/logForwarding', { diff --git a/src/RuntimeAPI.js b/src/RuntimeAPI.js index 08d06f4..c62642e 100644 --- a/src/RuntimeAPI.js +++ b/src/RuntimeAPI.js @@ -12,6 +12,9 @@ governing permissions and limitations under the License. const ow = require('openwhisk') const { codes } = require('./SDKErrors') const Triggers = require('./triggers') +const { getProxyForUrl } = require('proxy-from-env') +const deepCopy = require('lodash.clonedeep') +const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-lib-runtime:RuntimeAPI', { provider: 'debug', level: process.env.LOG_LEVEL }) const LogForwarding = require('./LogForwarding') /** @@ -50,20 +53,31 @@ class RuntimeAPI { * @returns {Promise} a RuntimeAPI object */ async init (options) { + aioLogger.debug(`init options: ${JSON.stringify(options, null, 2)}`) + const clonedOptions = deepCopy(options) + const initErrors = [] - if (!options || !options.api_key) { + if (!clonedOptions || !clonedOptions.api_key) { initErrors.push('api_key') } - if (!options || !options.apihost) { + if (!clonedOptions || !clonedOptions.apihost) { initErrors.push('apihost') } if (initErrors.length) { - const sdkDetails = { options } + const sdkDetails = { clonedOptions } throw new codes.ERROR_SDK_INITIALIZATION({ sdkDetails, messageValues: `${initErrors.join(', ')}` }) } - this.ow = ow(options) + const proxyUrl = getProxyForUrl(clonedOptions.apihost) + if (proxyUrl) { + aioLogger.debug(`using proxy url: ${proxyUrl}`) + clonedOptions.proxy = proxyUrl + } else { + aioLogger.debug('proxy settings not found') + } + + this.ow = ow(clonedOptions) const self = this return { diff --git a/src/utils.js b/src/utils.js index 944efab..d7cb89a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -13,10 +13,10 @@ governing permissions and limitations under the License. const fs = require('fs-extra') const sha1 = require('sha1') const cloneDeep = require('lodash.clonedeep') -const logger = require('@adobe/aio-lib-core-logging')('@adobe/aio-lib-runtime:index', { level: process.env.LOG_LEVEL }) -const debugLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-lib-runtime:utils', { provider: 'debug' }) +const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-lib-runtime:utils', { provider: 'debug', level: process.env.LOG_LEVEL }) const yaml = require('js-yaml') -const fetch = require('cross-fetch') +const { createFetch } = require('@adobe/aio-lib-core-networking') +const fetch = createFetch() const globby = require('globby') const path = require('path') const archiver = require('archiver') @@ -417,7 +417,7 @@ function getActionEntryFile (pkgJsonPath) { return pkgJsonContent.main } } catch (err) { - debugLogger.debug(`File not found or does not define 'main' : ${pkgJsonPath}`) + aioLogger.debug(`File not found or does not define 'main' : ${pkgJsonPath}`) } return 'index.js' } @@ -431,7 +431,7 @@ function getActionEntryFile (pkgJsonPath) { * @returns {Promise} returns with a blank promise when done */ function zip (filePath, out, pathInZip = false) { - debugLogger.debug(`Creating zip of file/folder ${filePath}`) + aioLogger.debug(`Creating zip of file/folder ${filePath}`) const stream = fs.createWriteStream(out) const archive = archiver('zip', { zlib: { level: 9 } }) @@ -496,7 +496,7 @@ function safeParse (val) { try { resultVal = JSON.parse(val) } catch (ex) { - debugLogger.debug(`JSON parse threw exception for value ${val}`) + aioLogger.debug(`JSON parse threw exception for value ${val}`) } } } @@ -1060,7 +1060,7 @@ function rewriteActionsWithAdobeAuthAnnotation (packages, deploymentPackages) { // check if the annotation is defined AND the action is a web action if ((isWeb || isWebExport) && thisAction.annotations && thisAction.annotations[ADOBE_AUTH_ANNOTATION]) { - debugLogger.debug(`found annotation '${ADOBE_AUTH_ANNOTATION}' in action '${key}/${actionName}', cli env = ${env}`) + aioLogger.debug(`found annotation '${ADOBE_AUTH_ANNOTATION}' in action '${key}/${actionName}', cli env = ${env}`) // 1. rename the action const renamedAction = REWRITE_ACTION_PREFIX + actionName @@ -1091,7 +1091,7 @@ function rewriteActionsWithAdobeAuthAnnotation (packages, deploymentPackages) { } delete newPackages[key].actions[renamedAction].annotations[ADOBE_AUTH_ANNOTATION] - debugLogger.debug(`renamed action '${key}/${actionName}' to '${key}/${renamedAction}'`) + aioLogger.debug(`renamed action '${key}/${actionName}' to '${key}/${renamedAction}'`) // 3. create the sequence if (newPackages[key].sequences === undefined) { @@ -1108,7 +1108,7 @@ function rewriteActionsWithAdobeAuthAnnotation (packages, deploymentPackages) { web: (isRaw && 'raw') || 'yes' } - debugLogger.debug(`defined new sequence '${key}/${actionName}': '${ADOBE_AUTH_ACTION},${key}/${renamedAction}'`) + aioLogger.debug(`defined new sequence '${key}/${actionName}': '${ADOBE_AUTH_ACTION},${key}/${renamedAction}'`) } }) } @@ -1330,7 +1330,7 @@ function setPaths (flags = {}) { } else { manifestPath = flags.manifest } - logger.debug(`Using manifest file: ${manifestPath}`) + aioLogger.debug(`Using manifest file: ${manifestPath}`) let deploymentPath let deploymentPackages = {} @@ -1428,7 +1428,7 @@ async function setupAdobeAuth (actions, owOptions, imsOrgId) { if (!res.ok) { throw new Error(`failed setting ims_org_id=${imsOrgId} into state lib, received status=${res.status}, please make sure your runtime credentials are correct`) } - logger.debug(`set IMS org id into cloud state, response: ${JSON.stringify(await res.json())}`) + aioLogger.debug(`set IMS org id into cloud state, response: ${JSON.stringify(await res.json())}`) } } diff --git a/test/LogForwarding.test.js b/test/LogForwarding.test.js index 240a875..5d434d5 100644 --- a/test/LogForwarding.test.js +++ b/test/LogForwarding.test.js @@ -1,7 +1,8 @@ -const fetch = require('cross-fetch') const LogForwarding = require('../src/LogForwarding') +const { createFetch } = require('@adobe/aio-lib-core-networking') +const mockFetch = jest.fn() -jest.mock('cross-fetch') +jest.mock('@adobe/aio-lib-core-networking') const apiUrl = 'host/runtime/namespaces/some_namespace/logForwarding' @@ -24,19 +25,20 @@ let logForwarding beforeEach(async () => { logForwarding = new LogForwarding('some_namespace', 'host', 'key') - fetch.mockReset() + createFetch.mockReturnValue(mockFetch) + mockFetch.mockReset() }) test('get', async () => { return new Promise(resolve => { - fetch.mockReturnValue(new Promise(resolve => { + mockFetch.mockReturnValue(new Promise(resolve => { resolve({ json: jest.fn().mockResolvedValue('result') }) })) return logForwarding.get() .then((res) => { - expect(fetch).toBeCalledTimes(1) + expect(mockFetch).toBeCalledTimes(1) expect(res).toBe('result') assertRequest('get') resolve() @@ -45,20 +47,20 @@ test('get', async () => { }) test('get failed', async () => { - fetch.mockRejectedValue(new Error('mocked error')) + mockFetch.mockRejectedValue(new Error('mocked error')) await expect(logForwarding.get()).rejects.toThrow("Could not get log forwarding settings for namespace 'some_namespace': mocked error") }) test.each(dataFixtures)('set %s', async (destination, fnName, input) => { return new Promise(resolve => { - fetch.mockReturnValue(new Promise(resolve => { + mockFetch.mockReturnValue(new Promise(resolve => { resolve({ text: jest.fn().mockResolvedValue(`result for ${destination}`) }) })) return logForwarding[fnName](...Object.values(input)) .then((res) => { - expect(fetch).toBeCalledTimes(1) + expect(mockFetch).toBeCalledTimes(1) expect(res).toBe(`result for ${destination}`) assertRequest('put', { [destination]: input }) resolve() @@ -67,14 +69,14 @@ test.each(dataFixtures)('set %s', async (destination, fnName, input) => { }) test.each(dataFixtures)('set %s failed', async (destination, fnName, input) => { - fetch.mockRejectedValue(new Error(`mocked error for ${destination}`)) + mockFetch.mockRejectedValue(new Error(`mocked error for ${destination}`)) await expect(logForwarding[fnName]()) .rejects .toThrow(`Could not update log forwarding settings for namespace 'some_namespace': mocked error for ${destination}`) }) const assertRequest = (expectedMethod, expectedData) => { - expect(fetch).toBeCalledWith(apiUrl, { + expect(mockFetch).toBeCalledWith(apiUrl, { method: expectedMethod, body: JSON.stringify(expectedData), headers: { diff --git a/test/LogForwardingUnderscoreNamespace.test.js b/test/LogForwardingUnderscoreNamespace.test.js index 8c9f5cb..ab2cb23 100644 --- a/test/LogForwardingUnderscoreNamespace.test.js +++ b/test/LogForwardingUnderscoreNamespace.test.js @@ -1,7 +1,9 @@ -const fetch = require('cross-fetch') const LogForwarding = require('../src/LogForwarding') -jest.mock('cross-fetch') +const { createFetch } = require('@adobe/aio-lib-core-networking') +const mockFetch = jest.fn() + +jest.mock('@adobe/aio-lib-core-networking') const dataFixtures = [ ['adobe_io_runtime', 'setAdobeIoRuntime', {}], @@ -22,7 +24,8 @@ let logForwarding beforeEach(async () => { logForwarding = new LogForwarding('_', 'host', 'key') - fetch.mockReset() + createFetch.mockReturnValue(mockFetch) + mockFetch.mockReset() }) test('get for namespace "_" is not supported', async () => { diff --git a/test/build.actions.test.js b/test/build.actions.test.js index b5f8496..b2b6aa4 100644 --- a/test/build.actions.test.js +++ b/test/build.actions.test.js @@ -217,13 +217,6 @@ describe('build by bundling js action file with webpack', () => { 'manifest.yml': global.fixtureFile('/sample-app/manifest.yml'), 'package.json': global.fixtureFile('/sample-app/package.json') }) - // mockAIOConfig.get.mockReturnValue(global.fakeConfig.tvm) - // scripts = await AppScripts() - // remove folder zip action , focus on bundled js use case - // todo use fixtures instead - /* vol.unlinkSync('/actions/action-zip/index.js') - vol.unlinkSync('/actions/action-zip/package.json') - vol.rmdirSync('/actions/action-zip') */ config = deepClone(global.sampleAppConfig) // delete config.manifest.package.actions['action-zip'] delete config.manifest.full.packages.__APP_PACKAGE__.actions['action-zip'] @@ -239,12 +232,6 @@ describe('build by bundling js action file with webpack', () => { await expect(buildActions(config)).rejects.toEqual(expect.objectContaining({ message: expect.stringContaining('ENOENT') })) }) - /* test('should fail if action js file is a symlink', async () => { - vol.unlinkSync('/actions/action.js') - vol.symlinkSync('somefile', '/actions/action.js') - await expect(buildActions(config)).rejects.toThrow('actions/action.js is not a valid file or directory') - }) */ - test('should fail for invalid file or directory', async () => { await buildActions(config) expect(webpackMock.run).toHaveBeenCalledTimes(1) diff --git a/test/index.test.js b/test/index.test.js index 40aeaa8..64fdd18 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -13,6 +13,10 @@ const sdk = require('../src') const { codes } = require('../src/SDKErrors') const Triggers = require('../src/triggers') const ow = require('openwhisk')() +const { getProxyForUrl } = require('proxy-from-env') + +jest.mock('proxy-from-env') + // ///////////////////////////////////////////// const gApiHost = 'test-host' @@ -34,6 +38,7 @@ const createSdkClient = async () => { // ///////////////////////////////////////////// beforeEach(() => { + getProxyForUrl.mockReset() }) test('sdk init test', async () => { @@ -53,7 +58,7 @@ test('sdk init test - no api_key', async () => { ) }) -test('proxy functionality', async () => { +test('javascript proxy functionality (ow object)', async () => { const runtimeLib = await sdk.init(createOptions()) // Call a function that is not proxied ow.mockResolved('triggers.list', '') @@ -63,6 +68,18 @@ test('proxy functionality', async () => { expect(runtimeLib.triggers.create).toBe(Triggers.prototype.create) }) +test('set http proxy', async () => { + let sdkClient + + getProxyForUrl.mockReturnValue('https://localhost:8081') // proxy settings available (url only) + sdkClient = await createSdkClient() + expect(Object.keys(sdkClient)).toEqual(expect.arrayContaining(['actions', 'activations', 'namespaces', 'packages', 'rules', 'triggers', 'routes'])) + + getProxyForUrl.mockReturnValue('https://user:hunter2@localhost:8081') // proxy settings available (url and auth) + sdkClient = await createSdkClient() + expect(Object.keys(sdkClient)).toEqual(expect.arrayContaining(['actions', 'activations', 'namespaces', 'packages', 'rules', 'triggers', 'routes'])) +}) + test('triggers.create', async () => { const runtimeLib = await sdk.init(createOptions()) // No args diff --git a/test/jest.setup.js b/test/jest.setup.js index b09a8d2..2917746 100644 --- a/test/jest.setup.js +++ b/test/jest.setup.js @@ -24,7 +24,7 @@ process.env.CI = true jest.setTimeout(30000) -jest.setMock('cross-fetch', fetch) +jest.setMock('node-fetch', fetch) // trap console log beforeEach(() => { stdout.start() }) diff --git a/test/print.action.logs.test.js b/test/print.action.logs.test.js index f84327e..9347a1a 100644 --- a/test/print.action.logs.test.js +++ b/test/print.action.logs.test.js @@ -8,11 +8,11 @@ const mockPrintFilteredActionLogs = jest.fn(async (runtime, logger, limit, filte }) runtimeLibUtils.printFilteredActionLogs = mockPrintFilteredActionLogs const printActionLogs = require('../src/print-action-logs') +const util = require('util') -jest.mock('util', () => ({ - promisify: jest.fn(), - inherits: jest.fn() -})) +jest.mock('util') +util.promisify = jest.fn() +util.inherits = jest.fn() jest.mock('../src/RuntimeAPI') const ioruntime = require('../src/RuntimeAPI') diff --git a/test/utils.test.js b/test/utils.test.js index 638939b..e85def8 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -14,11 +14,14 @@ const fs = require('fs-extra') const cloneDeep = require('lodash.clonedeep') const os = require('os') const path = require('path') - const archiver = require('archiver') -jest.mock('archiver') +const networking = require('@adobe/aio-lib-core-networking') +networking.createFetch = jest.fn() +const mockFetch = jest.fn() +networking.createFetch.mockReturnValue(mockFetch) -jest.mock('cross-fetch') +jest.mock('archiver') +jest.mock('@adobe/aio-lib-core-networking') jest.mock('globby') const utils = require('../src/utils') @@ -50,6 +53,7 @@ beforeEach(() => { 'basic_manifest_res_namesonly.json': global.fixtureFile('/deploy/basic_manifest_res_namesonly.json') } global.fakeFileSystem.addJson(json) + mockFetch.mockReset() }) afterEach(() => { @@ -581,12 +585,10 @@ describe('deployPackage', () => { const cmdRule = ow.mockResolved(owRules, '') ow.mockResolvedProperty('actions.client.options', { apiKey: 'my-key', namespace: 'my-namespace' }) - const rp = require('cross-fetch') - rp.mockImplementation(() => ({ + mockFetch.mockResolvedValue({ ok: true, json: jest.fn() - })) - + }) await utils.deployPackage(JSON.parse(fs.readFileSync('/basic_manifest_res.json')), ow, mockLogger, imsOrgId) expect(cmdPkg).toHaveBeenCalledWith(expect.objectContaining({ name: 'hello' })) expect(cmdPkg).toHaveBeenCalledWith(expect.objectContaining({ name: 'mypackage', package: { binding: { name: 'oauth', namespace: 'adobeio' } } })) @@ -596,7 +598,6 @@ describe('deployPackage', () => { expect(cmdRule).toHaveBeenCalled() // this assertion is specific to the tmp implementation of the require-adobe-annotation - const mockFetch = require('cross-fetch') expect(mockFetch).toHaveBeenCalledWith( 'https://adobeio.adobeioruntime.net/api/v1/web/state/put', { @@ -612,13 +613,12 @@ describe('deployPackage', () => { const mockLogger = jest.fn() ow.mockResolvedProperty('actions.client.options', { apiKey: 'my-key', namespace: 'my-namespace' }) - const rp = require('cross-fetch') const res = { ok: false, status: 403, json: jest.fn() } - rp.mockImplementation(() => res) + mockFetch.mockResolvedValue(res) await expect(utils.deployPackage(JSON.parse(fs.readFileSync('/basic_manifest_res.json')), ow, mockLogger, imsOrgId)) .rejects.toThrowError(`failed setting ims_org_id=${imsOrgId} into state lib, received status=${res.status}, please make sure your runtime credentials are correct`) @@ -1988,15 +1988,6 @@ describe('zip', () => { expect(fs.existsSync('/out.zip')).toEqual(true) }) - // test('should fail if symlink', async () => { - // global.addFakeFiles(vol, '/indir', ['fake1.js']) - // vol.symlinkSync('/indir/fake1.js', '/indir/symlink.js') - // await expect(utils.zip('/indir/symlink.js', '/out.zip')).rejects.toThrow('symlink.js is not a valid dir or file') - // expect(archiver.mockFile).toHaveBeenCalledTimes(0) - // expect(archiver.mockDirectory).toHaveBeenCalledTimes(0) - // expect(vol.existsSync('/out.zip')).toEqual(false) - // }) - test('should fail if file does not exists', async () => { await expect(utils.zip('/notexist.js', '/out.zip')).rejects.toEqual(expect.objectContaining({ message: expect.stringContaining('ENOENT')