blob: d81bee1885bdc3d92801b0ff6068842ce2528d29 [file] [log] [blame]
/*
* 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() {
process.env.WSK_CONFIG_FILE = path.join(process.cwd(), "test/wskprops");
// nock.recorder.rec({ enable_reqheaders_recording: true });
openwhisk = nock(FAKE_OPENWHISK_SERVER);
// openwhisk.log(console.log);
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;
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 => {
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, () => {
runBetween();
return {
response: {
result: Object.assign(params, { $activationId: activationId2 })
}
}
});
// wskdebug sending 2nd result back to agent
nockActivation(
action,
body => {
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
}