/** * MIT License * * Copyright (c) 2020-present, Elastic NV * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * */ import merge from 'deepmerge'; import { createOption } from 'commander'; import { readConfig } from './config'; import type { CliArgs, RunOptions } from './common_types'; import { isFile, THROTTLING_WARNING_MSG, warn } from './helpers'; import { readFileSync } from 'fs'; type Mode = 'run' | 'push'; /** * Normalize the options passed via CLI and Synthetics config file * * Order of preference for options: * 1. Local options configured via Runner API * 2. CLI flags * 3. Configuration file */ export async function normalizeOptions( cliArgs: CliArgs, mode: Mode = 'run' ): Promise { /** * Move filtering flags from the top level to filter object * and delete the old keys */ const grepOpts = { pattern: cliArgs.pattern, tags: cliArgs.tags, match: cliArgs.match, }; delete cliArgs.pattern; delete cliArgs.tags; delete cliArgs.match; const options: RunOptions = { ...cliArgs, grepOpts, environment: process.env['NODE_ENV'] || 'development', }; /** * Validate and read synthetics config file based on the environment */ const config = cliArgs.config || !cliArgs.inline ? await readConfig(options.environment, cliArgs.config) : {}; options.params = Object.freeze(merge(config.params, cliArgs.params || {})); options.fields = Object.freeze( merge(config.monitor?.fields ?? {}, cliArgs?.fields || {}) ); /** * Merge playwright options from CLI and Synthetics config * and prefer individual options over other option */ const playwrightOpts = merge( config.playwrightOptions, cliArgs.playwrightOptions || {}, { arrayMerge(target, source) { if (source && source.length > 0) { return [...new Set(source)]; } return target; }, } ); options.playwrightOptions = { ...playwrightOpts, headless: getHeadlessFlag(cliArgs.headless, playwrightOpts?.headless), chromiumSandbox: cliArgs.sandbox ?? playwrightOpts?.chromiumSandbox, ignoreHTTPSErrors: cliArgs.ignoreHttpsErrors ?? playwrightOpts?.ignoreHTTPSErrors, }; options.proxy = Object.freeze( merge(config?.proxy ?? {}, cliArgs?.proxy || {}) ); /** * Merge default options based on the mode of operation whether we are running tests locally * or pushing the project monitors */ switch (mode) { case 'run': if (cliArgs.capability) { const supportedCapabilities = [ 'trace', 'network', 'filmstrips', 'metrics', 'ssblocks', ]; /** * trace - record chrome trace events(LCP, FCP, CLS, etc.) for all journeys * network - capture network information for all journeys * filmstrips - record detailed filmstrips for all journeys * metrics - capture performance metrics (DOM Nodes, Heap size, etc.) for each step * ssblocks - Dedupes the screenshots in to blocks to save storage space */ for (const flag of cliArgs.capability) { if (supportedCapabilities.includes(flag)) { options[flag] = true; } else { console.warn( `Missing capability "${flag}", current supported capabilities are ${supportedCapabilities.join( ', ' )}` ); } } } /** * Group all events that can be consumed by heartbeat and * eventually by the Synthetics UI. */ if (cliArgs.richEvents) { options.reporter = cliArgs.reporter ?? 'json'; options.ssblocks = true; options.network = true; options.trace = true; options.quietExitCode = true; } options.screenshots = cliArgs.screenshots ?? 'on'; break; case 'push': /** * Merge the default monitor config from synthetics.config.ts file * with the CLI options passed via push command */ const monitor = config.monitor; for (const key of Object.keys(monitor || {})) { // screenshots require special handling as the flags are different if (key === 'screenshot') { options.screenshots = options.screenshots ?? monitor[key]; continue; } options[key] = options[key] ?? monitor[key]; } break; } return options; } export function getHeadlessFlag( cliHeadless: boolean, configHeadless?: boolean ) { // if cliHeadless is false, then we don't care about configHeadless if (!cliHeadless) { return false; } // default is headless return configHeadless ?? true; } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function toObject(value: boolean | Record): Record { const defaulVal = {}; if (typeof value === 'boolean') { return defaulVal; } return value || defaulVal; } /** * Parses the throttling CLI settings with `{download: 5, upload: 3, latency: * 20}` format * * Since throttling is disabled for now, we warn the users if throttling/nothrottling * flag is passed to the CLI */ export function parseThrottling() { warn(THROTTLING_WARNING_MSG); } export function getCommonCommandOpts() { const params = createOption( '-p, --params ', 'JSON object that defines any variables your tests require.' ).argParser(JSON.parse); const playwrightOpts = createOption( '--playwright-options ', 'JSON object to pass in custom Playwright options for the agent. Options passed will be merged with Playwright options defined in your synthetics.config.js file.' ).argParser(parsePlaywrightOptions); const pattern = createOption( '--pattern ', 'RegExp pattern to match journey files in the current working directory (default: /*.journey.(ts|js)$/)' ); const apiDocsLink = 'API key used for Kibana authentication(https://www.elastic.co/guide/en/kibana/master/api-keys.html).'; const auth = createOption('--auth ', apiDocsLink).env( 'SYNTHETICS_API_KEY' ); const authMandatory = createOption('--auth ', apiDocsLink) .env('SYNTHETICS_API_KEY') .makeOptionMandatory(true); const configOpt = createOption( '-c, --config ', 'path to the configuration file (default: synthetics.config.(js|ts))' ); const tags = createOption( '--tags ', 'run/push tests with the tag(s) matching a pattern' ); const match = createOption( '--match ', 'run/push tests with a name or tags that matches a pattern' ); const fields = createOption( '--fields ', 'add fields to the monitor(s) in the format { "key": "value"}' ).argParser((fieldsStr: string) => { const fields = JSON.parse(fieldsStr); if (typeof fields !== 'object') { throw new Error('Invalid fields format'); } // make sure all values are strings return Object.entries(fields).reduce((acc, [key, value]) => { if (typeof value !== 'string') { acc[key] = JSON.stringify(value); } else { acc[key] = value; } return acc; }, {}); }); const maintenanceWindows = createOption( '--maintenance-windows ', "List of Kibana's Maintenance Windows IDs assigned by default. More information on https://www.elastic.co/docs/explore-analyze/alerts-cases/alerts/maintenance-windows." ); return { auth, authMandatory, params, playwrightOpts, pattern, configOpt, tags, match, fields, maintenanceWindows, }; } export function parsePlaywrightOptions(playwrightOpts: string) { return JSON.parse(playwrightOpts, (key, value) => { if (key !== 'clientCertificates') { return value; } // Revive serialized clientCertificates buffer objects return (value ?? []).map(item => { const revived = { ...item }; if (item.cert && !Buffer.isBuffer(item.cert)) { revived.cert = parseAsBuffer(item.cert); } if (item.key && !Buffer.isBuffer(item.key)) { revived.key = parseAsBuffer(item.key); } if (item.pfx && !Buffer.isBuffer(item.pfx)) { revived.pfx = parseAsBuffer(item.pfx); } return revived; }); }); } function parseAsBuffer(value: any): Buffer { try { return Buffer.from(value); } catch (e) { return value; } } export function parseFileOption(opt: string) { return (value: string) => { if (!isFile(value)) { return value; } try { return readFileSync(value); } catch (e) { throw new Error(`${opt} - could not read provided path ${value}: ${e}`); } }; } // This is a generic util to collect multiple options into a single // dictionary export function collectOpts(key, accumulator, nextParser?) { return value => { accumulator[key] = nextParser ? nextParser(value) : value; return accumulator[key]; }; }