Skip to content
Snippets Groups Projects
Select Git revision
  • 531daff3dc2aa8dd2dc909414c181f94d14374b7
  • main default protected
  • revert-31645-feat/rename-gradle-wrapper-validation-action
  • next
  • renovate/main-redis-5.x
  • fix/36615b-branch-reuse-no-cache
  • chore/punycode
  • fix/36615-branch-reuse-bug
  • refactor/pin-new-value
  • feat/36219--git-x509-signing
  • feat/structured-logger
  • hotfix/39.264.1
  • feat/skip-dangling
  • gh-readonly-queue/next/pr-36034-7a061c4ca1024a19e2c295d773d9642625d1c2be
  • hotfix/39.238.3
  • refactor/gitlab-auto-approve
  • feat/template-strings
  • gh-readonly-queue/next/pr-35654-137d934242c784e0c45d4b957362214f0eade1d7
  • fix/32307-global-extends-merging
  • fix/32307-global-extends-repositories
  • gh-readonly-queue/next/pr-35009-046ebf7cb84ab859f7fefceb5fa53a54ce9736f8
  • 41.21.3
  • 41.21.2
  • 41.21.1
  • 41.21.0
  • 41.20.2
  • 41.20.1
  • 41.20.0
  • 41.19.0
  • 41.18.4
  • 41.18.3
  • 41.18.2
  • 41.18.1
  • 41.18.0
  • 41.17.2
  • 41.17.1
  • 41.17.0
  • 41.16.3
  • 41.16.2
  • 41.16.1
  • 41.16.0
41 results

validation.spec.ts

