Repository: elastic/synthetics
Pull request #508 (2022-05-23 19:16:39)

feat: interactive scaffolding for picking default monitor config
+ fix https://github.com/elastic/synthetics-dev/issues/119
+ part of #513 
+ `init` command scaffolding asks interactive questions for both `Locations` and `Schedule` which would be set as default on the Synthetics.config.ts file to be used when pushing the monitors. 
+ Error out when push command is invoked without passing the journey file paths
+ Make the location and schedule option as required `--locations <>` in the CLI and error out when not set. 
+ Allow users to configure the default values via `synthetics.config.ts` file


### How to test
1. Build the agent `npm run build`
2. Run `node dist/cli.js init test-directory`
3. Verify if it sets up all the default schedule and locations in the `synthetics.config.ts` generated file. 

apmmachine: 
## :broken_heart: Tests Failed


<!-- BUILD BADGES-->
> _the below badges are clickable and redirect to their specific view in the CI or DOCS_
[![Pipeline View](https://img.shields.io/badge/pipeline-pipeline%20-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-508/detail/PR-508/1//pipeline) [![Test View](https://img.shields.io/badge/test-test-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-508/detail/PR-508/1//tests) [![Changes](https://img.shields.io/badge/changes-changes-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-508/detail/PR-508/1//changes) [![Artifacts](https://img.shields.io/badge/artifacts-artifacts-yellow)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-508/detail/PR-508/1//artifacts) [![preview](https://img.shields.io/badge/docs-preview-yellowgreen)](http://synthetics_508.docs-preview.app.elstc.co/diff)  [![preview](https://img.shields.io/badge/elastic-observability-blue)](https://ci-stats.elastic.co/app/apm/services/apm-ci/transactions/view?rangeFrom=2022-05-23T19:06:55.963Z&rangeTo=2022-05-23T19:26:55.963Z&transactionName=BUILD+apm-agent-rum%2Felastic-synthetics%2FPR-%7Bnumber%7D&transactionType=job&latencyAggregationType=avg&traceId=ed3b1fcf8bfd4ad2487634217439ae5b&transactionId=c653a61ba81a4462)

<!-- BUILD SUMMARY-->
<details><summary>Expand to view the summary</summary>
<p>

#### Build stats




* Start Time: 2022-05-23T19:16:55.963+0000


* Duration: 7 min 20 sec




#### Test stats :test_tube:

| Test         | Results                         |
| ------------ | :-----------------------------: |
| Failed       | 2  |
| Passed       | 155  |
| Skipped      | 2 |
| Total        | 159   |


</p>
</details>

<!-- TEST RESULTS IF ANY-->

  ### Test errors [![2](https://img.shields.io/badge/2%20-red)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-508/detail/PR-508/1//tests)
<details><summary>Expand to view the tests failures</summary><p>
    
    
##### `Test / runner runner - build monitors with global config – runner runner - build monitors with global config`


<ul>
<details><summary>Expand to view the error details</summary><p>

```
 null 
 ```
</p></details>
<details><summary>Expand to view the stacktrace</summary><p>

```
 Error: expect(received).toMatchObject(expected)

- Expected  - 3
+ Received  + 1

  Object {
-   "locations": Array [
-     "US East",
-   ],
+   "locations": undefined,
    "schedule": 5,
    "throttling": Object {
      "latency": 1000,
    },
  }
    at Object.<anonymous> (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/__tests__/core/runner.test.ts:734:32)
    at Promise.then.completed (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/utils.js:333:28)
    at new Promise (<anonymous>)
    at callAsyncCircusFn (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/utils.js:259:10)
    at _callCircusTest (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/run.js:276:40)
    at _runTest (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/run.js:208:3)
    at _runTestsForDescribeBlock (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/run.js:96:9)
    at _runTestsForDescribeBlock (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/run.js:90:9)
    at run (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/run.js:31:3)
    at runAndTransformResultsToJestFormat (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:135:21) 
 ```
</p></details>
</ul>
    
##### `Test / runner runner - build monitors with local config – runner runner - build monitors with local config`


<ul>
<details><summary>Expand to view the error details</summary><p>

```
 null 
 ```
</p></details>
<details><summary>Expand to view the stacktrace</summary><p>

```
 Error: expect(received).toMatchObject(expected)

- Expected  - 3
+ Received  + 1

  Object {
-   "locations": Array [
-     "US East",
-   ],
+   "locations": undefined,
    "schedule": 10,
    "throttling": Object {
      "latency": 1000,
    },
  }
    at Object.<anonymous> (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/__tests__/core/runner.test.ts:698:32)
    at Promise.then.completed (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/utils.js:333:28)
    at new Promise (<anonymous>)
    at callAsyncCircusFn (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/utils.js:259:10)
    at _callCircusTest (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/run.js:276:40)
    at _runTest (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/run.js:208:3)
    at _runTestsForDescribeBlock (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/run.js:96:9)
    at _runTestsForDescribeBlock (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/run.js:90:9)
    at run (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/run.js:31:3)
    at runAndTransformResultsToJestFormat (/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-508/src/github.com/elastic/synthetics/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:135:21) 
 ```
</p></details>
</ul>
    
</p></details>


<!-- STEPS ERRORS IF ANY -->












### Steps errors [![1](https://img.shields.io/badge/1%20-red)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-508/detail/PR-508/1//pipeline)
  <details><summary>Expand to view the steps failures</summary>
  <p>


  
  
  
  
##### `Runs the tests`
<ul>
<li>Took 2 min 36 sec . View more details <a href="https://apm-ci.elastic.co//blue/rest/organizations/jenkins/pipelines/apm-agent-rum/pipelines/elastic-synthetics/pipelines/PR-508/runs/1/steps/341/log/?start=0">here</a></li>
<li>Description: <code>./node_modules/.bin/jest --ci --reporters=default --reporters=jest-junit</code></l1>
</ul>
  
  </p>
  </details>



## :robot: GitHub comments

To re-run your PR in the CI, just comment with:



- `/test` : Re-trigger the build.




<!--COMMENT_GENERATED_WITH_ID_comment.id-->

Patch:
diff --git a/package-lock.json b/package-lock.json
index 69067a0..4ca0ce1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
         "archiver": "^5.3.0",
         "commander": "^9.0.0",
         "deepmerge": "^4.2.2",
+        "enquirer": "^2.3.6",
         "esbuild": "^0.14.27",
         "expect": "^27.0.2",
         "http-proxy": "^1.18.1",
@@ -1981,7 +1982,6 @@
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
       "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
-      "dev": true,
       "engines": {
         "node": ">=6"
       }
@@ -2991,7 +2991,6 @@
       "version": "2.3.6",
       "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
       "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
-      "dev": true,
       "dependencies": {
         "ansi-colors": "^4.1.1"
       },
@@ -9899,8 +9898,7 @@
     "ansi-colors": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
-      "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
-      "dev": true
+      "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA=="
     },
     "ansi-escapes": {
       "version": "4.3.2",
@@ -10659,7 +10657,6 @@
       "version": "2.3.6",
       "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
       "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
-      "dev": true,
       "requires": {
         "ansi-colors": "^4.1.1"
       }
diff --git a/package.json b/package.json
index 5dd5b6e..6e3c85d 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
     "archiver": "^5.3.0",
     "commander": "^9.0.0",
     "deepmerge": "^4.2.2",
+    "enquirer": "^2.3.6",
     "esbuild": "^0.14.27",
     "expect": "^27.0.2",
     "http-proxy": "^1.18.1",
diff --git a/src/cli.ts b/src/cli.ts
index 44a9cfe..3bc6252 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -25,7 +25,7 @@
  *
  */
 
-import { program, Option } from 'commander';
+import { program, Option, Argument } from 'commander';
 import { CliArgs, PushOptions } from './common_types';
 import { reporters } from './reporters';
 import { normalizeOptions, parseThrottling } from './options';
@@ -146,9 +146,15 @@ program
 
 // Push command
 program
-  .command('push [files...]')
+  .command('push')
+  .addArgument(
+    new Argument(
+      '[journeys...]',
+      'file path to journey directory and individual files'
+    ).argRequired()
+  )
   .description(
-    'Push monitors to create new montors with Kibana monitor management UI'
+    'Push journeys to create montors within Kibana monitor management UI'
   )
   .option(
     '--schedule <time-in-minutes>',
@@ -162,8 +168,8 @@ program
     ).choices(SyntheticsLocations)
   )
   .requiredOption(
-    '--project <id/name>',
-    'project/repository that will be used for grouping the monitors.'
+    '--project <project-id>',
+    'id that will be used for logically grouping monitors'
   )
   .requiredOption('--url <url>', 'kibana URL to upload the monitors')
   .requiredOption(
@@ -176,15 +182,23 @@ program
     'default'
   )
   .option('--delete', 'automatically delete the stale monitors.')
-  .action(async (files, cmdOpts: PushOptions) => {
+  .action(async (journeys, cmdOpts: PushOptions) => {
     try {
-      const cliArgs = { inline: false };
-      await loadTestFiles(cliArgs, files);
+      await loadTestFiles({ inline: false }, journeys);
       const options = normalizeOptions({ ...program.opts(), ...cmdOpts });
+      if (!options.schedule) {
+        throw error(`Set default schedule in minutes for all monitors via '--schedule <time-in-minutes>' OR
+  configure via Synthetics config file under 'monitors.schedule' field.`);
+      }
+
+      if (!options.locations) {
+        throw error(`Set default location for all monitors via CLI as '--locations <locations...>' OR
+  configure via Synthetics config file under 'monitors.locations' field.`);
+      }
       const monitors = runner.buildMonitors(options);
       await push(monitors, cmdOpts);
     } catch (e) {
-      console.error(e);
+      e && console.error(e);
       process.exit(1);
     }
   });
@@ -198,7 +212,7 @@ program
       const generator = await new Generator(resolve(process.cwd(), dir));
       await generator.setup();
     } catch (e) {
-      error(e);
+      e && error(e);
       process.exit(1);
     }
   });
