diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e9e23fa395aaaa154366e5646233f133397865ae..f3eaa245343ebbb5a3a0c0dc89c2ad44f0cd17ab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -404,7 +404,7 @@ jobs: - name: Check coverage threshold run: | pnpm nyc check-coverage -t ./coverage/nyc \ - --branches 99.57 \ + --branches 99.61 \ --functions 100 \ --lines 100 \ --statements 100 diff --git a/lib/config/migration.spec.ts b/lib/config/migration.spec.ts index 81beb430f6695066b59a49e20bb2563d1d3765d8..8f3a907702fa2373803a50a1e838ee26ebffd84d 100644 --- a/lib/config/migration.spec.ts +++ b/lib/config/migration.spec.ts @@ -669,9 +669,6 @@ describe('config/migration', () => { it('it migrates gradle-lite', () => { const config: RenovateConfig = { - gradle: { - enabled: false, - }, 'gradle-lite': { enabled: true, fileMatch: ['foo'], diff --git a/lib/config/migrations/custom/package-rules-migration.ts b/lib/config/migrations/custom/package-rules-migration.ts index a1e0712ea470dc8ffc39de3c8bef257e8394d4c3..3e6450956a9b7018ed0a3a9b86f929138bfa270e 100644 --- a/lib/config/migrations/custom/package-rules-migration.ts +++ b/lib/config/migrations/custom/package-rules-migration.ts @@ -1,3 +1,4 @@ +import is from '@sindresorhus/is'; import type { PackageRule } from '../../types'; import { AbstractMigration } from '../base/abstract-migration'; @@ -30,11 +31,11 @@ export class PackageRulesMigration extends AbstractMigration { override readonly propertyName = 'packageRules'; override run(value: unknown): void { - let packageRules = (this.get('packageRules') as PackageRule[]) ?? []; - packageRules = Array.isArray(packageRules) ? [...packageRules] : []; + let packageRules = this.get('packageRules') as PackageRule[]; + if (is.nonEmptyArray(packageRules)) { + packageRules = packageRules.map(renameKeys); - packageRules = packageRules.map(renameKeys); - - this.rewrite(packageRules); + this.rewrite(packageRules); + } } } diff --git a/lib/modules/datasource/datasource.spec.ts b/lib/modules/datasource/datasource.spec.ts index bf74641b1c7b8f423c58df4048e9fc3726db8011..91b7b0d9d711622d54f3283fed8be979413ca84f 100644 --- a/lib/modules/datasource/datasource.spec.ts +++ b/lib/modules/datasource/datasource.spec.ts @@ -33,4 +33,14 @@ describe('modules/datasource/datasource', () => { testDatasource.getReleases(partial<GetReleasesConfig>()) ).rejects.toThrow(EXTERNAL_HOST_ERROR); }); + + it('should throw on statusCode >=500 && <600', async () => { + const testDatasource = new TestDatasource(); + + httpMock.scope(exampleUrl).get('/').reply(504); + + await expect( + testDatasource.getReleases(partial<GetReleasesConfig>()) + ).rejects.toThrow(EXTERNAL_HOST_ERROR); + }); }); diff --git a/lib/modules/datasource/datasource.ts b/lib/modules/datasource/datasource.ts index 0a427cefc6ce6e9bb0b4cb4507f487dd1f542223..343ca34ddcbe59d898e030f2b86dd60189dc8dfb 100644 --- a/lib/modules/datasource/datasource.ts +++ b/lib/modules/datasource/datasource.ts @@ -46,11 +46,7 @@ export abstract class Datasource implements DatasourceApi { const statusCode = err.response?.statusCode; if (statusCode) { - if (statusCode === 429) { - throw new ExternalHostError(err); - } - - if (statusCode >= 500 && statusCode < 600) { + if (statusCode === 429 || (statusCode >= 500 && statusCode < 600)) { throw new ExternalHostError(err); } } diff --git a/lib/modules/datasource/index.spec.ts b/lib/modules/datasource/index.spec.ts index 4646a4e53f8c74c2816928632fdb2417919416d5..28c673dc5d0a9d98b71ab8bbbe050810141ce70e 100644 --- a/lib/modules/datasource/index.spec.ts +++ b/lib/modules/datasource/index.spec.ts @@ -94,6 +94,28 @@ class DummyDatasource3 extends Datasource { } } +class DummyDatasource4 extends DummyDatasource3 { + override defaultRegistryUrls = undefined as never; +} + +class DummyDatasource5 extends Datasource { + override registryStrategy = undefined as never; + + constructor(private registriesMock: RegistriesMock = defaultRegistriesMock) { + super(datasource); + } + + override getReleases({ + registryUrl, + }: GetReleasesConfig): Promise<ReleaseResult | null> { + const fn = this.registriesMock[registryUrl!]; + if (typeof fn === 'function') { + return Promise.resolve(fn()); + } + return Promise.resolve(fn ?? null); + } +} + jest.mock('./metadata-manual', () => ({ manualChangelogUrls: { dummy: { @@ -313,6 +335,16 @@ describe('modules/datasource/index', () => { }); }); + // for coverage + it('undefined defaultRegistryUrls with customRegistrySupport works', async () => { + datasources.set(datasource, new DummyDatasource4()); + const res = await getPkgReleases({ + datasource, + packageName, + }); + expect(res).toBeNull(); + }); + it('applies extractVersion', async () => { const registries: RegistriesMock = { 'https://reg1.com': { @@ -454,6 +486,7 @@ describe('modules/datasource/index', () => { describe('merge', () => { class MergeRegistriesDatasource extends DummyDatasource { override readonly registryStrategy = 'merge'; + override caching = true; override readonly defaultRegistryUrls = [ 'https://reg1.com', 'https://reg2.com', @@ -472,6 +505,10 @@ describe('modules/datasource/index', () => { 'https://reg5.com': () => { throw new Error('b'); }, + // for coverage + 'https://reg6.com': null, + // has the same result as reg1 url, to test de-deplication of releases + 'https://reg7.com': () => ({ releases: [{ version: '1.0.0' }] }), }; beforeEach(() => { @@ -492,7 +529,7 @@ describe('modules/datasource/index', () => { }); }); - it('ignores custom defaultRegistryUrls if registrUrls are set', async () => { + it('ignores custom defaultRegistryUrls if registryUrls are set', async () => { const res = await getPkgReleases({ datasource, packageName, @@ -522,6 +559,20 @@ describe('modules/datasource/index', () => { }); }); + it('filters out duplicate releases', async () => { + const res = await getPkgReleases({ + datasource, + packageName, + registryUrls: ['https://reg1.com', 'https://reg7.com'], + }); + expect(res).toMatchObject({ + releases: [ + { registryUrl: 'https://reg1.com', version: '1.0.0' }, + // { registryUrl: 'https://reg2.com', version: '1.0.0' }, + ], + }); + }); + it('merges registries and aborts on ExternalHostError', async () => { await expect( getPkgReleases({ @@ -614,11 +665,20 @@ describe('modules/datasource/index', () => { it('returns null if no releases are found', async () => { const registries: RegistriesMock = { 'https://reg1.com': () => { - throw new Error('a'); + throw { statusCode: '404' }; }, 'https://reg2.com': () => { + throw { statusCode: '401' }; + }, + 'https://reg3.com': () => { + throw { statusCode: '403' }; + }, + 'https://reg4.com': () => { throw new Error('b'); }, + 'https://reg5.com': () => { + throw { code: '403' }; + }, }; const registryUrls = Object.keys(registries); datasources.set(datasource, new HuntRegistriyDatasource(registries)); @@ -631,6 +691,31 @@ describe('modules/datasource/index', () => { expect(res).toBeNull(); }); + + it('defaults to hunt strategy', async () => { + const registries: RegistriesMock = { + 'https://reg1.com': null, + 'https://reg2.com': () => { + throw new Error('unknown'); + }, + 'https://reg3.com': { releases: [{ version: '1.0.0' }] }, + 'https://reg4.com': { releases: [{ version: '2.0.0' }] }, + 'https://reg5.com': { releases: [{ version: '3.0.0' }] }, + }; + const registryUrls = Object.keys(registries); + datasources.set(datasource, new DummyDatasource5(registries)); + + const res = await getPkgReleases({ + datasource, + packageName, + registryUrls, + }); + + expect(res).toMatchObject({ + registryUrl: 'https://reg3.com', + releases: [{ version: '1.0.0' }], + }); + }); }); describe('relaseConstraintFiltering', () => { diff --git a/lib/modules/datasource/metadata.spec.ts b/lib/modules/datasource/metadata.spec.ts index da55b5bd5f1ddd0954959c46cd814a54c115ac36..36661f2490078e4012cb8ffcc8f87cafd5bcc43e 100644 --- a/lib/modules/datasource/metadata.spec.ts +++ b/lib/modules/datasource/metadata.spec.ts @@ -1,9 +1,11 @@ +import { partial } from '../../../test/util'; import { HelmDatasource } from './helm'; import { MavenDatasource } from './maven'; import { addMetaData, massageGithubUrl, massageUrl, + normalizeDate, shouldDeleteHomepage, } from './metadata'; import { NpmDatasource } from './npm'; @@ -502,4 +504,38 @@ describe('modules/datasource/metadata', () => { expect(shouldDeleteHomepage(sourceUrl, homepage)).toBe(expected); } ); + + // for coverage + it('should handle dep with no releases', () => { + const dep = partial<ReleaseResult>({}); + + const datasource = PypiDatasource.id; + const packageName = 'django'; + + addMetaData(dep, datasource, packageName); + expect(dep).toEqual({ + changelogUrl: + 'https://github.com/django/django/tree/master/docs/releases', + sourceUrl: 'https://github.com/django/django', + }); + }); + + describe('normalizeDate()', () => { + it('works for number input', () => { + const now = Date.now(); + expect(normalizeDate(now)).toBe(new Date(now).toISOString()); + }); + + it('works for string input', () => { + expect(normalizeDate('2021-01-01')).toBe( + new Date('2021-01-01').toISOString() + ); + }); + + it('works for Date instance', () => { + expect(normalizeDate(new Date('2021-01-01'))).toBe( + new Date('2021-01-01').toISOString() + ); + }); + }); });