| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| 'use strict'; |
| |
| const assert = require('assert'); |
| const nock = require('nock'); |
| const fs = require('fs'); |
| const { execSync } = require('child_process'); |
| const path = require('path'); |
| const getPort = require('get-port'); |
| |
| const FAKE_OPENWHISK_SERVER = "https://example.com"; |
| const FAKE_OPENWHISK_AUTH = "c3VwZXItc2VjcmV0LWtleQ=="; |
| const FAKE_OPENWHISK_NAMESPACE = "test"; |
| |
| const WSKDEBUG_BACKUP_ACTION_SUFFIX = "_wskdebug_original"; |
| |
| let openwhisk; |
| |
| function isDockerInstalled() { |
| try { |
| execSync("docker info", {stdio: 'ignore'}); |
| } catch (e) { |
| throw new Error("Docker not available or running on local system. These unit tests require it.") |
| } |
| } |
| |
| async function beforeEach() { |
| delete process.env.OW_AUTH; |
| delete process.env.OW_NAMESPACE; |
| delete process.env.OW_APIHOST; |
| process.env.WSK_CONFIG_FILE = path.join(process.cwd(), "test/wskprops"); |
| |
| openwhisk = nock(FAKE_OPENWHISK_SERVER); |
| mockOpenwhiskSwagger(openwhisk); |
| |
| // save current working dir |
| this.cwd = process.cwd(); |
| |
| // find free port |
| this.port = await getPort(9229); |
| console.log("[test] free port:", this.port); |
| } |
| |
| function afterEach() { |
| delete process.env.WSK_CONFIG_FILE; |
| delete process.env.DOCKER_HOST_IP; |
| nock.cleanAll(); |
| |
| // restore working dir from beforeEach() |
| process.chdir(this.cwd); |
| } |
| |
| function assertAllNocksInvoked() { |
| assert( |
| openwhisk.isDone(), |
| "Expected these HTTP requests: " + openwhisk.pendingMocks().join() |
| ); |
| } |
| |
| function openwhiskNock() { |
| return openwhisk; |
| } |
| |
| function openwhiskApiUrl() { |
| return `/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}`; |
| } |
| |
| |
| function openwhiskApiUrlActions() { |
| return `/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions`; |
| } |
| |
| function openwhiskApiAuthHeader() { |
| return `Basic ${FAKE_OPENWHISK_AUTH}`; |
| } |
| |
| function agentRetryResponse() { |
| return { |
| response: { |
| success: false, |
| result: { |
| error: { |
| error: "Please retry.", |
| code: 42 // retry |
| } |
| } |
| } |
| }; |
| } |
| |
| function agentExitResponse() { |
| return { |
| response: { |
| success: false, |
| result: { |
| error: { |
| error: "Please exit, thanks.", |
| code: 43 // graceful exit |
| } |
| } |
| } |
| }; |
| } |
| |
| function mockAction(name, code, binary=false) { |
| // reading action without code |
| openwhisk |
| .get(`${openwhiskApiUrlActions()}/${name}`) |
| .matchHeader("authorization", openwhiskApiAuthHeader()) |
| .query({"code":"false"}) |
| .reply(200, nodejsActionDescription(name, binary)); |
| |
| // with code |
| const action = nodejsActionDescription(name, binary); |
| action.exec.code = code; |
| |
| // reading action with code |
| openwhisk |
| .get(`${openwhiskApiUrlActions()}/${name}`) |
| .matchHeader("authorization", openwhiskApiAuthHeader()) |
| .reply(200, action); |
| } |
| |
| function mockCreateBackupAction(name, binary=false) { |
| const backupName = name + WSKDEBUG_BACKUP_ACTION_SUFFIX; |
| |
| // wskdebug creating the backup action |
| openwhisk |
| .put(`${openwhiskApiUrlActions()}/${backupName}?overwrite=true`) |
| .matchHeader("authorization", openwhiskApiAuthHeader()) |
| .reply(200, nodejsActionDescription(backupName, binary)); |
| } |
| |
| function mockInstallAgent(name) { |
| // wskdebug overwriting the action with the agent |
| openwhisk |
| .put( |
| `${openwhiskApiUrlActions()}/${name}?overwrite=true`, |
| body => body.annotations.some(v => v.key === "wskdebug" && v.value === true) |
| ) |
| .matchHeader("authorization", openwhiskApiAuthHeader()) |
| .reply(200, nodejsActionDescription(name)); |
| } |
| |
| function mockReadBackupAction(name, code, binary=false) { |
| const backupName = name + WSKDEBUG_BACKUP_ACTION_SUFFIX; |
| |
| // reading it later on restore |
| openwhisk |
| .get(`${openwhiskApiUrlActions()}/${backupName}`) |
| .matchHeader("authorization", openwhiskApiAuthHeader()) |
| .reply(200, Object.assign(nodejsActionDescription(backupName, binary), { exec: { code } })); |
| } |
| |
| function mockRestoreAction(name, code, binary=false) { |
| // restoring action |
| openwhisk |
| .put( |
| `${openwhiskApiUrlActions()}/${name}?overwrite=true`, |
| body => body.exec && body.exec.code === code |
| ) |
| .matchHeader("authorization", openwhiskApiAuthHeader()) |
| .reply(200, nodejsActionDescription(name, binary)); |
| } |
| |
| function mockRemoveBackupAction(name) { |
| const backupName = name + WSKDEBUG_BACKUP_ACTION_SUFFIX; |
| |
| // removing backup after restore |
| openwhisk |
| .delete(`${openwhiskApiUrlActions()}/${backupName}`) |
| .matchHeader("authorization", openwhiskApiAuthHeader()) |
| .reply(200); |
| } |
| |
| function expectAgent(name, code, binary=false) { |
| mockCreateBackupAction(name, binary); |
| mockInstallAgent(name); |
| |
| // shutdown/restore process |
| mockRestoreAction(name, code, binary); |
| } |
| |
| function nockActivation(name, bodyFn) { |
| return openwhisk |
| .post(`${openwhiskApiUrlActions()}/${name}`, bodyFn) |
| .query(true) // support both ?blocking=true and non blocking (no query params) |
| .matchHeader("authorization", openwhiskApiAuthHeader()); |
| } |
| |
| function mockAgentPoll(name) { |
| return nockActivation(name, body => body.$waitForActivation === true) |
| .optionally() |
| .reply(502, agentRetryResponse()) |
| .persist(); |
| } |
| |
| function expectAgentInvocation(name, params, result) { |
| params = params || {}; |
| const activationId = Date.now(); |
| result = result || {}; |
| result.$activationId = activationId; |
| |
| // wskdebug agent ping for new activation |
| nockActivation(name, body => body.$waitForActivation === true) |
| .reply(200, { |
| response: { |
| result: Object.assign(params, { $activationId: activationId }) |
| } |
| }); |
| |
| // wskdebug sending result back to agent |
| nockActivation( |
| name, |
| body => { |
| assert.deepStrictEqual(body, result); |
| return true; |
| } |
| ).reply(200, { |
| response: { |
| result: { |
| message: "Completed" |
| } |
| } |
| }); |
| |
| // graceful shutdown for wskdebug to end test |
| nockActivation(name, body => body.$waitForActivation === true) |
| .reply(502, { |
| response: { |
| success: false, |
| result: { |
| error: { |
| error: "Please exit, thanks.", |
| code: 43 // graceful exit |
| } |
| } |
| } |
| }); |
| } |
| |
| function mockActionAndInvocation(action, code, params, expectedResult, binary=false) { |
| mockAction(action, code, binary); |
| expectAgent(action, code, binary); |
| |
| expectAgentInvocation(action, params, expectedResult); |
| } |
| |
| function mockActionDoubleInvocation(action, code, params, result1, runBetween, result2, binary=false) { |
| params = params || {}; |
| const activationId = Date.now(); |
| result1.$activationId = activationId; |
| |
| mockAction(action, code, binary); |
| expectAgent(action, code, binary); |
| |
| // 1st activation |
| nockActivation(action, body => body.$waitForActivation === true) |
| .reply(200, { |
| response: { |
| result: Object.assign(params, { $activationId: activationId }) |
| } |
| }); |
| |
| // wskdebug sending result back to agent |
| nockActivation( |
| action, |
| body => { |
| console.log("[test] mockActionDoubleInvocation: asserting result1") |
| assert.deepStrictEqual(body, result1); |
| return true; |
| } |
| ).reply(200, { |
| response: { |
| result: { |
| message: "Completed" |
| } |
| } |
| }); |
| |
| // 2nd activation |
| const activationId2 = Date.now() + "-second"; |
| result2.$activationId = activationId2; |
| |
| nockActivation(action, body => body.$waitForActivation === true) |
| .reply(200, async () => { |
| await runBetween(); |
| return { |
| response: { |
| result: Object.assign(params, { $activationId: activationId2 }) |
| } |
| } |
| }); |
| |
| // wskdebug sending 2nd result back to agent |
| nockActivation( |
| action, |
| body => { |
| console.log("[test] mockActionDoubleInvocation: asserting result2") |
| assert.deepStrictEqual(body, result2); |
| return true; |
| } |
| ).reply(200, { |
| response: { |
| result: { |
| message: "Completed" |
| } |
| } |
| }); |
| |
| // graceful shutdown for wskdebug to end test |
| nockActivation(action, body => body.$waitForActivation === true) |
| .reply(502, { |
| response: { |
| success: false, |
| result: { |
| error: { |
| error: "Please exit, thanks.", |
| code: 43 // graceful exit |
| } |
| } |
| } |
| }); |
| } |
| |
| // --------------------------------------------< internal >--------------- |
| |
| function nodejsActionDescription(name, binary=false) { |
| return { |
| "annotations":[ |
| { "key": "exec", "value": "nodejs:10" }, |
| { "key": "provide-api-key", "value": true } |
| ], |
| "exec":{ |
| "kind": "nodejs:10", |
| "binary": binary |
| }, |
| "limits":{ |
| "concurrency": 200, |
| "logs": 10, |
| "memory": 256, |
| "timeout": 300000 |
| }, |
| "name": name, |
| "namespace": FAKE_OPENWHISK_NAMESPACE, |
| "parameters": [], |
| "publish": false, |
| "version": "0.0.1" |
| }; |
| } |
| |
| function mockOpenwhiskSwagger(openwhisk) { |
| // mock swagger api response |
| openwhisk |
| .get('/') |
| .optionally() |
| .matchHeader("accept", "application/json") |
| .matchHeader("authorization", openwhiskApiAuthHeader()) |
| .reply(200, { |
| "api_paths": ["/api/v1"], |
| "description": "OpenWhisk", |
| "limits": { |
| "actions_per_minute":600, |
| "concurrent_actions":100, |
| "max_action_duration":3600000, |
| "max_action_logs":10485760, |
| "max_action_memory":52428800000, |
| "min_action_duration":100, |
| "min_action_logs":0, |
| "min_action_memory":134217728, |
| "sequence_length":50, |
| "triggers_per_minute":600 |
| }, |
| "runtimes":{ |
| "nodejs": [ |
| { |
| "kind":"nodejs:10", |
| "attached":true, |
| "default":true, |
| "deprecated":false, |
| "image":"bladerunner/adobe-action-nodejs-v10:3.0.21", |
| "requireMain":false |
| },{ |
| "kind":"nodejs", |
| "attached":true, |
| "default":false, |
| "deprecated":true, |
| "image":"bladerunner/adobe-action-nodejs-v10-fat:3.0.17", |
| "requireMain":false |
| },{ |
| "kind":"nodejs:10-fat", |
| "attached":true, |
| "default":false, |
| "deprecated":true, |
| "image":"bladerunner/adobe-action-nodejs-v10-fat:3.0.17", |
| "requireMain":false |
| },{ |
| "kind":"nodejs:6", |
| "attached":true, |
| "default":false, |
| "deprecated":true, |
| "image":"bladerunner/adobe-action-nodejs-v10-fat:3.0.17", |
| "requireMain":false |
| },{ |
| "kind":"nodejs:8", |
| "attached":true, |
| "default":false, |
| "deprecated":true, |
| "image":"bladerunner/adobe-action-nodejs-v10-fat:3.0.17", |
| "requireMain":false |
| } |
| ] |
| }, |
| "support":{ |
| "github":"https://github.com/apache/openwhisk/issues", |
| "slack":"http://slack.openwhisk.org" |
| } |
| }); |
| |
| openwhisk |
| .get('/api/v1') |
| .optionally() |
| .matchHeader("accept", "application/json") |
| .matchHeader("authorization", openwhiskApiAuthHeader()) |
| .reply(200,{ |
| "api_version":"1.0.0", |
| "api_version_path":"v1", |
| "build":"2019-11-08 - a", |
| "buildno":"v58 - runtime-prs-v59-f7774d5", |
| "description":"OpenWhisk API", |
| "swagger_paths": { |
| "api-docs":"/api-docs", |
| "ui":"/docs" |
| } |
| }); |
| |
| openwhisk |
| .get('/api/v1/api-docs') |
| .optionally() |
| .matchHeader("accept", "application/json") |
| .matchHeader("authorization", openwhiskApiAuthHeader()) |
| .reply(200, JSON.parse(fs.readFileSync("./test/openwhisk-swagger.json"))); |
| } |
| |
| // --------------------------------------------< utils >--------------- |
| |
| let capture; |
| |
| function startCaptureStdout() { |
| endCaptureStdout(); |
| global.disableMochaLogFile = true; |
| |
| capture = { |
| stdout: "", |
| stderr: "", |
| original: { |
| stdoutWrite: process.stdout.write, |
| stderrWrite: process.stderr.write |
| } |
| }; |
| process.stdout.write = function(string) { |
| capture.stdout += string; |
| }; |
| process.stderr.write = function(string) { |
| capture.stderr += string; |
| }; |
| } |
| |
| function endCaptureStdout() { |
| delete global.disableMochaLogFile; |
| |
| if (capture && capture.original) { |
| process.stdout.write = capture.original.stdoutWrite; |
| process.stderr.write = capture.original.stderrWrite; |
| delete capture.original; |
| } |
| if (capture) { |
| return { |
| stdout: capture.stdout, |
| stderr: capture.stderr |
| }; |
| } else { |
| return {}; |
| } |
| } |
| |
| async function sleep(millis) { |
| return new Promise(resolve => setTimeout(resolve, millis)); |
| } |
| |
| function touchFile(file) { |
| fs.utimesSync(file, Date.now(), Date.now()); |
| } |
| |
| function hasNotTimedOut(testCtx) { |
| if (!testCtx.test) { |
| return false; |
| } |
| if (testCtx.test.__deadline === undefined) { |
| testCtx.test.__deadline = Date.now() + testCtx.timeout() * 0.8; |
| return true; |
| } |
| return Date.now() < testCtx.test.__deadline; |
| } |
| |
| // --------------------------------------------< exports >--------------- |
| |
| module.exports = { |
| isDockerInstalled, |
| beforeEach, |
| afterEach, |
| assertAllNocksInvoked, |
| // mock |
| mockActionAndInvocation, |
| mockActionDoubleInvocation, |
| // advanced |
| openwhiskNock, |
| openwhiskApiUrl, |
| openwhiskApiUrlActions, |
| openwhiskApiAuthHeader, |
| mockAction, |
| mockCreateBackupAction, |
| mockInstallAgent, |
| mockReadBackupAction, |
| mockRestoreAction, |
| mockRemoveBackupAction, |
| nodejsActionDescription, |
| expectAgent, |
| nockActivation, |
| expectAgentInvocation, |
| mockAgentPoll, |
| agentRetryResponse, |
| agentExitResponse, |
| // utils |
| startCaptureStdout, |
| endCaptureStdout, |
| sleep, |
| touchFile, |
| hasNotTimedOut |
| } |