From 43660d51c445162aa8fa381c582b6e6a70b4b8e6 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@keylocation.sg>
Date: Mon, 14 Aug 2017 11:09:14 +0200
Subject: [PATCH] feat: validate schedule (#674)

Also enables config errors to be shown in Configure Renovate PR.

Closes #550
---
 lib/config/validation.js                      |  9 ++++
 lib/workers/branch/schedule.js                | 39 ++++++++-------
 lib/workers/repository/apis.js                |  4 ++
 .../__snapshots__/validation.spec.js.snap     |  6 ++-
 test/config/validation.spec.js                | 11 +++--
 test/workers/branch/schedule.spec.js          | 47 +++++++++++--------
 .../__snapshots__/apis.spec.js.snap           |  2 +
 test/workers/repository/apis.spec.js          |  2 +
 8 files changed, 80 insertions(+), 40 deletions(-)

diff --git a/lib/config/validation.js b/lib/config/validation.js
index d8055caeb1..6910e3e26c 100644
--- a/lib/config/validation.js
+++ b/lib/config/validation.js
@@ -1,4 +1,5 @@
 const options = require('./definitions').getOptions();
+const { hasValidSchedule } = require('../workers/branch/schedule');
 
 const optionTypes = {};
 options.forEach(option => {
@@ -42,6 +43,14 @@ function validateConfig(config) {
           depName: 'Configuration Error',
           message: `Invalid configuration option: \`${key}\``,
         });
+      } else if (key === 'schedule') {
+        const [validSchedule, errorMessage] = hasValidSchedule(val);
+        if (!validSchedule) {
+          errors.push({
+            depName: 'Configuration Error',
+            message: `Invalid schedule: \`${errorMessage}\``,
+          });
+        }
       } else if (val != null) {
         const type = optionTypes[key];
         if (type === 'boolean') {
diff --git a/lib/workers/branch/schedule.js b/lib/workers/branch/schedule.js
index e5d2478227..a4e96ed2aa 100644
--- a/lib/workers/branch/schedule.js
+++ b/lib/workers/branch/schedule.js
@@ -10,29 +10,25 @@ function fixShortHours(input) {
   return input.replace(/( \d?\d)((a|p)m)/g, '$1:00$2');
 }
 
-function hasValidSchedule(schedule, logger) {
+function hasValidSchedule(schedule) {
   if (!Array.isArray(schedule)) {
-    logger.debug({ schedule }, `Invalid schedule`);
-    return false;
-  }
-  if (schedule.length === 0) {
-    logger.debug('No schedule defined');
-    return false;
-  }
-  if (schedule.length === 1 && schedule[0] === '') {
-    logger.debug('Empty schedule');
-    return false;
+    return [false, 'Invalid schedule'];
   }
+  let message;
   // check if any of the schedules fail to parse
   const hasFailedSchedules = schedule.some(scheduleText => {
     const parsedSchedule = later.parse.text(fixShortHours(scheduleText));
     if (parsedSchedule.error !== -1) {
-      logger.debug(`Failed to parse schedule ${scheduleText}`);
+      message = `Failed to parse schedule ${scheduleText}`;
       // It failed to parse
       return true;
     }
+    if (parsedSchedule.schedules.some(s => s.m)) {
+      message = 'Schedules should not specify minutes';
+      return true;
+    }
     if (!parsedSchedule.schedules.some(s => s.d || s.t_a || s.t_b)) {
-      logger.debug('Schedule has no days of week or time of day');
+      message = 'Schedule has no days of week or time of day';
       return true;
     }
     // It must be OK
@@ -40,15 +36,24 @@ function hasValidSchedule(schedule, logger) {
   });
   if (hasFailedSchedules) {
     // If any fail then we invalidate the whole thing
-    return false;
+    return [false, message];
   }
-  return true;
+  return [true];
 }
 
 function isScheduledNow(config) {
   config.logger.debug({ schedule: config.schedule }, `Checking schedule`);
-  if (!module.exports.hasValidSchedule(config.schedule, config.logger)) {
-    // Return true if the schedule is invalid
+  if (
+    !config.schedule ||
+    config.schedule.length === 0 ||
+    config.schedule[0] === ''
+  ) {
+    config.logger.debug('No schedule defined');
+    return true;
+  }
+  const [validSchedule, errorMessage] = hasValidSchedule(config.schedule);
+  if (!validSchedule) {
+    config.logger.error(errorMessage);
     return true;
   }
   let now = moment();
diff --git a/lib/workers/repository/apis.js b/lib/workers/repository/apis.js
index a90bb92fc5..3976b7c63f 100644
--- a/lib/workers/repository/apis.js
+++ b/lib/workers/repository/apis.js
@@ -124,6 +124,10 @@ function migrateAndValidate(config, input) {
       );
     }); */
   }
+  if (!config.repoIsOnboarded) {
+    migratedConfig.warnings = (migratedConfig.warnings || []).concat(warnings);
+    migratedConfig.errors = (migratedConfig.errors || []).concat(errors);
+  }
   return migratedConfig;
 }
 
diff --git a/test/config/__snapshots__/validation.spec.js.snap b/test/config/__snapshots__/validation.spec.js.snap
index 17be67ac69..0221af410d 100644
--- a/test/config/__snapshots__/validation.spec.js.snap
+++ b/test/config/__snapshots__/validation.spec.js.snap
@@ -8,7 +8,11 @@ Array [
   },
   Object {
     "depName": "Configuration Error",
-    "message": "Configuration option \`schedule\` should be a list (Array)",
+    "message": "Invalid schedule: \`Schedules should not specify minutes\`",
+  },
+  Object {
+    "depName": "Configuration Error",
+    "message": "Configuration option \`labels\` should be a list (Array)",
   },
   Object {
     "depName": "Configuration Error",
diff --git a/test/config/validation.spec.js b/test/config/validation.spec.js
index 8b51001ae4..deef5300b4 100644
--- a/test/config/validation.spec.js
+++ b/test/config/validation.spec.js
@@ -1,4 +1,5 @@
 const configValidation = require('../../lib/config/validation.js');
+const logger = require('../_fixtures/logger');
 
 describe('config/validation', () => {
   describe('validateConfig(config)', () => {
@@ -19,7 +20,8 @@ describe('config/validation', () => {
     it('errors for all types', () => {
       const config = {
         enabled: 1,
-        schedule: 5,
+        schedule: ['every 15 mins every weekday'],
+        labels: 5,
         semanticPrefix: 7,
         githubAppId: 'none',
         lockFileMaintenance: false,
@@ -29,9 +31,12 @@ describe('config/validation', () => {
           },
         ],
       };
-      const { warnings, errors } = configValidation.validateConfig(config);
+      const { warnings, errors } = configValidation.validateConfig(
+        config,
+        logger
+      );
       expect(warnings).toHaveLength(0);
-      expect(errors).toHaveLength(6);
+      expect(errors).toHaveLength(7);
       expect(errors).toMatchSnapshot();
     });
   });
diff --git a/test/workers/branch/schedule.spec.js b/test/workers/branch/schedule.spec.js
index 98789f23c3..ad668d7fd9 100644
--- a/test/workers/branch/schedule.spec.js
+++ b/test/workers/branch/schedule.spec.js
@@ -8,44 +8,48 @@ describe('workers/branch/schedule', () => {
       jest.resetAllMocks();
     });
     it('returns false if schedule is not an array', () => {
-      expect(schedule.hasValidSchedule({ a: 1 }, logger)).toBe(false);
-    });
-    it('returns false if schedule array is empty', () => {
-      expect(schedule.hasValidSchedule([], logger)).toBe(false);
-    });
-    it('returns false if only schedule is empty', () => {
-      expect(schedule.hasValidSchedule([''], logger)).toBe(false);
+      expect(schedule.hasValidSchedule({ a: 1 }, logger)[0]).toBe(false);
     });
     it('returns false for invalid schedule', () => {
-      expect(schedule.hasValidSchedule(['foo'], logger)).toBe(false);
+      expect(schedule.hasValidSchedule(['foo'], logger)[0]).toBe(false);
     });
     it('returns false if any schedule fails to parse', () => {
-      expect(schedule.hasValidSchedule(['after 5:00pm', 'foo'], logger)).toBe(
-        false
-      );
+      expect(
+        schedule.hasValidSchedule(['after 5:00pm', 'foo'], logger)[0]
+      ).toBe(false);
+    });
+    it('returns false if using minutes', () => {
+      expect(
+        schedule.hasValidSchedule(['every 15 mins every weekday'], logger)[0]
+      ).toBe(false);
     });
     it('returns false if schedules have no days or time range', () => {
-      expect(schedule.hasValidSchedule(['at 5:00pm'], logger)).toBe(false);
+      expect(schedule.hasValidSchedule(['at 5:00pm'], logger)[0]).toBe(false);
     });
     it('returns false if any schedule has no days or time range', () => {
       expect(
-        schedule.hasValidSchedule(['at 5:00pm', 'on saturday'], logger)
+        schedule.hasValidSchedule(['at 5:00pm', 'on saturday'], logger)[0]
       ).toBe(false);
     });
     it('returns true if schedule has days of week', () => {
       expect(
-        schedule.hasValidSchedule(['on friday and saturday'], logger)
+        schedule.hasValidSchedule(['on friday and saturday'], logger)[0]
       ).toBe(true);
     });
     it('returns true if schedule has a start time', () => {
-      expect(schedule.hasValidSchedule(['after 8:00pm'], logger)).toBe(true);
+      expect(schedule.hasValidSchedule(['after 8:00pm'], logger)[0]).toBe(true);
     });
     it('returns true if schedule has an end time', () => {
-      expect(schedule.hasValidSchedule(['before 6:00am'], logger)).toBe(true);
+      expect(schedule.hasValidSchedule(['before 6:00am'], logger)[0]).toBe(
+        true
+      );
     });
     it('returns true if schedule has a start and end time', () => {
       expect(
-        schedule.hasValidSchedule(['after 11:00pm and before 6:00am'], logger)
+        schedule.hasValidSchedule(
+          ['after 11:00pm and before 6:00am'],
+          logger
+        )[0]
       ).toBe(true);
     });
     it('returns true if schedule has days and a start and end time', () => {
@@ -53,11 +57,11 @@ describe('workers/branch/schedule', () => {
         schedule.hasValidSchedule(
           ['after 11:00pm and before 6:00am every weekday'],
           logger
-        )
+        )[0]
       ).toBe(true);
     });
     it('supports hours shorthand', () => {
-      const res = schedule.hasValidSchedule(
+      const [res] = schedule.hasValidSchedule(
         [
           'after 11pm and before 6am every weekend',
           'after 11pm',
@@ -86,6 +90,11 @@ describe('workers/branch/schedule', () => {
       const res = schedule.isScheduledNow(config);
       expect(res).toBe(true);
     });
+    it('returns true if invalid schedule', () => {
+      config.schedule = ['every 15 minutes'];
+      const res = schedule.isScheduledNow(config);
+      expect(res).toBe(true);
+    });
     it('supports before hours true', () => {
       config.schedule = ['before 4:00pm'];
       const res = schedule.isScheduledNow(config);
diff --git a/test/workers/repository/__snapshots__/apis.spec.js.snap b/test/workers/repository/__snapshots__/apis.spec.js.snap
index 26408404c3..7f17464e1f 100644
--- a/test/workers/repository/__snapshots__/apis.spec.js.snap
+++ b/test/workers/repository/__snapshots__/apis.spec.js.snap
@@ -78,9 +78,11 @@ exports[`workers/repository/apis resolvePackageFiles includes files with content
 Array [
   Object {
     "content": Object {},
+    "errors": Array [],
     "hasPackageLock": true,
     "hasYarnLock": true,
     "packageFile": "package.json",
+    "warnings": Array [],
   },
   Object {
     "content": Object {},
diff --git a/test/workers/repository/apis.spec.js b/test/workers/repository/apis.spec.js
index 48e42687de..adcbdae562 100644
--- a/test/workers/repository/apis.spec.js
+++ b/test/workers/repository/apis.spec.js
@@ -159,6 +159,7 @@ describe('workers/repository/apis', () => {
       expect(returnConfig.errors).toHaveLength(0);
     });
     it('returns warning + error plus extended config if unknown keys', async () => {
+      config.repoIsOnboarded = true;
       config.api.getFileContent.mockReturnValueOnce(
         '{ "enabled": true, "foo": false, "maintainYarnLock": true, "schedule": "before 5am", "minor": {} }'
       );
@@ -169,6 +170,7 @@ describe('workers/repository/apis', () => {
       expect(returnConfig.errors).toMatchSnapshot();
     });
     it('returns error plus extended config if duplicate keys', async () => {
+      config.repoIsOnboarded = true;
       config.api.getFileContent.mockReturnValueOnce(
         '{ "enabled": true, "enabled": false }'
       );
-- 
GitLab