diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e2b714c7aeb12ec65aa30e46289eeea9cadc8800..276e52ae1eefdcce73bd5468c33dd5a3ff2295ef 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.61 \ + --branches 100 \ --functions 100 \ --lines 100 \ --statements 100 diff --git a/lib/modules/manager/maven/extract.ts b/lib/modules/manager/maven/extract.ts index 1c9c3a6ee3ba03f119bfc8b1a0ccebb9a2f9071c..5ab8652717bca5a025231dca01cead49fc5ebf25 100644 --- a/lib/modules/manager/maven/extract.ts +++ b/lib/modules/manager/maven/extract.ts @@ -39,7 +39,7 @@ function containsPlaceholder(str: string | null | undefined): boolean { function depFromNode( node: XmlElement, - underBuildSettingsElement = false + underBuildSettingsElement: boolean ): PackageDependency | null { if (!('valueWithPath' in node)) { return null; @@ -198,7 +198,10 @@ function applyPropsInternal( groupName = propKey; } fileReplacePosition = propValue.fileReplacePosition; - propSource = propValue.packageFile ?? undefined; + propSource = + propValue.packageFile ?? + // istanbul ignore next + undefined; anyChange = true; if (previouslySeenProps.has(propKey)) { fatal = true; diff --git a/lib/modules/manager/npm/extract/locked-versions.spec.ts b/lib/modules/manager/npm/extract/locked-versions.spec.ts index 8d4e8ce57ce8b0d09804caf09299d4440e268b0a..04f6c692865a35070e3ae1e95e607f8c943c1f0f 100644 --- a/lib/modules/manager/npm/extract/locked-versions.spec.ts +++ b/lib/modules/manager/npm/extract/locked-versions.spec.ts @@ -295,6 +295,34 @@ describe('modules/manager/npm/extract/locked-versions', () => { ]); }); + it('does nothing if managerData is not present', async () => { + npm.getNpmLock.mockResolvedValue({ + lockedVersions: { a: '1.0.0', b: '2.0.0', c: '3.0.0' }, + lockfileVersion: 1, + }); + const packageFiles = [ + { + extractedConstraints: {}, + deps: [ + { depName: 'a', currentValue: '1.0.0' }, + { depName: 'b', currentValue: '2.0.0' }, + ], + packageFile: 'some-file', + }, + ]; + await getLockedVersions(packageFiles); + expect(packageFiles).toEqual([ + { + extractedConstraints: {}, + deps: [ + { currentValue: '1.0.0', depName: 'a' }, + { currentValue: '2.0.0', depName: 'b' }, + ], + packageFile: 'some-file', + }, + ]); + }); + it('uses package-lock.json with npm v7.0.0', async () => { npm.getNpmLock.mockResolvedValue({ lockedVersions: { a: '1.0.0', b: '2.0.0', c: '3.0.0' }, diff --git a/lib/modules/manager/npm/extract/monorepo.spec.ts b/lib/modules/manager/npm/extract/monorepo.spec.ts index ce5c9e3e461ae12885b3475758a97b8059f75e8c..f7e46f2ab69ee7b2986cb33be749b66ef2b1cabb 100644 --- a/lib/modules/manager/npm/extract/monorepo.spec.ts +++ b/lib/modules/manager/npm/extract/monorepo.spec.ts @@ -165,6 +165,10 @@ describe('modules/manager/npm/extract/monorepo', () => { packageFile: 'packages/b/package.json', managerData: { packageJsonName: '@org/b' }, }, + // for coverage + { + packageFile: 'packages/c/package.json', + }, ]; await detectMonorepos(packageFiles); expect(packageFiles[1].managerData?.lernaJsonFile).toBe('lerna.json'); diff --git a/lib/modules/manager/npm/post-update/index.spec.ts b/lib/modules/manager/npm/post-update/index.spec.ts index e6fd13d8496eab49f805f94e014d44d0169b4499..c6eaaacc1497d1358a1cbf98f1e3b7a7607d58e2 100644 --- a/lib/modules/manager/npm/post-update/index.spec.ts +++ b/lib/modules/manager/npm/post-update/index.spec.ts @@ -454,7 +454,7 @@ describe('modules/manager/npm/post-update/index', () => { ]); }); - it('detects if lock file contents are unchanged', async () => { + it('detects if lock file contents are unchanged(reuseExistingBranch=true)', async () => { spyNpm.mockResolvedValueOnce({ error: false, lockFile: '{}' }); fs.readLocalFile.mockImplementation((f): Promise<any> => { if (f === 'package-lock.json') { @@ -482,6 +482,36 @@ describe('modules/manager/npm/post-update/index', () => { ).toBeUndefined(); }); + // for coverage run once when not reusing the branch + it('detects if lock file contents are unchanged(reuseExistingBranch=false)', async () => { + spyNpm.mockResolvedValueOnce({ error: false, lockFile: '{}' }); + fs.readLocalFile.mockImplementation((f): Promise<any> => { + if (f === 'package-lock.json') { + return Promise.resolve('{}'); + } + return Promise.resolve(null); + }); + git.getFile.mockImplementation((f) => { + if (f === 'package-lock.json') { + return Promise.resolve('{}'); + } + return Promise.resolve(null); + }); + expect( + ( + await getAdditionalFiles( + { + ...updateConfig, + updateLockFiles: true, + reuseExistingBranch: false, + baseBranch: 'base', + }, + additionalFiles + ) + ).updatedArtifacts.find((a) => a.path === 'package-lock.json') + ).toBeUndefined(); + }); + it('works for yarn', async () => { spyYarn.mockResolvedValueOnce({ error: false, lockFile: '{}' }); expect( diff --git a/lib/modules/manager/npm/post-update/npm.ts b/lib/modules/manager/npm/post-update/npm.ts index e6ff36f57d5df8cb30bdd8bc616f84fb6fdc079b..f81921d8eb75f1d737ee3b069db8f75ed3cd3f82 100644 --- a/lib/modules/manager/npm/post-update/npm.ts +++ b/lib/modules/manager/npm/post-update/npm.ts @@ -251,7 +251,7 @@ export function divideWorkspaceAndRootDeps( // compare workspaceDir to workspace patterns // stop when the first match is found and // add workspaceDir to workspaces set and upgrade object - for (const workspacePattern of workspacePatterns ?? []) { + for (const workspacePattern of workspacePatterns) { if (minimatch(workspacePattern).match(workspaceDir)) { workspaceName = workspaceDir; break; diff --git a/lib/modules/manager/npm/update/dependency/index.spec.ts b/lib/modules/manager/npm/update/dependency/index.spec.ts index 01b3b91f04f5bf02278dd23499cd56f0881be52a..e243622063f19739af9c90322eb1e3ec6d2dc254 100644 --- a/lib/modules/manager/npm/update/dependency/index.spec.ts +++ b/lib/modules/manager/npm/update/dependency/index.spec.ts @@ -347,5 +347,33 @@ describe('modules/manager/npm/update/dependency/index', () => { }); expect(testContent).toEqual(expected); }); + + it('handles override dependency object where lastParent === depName', () => { + const upgrade = { + depType: 'overrides', + depName: 'typescript', + newValue: '0.60.0', + managerData: { parents: ['typescript'] }, + }; + const overrideDependencies = `{ + "overrides": { + "typescript": { + ".": "3.0.0" + } + } + }`; + const expected = `{ + "overrides": { + "typescript": { + ".": "0.60.0" + } + } + }`; + const testContent = npmUpdater.updateDependency({ + fileContent: overrideDependencies, + upgrade, + }); + expect(testContent).toEqual(expected); + }); }); }); diff --git a/lib/modules/platform/bitbucket-server/index.spec.ts b/lib/modules/platform/bitbucket-server/index.spec.ts index 9084bb165cdc4399aa409ae36f63a5ad0336ae21..72c6c9c094280e31f8458322d1c35f2692a3fa30 100644 --- a/lib/modules/platform/bitbucket-server/index.spec.ts +++ b/lib/modules/platform/bitbucket-server/index.spec.ts @@ -1244,9 +1244,23 @@ describe('modules/platform/bitbucket-server/index', () => { }); expect( - await bitbucket.findPr({ - branchName: 'userName1/pullRequest1', - }) + await bitbucket.getBranchPr('userName1/pullRequest1') + ).toBeNull(); + }); + + it('has no existing pr', async () => { + const scope = await initRepo(); + scope + .get( + `${urlPath}/rest/api/1.0/projects/SOME/repos/repo/pull-requests?state=ALL&role.1=AUTHOR&username.1=abc&limit=100` + ) + .reply(200, { + isLastPage: true, + values: [], + }); + + expect( + await bitbucket.getBranchPr('userName1/pullRequest1') ).toBeNull(); }); }); diff --git a/lib/modules/platform/bitbucket/index.ts b/lib/modules/platform/bitbucket/index.ts index b0343723d6edc38f781e47ff546db7d77a9411d6..96a23aa49c3484ac9fe35bfed0118892fe51c9e6 100644 --- a/lib/modules/platform/bitbucket/index.ts +++ b/lib/modules/platform/bitbucket/index.ts @@ -366,7 +366,7 @@ export async function getPr(prNo: number): Promise<Pr | null> { } const escapeHash = (input: string): string => - input ? input.replace(regEx(/#/g), '%23') : input; + input?.replace(regEx(/#/g), '%23'); // Return the commit SHA for a branch async function getBranchCommit( diff --git a/lib/modules/platform/codecommit/index.spec.ts b/lib/modules/platform/codecommit/index.spec.ts index e82ff378cfda9d0550ef796e55710fe8552f255a..f8fd754ccf15f0d2dabbf46dacfd69bcc21c30d3 100644 --- a/lib/modules/platform/codecommit/index.spec.ts +++ b/lib/modules/platform/codecommit/index.spec.ts @@ -15,6 +15,7 @@ import { UpdatePullRequestTitleCommand, } from '@aws-sdk/client-codecommit'; import { mockClient } from 'aws-sdk-client-mock'; +import * as aws4 from 'aws4'; import { logger } from '../../../../test/util'; import { PLATFORM_BAD_CREDENTIALS, @@ -48,6 +49,7 @@ describe('modules/platform/codecommit/index', () => { codeCommitClient.reset(); config.prList = undefined; config.repository = undefined; + jest.useRealTimers(); }); it('validates massageMarkdown functionality', () => { @@ -84,13 +86,19 @@ describe('modules/platform/codecommit/index', () => { }); }); - it('should ', async () => { + it('should', async () => { await expect( codeCommit.initPlatform({ endpoint: 'non://parsable.url' }) ).resolves.toEqual({ endpoint: 'non://parsable.url', }); }); + + it('should as well', async () => { + await expect(codeCommit.initPlatform({})).resolves.toEqual({ + endpoint: 'https://git-codecommit.us-east-1.amazonaws.com/', + }); + }); }); describe('initRepos()', () => { @@ -190,6 +198,33 @@ describe('modules/platform/codecommit/index', () => { ) ).toBe('https://git-codecommit.eu-central-1.amazonaws.com/v1/repos/name'); }); + + it('gets url with username and token', () => { + jest.useFakeTimers().setSystemTime(new Date('2020-01-01')); + process.env.AWS_ACCESS_KEY_ID = 'access-key-id'; + process.env.AWS_SECRET_ACCESS_KEY = 'secret-access-key'; + process.env.AWS_REGION = 'eu-central-1'; + process.env.AWS_SESSION_TOKEN = ''; + const signer = new aws4.RequestSigner({ + service: 'codecommit', + host: 'git-codecommit.eu-central-1.amazonaws.com', + method: 'GIT', + path: 'v1/repos/name', + }); + const dateTime = signer.getDateTime(); + const token = `${dateTime}Z${signer.signature()}`; + expect( + getCodeCommitUrl( + { + defaultBranch: 'main', + repositoryId: 'id', + }, + 'name' + ) + ).toBe( + `https://access-key-id:${token}@git-codecommit.eu-central-1.amazonaws.com/v1/repos/name` + ); + }); }); describe('getRepos()', () => { diff --git a/lib/modules/platform/github/index.spec.ts b/lib/modules/platform/github/index.spec.ts index 0a5cdbc72d35daa4918e67c4464de6092681f6b3..7f456f21ad8c80b1d2d00d266d6baaf4ac93fa54 100644 --- a/lib/modules/platform/github/index.spec.ts +++ b/lib/modules/platform/github/index.spec.ts @@ -1406,6 +1406,23 @@ describe('modules/platform/github/index', () => { const res = await github.getBranchStatusCheck('somebranch', 'context-4'); expect(res).toBeNull(); }); + + it('returns yellow if state not present in context object', async () => { + const scope = httpMock.scope(githubApiHost); + initRepoMock(scope, 'some/repo'); + scope + .get( + '/repos/some/repo/commits/0d9c7726c3d628b7e28af234595cfd20febdbf8e/statuses' + ) + .reply(200, [ + { + context: 'context-1', + }, + ]); + await github.initRepo({ repository: 'some/repo' }); + const res = await github.getBranchStatusCheck('somebranch', 'context-1'); + expect(res).toBe('yellow'); + }); }); describe('setBranchStatus', () => { diff --git a/lib/modules/platform/github/index.ts b/lib/modules/platform/github/index.ts index 773fe122f942ddf25706706ff4862bb4794d89e7..c6a14a41da28a4e89d71421850c66cde665cfec2 100644 --- a/lib/modules/platform/github/index.ts +++ b/lib/modules/platform/github/index.ts @@ -24,6 +24,7 @@ import { logger } from '../../../logger'; import type { BranchStatus, VulnerabilityAlert } from '../../../types'; import { ExternalHostError } from '../../../types/errors/external-host-error'; import { isGithubFineGrainedPersonalAccessToken } from '../../../util/check-token'; +import { coerceToNull } from '../../../util/coerce'; import * as git from '../../../util/git'; import { listCommitTree, pushCommitToRenovateRef } from '../../../util/git'; import type { @@ -35,9 +36,10 @@ import * as hostRules from '../../../util/host-rules'; import * as githubHttp from '../../../util/http/github'; import type { GithubHttpOptions } from '../../../util/http/github'; import type { HttpResponse } from '../../../util/http/types'; +import { coerceObject } from '../../../util/object'; import { regEx } from '../../../util/regex'; import { sanitize } from '../../../util/sanitize'; -import { fromBase64, looseEquals } from '../../../util/string'; +import { coerceString, fromBase64, looseEquals } from '../../../util/string'; import { ensureTrailingSlash } from '../../../util/url'; import type { AggregatedVulnerabilities, @@ -112,7 +114,7 @@ export async function detectGhe(token: string): Promise<void> { if (platformConfig.isGhe) { const gheHeaderKey = 'x-github-enterprise-version'; const gheQueryRes = await githubApi.headJson('/', { token }); - const gheHeaders = gheQueryRes?.headers || {}; + const gheHeaders = coerceObject(gheQueryRes?.headers); const [, gheVersion] = Object.entries(gheHeaders).find( ([k]) => k.toLowerCase() === gheHeaderKey @@ -580,7 +582,7 @@ export async function initRepo({ sha, force: true, }, - token: forkToken ?? opts.token, + token: coerceString(forkToken, opts.token), }); } catch (err) /* istanbul ignore next */ { logger.warn( @@ -603,7 +605,7 @@ export async function initRepo({ // istanbul ignore else if (forkToken) { logger.debug('Using forkToken for git init'); - parsedEndpoint.auth = config.forkToken ?? null; + parsedEndpoint.auth = coerceToNull(config.forkToken); } else { const tokenType = opts.token?.startsWith('x-access-token:') ? 'app' @@ -736,7 +738,7 @@ export async function getPrList(): Promise<GhPr[]> { // TODO: check null `repo` (#22198) const prCache = await getPrCache(githubApi, repo!, username); config.prList = Object.values(prCache).sort( - ({ number: a }, { number: b }) => (a > b ? -1 : 1) + ({ number: a }, { number: b }) => b - a ); } @@ -1826,7 +1828,7 @@ export async function getVulnerabilityAlerts(): Promise<VulnerabilityAlert[]> { const key = `${ecosystem.toLowerCase()}/${name}`; const range = vulnerableVersionRange; const elem = shortAlerts[key] || {}; - elem[range] = patch ?? null; + elem[range] = coerceToNull(patch); shortAlerts[key] = elem; } logger.debug({ alerts: shortAlerts }, 'GitHub vulnerability details'); diff --git a/lib/modules/versioning/conan/common.ts b/lib/modules/versioning/conan/common.ts index cf7cc094abf4ff9aa7fdc02ba63599f55b3024d7..864a90f07b5999123d3d3d8a946d03bebd2b9b38 100644 --- a/lib/modules/versioning/conan/common.ts +++ b/lib/modules/versioning/conan/common.ts @@ -1,5 +1,6 @@ import * as semver from 'semver'; import { regEx } from '../../../util/regex'; +import { coerceString } from '../../../util/string'; export function makeVersion( version: string, @@ -66,7 +67,7 @@ export function matchesWithOptions( options.includePrerelease ) { const coercedVersion = semver.coerce(cleanedVersion)?.raw; - cleanedVersion = coercedVersion ? coercedVersion : ''; + cleanedVersion = coerceString(coercedVersion); } return semver.satisfies(cleanedVersion, cleanRange, options); } diff --git a/lib/modules/versioning/conan/range.ts b/lib/modules/versioning/conan/range.ts index f559c17936d8b447633b2ca4604040f194dc6b8c..efda646033903382f6de898d00ea308952c4ae83 100644 --- a/lib/modules/versioning/conan/range.ts +++ b/lib/modules/versioning/conan/range.ts @@ -1,6 +1,7 @@ import * as semver from 'semver'; import { SemVer, parseRange } from 'semver-utils'; import { logger } from '../../../logger'; +import { coerceString } from '../../../util/string'; import type { NewValueConfig } from '../types'; import { cleanVersion, @@ -89,7 +90,7 @@ export function fixParsedRange(range: string): any { major, }; - let full = `${operator ?? ''}${major}`; + let full = `${coerceString(operator)}${major}`; if (minor) { NewSemVer.minor = minor; full = `${full}.${minor}`; diff --git a/lib/modules/versioning/gradle/index.spec.ts b/lib/modules/versioning/gradle/index.spec.ts index a4a727a4e47c6a0a5f75b9367388282b01cc276e..9141d62826adbc401245b7168364f139da064d31 100644 --- a/lib/modules/versioning/gradle/index.spec.ts +++ b/lib/modules/versioning/gradle/index.spec.ts @@ -76,6 +76,7 @@ describe('modules/versioning/gradle/index', () => { ${'Hoxton.SR1'} | ${'Hoxton.RELEASE'} | ${1} ${'1.0-sp-1'} | ${'1.0-release'} | ${1} ${'1.0-sp-2'} | ${'1.0-sp-1'} | ${1} + ${''} | ${''} | ${0} `('compare("$a", "$b") === $expected', ({ a, b, expected }) => { expect(compare(a, b)).toEqual(expected); }); diff --git a/lib/modules/versioning/maven/compare.spec.ts b/lib/modules/versioning/maven/compare.spec.ts index 5de6a0185de6bab03bec4da8dcec6e1afc1908bc..1fe6a033292c3320468dee4ec9402dbfc0808ebf 100644 --- a/lib/modules/versioning/maven/compare.spec.ts +++ b/lib/modules/versioning/maven/compare.spec.ts @@ -1,8 +1,10 @@ import { autoExtendMavenRange, compare, + isSubversion, parseRange, rangeToStr, + tokenize, } from './compare'; describe('modules/versioning/maven/compare', () => { @@ -220,6 +222,196 @@ describe('modules/versioning/maven/compare', () => { }); }); + describe('isSubversion', () => { + it.each` + majorVersion | minorVersion | expected + ${'1.2.3'} | ${'1.2.3'} | ${true} + ${'1.2.3'} | ${'1.0.0'} | ${false} + ${'2.0.0'} | ${'2.0.1'} | ${true} + ${'3.1.0'} | ${'3.01.00'} | ${true} + ${'4.0.0'} | ${''} | ${false} + ${'5.0.0'} | ${'4.5.2'} | ${false} + ${'6.0.0'} | ${'6.0.0-beta'} | ${true} + ${'invalid.version'} | ${''} | ${false} + ${''} | ${'1.2.3'} | ${false} + ${'v1.2.3'} | ${'1.2.3'} | ${true} + ${'v1.2.3'} | ${'v1.2.3'} | ${true} + `( + 'isSubversion("$majorVersion", "$minorVersion") === $expected', + ({ majorVersion, minorVersion, expected }) => { + expect(isSubversion(majorVersion, minorVersion)).toBe(expected); + } + ); + }); + + describe('tokenize', () => { + const zeroToken = { + prefix: 'PREFIX_HYPHEN', + type: 'TYPE_NUMBER', + val: 0, + isTransition: false, + }; + const testObj = [ + { + input: '1.2.3', + expected: [ + { + isTransition: false, + prefix: 'PREFIX_HYPHEN', + type: 'TYPE_NUMBER', + val: 1, + }, + { + isTransition: false, + prefix: 'PREFIX_DOT', + type: 'TYPE_NUMBER', + val: 2, + }, + { + isTransition: false, + prefix: 'PREFIX_DOT', + type: 'TYPE_NUMBER', + val: 3, + }, + ], + }, + { + input: 'alpha.beta.rc', + expected: [ + { + isTransition: false, + prefix: 'PREFIX_HYPHEN', + type: 'TYPE_QUALIFIER', + val: 'alpha', + }, + { + isTransition: false, + prefix: 'PREFIX_DOT', + type: 'TYPE_QUALIFIER', + val: 'beta', + }, + { + isTransition: false, + prefix: 'PREFIX_DOT', + type: 'TYPE_QUALIFIER', + val: 'rc', + }, + ], + }, + { + input: '1.2.3-alpha.beta', + expected: [ + { + isTransition: false, + prefix: 'PREFIX_HYPHEN', + type: 'TYPE_NUMBER', + val: 1, + }, + { + isTransition: false, + prefix: 'PREFIX_DOT', + type: 'TYPE_NUMBER', + val: 2, + }, + { + isTransition: false, + prefix: 'PREFIX_DOT', + type: 'TYPE_NUMBER', + val: 3, + }, + { + isTransition: false, + prefix: 'PREFIX_HYPHEN', + type: 'TYPE_QUALIFIER', + val: 'alpha', + }, + { + isTransition: false, + prefix: 'PREFIX_DOT', + type: 'TYPE_QUALIFIER', + val: 'beta', + }, + ], + }, + { + input: '1.2.x-3', + expected: [ + { + isTransition: false, + prefix: 'PREFIX_HYPHEN', + type: 'TYPE_NUMBER', + val: 1, + }, + { + isTransition: false, + prefix: 'PREFIX_DOT', + type: 'TYPE_NUMBER', + val: 2, + }, + { + isTransition: false, + prefix: 'PREFIX_DOT', + type: 'TYPE_QUALIFIER', + val: 'x', + }, + { + isTransition: false, + prefix: 'PREFIX_HYPHEN', + type: 'TYPE_NUMBER', + val: 3, + }, + ], + }, + { + input: '00.02.003', + expected: [ + { + isTransition: false, + prefix: 'PREFIX_HYPHEN', + type: 'TYPE_NUMBER', + val: 0, + }, + { + isTransition: false, + prefix: 'PREFIX_DOT', + type: 'TYPE_NUMBER', + val: 2, + }, + { + isTransition: false, + prefix: 'PREFIX_DOT', + type: 'TYPE_NUMBER', + val: 3, + }, + ], + }, + { + input: 'invalid.version', + expected: [ + { + isTransition: false, + prefix: 'PREFIX_HYPHEN', + type: 'TYPE_QUALIFIER', + val: 'invalid', + }, + { + isTransition: false, + prefix: 'PREFIX_DOT', + type: 'TYPE_QUALIFIER', + val: 'version', + }, + ], + }, + { input: '', expected: [zeroToken] }, + ]; + + it('should tokenize', () => { + for (const { input, expected } of testObj) { + expect(tokenize(input)).toEqual(expected); + } + }); + }); + describe('Non-standard behavior', () => { describe('equality', () => { it.each` diff --git a/lib/modules/versioning/maven/compare.ts b/lib/modules/versioning/maven/compare.ts index 9ede3007e1c40951a7573a2d82eadbb1b2f801cb..f70d7965e70afe528fe9e6a9a8f9e165ff358e93 100644 --- a/lib/modules/versioning/maven/compare.ts +++ b/lib/modules/versioning/maven/compare.ts @@ -111,13 +111,6 @@ function isNull(token: Token): boolean { ); } -const zeroToken: NumberToken = { - prefix: PREFIX_HYPHEN, - type: TYPE_NUMBER, - val: 0, - isTransition: false, -}; - function tokenize(versionStr: string, preserveMinorZeroes = false): Token[] { let buf: Token[] = []; let result: Token[] = []; @@ -136,7 +129,7 @@ function tokenize(versionStr: string, preserveMinorZeroes = false): Token[] { buf = []; } }); - return result.length ? result : [zeroToken]; + return result; } function nullFor(token: Token): Token { diff --git a/lib/modules/versioning/maven/index.ts b/lib/modules/versioning/maven/index.ts index 1ce3e218d1fdc3a15b74002786050614ab39762e..67521565f70e405cc25cae5181dda1f0295adcb1 100644 --- a/lib/modules/versioning/maven/index.ts +++ b/lib/modules/versioning/maven/index.ts @@ -1,4 +1,5 @@ import type { RangeStrategy } from '../../../types/versioning'; +import { coerceString } from '../../../util/string'; import type { NewValueConfig, VersioningApi } from '../types'; import { EXCLUDING_POINT, @@ -152,7 +153,10 @@ function getNewValue({ if (isVersion(currentValue) || rangeStrategy === 'pin') { return newVersion; } - return autoExtendMavenRange(currentValue, newVersion) ?? currentValue; + return coerceString( + autoExtendMavenRange(currentValue, newVersion), + currentValue + ); } export { isValid }; diff --git a/lib/util/coerce.spec.ts b/lib/util/coerce.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..bc0d14b291d1ce3a19a96cc16bb7fa3099da073f --- /dev/null +++ b/lib/util/coerce.spec.ts @@ -0,0 +1,29 @@ +import { coerceToNull, coerceToUndefined } from './coerce'; + +describe('util/coerce', () => { + describe('coerceToNull', () => { + it('should return null', () => { + expect(coerceToNull(undefined)).toBeNull(); + expect(coerceToNull(null)).toBeNull(); + }); + + it('should return original value', () => { + expect(coerceToNull({})).toEqual({}); + expect(coerceToNull('str')).toBe('str'); + expect(coerceToNull(false)).toBe(false); + }); + }); + + describe('coerceToUndefined', () => { + it('should return undefined', () => { + expect(coerceToUndefined(undefined)).toBeUndefined(); + expect(coerceToUndefined(null)).toBeUndefined(); + }); + + it('should return original value', () => { + expect(coerceToUndefined({})).toEqual({}); + expect(coerceToUndefined('str')).toBe('str'); + expect(coerceToUndefined(false)).toBe(false); + }); + }); +}); diff --git a/lib/util/coerce.ts b/lib/util/coerce.ts new file mode 100644 index 0000000000000000000000000000000000000000..bad74fefe0caf8166fc7c1c4e5b61f16ac3b0f06 --- /dev/null +++ b/lib/util/coerce.ts @@ -0,0 +1,9 @@ +export function coerceToNull<T>(input: T | null | undefined): T | null { + return input ?? null; +} + +export function coerceToUndefined<T>( + input: T | null | undefined +): T | undefined { + return input ?? undefined; +} diff --git a/lib/util/object.spec.ts b/lib/util/object.spec.ts index 3abbe23f097617f34b89b522ad57a699df8188ce..046c7c0f47292431a550927eabf081562bee533f 100644 --- a/lib/util/object.spec.ts +++ b/lib/util/object.spec.ts @@ -1,4 +1,4 @@ -import { hasKey } from './object'; +import { coerceObject, hasKey } from './object'; describe('util/object', () => { beforeEach(() => { @@ -16,4 +16,19 @@ describe('util/object', () => { it('returns false for wrong instance type', () => { expect(hasKey('foo', 'i-am-not-an-object')).toBeFalse(); }); + + describe('coerceObject', () => { + it('should return empty object', () => { + expect(coerceObject(undefined)).toEqual({}); + expect(coerceObject(null)).toEqual({}); + }); + + it('should return input object', () => { + expect(coerceObject({})).toEqual({}); + expect(coerceObject({ name: 'name' })).toEqual({ name: 'name' }); + expect(coerceObject(undefined, { name: 'name' })).toEqual({ + name: 'name', + }); + }); + }); }); diff --git a/lib/util/object.ts b/lib/util/object.ts index 560b3f4ff93003ac73b958ce9b0b0b86af648989..a9c22dc9c093e81441996278f2cd45926bb33969 100644 --- a/lib/util/object.ts +++ b/lib/util/object.ts @@ -11,3 +11,12 @@ export function hasKey<K extends string, T>( ): o is T & Record<K, unknown> { return o && typeof o === 'object' && k in o; } + +/** + * Coerce a value to a object with optional default value. + * @param val value to coerce + * @returns the coerced value. + */ +export function coerceObject<T>(val: T | null | undefined, def?: T): T { + return val ?? def ?? ({} as T); +} diff --git a/lib/workers/repository/errors-warnings.spec.ts b/lib/workers/repository/errors-warnings.spec.ts index 2123c920003a3bb3ff4e3b688bf362533943a2e9..0c508c1d21c766cabf08d809ebd220ffca268fa9 100644 --- a/lib/workers/repository/errors-warnings.spec.ts +++ b/lib/workers/repository/errors-warnings.spec.ts @@ -275,6 +275,7 @@ describe('workers/repository/errors-warnings', () => { {}, ], }, + partial<PackageFile>(), // for coverage { packageFile: 'backend/package.json', deps: [ @@ -293,6 +294,10 @@ describe('workers/repository/errors-warnings', () => { }, ], }, + // coverage + partial<PackageFile>({ + packageFile: 'Dockerfile', + }), ], }; const res = getDepWarningsOnboardingPR(packageFiles, config); @@ -313,6 +318,17 @@ describe('workers/repository/errors-warnings', () => { `); }); + it('handle empty package files', () => { + const config: RenovateConfig = {}; + const packageFiles: Record<string, PackageFile[]> = { + npm: undefined as never, + }; + let res = getDepWarningsOnboardingPR(packageFiles, config); + expect(res).toBe(''); + res = getDepWarningsOnboardingPR(undefined as never, config); + expect(res).toBe(''); + }); + it('suppress notifications contains dependencyLookupWarnings flag then return empty string', () => { const config: RenovateConfig = { suppressNotifications: ['dependencyLookupWarnings'], diff --git a/lib/workers/repository/init/merge.spec.ts b/lib/workers/repository/init/merge.spec.ts index 5a191d4e205bc4c247318c187647e924c5122454..b7a4b5d91cd29c31cd8b6fc8cbfbdac4a7ababba 100644 --- a/lib/workers/repository/init/merge.spec.ts +++ b/lib/workers/repository/init/merge.spec.ts @@ -363,5 +363,23 @@ describe('workers/repository/init/merge', () => { config.extends = [':automergeDisabled']; expect(await mergeRenovateConfig(config)).toBeDefined(); }); + + it('continues if no errors-2', async () => { + scm.getFileList.mockResolvedValue(['package.json', '.renovaterc.json']); + fs.readLocalFile.mockResolvedValue('{}'); + migrateAndValidate.migrateAndValidate.mockResolvedValue({ + warnings: [], + errors: [], + }); + expect( + await mergeRenovateConfig({ + ...config, + requireConfig: 'ignored', + configFileParsed: undefined, + warnings: undefined, + secrets: undefined, + }) + ).toBeDefined(); + }); }); }); diff --git a/lib/workers/repository/onboarding/branch/index.spec.ts b/lib/workers/repository/onboarding/branch/index.spec.ts index aaecdb04bd56a892fa51e77aaae105feaa57207d..bc6e748928daa819b92b770a4d97d23409418194 100644 --- a/lib/workers/repository/onboarding/branch/index.spec.ts +++ b/lib/workers/repository/onboarding/branch/index.spec.ts @@ -387,7 +387,7 @@ describe('workers/repository/onboarding/branch/index', () => { }); it('detects missing rebase checkbox', async () => { - const pr = { bodyStruct: { rebaseRequested: undefined } }; + const pr = { bodyStruct: undefined }; platform.getBranchPr.mockResolvedValueOnce(mock<Pr>(pr)); await checkOnboardingBranch(config); diff --git a/lib/workers/repository/process/lookup/filter-checks.ts b/lib/workers/repository/process/lookup/filter-checks.ts index 74c02cc24ea0b226a9514df87bf0e5cf7796de55..3959e36f77e509d1945492ec656b6a81c758f21e 100644 --- a/lib/workers/repository/process/lookup/filter-checks.ts +++ b/lib/workers/repository/process/lookup/filter-checks.ts @@ -9,6 +9,7 @@ import { isActiveConfidenceLevel, satisfiesConfidenceLevel, } from '../../../../util/merge-confidence'; +import { coerceNumber } from '../../../../util/number'; import { applyPackageRules } from '../../../../util/package-rules'; import { toMs } from '../../../../util/pretty-time'; import type { LookupUpdateConfig, UpdateResult } from './types'; @@ -61,7 +62,10 @@ export async function filterInternalChecks( updateType, } = releaseConfig; if (is.nonEmptyString(minimumReleaseAge) && releaseTimestamp) { - if (getElapsedMs(releaseTimestamp) < (toMs(minimumReleaseAge) ?? 0)) { + if ( + getElapsedMs(releaseTimestamp) < + coerceNumber(toMs(minimumReleaseAge), 0) + ) { // Skip it if it doesn't pass checks logger.trace( { depName, check: 'minimumReleaseAge' }, diff --git a/lib/workers/repository/process/lookup/filter.spec.ts b/lib/workers/repository/process/lookup/filter.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..2ba090b5b49cd9b4db807b4a48c30ad86b4ee5e7 --- /dev/null +++ b/lib/workers/repository/process/lookup/filter.spec.ts @@ -0,0 +1,61 @@ +import { partial } from '../../../../../test/util'; +import * as allVersioning from '../../../../modules/versioning'; +import { filterVersions } from './filter'; +import type { FilterConfig } from './types'; + +const versioning = allVersioning.get('semver'); + +const releases = [ + { + version: '1.0.1', + releaseTimestamp: '2021-01-01T00:00:01.000Z', + }, + { + version: '1.2.0', + releaseTimestamp: '2021-01-03T00:00:00.000Z', + }, + { + version: '2.0.0', + releaseTimestamp: '2021-01-05T00:00:00.000Z', + }, + { + version: '2.1.0', + releaseTimestamp: '2021-01-07T00:00:00.000Z', + }, + // for coverage + { + version: 'invalid.version', + releaseTimestamp: '2021-01-07T00:00:00.000Z', + }, +]; + +describe('workers/repository/process/lookup/filter', () => { + describe('.filterVersions()', () => { + it('should filter versions allowed by semver syntax when allowedVersions is not valid version, range or pypi syntax', () => { + const config = partial<FilterConfig>({ + ignoreUnstable: false, + ignoreDeprecated: false, + respectLatest: false, + allowedVersions: '>1', + }); + const currentVersion = '1.0.0'; + const latestVersion = '2.0.0'; + + jest.spyOn(versioning, 'isVersion').mockReturnValue(true); + jest.spyOn(versioning, 'isGreaterThan').mockReturnValue(true); + + const filteredVersions = filterVersions( + config, + currentVersion, + latestVersion, + releases, + versioning + ); + + expect(filteredVersions).toEqual([ + { version: '2.0.0', releaseTimestamp: '2021-01-05T00:00:00.000Z' }, + { version: '2.1.0', releaseTimestamp: '2021-01-07T00:00:00.000Z' }, + ]); + }); + }); +}); diff --git a/lib/workers/repository/process/sort.spec.ts b/lib/workers/repository/process/sort.spec.ts index 07ff392ecbf0bfeee85cd848a830b5a825aa4523..64e6993b01cbce4cb3ffb165efbb36ea25cf619a 100644 --- a/lib/workers/repository/process/sort.spec.ts +++ b/lib/workers/repository/process/sort.spec.ts @@ -13,17 +13,22 @@ describe('workers/repository/process/sort', () => { updateType: 'pin' as UpdateType, prTitle: 'some pin', }, + { + updateType: 'minor' as UpdateType, + prTitle: 'a minor update', + }, { updateType: 'pin' as UpdateType, - prTitle: 'some other pin', + prTitle: 'some other other pin', }, { - updateType: 'minor' as UpdateType, - prTitle: 'a minor update', + updateType: 'pin' as UpdateType, + prTitle: 'some other pin', }, ]; sortBranches(branches); expect(branches).toEqual([ + { prTitle: 'some other other pin', updateType: 'pin' }, { prTitle: 'some other pin', updateType: 'pin' }, { prTitle: 'some pin', updateType: 'pin' }, { prTitle: 'a minor update', updateType: 'minor' }, diff --git a/lib/workers/repository/update/branch/auto-replace.spec.ts b/lib/workers/repository/update/branch/auto-replace.spec.ts index 3a6a1f6cf5780cc9f4d3a747d3b4e9847e77a06c..04f7a440c15bb327789de9aaa86eefb1de56911f 100644 --- a/lib/workers/repository/update/branch/auto-replace.spec.ts +++ b/lib/workers/repository/update/branch/auto-replace.spec.ts @@ -52,6 +52,29 @@ describe('workers/repository/update/branch/auto-replace', () => { expect(res).toBeNull(); }); + // for coverage + it('uses depName or packageName', async () => { + upgrade.baseDeps = [ + { + datasource: 'cdnjs', + packageName: 'react-router/react-router-test.min.js', + currentValue: '4.2.1', + replaceString: + '<script src=" https://cdnjs.cloudflare.com/ajax/libs/react-router/4.3.1/react-router.min.js">', + }, + { + datasource: 'cdnjs', + depName: 'react-router-test', + currentValue: '4.1.1', + replaceString: + '<script src=" https://cdnjs.cloudflare.com/ajax/libs/react-router/4.3.1/react-router.min.js">', + }, + ]; + reuseExistingBranch = true; + const res = await doAutoReplace(upgrade, sampleHtml, reuseExistingBranch); + expect(res).toBeNull(); + }); + it('updates version only', async () => { const script = '<script src="https://cdnjs.cloudflare.com/ajax/libs/reactstrap/7.1.0/reactstrap.min.js">'; diff --git a/lib/workers/repository/update/branch/execute-post-upgrade-commands.spec.ts b/lib/workers/repository/update/branch/execute-post-upgrade-commands.spec.ts index ec5e6fac3a302349cf03abdd436b4311e9831584..82d48d621c6ca41d300e28c15fd948ead37a1a95 100644 --- a/lib/workers/repository/update/branch/execute-post-upgrade-commands.spec.ts +++ b/lib/workers/repository/update/branch/execute-post-upgrade-commands.spec.ts @@ -66,5 +66,53 @@ describe('workers/repository/update/branch/execute-post-upgrade-commands', () => expect(res.updatedArtifacts).toHaveLength(3); expect(fs.writeLocalFile).toHaveBeenCalledTimes(1); }); + + it('executes commands on update package files', async () => { + const commands = partial<BranchUpgradeConfig>([ + { + manager: 'some-manager', + branchName: 'main', + postUpgradeTasks: { + executionMode: 'update', + commands: ['disallowed_command'], + }, + }, + ]); + const config: BranchConfig = { + manager: 'some-manager', + updatedPackageFiles: [ + { type: 'addition', path: 'some-existing-dir', contents: '' }, + { type: 'addition', path: 'artifact', contents: '' }, + ], + upgrades: [], + branchName: 'main', + baseBranch: 'base', + }; + git.getRepoStatus.mockResolvedValueOnce( + partial<StatusResult>({ + modified: [], + not_added: [], + deleted: [], + }) + ); + GlobalConfig.set({ + localDir: __dirname, + allowedPostUpgradeCommands: ['some-command'], + }); + fs.localPathIsFile + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false); + fs.localPathExists + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(true); + + const res = await postUpgradeCommands.postUpgradeCommandsExecutor( + commands, + config + ); + + expect(res.updatedArtifacts).toHaveLength(0); + expect(fs.writeLocalFile).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/lib/workers/repository/update/branch/execute-post-upgrade-commands.ts b/lib/workers/repository/update/branch/execute-post-upgrade-commands.ts index 2dce9840bb69884f9c2f92807ba69f2b0939775f..bfbf825f80e8801f4303627c3a8c01b690478bf2 100644 --- a/lib/workers/repository/update/branch/execute-post-upgrade-commands.ts +++ b/lib/workers/repository/update/branch/execute-post-upgrade-commands.ts @@ -4,6 +4,7 @@ import { mergeChildConfig } from '../../../../config'; import { GlobalConfig } from '../../../../config/global'; import { addMeta, logger } from '../../../../logger'; import type { ArtifactError } from '../../../../modules/manager/types'; +import { coerceArray } from '../../../../util/array'; import { exec } from '../../../../util/exec'; import { localPathIsFile, @@ -45,7 +46,7 @@ export async function postUpgradeCommandsExecutor( }, `Checking for post-upgrade tasks` ); - const commands = upgrade.postUpgradeTasks?.commands ?? []; + const commands = upgrade.postUpgradeTasks?.commands; const fileFilters = upgrade.postUpgradeTasks?.fileFilters ?? ['**/*']; if (is.nonEmptyArray(commands)) { // Persist updated files in file system so any executed commands can see them @@ -136,7 +137,7 @@ export async function postUpgradeCommandsExecutor( } } - for (const relativePath of status.deleted || []) { + for (const relativePath of coerceArray(status.deleted)) { for (const pattern of fileFilters) { if (minimatch(pattern, { dot: true }).match(relativePath)) { logger.debug( diff --git a/lib/workers/repository/update/branch/get-updated.ts b/lib/workers/repository/update/branch/get-updated.ts index 676c7dfff2bb4b73ef16b0f6853b01d2f59fd613..94b329e93b184c61531674c4920d0076690ebdd8 100644 --- a/lib/workers/repository/update/branch/get-updated.ts +++ b/lib/workers/repository/update/branch/get-updated.ts @@ -9,6 +9,7 @@ import type { } from '../../../../modules/manager/types'; import { getFile } from '../../../../util/git'; import type { FileAddition, FileChange } from '../../../../util/git/types'; +import { coerceString } from '../../../../util/string'; import type { BranchConfig } from '../../../types'; import { doAutoReplace } from './auto-replace'; @@ -248,7 +249,9 @@ export async function getUpdatedPackageFiles( reuseExistingBranch: false, }); } - logger.debug(`Updating ${depName} in ${packageFile || lockFile}`); + logger.debug( + `Updating ${depName} in ${coerceString(packageFile, lockFile)}` + ); updatedFileContents[packageFile] = newContent; delete nonUpdatedFileContents[packageFile]; } @@ -334,10 +337,7 @@ export async function getUpdatedPackageFiles( if (updateArtifacts) { const packageFileContents = updatedFileContents[packageFile] || - (await getFile( - packageFile, - reuseExistingBranch ? config.branchName : config.baseBranch - )); + (await getFile(packageFile, config.baseBranch)); const results = await updateArtifacts({ packageFileName: packageFile, updatedDeps: [], diff --git a/lib/workers/repository/update/branch/index.spec.ts b/lib/workers/repository/update/branch/index.spec.ts index 3a07c835ff92c8508542feaf631f8bb59efb9cc1..df1b53166e4b9f48dea96fa3ca0ba689e1e5ccc3 100644 --- a/lib/workers/repository/update/branch/index.spec.ts +++ b/lib/workers/repository/update/branch/index.spec.ts @@ -1392,7 +1392,7 @@ describe('workers/repository/update/branch/index', () => { ); npmPostExtract.getAdditionalFiles.mockResolvedValueOnce({ artifactErrors: [], - updatedArtifacts: [partial<FileChange>()], + updatedArtifacts: [{ type: 'deletion', path: 'dummy' }], }); scm.branchExists.mockResolvedValue(true); platform.getBranchPr.mockResolvedValueOnce( diff --git a/lib/workers/repository/update/branch/index.ts b/lib/workers/repository/update/branch/index.ts index 54abd336fc9d51af60913e5f33bdbe29a9d7e8b3..a78d2f1fcae6f3c41fb8a158a37596fa36d25dbc 100644 --- a/lib/workers/repository/update/branch/index.ts +++ b/lib/workers/repository/update/branch/index.ts @@ -30,6 +30,7 @@ import { isActiveConfidenceLevel, satisfiesConfidenceLevel, } from '../../../../util/merge-confidence'; +import { coerceNumber } from '../../../../util/number'; import { toMs } from '../../../../util/pretty-time'; import * as template from '../../../../util/template'; import { isLimitReached } from '../../../global/limits'; @@ -346,7 +347,7 @@ export async function processBranch( upgrade.releaseTimestamp ) { const timeElapsed = getElapsedMs(upgrade.releaseTimestamp); - if (timeElapsed < (toMs(upgrade.minimumReleaseAge) ?? 0)) { + if (timeElapsed < coerceNumber(toMs(upgrade.minimumReleaseAge))) { logger.debug( { depName: upgrade.depName, @@ -852,7 +853,7 @@ export async function processBranch( } else if (config.automerge) { logger.debug('PR is configured for automerge'); // skip automerge if there is a new commit since status checks aren't done yet - if (!commitSha || config.ignoreTests) { + if (config.ignoreTests === true || !commitSha) { logger.debug('checking auto-merge'); const prAutomergeResult = await checkAutoMerge(pr, config); if (prAutomergeResult?.automerged) { diff --git a/lib/workers/repository/update/pr/body/updates-table.ts b/lib/workers/repository/update/pr/body/updates-table.ts index f6718c3099f30959566e4830cce186f9b9862ec2..568e3c2f28a641d3cb036d745e87d49e091f145c 100644 --- a/lib/workers/repository/update/pr/body/updates-table.ts +++ b/lib/workers/repository/update/pr/body/updates-table.ts @@ -48,10 +48,7 @@ export function getPrUpdatesTable(config: BranchConfig): string { .filter((upgrade) => upgrade !== undefined) .map((upgrade) => { const res: Record<string, string> = {}; - const rowDefinition = getRowDefinition( - config.prBodyColumns ?? [], - upgrade - ); + const rowDefinition = getRowDefinition(config.prBodyColumns!, upgrade); for (const column of rowDefinition) { const { header, value } = column; try { diff --git a/lib/workers/repository/update/pr/index.ts b/lib/workers/repository/update/pr/index.ts index 705f6b0951545ba89d4cd4aae655f1fd11971943..8aefec5074c093b0a450c25057fcb7ef5f97205c 100644 --- a/lib/workers/repository/update/pr/index.ts +++ b/lib/workers/repository/update/pr/index.ts @@ -99,7 +99,7 @@ function hasNotIgnoredReviewers(pr: Pr, config: BranchConfig): boolean { 0 ); } - return pr.reviewers ? pr.reviewers.length > 0 : false; + return is.nonEmptyArray(pr.reviewers); } // Ensures that PR exists with matching title/body diff --git a/lib/workers/repository/update/pr/participants.spec.ts b/lib/workers/repository/update/pr/participants.spec.ts index 699da1190fe2b1f2ecb54bb1f92e360e0fca893a..04e3f148cca646de6dbdc5c6c59c047afc49190a 100644 --- a/lib/workers/repository/update/pr/participants.spec.ts +++ b/lib/workers/repository/update/pr/participants.spec.ts @@ -28,6 +28,11 @@ describe('workers/repository/update/pr/participants', () => { }); describe('assignees', () => { + it('does not assignees when there are none', async () => { + await addParticipants({ ...config, assignees: undefined }, pr); + expect(platform.addAssignees).not.toHaveBeenCalled(); + }); + it('adds assignees', async () => { await addParticipants(config, pr); expect(platform.addAssignees).toHaveBeenCalledWith(123, ['a', 'b', 'c']); @@ -73,6 +78,11 @@ describe('workers/repository/update/pr/participants', () => { }); describe('reviewers', () => { + it('does not assignees when there are none', async () => { + await addParticipants({ ...config, reviewers: undefined }, pr); + expect(platform.addReviewers).not.toHaveBeenCalled(); + }); + it('adds reviewers', async () => { await addParticipants(config, pr); expect(platform.addReviewers).toHaveBeenCalledWith(123, ['x', 'y', 'z']); diff --git a/lib/workers/repository/update/pr/pr-cache.spec.ts b/lib/workers/repository/update/pr/pr-cache.spec.ts index 65f704241d1817373f96811a06caff6458b05a20..51455f04190227210dbd67614aab57008dc093ff 100644 --- a/lib/workers/repository/update/pr/pr-cache.spec.ts +++ b/lib/workers/repository/update/pr/pr-cache.spec.ts @@ -57,7 +57,7 @@ describe('workers/repository/update/pr/pr-cache', () => { ); }); - it('set prCache', () => { + it('updates cache', () => { cache.getCache.mockReturnValue(dummyCache); jest.useFakeTimers().setSystemTime(new Date('2020-01-01')); setPrCache('branch_name', 'fingerprint_hash', true); @@ -73,5 +73,33 @@ describe('workers/repository/update/pr/pr-cache', () => { ], }); }); + + it('does not update details if pr not modified', () => { + const dummyCache2 = { + branches: [ + { + ...branchCache, + prCache: { + bodyFingerprint: 'fingerprint_hash', + lastEdited: new Date('2020-01-01').toISOString(), + }, + }, + ], + }; + cache.getCache.mockReturnValue(dummyCache); + jest.useFakeTimers().setSystemTime(new Date('2020-01-02')); + setPrCache('branch_name', 'fingerprint_hash', false); + expect(dummyCache2).toStrictEqual({ + branches: [ + { + ...branchCache, + prCache: { + bodyFingerprint: 'fingerprint_hash', + lastEdited: new Date('2020-01-01').toISOString(), + }, + }, + ], + }); + }); }); });