mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-31 09:41:46 +00:00
3b699b3698
* configure limiting security updates * separate security limit from other limits * rename type * fix coverage * add comments * add test
476 lines
14 KiB
TypeScript
476 lines
14 KiB
TypeScript
import { logger, partial } from '~test/util.ts';
|
|
import type { BranchConfig, BranchUpgradeConfig } from '../types.ts';
|
|
import {
|
|
calcLimit,
|
|
hasMultipleLimits,
|
|
incCountValue,
|
|
incLimitedValue,
|
|
isLimitReached,
|
|
resetAllLimits,
|
|
setCount,
|
|
setMaxLimit,
|
|
} from './limits.ts';
|
|
|
|
describe('workers/global/limits', () => {
|
|
beforeEach(() => {
|
|
resetAllLimits();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
resetAllLimits();
|
|
});
|
|
|
|
it('increments limited value', () => {
|
|
setMaxLimit('Commits', 3);
|
|
|
|
expect(isLimitReached('Commits')).toBeFalse();
|
|
|
|
incLimitedValue('Commits', 2);
|
|
expect(isLimitReached('Commits')).toBeFalse();
|
|
|
|
incLimitedValue('Commits');
|
|
expect(isLimitReached('Commits')).toBeTrue();
|
|
|
|
incLimitedValue('Commits');
|
|
expect(isLimitReached('Commits')).toBeTrue();
|
|
});
|
|
|
|
it('defaults to unlimited', () => {
|
|
expect(isLimitReached('Commits')).toBeFalse();
|
|
});
|
|
|
|
it('increments undefined', () => {
|
|
incLimitedValue('Commits');
|
|
expect(isLimitReached('Commits')).toBeFalse();
|
|
});
|
|
|
|
it('resets counter', () => {
|
|
setMaxLimit('Commits', 1);
|
|
incLimitedValue('Commits');
|
|
expect(isLimitReached('Commits')).toBeTrue();
|
|
setMaxLimit('Commits', 1);
|
|
expect(isLimitReached('Commits')).toBeFalse();
|
|
});
|
|
|
|
it('resets limit', () => {
|
|
setMaxLimit('Commits', 1);
|
|
incLimitedValue('Commits');
|
|
expect(isLimitReached('Commits')).toBeTrue();
|
|
setMaxLimit('Commits', null);
|
|
expect(isLimitReached('Commits')).toBeFalse();
|
|
});
|
|
|
|
it('sets non-positive limit as reached', () => {
|
|
setMaxLimit('Commits', 0);
|
|
expect(isLimitReached('Commits')).toBeTrue();
|
|
setMaxLimit('Commits', -1000);
|
|
expect(isLimitReached('Commits')).toBeTrue();
|
|
});
|
|
|
|
describe('calcLimit', () => {
|
|
it('handles single upgrade', () => {
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{
|
|
prHourlyLimit: 10,
|
|
branchConcurrentLimit: 11,
|
|
prConcurrentLimit: 12,
|
|
},
|
|
]);
|
|
|
|
expect(calcLimit(upgrades, 'prHourlyLimit')).toBe(10);
|
|
expect(calcLimit(upgrades, 'branchConcurrentLimit')).toBe(11);
|
|
expect(calcLimit(upgrades, 'prConcurrentLimit')).toBe(12);
|
|
});
|
|
|
|
it('inherits prConcurrentLimit if branchConcurrentLimit is null', () => {
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{
|
|
prHourlyLimit: 10,
|
|
branchConcurrentLimit: null,
|
|
prConcurrentLimit: 12,
|
|
},
|
|
]);
|
|
|
|
expect(calcLimit(upgrades, 'prHourlyLimit')).toBe(10);
|
|
expect(calcLimit(upgrades, 'branchConcurrentLimit')).toBe(12);
|
|
expect(calcLimit(upgrades, 'prConcurrentLimit')).toBe(12);
|
|
});
|
|
|
|
it('returns 0 if at least one upgrade has no limit in the branch', () => {
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{
|
|
prHourlyLimit: 10,
|
|
branchConcurrentLimit: 11,
|
|
prConcurrentLimit: 12,
|
|
},
|
|
{
|
|
prHourlyLimit: 0,
|
|
branchConcurrentLimit: 0,
|
|
prConcurrentLimit: 0,
|
|
},
|
|
{
|
|
prHourlyLimit: 1,
|
|
branchConcurrentLimit: 1,
|
|
prConcurrentLimit: 1,
|
|
},
|
|
]);
|
|
|
|
expect(calcLimit(upgrades, 'prHourlyLimit')).toBe(0);
|
|
expect(calcLimit(upgrades, 'branchConcurrentLimit')).toBe(0);
|
|
expect(calcLimit(upgrades, 'prConcurrentLimit')).toBe(0);
|
|
});
|
|
|
|
it('computes the lowest limit if multiple limits are present', () => {
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{
|
|
commitHourlyLimit: 5,
|
|
prHourlyLimit: 10,
|
|
branchConcurrentLimit: 11,
|
|
prConcurrentLimit: 12,
|
|
},
|
|
{
|
|
prHourlyLimit: 10,
|
|
branchConcurrentLimit: 11,
|
|
prConcurrentLimit: 12,
|
|
},
|
|
{
|
|
commitHourlyLimit: 3,
|
|
prHourlyLimit: 1,
|
|
branchConcurrentLimit: 1,
|
|
prConcurrentLimit: 1,
|
|
},
|
|
{
|
|
prHourlyLimit: 5,
|
|
branchConcurrentLimit: 6,
|
|
prConcurrentLimit: 3,
|
|
},
|
|
{
|
|
prHourlyLimit: 5,
|
|
branchConcurrentLimit: null,
|
|
prConcurrentLimit: undefined,
|
|
},
|
|
{
|
|
prHourlyLimit: 5,
|
|
branchConcurrentLimit: 6,
|
|
prConcurrentLimit: 2,
|
|
},
|
|
]);
|
|
|
|
expect(calcLimit(upgrades, 'commitHourlyLimit')).toBe(3);
|
|
expect(calcLimit(upgrades, 'prHourlyLimit')).toBe(1);
|
|
expect(calcLimit(upgrades, 'branchConcurrentLimit')).toBe(1);
|
|
expect(calcLimit(upgrades, 'prConcurrentLimit')).toBe(1);
|
|
});
|
|
|
|
it('de-duplicates upgrades by depName from debug log', () => {
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{
|
|
depName: 'depA',
|
|
prHourlyLimit: 10,
|
|
},
|
|
{
|
|
depName: 'depA',
|
|
prHourlyLimit: 10,
|
|
},
|
|
{
|
|
depName: 'depB',
|
|
prHourlyLimit: 1,
|
|
},
|
|
]);
|
|
|
|
expect(calcLimit(upgrades, 'prHourlyLimit')).toBe(1);
|
|
expect(logger.logger.debug).toHaveBeenCalledWith(
|
|
{
|
|
limits: [
|
|
{ depName: 'depA', prHourlyLimit: 10 },
|
|
{ depName: 'depB', prHourlyLimit: 1 },
|
|
],
|
|
},
|
|
'prHourlyLimit of the upgrades present in this branch',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('hasMultipleLimits', () => {
|
|
it('handles single limit', () => {
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{
|
|
prHourlyLimit: 10,
|
|
branchConcurrentLimit: 11,
|
|
prConcurrentLimit: 12,
|
|
},
|
|
]);
|
|
expect(hasMultipleLimits(upgrades, 'prHourlyLimit')).toBe(false);
|
|
expect(hasMultipleLimits(upgrades, 'branchConcurrentLimit')).toBe(false);
|
|
expect(hasMultipleLimits(upgrades, 'prConcurrentLimit')).toBe(false);
|
|
});
|
|
|
|
it('returns false if there are multiple limits with value', () => {
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{
|
|
prHourlyLimit: 10,
|
|
branchConcurrentLimit: 11,
|
|
prConcurrentLimit: 12,
|
|
},
|
|
{
|
|
prHourlyLimit: 10,
|
|
branchConcurrentLimit: 11,
|
|
prConcurrentLimit: 12,
|
|
},
|
|
]);
|
|
expect(hasMultipleLimits(upgrades, 'prHourlyLimit')).toBe(false);
|
|
expect(hasMultipleLimits(upgrades, 'branchConcurrentLimit')).toBe(false);
|
|
expect(hasMultipleLimits(upgrades, 'prConcurrentLimit')).toBe(false);
|
|
});
|
|
|
|
it('handles multiple limits', () => {
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{
|
|
prHourlyLimit: 10,
|
|
branchConcurrentLimit: 11,
|
|
prConcurrentLimit: 12,
|
|
},
|
|
{
|
|
prHourlyLimit: 11,
|
|
branchConcurrentLimit: 12,
|
|
prConcurrentLimit: 13,
|
|
},
|
|
{
|
|
prHourlyLimit: 0,
|
|
branchConcurrentLimit: null,
|
|
prConcurrentLimit: 3,
|
|
},
|
|
]);
|
|
expect(hasMultipleLimits(upgrades, 'prHourlyLimit')).toBe(true);
|
|
expect(hasMultipleLimits(upgrades, 'branchConcurrentLimit')).toBe(true);
|
|
expect(hasMultipleLimits(upgrades, 'prConcurrentLimit')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('isLimitReached', () => {
|
|
it('counts vulnerability alerts against their own budget', () => {
|
|
setCount('ConcurrentPRs', 5);
|
|
setCount('Branches', 5);
|
|
setCount('VulnerabilityConcurrentPRs', 1);
|
|
setCount('VulnerabilityBranches', 1);
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{ prConcurrentLimit: 5, branchConcurrentLimit: 5 },
|
|
]);
|
|
|
|
// the repository-wide budget is spent, but the vulnerability one is not
|
|
expect(
|
|
isLimitReached('ConcurrentPRs', partial<BranchConfig>({ upgrades })),
|
|
).toBe(true);
|
|
expect(
|
|
isLimitReached(
|
|
'ConcurrentPRs',
|
|
partial<BranchConfig>({ upgrades, isVulnerabilityAlert: true }),
|
|
),
|
|
).toBe(false);
|
|
expect(
|
|
isLimitReached('Branches', partial<BranchConfig>({ upgrades })),
|
|
).toBe(true);
|
|
expect(
|
|
isLimitReached(
|
|
'Branches',
|
|
partial<BranchConfig>({ upgrades, isVulnerabilityAlert: true }),
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('applies vulnerabilityAlerts.branchConcurrentLimit to vulnerability branches', () => {
|
|
setCount('VulnerabilityBranches', 2);
|
|
setCount('VulnerabilityConcurrentPRs', 2);
|
|
// a lower branchConcurrentLimit blocks branches while PRs are still allowed
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{ branchConcurrentLimit: 2, prConcurrentLimit: 10 },
|
|
]);
|
|
const config = partial<BranchConfig>({
|
|
upgrades,
|
|
isVulnerabilityAlert: true,
|
|
});
|
|
|
|
expect(isLimitReached('Branches', config)).toBe(true);
|
|
expect(isLimitReached('ConcurrentPRs', config)).toBe(false);
|
|
});
|
|
|
|
it('inherits vulnerabilityAlerts.prConcurrentLimit when branchConcurrentLimit is null', () => {
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{ branchConcurrentLimit: null, prConcurrentLimit: 3 },
|
|
]);
|
|
const config = partial<BranchConfig>({
|
|
upgrades,
|
|
isVulnerabilityAlert: true,
|
|
});
|
|
|
|
setCount('VulnerabilityBranches', 2);
|
|
expect(isLimitReached('Branches', config)).toBe(false);
|
|
|
|
setCount('VulnerabilityBranches', 3);
|
|
expect(isLimitReached('Branches', config)).toBe(true);
|
|
});
|
|
|
|
it('applies the vulnerability budget once it is spent', () => {
|
|
setCount('VulnerabilityConcurrentPRs', 5);
|
|
const upgrades = partial<BranchUpgradeConfig>([{ prConcurrentLimit: 5 }]);
|
|
|
|
expect(
|
|
isLimitReached(
|
|
'ConcurrentPRs',
|
|
partial<BranchConfig>({ upgrades, isVulnerabilityAlert: true }),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('ignores the hourly PR limit for vulnerability alerts', () => {
|
|
setCount('HourlyPRs', 5);
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{ prHourlyLimit: 2, prConcurrentLimit: 0 },
|
|
]);
|
|
|
|
expect(
|
|
isLimitReached('ConcurrentPRs', partial<BranchConfig>({ upgrades })),
|
|
).toBe(true);
|
|
expect(
|
|
isLimitReached(
|
|
'ConcurrentPRs',
|
|
partial<BranchConfig>({ upgrades, isVulnerabilityAlert: true }),
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('returns false based on concurrent limits', () => {
|
|
setCount('ConcurrentPRs', 1);
|
|
setCount('HourlyPRs', 1);
|
|
incCountValue('Branches'); // using incCountValue so it gets test coverage
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{
|
|
prHourlyLimit: 10,
|
|
branchConcurrentLimit: 11,
|
|
prConcurrentLimit: 12,
|
|
},
|
|
{
|
|
prHourlyLimit: 11,
|
|
branchConcurrentLimit: 12,
|
|
prConcurrentLimit: 13,
|
|
},
|
|
{
|
|
prHourlyLimit: 0,
|
|
branchConcurrentLimit: null,
|
|
prConcurrentLimit: 3,
|
|
},
|
|
]);
|
|
expect(
|
|
isLimitReached('Branches', partial<BranchConfig>({ upgrades })),
|
|
).toBe(false);
|
|
expect(
|
|
isLimitReached('ConcurrentPRs', partial<BranchConfig>({ upgrades })),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('returns true when pr hourly limit is reached', () => {
|
|
setCount('Branches', 2);
|
|
setCount('ConcurrentPRs', 2);
|
|
setCount('HourlyPRs', 2);
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{
|
|
prHourlyLimit: 10,
|
|
branchConcurrentLimit: 11,
|
|
prConcurrentLimit: 12,
|
|
},
|
|
{
|
|
prHourlyLimit: 11,
|
|
branchConcurrentLimit: 12,
|
|
prConcurrentLimit: 13,
|
|
},
|
|
{
|
|
prHourlyLimit: 2,
|
|
branchConcurrentLimit: null,
|
|
prConcurrentLimit: 3,
|
|
},
|
|
]);
|
|
expect(
|
|
isLimitReached('Branches', partial<BranchConfig>({ upgrades })),
|
|
).toBe(true);
|
|
expect(
|
|
isLimitReached('ConcurrentPRs', partial<BranchConfig>({ upgrades })),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('returns true when concurrent limit is reached', () => {
|
|
setCount('Branches', 3);
|
|
setCount('ConcurrentPRs', 3);
|
|
setCount('HourlyPRs', 4);
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{
|
|
prHourlyLimit: 10,
|
|
branchConcurrentLimit: 11,
|
|
prConcurrentLimit: 12,
|
|
},
|
|
{
|
|
prHourlyLimit: 11,
|
|
branchConcurrentLimit: 12,
|
|
prConcurrentLimit: 13,
|
|
},
|
|
{
|
|
prHourlyLimit: 5,
|
|
branchConcurrentLimit: null,
|
|
prConcurrentLimit: 3,
|
|
},
|
|
]);
|
|
expect(
|
|
isLimitReached('Branches', partial<BranchConfig>({ upgrades })),
|
|
).toBe(true);
|
|
expect(
|
|
isLimitReached('ConcurrentPRs', partial<BranchConfig>({ upgrades })),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('commit hourly limit only affects HourlyCommits check', () => {
|
|
setCount('HourlyCommits', 3);
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{
|
|
commitHourlyLimit: 3,
|
|
},
|
|
]);
|
|
// Commit hourly limit should block HourlyCommits when reached
|
|
expect(
|
|
isLimitReached('HourlyCommits', partial<BranchConfig>({ upgrades })),
|
|
).toBe(true);
|
|
// Should NOT block when limit is not reached
|
|
setCount('HourlyCommits', 2);
|
|
expect(
|
|
isLimitReached('HourlyCommits', partial<BranchConfig>({ upgrades })),
|
|
).toBe(false);
|
|
// Should be unlimited when set to 0
|
|
setCount('HourlyCommits', 100);
|
|
upgrades[0].commitHourlyLimit = 0;
|
|
expect(
|
|
isLimitReached('HourlyCommits', partial<BranchConfig>({ upgrades })),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('commit hourly limit does not block branch or PR checks', () => {
|
|
setCount('Branches', 0);
|
|
setCount('ConcurrentPRs', 1);
|
|
setCount('HourlyCommits', 10); // Commit limit reached
|
|
setCount('HourlyPRs', 0);
|
|
const upgrades = partial<BranchUpgradeConfig>([
|
|
{
|
|
commitHourlyLimit: 2, // Limit reached
|
|
prHourlyLimit: 10, // Limit not reached
|
|
branchConcurrentLimit: 10,
|
|
prConcurrentLimit: 0, // Unlimited
|
|
},
|
|
]);
|
|
// Should NOT block branch or PR creation checks
|
|
expect(
|
|
isLimitReached('Branches', partial<BranchConfig>({ upgrades })),
|
|
).toBe(false);
|
|
expect(
|
|
isLimitReached('ConcurrentPRs', partial<BranchConfig>({ upgrades })),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
});
|