diff --git a/src/generator/index.ts b/src/generator/index.ts
index dabe8c4..b59cbec 100644
--- a/src/generator/index.ts
+++ b/src/generator/index.ts
@@ -27,13 +27,20 @@ import { existsSync } from 'fs';
 import { mkdir, readFile, writeFile } from 'fs/promises';
 import { bold, cyan, yellow } from 'kleur/colors';
 import { join, relative, dirname, basename } from 'path';
+import { prompt } from 'enquirer';
+import { SyntheticsLocations } from '../dsl/monitor';
 import { progress, write as stdWrite } from '../helpers';
-import { getPackageManager, runCommand } from './utils';
+import { getPackageManager, replaceTemplates, runCommand } from './utils';
 
 // Templates that are required for setting up new
 // synthetics project
 const templateDir = join(__dirname, '..', '..', 'templates');
 
+type PromptOptions = {
+  locations: string;
+  schedule: number;
+};
+
 export class Generator {
   pkgManager = 'npm';
   constructor(public projectDir: string) {}
@@ -44,13 +51,37 @@ export class Generator {
     }
   }
 
-  async files() {
+  async questions() {
+    if (process.env.TEST_QUESTIONS) {
+      return JSON.parse(process.env.TEST_QUESTIONS);
+    }
+    const question = [
+      {
+        type: 'select',
+        name: 'locations',
+        message: 'Select the default location where you want to run monitors.',
+        choices: SyntheticsLocations,
+      },
+      {
+        type: 'numeral',
+        name: 'schedule',
+        message: 'Set default schedule in minutes for all monitors',
+        initial: 10,
+      },
+    ];
+    return await prompt<PromptOptions>(question);
+  }
+
+  async files(answers: PromptOptions) {
     const fileMap = new Map<string, string>();
     // Setup Synthetics config file
     const configFile = 'synthetics.config.ts';
     fileMap.set(
       configFile,
-      await readFile(join(templateDir, configFile), 'utf-8')
+      replaceTemplates(
+        await readFile(join(templateDir, configFile), 'utf-8'),
+        answers
+      )
     );
 
     // Setup example journey file
@@ -160,8 +191,9 @@ Visit https://www.elastic.co/guide/en/observability/master/synthetics-journeys.h
 
   async setup() {
     await this.directory();
+    const answers = await this.questions();
     await this.package();
-    await this.files();
+    await this.files(answers);
     await this.patchPkgJSON();
     await this.patchGitIgnore();
     this.banner();
diff --git a/src/generator/utils.ts b/src/generator/utils.ts
index 5745dc6..8bc148c 100644
--- a/src/generator/utils.ts
+++ b/src/generator/utils.ts
@@ -39,3 +39,16 @@ export function runCommand(pkgManager: string, command: string) {
   }
   return `npm run ${command}`;
 }
+
+export function replaceTemplates(input: string, literals: Record<string, any>) {
+  for (const key in literals) {
+    const finalValue = literals[key];
+    input = input.replace(new RegExp(`'{{` + key + `}}'`, 'g'), () => {
+      if (typeof finalValue == 'number') {
+        return Number(finalValue);
+      }
+      return finalValue;
+    });
+  }
+  return input;
+}
diff --git a/src/options.ts b/src/options.ts
index 858fac0..6491606 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -27,7 +27,6 @@ import merge from 'deepmerge';
 import { readConfig } from './config';
 import { getNetworkConditions, DEFAULT_THROTTLING_OPTIONS } from './helpers';
 import type { CliArgs, RunOptions, ThrottlingOptions } from './common_types';
-import { MonitorConfig } from './dsl/monitor';
 
 /**
  * Set debug based on DEBUG ENV and -d flags
@@ -110,11 +109,14 @@ export function normalizeOptions(cliArgs: CliArgs): RunOptions {
     },
   ]);
 
-  const defaults = getDefaultMonitorConfig();
+  /**
+   * Get the default monitor config from synthetics.config.ts file
+   */
+  const monitor = config.monitor;
   if (cliArgs.throttling) {
     const throttleConfig = merge.all([
-      defaults.throttling,
-      config.monitor?.throttling || {},
+      DEFAULT_THROTTLING_OPTIONS,
+      monitor?.throttling || {},
       cliArgs.throttling as ThrottlingOptions,
     ]);
     options.throttling = throttleConfig;
@@ -126,26 +128,12 @@ export function normalizeOptions(cliArgs: CliArgs): RunOptions {
     options.throttling = {};
   }
 
-  options.locations =
-    cliArgs.locations ?? config.monitor?.locations ?? defaults.locations;
-
-  options.schedule =
-    cliArgs.schedule ?? config.monitor?.schedule ?? defaults.schedule;
+  options.schedule = cliArgs.schedule ?? monitor?.schedule;
+  options.locations = cliArgs.locations ?? monitor?.locations;
 
   return options;
 }
 
-/**
- * Get the default monitor configuration for all journeys
- */
-export function getDefaultMonitorConfig(): MonitorConfig {
-  return {
-    throttling: DEFAULT_THROTTLING_OPTIONS,
-    locations: ['us_east'],
-    schedule: 10,
-  };
-}
-
 /**
  * Parses the throttling CLI settings and also
  * adapts to the format to keep the backwards compatability
diff --git a/templates/synthetics.config.ts b/templates/synthetics.config.ts
index 93109b1..7f0f385 100644
--- a/templates/synthetics.config.ts
+++ b/templates/synthetics.config.ts
@@ -11,7 +11,10 @@ export default env => {
     /**
      * Configure global monitor settings
      */
-    monitor: {},
+    monitor: {
+      schedule: '{{schedule}}',
+      locations: ["'{{locations}}'"],
+    },
   };
   if (env !== 'development') {
     /**


Test patch:
diff --git a/__tests__/core/runner.test.ts b/__tests__/core/runner.test.ts
index 8d564a0..1f731a4 100644
--- a/__tests__/core/runner.test.ts
+++ b/__tests__/core/runner.test.ts
@@ -29,10 +29,13 @@ import Runner from '../../src/core/runner';
 import { step, journey } from '../../src/core';
 import { Journey, Step } from '../../src/dsl';
 import { Server } from '../utils/server';
-import { generateTempPath, noop } from '../../src/helpers';
+import {
+  DEFAULT_THROTTLING_OPTIONS,
+  generateTempPath,
+  noop,
+} from '../../src/helpers';
 import { wsEndpoint } from '../utils/test-config';
 import { Reporter } from '../../src/reporters';
-import { getDefaultMonitorConfig } from '../../src/options';
 import {
   JourneyEndResult,
   JourneyStartResult,
@@ -686,7 +689,7 @@ describe('runner', () => {
     runner.addJourney(j2);
 
     const monitors = runner.buildMonitors({
-      ...getDefaultMonitorConfig(),
+      throttling: DEFAULT_THROTTLING_OPTIONS,
     });
     expect(monitors.length).toBe(2);
     expect(monitors[0].config).toEqual({
@@ -700,8 +703,6 @@ describe('runner', () => {
       throttling: { download: 5, latency: 20, upload: 3 },
     });
     expect(monitors[1].config).toMatchObject({
-      locations: ['us_east'],
-      schedule: 10,
       throttling: { latency: 1000 },
     });
   });
@@ -709,6 +710,7 @@ describe('runner', () => {
   it('runner - build monitors with global config', async () => {
     runner.updateMonitor({
       schedule: 5,
+      locations: ['us_east'],
       throttling: { download: 100, upload: 50 },
       params: { env: 'test' },
       playwrightOptions: { ignoreHTTPSErrors: true },
@@ -726,7 +728,7 @@ describe('runner', () => {
     runner.addJourney(j2);
 
     const monitors = runner.buildMonitors({
-      ...getDefaultMonitorConfig(),
+      throttling: DEFAULT_THROTTLING_OPTIONS,
     });
     expect(monitors.length).toBe(2);
     expect(monitors[0].config).toEqual({
diff --git a/__tests__/generator/index.test.ts b/__tests__/generator/index.test.ts
index b00d6ea..e3f2d1a 100644
--- a/__tests__/generator/index.test.ts
+++ b/__tests__/generator/index.test.ts
@@ -42,7 +42,15 @@ describe('Generator', () => {
     });
   });
   it('generate synthetics project - NPM', async () => {
-    const cli = new CLIMock().args(['init', scaffoldDir]).run();
+    const cli = new CLIMock().args(['init', scaffoldDir]).run({
+      env: {
+        ...process.env,
+        TEST_QUESTIONS: JSON.stringify({
+          locations: 'us_east',
+          schedule: 30,
+        }),
+      },
+    });
     expect(await cli.exitCode).toBe(0);
 
     // Verify files
@@ -62,6 +70,13 @@ describe('Generator', () => {
       existsSync(join(scaffoldDir, 'journeys', 'example.journey.ts'))
     ).toBeTruthy();
     expect(existsSync(join(scaffoldDir, 'synthetics.config.ts'))).toBeTruthy();
+    // Verify schedule and locations
+    const configFile = await readFile(
+      join(scaffoldDir, 'synthetics.config.ts'),
+      'utf-8'
+    );
+    expect(configFile).toContain(`locations: ["us_east"]`);
+    expect(configFile).toContain(`schedule: 30`);
 
     // Verify stdout
     const stderr = cli.stderr();
diff --git a/__tests__/options.test.ts b/__tests__/options.test.ts
index 5d88ae3..2516fe0 100644
--- a/__tests__/options.test.ts
+++ b/__tests__/options.test.ts
@@ -75,15 +75,12 @@ describe('options', () => {
 
   it('normalize monitor configs', () => {
     expect(normalizeOptions({ throttling: false })).toMatchObject({
-      locations: ['us_east'],
-      schedule: 10,
       throttling: {},
     });
 
     expect(
       normalizeOptions({ throttling: { download: 50 }, schedule: 2 })
     ).toMatchObject({
-      locations: ['us_east'],
       schedule: 2,
       throttling: {
         download: 50,
diff --git a/__tests__/push/index.test.ts b/__tests__/push/index.test.ts
new file mode 100644
index 0000000..054413a
--- /dev/null
+++ b/__tests__/push/index.test.ts
@@ -0,0 +1,67 @@
+/**
+ * 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 { join } from 'path';
+import { CLIMock } from '../utils/test-config';
+
+const FIXTURES_DIR = join(__dirname, '..', 'fixtures');
+
+describe('Push CLI', () => {
+  const args = [
+    '--url',
+    'http://localhost:8000',
+    '--auth',
+    'foo',
+    '--project',
+    'test',
+  ];
+  const journeyFile = join(FIXTURES_DIR, 'example.journey.ts');
+  it('errors when no journey files are passed', async () => {
+    const cli = new CLIMock().args(['push', ...args]).run();
+    expect(await cli.exitCode).toBe(1);
+
+    expect(cli.stderr()).toContain(
+      `error: missing required argument 'journeys'`
+    );
+  });
+
+  it('erorr when schedule option is empty', async () => {
+    const cli = new CLIMock().args(['push', journeyFile, ...args]).run();
+    expect(await cli.exitCode).toBe(1);
+
+    expect(cli.stderr()).toContain(
+      `Set default schedule in minutes for all monitors`
+    );
+  });
+
+  it('errors when locations option is empty', async () => {
+    const cli = new CLIMock()
+      .args(['push', journeyFile, ...args, '--schedule', '20'])
+      .run();
+    expect(await cli.exitCode).toBe(1);
+
+    expect(cli.stderr()).toContain(`Set default location for all monitors`);
+  });
+});
diff --git a/__tests__/utils/test-config.ts b/__tests__/utils/test-config.ts
index 5e67f84..31f2c48 100644
--- a/__tests__/utils/test-config.ts
+++ b/__tests__/utils/test-config.ts
@@ -55,7 +55,7 @@ export class CLIMock {
   private waitForPromise: () => void;
   private cliArgs: string[] = [];
   private stdinStr?: string;
-  private stderrStr: string;
+  private stderrStr = '';
   exitCode: Promise<number>;
 
   constructor(public debug: boolean = false) {}
@@ -75,7 +75,7 @@ export class CLIMock {
     return this;
   }
 
-  run(spawnOverrides?: { cwd?: string }): CLIMock {
+  run(spawnOverrides?: { cwd?: string; env?: NodeJS.ProcessEnv }): CLIMock {
     this.process = spawn(
       'node',
       [join(__dirname, '..', '..', 'dist', 'cli.js'), ...this.cliArgs],