UNPKG

cumulocity-cypress

Version:
556 lines (549 loc) 20.8 kB
'use strict'; 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;