Blame
  • validation.spec.ts 20.45 KiB
    import type { RenovateConfig } from './types';
    import * as configValidation from './validation';
    
    describe('config/validation', () => {
      describe('getParentName()', () => {
        it('ignores encrypted in root', () => {
          expect(configValidation.getParentName('encrypted')).toBeEmptyString();
        });
        it('handles array types', () => {
          expect(configValidation.getParentName('hostRules[1]')).toBe('hostRules');
        });
        it('handles encrypted within array types', () => {
          expect(configValidation.getParentName('hostRules[0].encrypted')).toBe(
            'hostRules'
          );
        });
      });
      describe('validateConfig(config)', () => {
        it('returns deprecation warnings', async () => {
          const config = {
            prTitle: 'something',
          };
          const { warnings } = await configValidation.validateConfig(config);
          expect(warnings).toHaveLength(1);
          expect(warnings).toMatchSnapshot();
        });
        it('catches invalid templates', async () => {
          const config = {
            commitMessage: '{{{something}}',
          };
          const { errors } = await configValidation.validateConfig(config);
          expect(errors).toHaveLength(1);
          expect(errors).toMatchSnapshot();
        });
        it('catches invalid allowedVersions regex', async () => {
          const config = {
            packageRules: [
              {
                matchPackageNames: ['foo'],
                allowedVersions: '/^2/',
              },
              {
                matchPackageNames: ['bar'],
                allowedVersions: '/***$}{]][/',
              },
              {
                matchPackageNames: ['baz'],
                allowedVersions: '!/^2/',
              },
              {
                matchPackageNames: ['quack'],
                allowedVersions: '!/***$}{]][/',
              },
            ],
          };
          const { errors } = await configValidation.validateConfig(config);
          expect(errors).toHaveLength(2);
          expect(errors).toMatchSnapshot();
        });
        it('catches invalid matchCurrentVersion regex', async () => {
          const config = {
            packageRules: [
              {
                matchPackageNames: ['foo'],
                matchCurrentVersion: '/^2/',
                enabled: true,
              },
              {
                matchPackageNames: ['bar'],
                matchCurrentVersion: '/***$}{]][/',
                enabled: true,
              },
              {
                matchPackageNames: ['baz'],
                matchCurrentVersion: '!/^2/',
                enabled: true,
              },
              {
                matchPackageNames: ['quack'],
                matchCurrentVersion: '!/***$}{]][/',
                enabled: true,
              },
            ],
          };
          const { errors } = await configValidation.validateConfig(config);
          expect(errors).toHaveLength(2);
          expect(errors).toMatchSnapshot();
        });
        it('returns nested errors', async () => {
          const config: RenovateConfig = {
            foo: 1,
            schedule: ['after 5pm'],
            timezone: 'Asia/Singapore',
            packageRules: [
              {
                matchPackagePatterns: ['*'],
                excludePackagePatterns: ['abc ([a-z]+) ([a-z]+))'],
                enabled: true,
              },
            ],
            lockFileMaintenance: {
              bar: 2,
            },
            major: null,
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(3);
          expect(errors).toMatchSnapshot();
        });
        it('included unsupported manager', async () => {
          const config = {
            packageRules: [
              {
                matchManagers: ['foo'],
                enabled: true,
              },
            ],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(1);
          expect(errors[0].message).toContain('ansible');
        });
        it('included managers of the wrong type', async () => {
          const config = {
            packageRules: [
              {
                matchManagers: 'string not an array',
                enabled: true,
              },
            ],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(2);
          expect(errors).toMatchSnapshot();
        });
    
        it.each([
          ['empty configuration', {}],
          ['configuration with enabledManagers empty', { enabledManagers: [] }],
          ['single enabled manager', { enabledManagers: ['npm'] }],
          [
            'multiple enabled managers',
            { enabledManagers: ['npm', 'gradle', 'maven'] },
          ],
        ])('validates enabled managers for %s', async (_case, config) => {
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(0);
        });
    
        it.each([
          ['single not supported manager', { enabledManagers: ['foo'] }],
          ['multiple not supported managers', { enabledManagers: ['foo', 'bar'] }],
          [
            'combined supported and not supported managers',
            { enabledManagers: ['foo', 'npm', 'gradle', 'maven'] },
          ],
        ])(
          'errors if included not supported enabled managers for %s',
          async (_case, config) => {
            const { warnings, errors } = await configValidation.validateConfig(
              config
            );
            expect(warnings).toHaveLength(0);
            expect(errors).toHaveLength(1);
            expect(errors).toMatchSnapshot();
          }
        );
        it('errors for all types', async () => {
          const config: RenovateConfig = {
            allowedVersions: 'foo',
            enabled: 1 as any,
            enabledManagers: ['npm'],
            schedule: ['every 15 mins every weekday'],
            timezone: 'Asia',
            labels: 5 as any,
            semanticCommitType: 7 as any,
            lockFileMaintenance: false as any,
            extends: [':timezone(Europe/Brussel)'],
            packageRules: [
              {
                excludePackageNames: ['foo'],
                enabled: true,
              },
              {
                foo: 1,
              },
              'what?' as any,
              {
                matchPackagePatterns: 'abc ([a-z]+) ([a-z]+))',
                excludePackagePatterns: ['abc ([a-z]+) ([a-z]+))'],
                enabled: false,
              },
            ],
            major: null,
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(warnings).toHaveLength(1);
          expect(errors).toMatchSnapshot();
          expect(errors).toHaveLength(12);
        });
        it('selectors outside packageRules array trigger errors', async () => {
          const config = {
            matchPackageNames: ['angular'],
            meteor: {
              packageRules: [
                {
                  matchPackageNames: ['meteor'],
                  enabled: true,
                },
              ],
            },
            docker: {
              minor: {
                matchPackageNames: ['testPackage'],
              },
            },
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(warnings).toHaveLength(2);
          expect(errors).toMatchSnapshot();
          expect(errors).toHaveLength(2);
        });
        it('ignore packageRule nesting validation for presets', async () => {
          const config = {
            description: ['All angular.js packages'],
            matchPackageNames: [
              'angular',
              'angular-animate',
              'angular-scroll',
              'angular-sanitize',
            ],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config,
            true
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toMatchSnapshot();
          expect(errors).toHaveLength(0);
        });
    
        it('errors for unsafe fileMatches', async () => {
          const config = {
            npm: {
              fileMatch: ['abc ([a-z]+) ([a-z]+))'],
            },
            dockerfile: {
              fileMatch: ['x?+'],
            },
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(2);
          expect(errors).toMatchSnapshot();
        });
    
        it('validates regEx for each fileMatch', async () => {
          const config = {
            regexManagers: [
              {
                fileMatch: ['js', '***$}{]]['],
                matchStrings: ['^(?<depName>foo)(?<currentValue>bar)$'],
                datasourceTemplate: 'maven',
                versioningTemplate: 'gradle',
              },
            ],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config,
            true
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(1);
          expect(errors).toMatchSnapshot();
        });
        it('errors if no regexManager matchStrings', async () => {
          const config = {
            regexManagers: [
              {
                fileMatch: [],
              },
            ],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config as any,
            true
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(1);
          expect(errors).toMatchInlineSnapshot(`
            Array [
              Object {
                "message": "Each Regex Manager must contain a non-empty fileMatch array",
                "topic": "Configuration Error",
              },
            ]
          `);
        });
        it('errors if empty regexManager matchStrings', async () => {
          const config = {
            regexManagers: [
              {
                fileMatch: ['foo'],
                matchStrings: [],
              },
              {
                fileMatch: ['foo'],
              },
            ],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config as RenovateConfig,
            true
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(2);
          expect(errors).toMatchInlineSnapshot(`
            Array [
              Object {
                "message": "Each Regex Manager must contain a non-empty matchStrings array",
                "topic": "Configuration Error",
              },
              Object {
                "message": "Each Regex Manager must contain a non-empty matchStrings array",
                "topic": "Configuration Error",
              },
            ]
          `);
        });
        it('errors if no regexManager fileMatch', async () => {
          const config = {
            regexManagers: [
              {
                matchStrings: ['^(?<depName>foo)(?<currentValue>bar)$'],
                datasourceTemplate: 'maven',
                versioningTemplate: 'gradle',
              },
            ],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config as any,
            true
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(1);
        });
        it('validates regEx for each matchStrings', async () => {
          const config = {
            regexManagers: [
              {
                fileMatch: ['Dockerfile'],
                matchStrings: ['***$}{]]['],
              },
            ],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config,
            true
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(1);
        });
        it('passes if regexManager fields are present', async () => {
          const config = {
            regexManagers: [
              {
                fileMatch: ['Dockerfile'],
                matchStrings: ['ENV (?<currentValue>.*?)\\s'],
                depNameTemplate: 'foo',
                datasourceTemplate: 'bar',
                registryUrlTemplate: 'foobar',
                extractVersionTemplate: '^(?<version>v\\d+\\.\\d+)',
                depTypeTemplate: 'apple',
              },
            ],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config,
            true
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(0);
        });
        it('errors if extra regexManager fields are present', async () => {
          const config = {
            regexManagers: [
              {
                fileMatch: ['Dockerfile'],
                matchStrings: ['ENV (?<currentValue>.*?)\\s'],
                depNameTemplate: 'foo',
                datasourceTemplate: 'bar',
                depTypeTemplate: 'apple',
                automerge: true,
              },
            ],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config,
            true
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(1);
        });
        it('errors if regexManager fields are missing', async () => {
          const config = {
            regexManagers: [
              {
                fileMatch: ['Dockerfile'],
                matchStrings: ['ENV (.*?)\\s'],
                depNameTemplate: 'foo',
                datasourceTemplate: 'bar',
              },
            ],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config,
            true
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toMatchSnapshot();
          expect(errors).toHaveLength(1);
        });
        it('ignore keys', async () => {
          const config = {
            $schema: 'renovate.json',
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config,
            true
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(0);
        });
    
        it('validates timezone preset', async () => {
          const config = {
            extends: [':timezone', ':timezone(Europe/Berlin)'],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config,
            true
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(0);
        });
    
        it('does not validate constraints children', async () => {
          const config = {
            constraints: { packageRules: [{}] },
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config,
            true
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(0);
        });
    
        it('validates object with ignored children', async () => {
          const config = {
            prBodyDefinitions: {},
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config,
            true
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(0);
        });
    
        it('validates valid alias objects', async () => {
          const config = {
            aliases: {
              example1: 'http://www.example.com',
              example2: 'https://www.example2.com/example',
            },
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(0);
        });
    
        it('errors if aliases depth is more than 1', async () => {
          const config = {
            aliases: {
              sample: {
                example1: 'http://www.example.com',
              },
            },
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toMatchObject([
            {
              message:
                'Invalid `aliases.aliases.sample` configuration: value is not a url',
              topic: 'Configuration Error',
            },
          ]);
        });
    
        it('errors if aliases have invalid url', async () => {
          const config = {
            aliases: {
              example1: 'noturl',
              example2: 'http://www.example.com',
            },
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toMatchObject([
            {
              message:
                'Invalid `aliases.aliases.example1` configuration: value is not a url',
              topic: 'Configuration Error',
            },
          ]);
        });
    
        it('errors if fileMatch has wrong parent', async () => {
          const config = {
            fileMatch: ['foo'],
            npm: {
              fileMatch: ['package\\.json'],
              minor: {
                fileMatch: ['bar'],
              },
            },
            regexManagers: [
              {
                fileMatch: ['build.gradle'],
                matchStrings: ['^(?<depName>foo)(?<currentValue>bar)$'],
                datasourceTemplate: 'maven',
                versioningTemplate: 'gradle',
              },
            ],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(errors).toHaveLength(1);
          expect(warnings).toHaveLength(1);
          expect(errors).toMatchSnapshot();
          expect(warnings).toMatchSnapshot();
        });
    
        it('errors if language or manager objects are nested', async () => {
          const config = {
            python: {
              enabled: false,
            },
            java: {
              gradle: {
                enabled: false,
              },
            },
            major: {
              minor: {
                docker: {
                  automerge: true,
                },
              },
            },
          } as never;
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(errors).toHaveLength(2);
          expect(warnings).toHaveLength(0);
          expect(errors).toMatchSnapshot();
        });
    
        it('warns if hostType has the wrong parent', async () => {
          const config = {
            hostType: 'npm',
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(errors).toHaveLength(0);
          expect(warnings).toHaveLength(1);
          expect(warnings).toMatchSnapshot();
        });
    
        it('validates preset values', async () => {
          const config = {
            extends: ['foo', 'bar', 42] as never,
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config,
            true
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(1);
        });
    
        it('warns if only selectors in packageRules', async () => {
          const config = {
            packageRules: [
              { matchDepTypes: ['foo'], excludePackageNames: ['bar'] },
            ],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config,
            true
          );
          expect(warnings).toHaveLength(1);
          expect(warnings).toMatchSnapshot();
          expect(errors).toHaveLength(0);
        });
        it('errors if invalid combinations in packageRules', async () => {
          const config = {
            packageRules: [
              {
                matchUpdateTypes: ['major'],
                registryUrls: ['https://registry.npmjs.org'],
              },
            ],
          } as any;
          const { warnings, errors } = await configValidation.validateConfig(
            config,
            true
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(1);
          expect(errors).toMatchSnapshot();
        });
        it('warns on nested group packageRules', async () => {
          const config = {
            extends: ['group:fortawesome'],
            packageRules: [
              {
                automerge: true,
                extends: ['group:fortawesome'],
              },
            ],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config,
            true
          );
          expect(errors).toHaveLength(0);
          expect(warnings).toHaveLength(1);
        });
    
        it('validates valid customEnvVariables objects', async () => {
          const config = {
            customEnvVariables: {
              example1: 'abc',
              example2: 'https://www.example2.com/example',
            },
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toHaveLength(0);
        });
        it('errors on invalid customEnvVariables objects', async () => {
          const config = {
            customEnvVariables: {
              example1: 'abc',
              example2: 123,
            },
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toMatchObject([
            {
              message:
                'Invalid `customEnvVariables.customEnvVariables.example2` configuration: value is not a string',
              topic: 'Configuration Error',
            },
          ]);
        });
    
        it('errors if schedule is cron and has no * minutes', async () => {
          const config = {
            schedule: ['30 5 * * *'],
          };
          const { warnings, errors } = await configValidation.validateConfig(
            config
          );
          expect(warnings).toHaveLength(0);
          expect(errors).toMatchObject([
            {
              message:
                'Invalid schedule: `Invalid schedule: "30 5 * * *" has cron syntax, but doesn\'t have * as minutes`',
              topic: 'Configuration Error',
            },
          ]);
        });
      });
    });