cumulocity-cypress
Version:
Cypress commands for Cumulocity IoT
556 lines (549 loc) • 20.8 kB
JavaScript
;
var _$1 = require('lodash');
var fs = require('fs');
var path = require('path');
var yargs = require('yargs/yargs');
var helpers = require('yargs/helpers');
var winston = require('winston');
var transportsDirect = require('winston/lib/winston/transports/');
var morgan = require('morgan');
var httpcontroller = require('./httpcontroller-BmRpHFCn.js');
require('date-fns');
require('@c8y/client');
require('set-cookie-parser');
var glob = require('glob');
var debug = require('debug');
require('util');
require('express');
require('raw-body');
require('cookie-parser');
require('cookie');
require('http-proxy-middleware');
require('semver');
require('swagger-ui-express');
require('yaml');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var ___namespace = /*#__PURE__*/_interopNamespaceDefault(_$1);
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
var glob__namespace = /*#__PURE__*/_interopNamespaceDefault(glob);
const _ = _$1 || ___namespace;
const log$1 = debug("c8y:fileadapter");
/**
* Default implementation of C8yPactFileAdapter which loads and saves pact objects from/to
* json files using C8yPact objects.
*/
class C8yPactDefaultFileAdapter {
constructor(folder) {
this.folder = path__namespace.isAbsolute(folder)
? folder
: this.toAbsolutePath(folder);
}
description() {
return `C8yPactDefaultFileAdapter: ${this.folder}`;
}
getFolder() {
return this.folder;
}
loadPacts() {
const jsonFiles = this.loadPactObjects();
log$1(`loadPacts() - ${jsonFiles.length} pact files from ${this.folder}`);
return jsonFiles.reduce((acc, obj) => {
if (!obj?.info?.id)
return acc;
acc[obj.info.id] = obj;
return acc;
}, {});
}
loadPact(id) {
log$1(`loadPact() - ${id}`);
const pId = httpcontroller.pactId(id);
if (pId == null) {
log$1(`loadPact() - invalid pact id ${id} -> ${pId}`);
return null;
}
if (!this.folder || !fs__namespace.existsSync(this.folder)) {
log$1(`loadPact() - folder ${this.folder} does not exist`);
return null;
}
const file = path__namespace.join(this.folder, `${pId}.json`);
if (fs__namespace.existsSync(file)) {
const pact = fs__namespace.readFileSync(file, "utf-8");
log$1(`loadPact() - ${file} loaded`);
const json = JSON.parse(pact);
log$1(`loadPact() - parsed as json`);
return json || null;
}
else {
log$1(`loadPact() - ${file} does not exist`);
}
return null;
}
pactExists(id) {
return fs__namespace.existsSync(path__namespace.join(this.folder, `${httpcontroller.pactId(id)}.json`));
}
savePact(pact) {
this.createFolderRecursive(this.folder);
const pId = httpcontroller.pactId(pact.id);
if (pId == null) {
log$1(`savePact() - invalid pact id ${pact.id} -> ${pId}`);
return;
}
const file = path__namespace.join(this.folder, `${pId}.json`);
log$1(`savePact() - write ${file} (${pact.records?.length || 0} records)`);
try {
fs__namespace.writeFileSync(file, httpcontroller.safeStringify({
id: pact.id,
info: pact.info,
records: pact.records,
}, 2), "utf-8");
}
catch (error) {
console.error(`Failed to save pact.`, error);
}
}
deletePact(id) {
const pId = httpcontroller.pactId(id);
if (pId == null) {
log$1(`deletePact() - invalid pact id ${id} -> ${pId}`);
return;
}
const filePath = path__namespace.join(this.folder, `${pId}.json`);
if (fs__namespace.existsSync(filePath)) {
fs__namespace.unlinkSync(filePath);
log$1(`deletePact() - deleted ${filePath}`);
}
else {
log$1(`deletePact() - ${filePath} does not exist`);
}
}
readJsonFiles() {
log$1(`readJsonFiles() - ${this.folder}`);
if (!this.folder || !fs__namespace.existsSync(this.folder)) {
log$1(`readJsonFiles() - ${this.folder} does not exist`);
return [];
}
const jsonFiles = glob__namespace.sync(path__namespace.join(this.folder, "*.json"));
log$1(`readJsonFiles() - reading ${jsonFiles.length} json files from ${this.folder}`);
const pacts = jsonFiles.map((file) => {
return fs__namespace.readFileSync(file, "utf-8");
});
return pacts;
}
deleteJsonFiles() {
if (!this.folder || !fs__namespace.existsSync(this.folder)) {
log$1(`deleteJsonFiles() - ${this.folder} does not exist`);
return;
}
const jsonFiles = glob__namespace.sync(path__namespace.join(this.folder, "*.json"));
log$1(`deleteJsonFiles() - deleting ${jsonFiles.length} json files from ${this.folder}`);
jsonFiles.forEach((file) => {
fs__namespace.unlinkSync(file);
});
}
loadPactObjects() {
const pacts = this.readJsonFiles();
return pacts.map((pact) => JSON.parse(pact));
}
createFolderRecursive(f) {
log$1(`createFolderRecursive() - ${f}`);
if (!f || !_.isString(f))
return undefined;
const absolutePath = !path__namespace.isAbsolute(f) ? this.toAbsolutePath(f) : f;
if (f !== absolutePath) {
log$1(`createFolderRecursive() - resolved ${f} to ${absolutePath}`);
}
if (fs__namespace.existsSync(f))
return undefined;
const result = fs__namespace.mkdirSync(absolutePath, { recursive: true });
if (result) {
log$1(`createFolderRecursive() - created ${absolutePath}`);
}
return result;
}
toAbsolutePath(f) {
return path__namespace.isAbsolute(f) ? f : path__namespace.resolve(process.cwd(), f);
}
isNodeError(error, type) {
return error instanceof type;
}
}
const log = debug("c8y:ctrl:startup");
function getEnvVar(name) {
return (process.env[name] ||
process.env[_$1.camelCase(name)] ||
process.env[`CYPRESS_${name}`] ||
process.env[name.replace(/^C8Y_/i, "")] ||
process.env[_$1.camelCase(`CYPRESS_${name}`)] ||
process.env[`CYPRESS_${_$1.camelCase(name.replace(/^C8Y_/i, ""))}`]);
}
function getConfigFromArgs() {
// doc: https://github.com/yargs/yargs/blob/0c95f9c79e1810cf9c8964fbf7d139009412f7e7/docs/api.md
const result = yargs(helpers.hideBin(process.argv))
.usage("Usage: $0 [options]")
.scriptName("c8yctrl")
.option("folder", {
alias: "pactFolder",
type: "string",
requiresArg: true,
description: "The folder recordings are stored in",
})
.option("port", {
type: "number",
requiresArg: true,
default: +(getEnvVar("C8YCTRL_PORT") ||
getEnvVar("C8Y_HTTP_PORT") ||
3000),
defaultDescription: "3000",
description: "The HTTP port c8yctrl listens on",
})
.option("baseUrl", {
alias: "baseurl",
type: "string",
requiresArg: true,
description: "The Cumulocity URL for proxying requests",
})
.option("user", {
alias: "username",
requiresArg: true,
type: "string",
description: "The username to login at baseUrl",
})
.option("password", {
type: "string",
requiresArg: true,
description: "The password to login at baseUrl",
})
.option("tenant", {
type: "string",
requiresArg: true,
description: "The tenant id of baseUrl",
})
.option("staticRoot", {
alias: "static",
requiresArg: true,
type: "string",
description: "The root folder to serve static files from",
})
.option("mode", {
type: "string",
requiresArg: true,
default: getEnvVar("C8YCTRL_MODE") || httpcontroller.C8yPactHttpControllerDefaultMode,
defaultDescription: httpcontroller.C8yPactHttpControllerDefaultMode,
description: `One of ${Object.values(httpcontroller.C8yPactModeValues)
.filter((m) => m !== "recording")
.join(", ")}`,
})
.option("recordingMode", {
type: "string",
requiresArg: true,
default: getEnvVar("C8YCTRL_RECORDING_MODE") ||
httpcontroller.C8yPactHttpControllerDefaultRecordingMode,
defaultDescription: httpcontroller.C8yPactHttpControllerDefaultRecordingMode,
description: `One of ${Object.values(httpcontroller.C8yPactRecordingModeValues).join(", ")}`,
})
.option("config", {
type: "string",
requiresArg: true,
default: getEnvVar("C8YCTRL_CONFIG") || "c8yctrl.config.ts",
description: "The path to the config file",
})
.option("log", {
type: "boolean",
default: getEnvVar("C8YCTRL_LOG") !== "false",
defaultDescription: "true",
requiresArg: false,
description: "Enable or disable logging",
})
.option("logLevel", {
type: "string",
default: getEnvVar("C8YCTRL_LOG_LEVEL") || "info",
defaultDescription: "info",
requiresArg: true,
description: "The log level used for logging",
})
.option("logFile", {
type: "string",
requiresArg: true,
description: "The path of the logfile",
})
.option("accessLogFile", {
type: "string",
requiresArg: true,
description: "The path of the access logfile",
})
.option("apps", {
type: "array",
requiresArg: true,
description: "Array of of static folder app names and semver ranges separated by '/'",
coerce: (arg) => parseApps(arg),
})
.help()
.wrap(120)
.version(httpcontroller.getPackageVersion())
.parseSync();
const logLevelValues = Object.values(httpcontroller.C8yPactHttpControllerLogLevel);
// pick only the options that are set and apply defaults
// yargs creates properties we do not want, this way we can filter them out
return [
{
folder: result.folder,
port: result.port,
baseUrl: result.baseUrl,
user: result.user,
password: result.password,
tenant: result.tenant,
staticRoot: result.staticRoot,
logFilename: result.logFile,
accessLogFilename: result.accessLogFile,
log: result.log,
logLevel: logLevelValues.includes(result.logLevel || "")
? result.logLevel
: undefined,
appsVersions: result.apps,
mode: result.mode,
recordingMode: result.recordingMode,
},
result.config,
];
}
function getConfigFromEnvironment() {
return {
folder: getEnvVar("C8YCTRL_FOLDER"),
port: +(getEnvVar("C8YCTRL_PORT") || getEnvVar("C8Y_HTTP_PORT") || 3000),
baseUrl: getEnvVar("C8YCTRL_BASEURL") || getEnvVar("C8Y_BASE_URL"),
user: getEnvVar("C8YCTRL_USERNAME") || getEnvVar("C8Y_USERNAME"),
password: getEnvVar("C8YCTRL_PASSWORD") || getEnvVar("C8Y_PASSWORD"),
tenant: getEnvVar("C8YCTRL_TENANT") || getEnvVar("C8Y_TENANT"),
staticRoot: getEnvVar("C8YCTRL_ROOT") ||
// compatibility with old env var names
getEnvVar("C8Y_STATIC") ||
getEnvVar("C8Y_STATIC_ROOT"),
logFilename: getEnvVar("C8YCTRL_LOG_FILE"),
accessLogFilename: getEnvVar("C8YCTRL_ACCESS_LOG_FILE"),
log: getEnvVar("C8YCTRL_LOG") !== "false",
logLevel: getEnvVar("C8YCTRL_LOG_LEVEL"),
mode: getEnvVar("C8YCTRL_MODE"),
recordingMode: getEnvVar("C8YCTRL_RECORDING_MODE"),
config: getEnvVar("C8YCTRL_CONFIG"),
appsVersions: parseApps(getEnvVar("C8YCTRL_APPS")),
};
}
function getConfigFromArgsOrEnvironment() {
const [args, config] = getConfigFromArgs();
const env = getConfigFromEnvironment();
return [_$1.defaults(args, env), config];
}
function validateConfig(config) {
if (!httpcontroller.C8yPactModeValues.includes(config.mode)) {
throw new Error(`Configured mode "${config.mode}" is not valid. Must be one of ${httpcontroller.C8yPactModeValues.join(", ")}.`);
}
if (!httpcontroller.C8yPactRecordingModeValues.includes(config.recordingMode)) {
throw new Error(`Configured recording mode "${config.recordingMode}" is not valid. Must be one of ${httpcontroller.C8yPactRecordingModeValues.join(", ")}.`);
}
}
const safeTransports = !_$1.isEmpty(winston.transports) ? winston.transports : transportsDirect;
/**
* Default logger for the HTTP controller. It logs to the console with colors and simple format.
* This needs to be passed to the config, so it must be created before applying the default config.
*/
const defaultLogger = winston.createLogger({
transports: [
new safeTransports.Console({
format: winston.format.combine(winston.format.colorize({
all: true,
colors: {
info: "green",
error: "red",
warn: "yellow",
debug: "white",
},
}), winston.format.simple()),
}),
],
});
/**
* Default config object for the HTTP controller. It takes a configuration object and
* adds required defaults, as for example the adapter, an error response record or the logger.
*
* This config can be overwritten by a config file, which is loaded by cosmiconfig.
*/
const applyDefaultConfig = (config) => {
if (!config?.auth) {
log("no auth options provided, trying to create from user and password");
const { user, password, tenant } = config;
config.auth = user && password ? { user, password, tenant } : undefined;
}
if (!("on" in config)) {
config.on = {};
log("configured empty object callback 'on' property of config");
}
// check all default properties as _.defaults seems to still overwrite in some cases
if (!("adapter" in config)) {
config.adapter = new C8yPactDefaultFileAdapter(config.folder || "./c8ypact");
log(`configured default file adapter for folder ${config.folder || "./c8ypact"}.`);
}
if (!("mockNotFoundResponse" in config)) {
config.mockNotFoundResponse = (url) => {
return {
status: 404,
statusText: "Not Found",
body: `Not Found: ${url}`,
headers: {
"content-type": "application/text",
},
};
};
log("configured default 404 text mockNotFoundResponse");
}
if (!("requestMatching" in config)) {
config.requestMatching = {
ignoreUrlParameters: ["dateFrom", "dateTo", "_", "nocache"],
baseUrl: config.baseUrl,
};
log("configured default requestMatching");
}
if (!("preprocessor" in config)) {
// use default preprocessor config
config.preprocessor = new httpcontroller.C8yDefaultPactPreprocessor();
log("configured default preprocessor");
}
applyDefaultLogConfig(config);
return config;
};
const applyDefaultLogConfig = (config) => {
if ("log" in config && config.log === false) {
log("disabled logging as config.log == false");
config.logger = undefined;
config.requestLogger = undefined;
return;
}
if (!("logger" in config)) {
config.logger = defaultLogger;
log("configured default logger");
}
if ("logFilename" in config &&
config.logFilename != null &&
config.logger != null) {
const p = path.isAbsolute(config.logFilename)
? config.accessLogFilename
: path.join(process.cwd(), config.logFilename);
config.logger.add(new safeTransports.File({
format: winston.format.simple(),
filename: p,
}));
log(`configured default logger file transport ${p}.`);
}
if ("logLevel" in config &&
config.logLevel != null &&
config.logger != null) {
config.logger.level = config.logLevel;
log(`configured log level ${config.logLevel}.`);
}
if (!("requestLogger" in config)) {
config.requestLogger = [
morgan("[c8yctrl] :method :url :status :res[content-length] - :response-time ms", {
skip: (req) => {
return (!req.url.startsWith("/c8yctrl") ||
req.url.startsWith("/c8yctrl/log"));
},
stream: {
write: (message) => {
config.logger?.warn(message.trim());
},
},
}),
];
log("configured default requestLogger for /c8yctrl interface and errors");
}
if (!("errorLogger" in config) && config.errorLogger == null) {
if (morgan["error-object"] == null) {
morgan.token("error-object", (req, res) => {
let resBody = res.body;
if (_$1.isString(resBody) &&
// parse as json only if body is a cumulocity error response
/"error"\s*:\s*"/.test(resBody) &&
/"message"\s*:\s*"/.test(resBody)) {
try {
resBody = JSON.parse(resBody);
}
catch {
// ignore, use body as string
}
}
// make sure we do not log too much
if (_$1.isString(resBody)) {
resBody = resBody.slice(0, 1000);
}
const errorObject = {
url: req.url,
status: `${res.statusCode} ${res.statusMessage}`,
requestHeader: req.headers,
responseHeader: res.getHeaders(),
responseBody: resBody,
requestBody: req.body,
};
return httpcontroller.safeStringify(errorObject);
});
log("default morgan error-object token compiled and registered");
}
config.errorLogger = morgan(":error-object", httpcontroller.morganErrorOptions(config.logger));
log("configured default error logger");
}
if ("accessLogFilename" in config && config.accessLogFilename != null) {
const p = path.isAbsolute(config.accessLogFilename)
? config.accessLogFilename
: path.join(process.cwd(), config.accessLogFilename);
const accessLogger = morgan("common", {
stream: fs.createWriteStream(p, {
flags: "a",
}),
});
if (config.requestLogger != null) {
if (_$1.isArrayLike(config.requestLogger)) {
config.requestLogger.push(accessLogger);
log(`configured file access logger to existing logger ${p}`);
}
}
else {
config.requestLogger = [accessLogger];
log(`configured file access logger ${p}`);
}
}
};
const parseApps = (value) => {
if (value == null)
return undefined;
const apps = {};
(_$1.isArray(value) ? value : value.split(",")).forEach((item) => {
const [key, ...value] = item.trim().split("/");
const semverRange = value.join("/");
if (key != null && value != null && semverRange != null) {
apps[key] = semverRange;
}
});
return apps;
};
exports.applyDefaultConfig = applyDefaultConfig;
exports.defaultLogger = defaultLogger;
exports.getConfigFromArgs = getConfigFromArgs;
exports.getConfigFromArgsOrEnvironment = getConfigFromArgsOrEnvironment;
exports.getConfigFromEnvironment = getConfigFromEnvironment;
exports.getEnvVar = getEnvVar;
exports.parseApps = parseApps;
exports.validateConfig = validateConfig;