Files
renovate/lib/modules/manager/maven-wrapper/artifacts.spec.ts
Jamie Tanna 5b1749983c feat: always set CI=true for subprocesses
As noted in #43761, we had recently set `CI=true` for the Renovate and
Containerbase orgs on Mend-hosted apps due to a pnpm v11 requirement:

    ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY  Aborted removal of
      modules directory due to no TTY

    If you are running pnpm in CI, set the CI environment variable to
      "true", or set "confirmModulesPurge" to "false".

Similarly in #43606, it was noted that we should do this for `mise lock`
to mark the workspace as "trusted".

Although we went with a different situation, there will be cases where
this is useful for tools being executed, for instance when we invoke `yarn`.

We also make sure to keep our documentation up-to-date as we add to the
specific overrides.

With help from Claude Sonnet and Opus to:

- perform some of the mechanical updates as part of this change
- ensure that the Docker sidecar always has `hardcodedProcessEnv` passed
  into it
- catch a needed change to `exposeAllEnv` handling

Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
2026-08-24 17:21:24 +01:00

1016 lines
37 KiB
TypeScript

import type { Stats } from 'node:fs';
import os from 'node:os';
import { codeBlock } from 'common-tags';
import type { StatusResult } from 'simple-git';
import upath from 'upath';
import { mockDeep } from 'vitest-mock-extended';
import { envMock, mockExecAll } from '~test/exec-util.ts';
import * as httpMock from '~test/http-mock.ts';
import { env, fs, git, partial } from '~test/util.ts';
import { GlobalConfig } from '../../../config/global.ts';
import * as withCacheModule from '../../../util/cache/package/with-cache.ts';
import { resetPrefetchedImages } from '../../../util/exec/docker/index.ts';
import { getPkgReleases } from '../../datasource/index.ts';
import { updateArtifacts } from './index.ts';
vi.mock('../../../util/fs/index.ts');
vi.mock('../../../util/exec/env.ts');
vi.mock('../../datasource/index.ts', () => mockDeep());
process.env.CONTAINERBASE = 'true';
const osPlatformSpy = vi.spyOn(os, 'platform');
function mockMavenFileChangedInGit(fileName = 'maven-wrapper.properties') {
git.getRepoStatus.mockResolvedValueOnce(
partial<StatusResult>({
modified: [`maven.mvn/wrapper/${fileName}`],
}),
);
}
describe('modules/manager/maven-wrapper/artifacts', () => {
beforeEach(() => {
osPlatformSpy.mockImplementation(() => 'linux');
GlobalConfig.set({
localDir: upath.join('/tmp/github/some/repo'),
binarySource: 'global',
});
fs.statLocalFile.mockResolvedValue(
partial<Stats>({
isFile: () => true,
mode: 0o555,
}),
);
resetPrefetchedImages();
env.getChildProcessEnv.mockReturnValue({
...envMock.basic,
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US',
});
vi.mocked(getPkgReleases).mockResolvedValueOnce({
releases: [
{ version: '8.0.1' },
{ version: '11.0.1' },
{ version: '16.0.1' },
{ version: '17.0.0' },
],
});
});
it('Should not update if there is no dep with maven:wrapper', async () => {
const execSnapshots = mockExecAll({ stdout: '', stderr: '' });
const updatedDeps = await updateArtifacts({
packageFileName: 'maven-wrapper',
newPackageFileContent: '',
updatedDeps: [{ depName: 'not-mavenwrapper' }],
config: {},
});
expect(updatedDeps).toBeNull();
expect(execSnapshots).toBeEmptyArray();
});
it('Docker should use java 8 if version is lower then 2.0.0', async () => {
mockMavenFileChangedInGit();
const execSnapshots = mockExecAll();
GlobalConfig.set({ localDir: './', binarySource: 'docker' });
const updatedDeps = await updateArtifacts({
packageFileName: 'maven',
newPackageFileContent: '',
updatedDeps: [{ depName: 'maven-wrapper' }],
config: {
currentValue: '2.0.0',
newValue: '3.3.1',
constraints: undefined,
},
});
const expected = [
{
file: {
contents: undefined,
path: 'maven.mvn/wrapper/maven-wrapper.properties',
type: 'addition',
},
},
];
expect(execSnapshots[2].cmd).toContain('java 8.0.1');
expect(updatedDeps).toEqual(expected);
expect(git.getRepoStatus).toHaveBeenCalledExactlyOnceWith();
});
it('Should update when it is maven wrapper', async () => {
mockMavenFileChangedInGit();
const execSnapshots = mockExecAll({ stdout: '', stderr: '' });
const updatedDeps = await updateArtifacts({
packageFileName: 'maven',
newPackageFileContent: '',
updatedDeps: [{ depName: 'maven-wrapper' }],
config: { currentValue: '3.3.1', newValue: '3.3.1' },
});
const expected = [
{
file: {
contents: undefined,
path: 'maven.mvn/wrapper/maven-wrapper.properties',
type: 'addition',
},
},
];
expect(updatedDeps).toEqual(expected);
expect(execSnapshots).toEqual([
{
cmd: './mvnw wrapper:wrapper -Dtype=script',
options: {
cwd: '/tmp/github',
env: {
CI: 'true',
HOME: '/home/user',
HTTPS_PROXY: 'https://example.com',
HTTP_PROXY: 'http://example.com',
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US',
NO_PROXY: 'localhost',
PATH: '/tmp/path',
},
maxBuffer: 10485760,
timeout: 900000,
stdin: 'pipe',
stdout: 'pipe',
stderr: 'pipe',
},
},
]);
expect(git.getRepoStatus).toHaveBeenCalledExactlyOnceWith();
});
it('quotes a distributionType containing shell metacharacters', async () => {
mockMavenFileChangedInGit();
const execSnapshots = mockExecAll({ stdout: '', stderr: '' });
await updateArtifacts({
packageFileName: 'maven',
// distributionType as it would be parsed from an attacker-controlled
// maven-wrapper.properties
newPackageFileContent: 'distributionType=script;touch pwned',
updatedDeps: [{ depName: 'maven-wrapper' }],
config: { currentValue: '3.3.1', newValue: '3.3.1' },
});
expect(execSnapshots[0]).toMatchObject({
cmd: "./mvnw wrapper:wrapper -Dtype='script;touch pwned'",
});
});
it('Should not update deps when maven-wrapper.properties is not in git change', async () => {
mockMavenFileChangedInGit('not-maven-wrapper.properties');
const execSnapshots = mockExecAll({ stdout: '', stderr: '' });
const updatedDeps = await updateArtifacts({
packageFileName: 'maven',
newPackageFileContent: '',
updatedDeps: [{ depName: 'maven-wrapper' }],
config: { newValue: '3.3.1' },
});
expect(updatedDeps).toEqual([]);
expect(execSnapshots).toEqual([
{
cmd: './mvnw wrapper:wrapper -Dtype=script',
options: {
cwd: '/tmp/github',
env: {
CI: 'true',
HOME: '/home/user',
HTTPS_PROXY: 'https://example.com',
HTTP_PROXY: 'http://example.com',
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US',
NO_PROXY: 'localhost',
PATH: '/tmp/path',
},
maxBuffer: 10485760,
timeout: 900000,
stdin: 'pipe',
stdout: 'pipe',
stderr: 'pipe',
},
},
]);
expect(git.getRepoStatus).toHaveBeenCalledExactlyOnceWith();
});
it('updates with docker', async () => {
mockMavenFileChangedInGit();
GlobalConfig.set({
localDir: './',
binarySource: 'docker',
dockerSidecarImage: 'ghcr.io/renovatebot/base-image',
});
const execSnapshots = mockExecAll({ stdout: '', stderr: '' });
const result = await updateArtifacts({
packageFileName: 'maven',
newPackageFileContent: '',
updatedDeps: [{ depName: 'maven-wrapper' }],
config: { currentValue: '3.3.0', newValue: '3.3.1' },
});
expect(result).toEqual([
{
file: {
contents: undefined,
path: 'maven.mvn/wrapper/maven-wrapper.properties',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'docker pull ghcr.io/renovatebot/base-image',
options: {},
},
{ cmd: 'docker ps --filter name=renovate_sidecar -aq' },
{
cmd:
'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
'-v "./":"./" ' +
'-e CI ' +
'-e CONTAINERBASE_CACHE_DIR ' +
'-w "../.." ' +
'ghcr.io/renovatebot/base-image' +
" bash -l -c '" +
'install-tool java 17.0.0 ' +
'&& ' +
"./mvnw wrapper:wrapper -Dtype=script'",
options: {
cwd: '../..',
env: {
HOME: '/home/user',
HTTPS_PROXY: 'https://example.com',
HTTP_PROXY: 'http://example.com',
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US',
NO_PROXY: 'localhost',
PATH: '/tmp/path',
},
maxBuffer: 10485760,
timeout: 900000,
stdin: 'pipe',
stdout: 'pipe',
stderr: 'pipe',
},
},
]);
expect(git.getRepoStatus).toHaveBeenCalledExactlyOnceWith();
});
it('Should return null when cmd is not found', async () => {
osPlatformSpy.mockImplementation(() => 'win32');
const execSnapshots = mockExecAll({ stdout: '', stderr: '' });
fs.statLocalFile.mockResolvedValue(null);
const updatedDeps = await updateArtifacts({
packageFileName: 'maven',
newPackageFileContent: '',
updatedDeps: [{ depName: 'maven-wrapper' }],
config: { newValue: '3.3.1' },
});
expect(updatedDeps).toBeNull();
expect(execSnapshots).toMatchObject([]);
expect(git.getRepoStatus).not.toHaveBeenCalled();
});
it('Should throw an error when it cant execute', async () => {
mockExecAll(new Error('temporary-error'));
const updatedDeps = await updateArtifacts({
packageFileName: 'maven',
newPackageFileContent: '',
updatedDeps: [{ depName: 'maven-wrapper' }],
config: { currentValue: '3.0.0', newValue: '3.3.1' },
});
expect(updatedDeps).toEqual([
{
artifactError: {
fileName: 'maven',
stderr: 'temporary-error',
},
},
]);
expect(git.getRepoStatus).not.toHaveBeenCalled();
});
it('updates with binarySource install', async () => {
const execSnapshots = mockExecAll({ stdout: '', stderr: '' });
mockMavenFileChangedInGit();
GlobalConfig.set({
localDir: upath.join('/tmp/github/some/repo'),
binarySource: 'install',
});
const updatedDeps = await updateArtifacts({
packageFileName: 'maven',
newPackageFileContent: '',
updatedDeps: [{ depName: 'maven-wrapper' }],
config: { currentValue: '3.0.0', newValue: '3.3.1' },
});
expect(execSnapshots).toMatchObject([
{ cmd: 'install-tool java 17.0.0' },
{
cmd: './mvnw wrapper:wrapper -Dtype=script',
options: {
cwd: '/tmp/github',
env: {
HOME: '/home/user',
HTTPS_PROXY: 'https://example.com',
HTTP_PROXY: 'http://example.com',
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US',
NO_PROXY: 'localhost',
PATH: '/tmp/path',
},
maxBuffer: 10485760,
timeout: 900000,
stdin: 'pipe',
stdout: 'pipe',
stderr: 'pipe',
},
},
]);
expect(updatedDeps).toEqual([
{
file: {
contents: undefined,
path: 'maven.mvn/wrapper/maven-wrapper.properties',
type: 'addition',
},
},
]);
expect(git.getRepoStatus).toHaveBeenCalledExactlyOnceWith();
});
it('updates with binarySource install after detecting wrapper version from mvnw script', async () => {
const execSnapshots = mockExecAll({ stdout: '', stderr: '' });
mockMavenFileChangedInGit();
GlobalConfig.set({
localDir: upath.join('/tmp/github/some/repo'),
binarySource: 'install',
});
const updatedDeps = await updateArtifacts({
packageFileName: './mvnw',
newPackageFileContent: '',
updatedDeps: [{ depName: 'maven-wrapper' }],
config: { currentValue: '3.0.0', newValue: '3.3.1' },
});
expect(execSnapshots).toMatchObject([
{ cmd: 'install-tool java 17.0.0' },
{
cmd: './mvnw wrapper:wrapper -Dtype=script',
options: {
cwd: '/tmp/github/some/repo',
env: {
HOME: '/home/user',
HTTPS_PROXY: 'https://example.com',
HTTP_PROXY: 'http://example.com',
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US',
NO_PROXY: 'localhost',
PATH: '/tmp/path',
},
maxBuffer: 10485760,
timeout: 900000,
stdin: 'pipe',
stdout: 'pipe',
stderr: 'pipe',
},
},
]);
expect(updatedDeps).toEqual([]);
expect(git.getRepoStatus).toHaveBeenCalledExactlyOnceWith();
});
it('should run wrapper:wrapper with MVNW_REPOURL if it is a custom artifactory', async () => {
const execSnapshots = mockExecAll({ stdout: '', stderr: '' });
mockMavenFileChangedInGit();
await updateArtifacts({
packageFileName: 'maven-wrapper',
newPackageFileContent: '',
updatedDeps: [
{
depName: 'maven-wrapper',
replaceString:
'https://internal.local/maven-public/org/apache/maven/wrapper/maven-wrapper/3.0.0/maven-wrapper-3.0.0.jar',
},
],
config: { currentValue: '3.0.0', newValue: '3.3.1' },
});
expect(execSnapshots).toMatchObject([
{
cmd: './mvnw wrapper:wrapper -Dtype=script',
options: {
cwd: '/tmp/github',
env: {
HOME: '/home/user',
HTTPS_PROXY: 'https://example.com',
HTTP_PROXY: 'http://example.com',
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US',
NO_PROXY: 'localhost',
PATH: '/tmp/path',
MVNW_REPOURL: 'https://internal.local/maven-public',
},
maxBuffer: 10485760,
timeout: 900000,
stdin: 'pipe',
stdout: 'pipe',
stderr: 'pipe',
},
},
]);
expect(git.getRepoStatus).toHaveBeenCalledExactlyOnceWith();
});
it('should run not include MVNW_REPOURL when run with default maven repo url', async () => {
const execSnapshots = mockExecAll({ stdout: '', stderr: '' });
mockMavenFileChangedInGit();
await updateArtifacts({
packageFileName: 'maven-wrapper',
newPackageFileContent: '',
updatedDeps: [
{
depName: 'maven-wrapper',
replaceString:
'https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar',
},
],
config: { currentValue: '3.0.0', newValue: '3.3.1' },
});
expect(execSnapshots).toMatchObject([
{
cmd: './mvnw wrapper:wrapper -Dtype=script',
options: {
cwd: '/tmp/github',
env: {
HOME: '/home/user',
HTTPS_PROXY: 'https://example.com',
HTTP_PROXY: 'http://example.com',
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US',
NO_PROXY: 'localhost',
PATH: '/tmp/path',
},
maxBuffer: 10485760,
timeout: 900000,
stdin: 'pipe',
stdout: 'pipe',
stderr: 'pipe',
},
},
]);
expect(execSnapshots[0].options!.env).not.toHaveProperty('MVNW_REPOURL');
expect(git.getRepoStatus).toHaveBeenCalledExactlyOnceWith();
});
it('should run not include MVNW_REPOURL when run with a malformed replaceString', async () => {
const execSnapshots = mockExecAll({ stdout: '', stderr: '' });
mockMavenFileChangedInGit();
await updateArtifacts({
packageFileName: 'maven-wrapper',
newPackageFileContent: '',
updatedDeps: [
{
depName: 'maven-wrapper',
replaceString: 'not a good url',
},
],
config: { currentValue: '3.0.0', newValue: '3.3.1' },
});
expect(execSnapshots).toMatchObject([
{
cmd: './mvnw wrapper:wrapper -Dtype=script',
options: {
cwd: '/tmp/github',
env: {
HOME: '/home/user',
HTTPS_PROXY: 'https://example.com',
HTTP_PROXY: 'http://example.com',
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US',
NO_PROXY: 'localhost',
PATH: '/tmp/path',
},
maxBuffer: 10485760,
timeout: 900000,
stdin: 'pipe',
stdout: 'pipe',
stderr: 'pipe',
},
},
]);
expect(execSnapshots[0].options!.env).not.toHaveProperty('MVNW_REPOURL');
expect(git.getRepoStatus).toHaveBeenCalledExactlyOnceWith();
});
describe('checksum updates', () => {
const propertiesWithDistributionChecksumOnly = codeBlock`
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
distributionSha256Sum=oldhash123
`;
it('should not delete wrapper jar when only maven distribution checksum is updated', async () => {
httpMock
.scope('https://repo.maven.apache.org')
.get(
'/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip',
)
.reply(200, Buffer.from('fake-maven-distribution-content'));
mockMavenFileChangedInGit();
fs.readLocalFile.mockResolvedValueOnce(
propertiesWithDistributionChecksumOnly,
);
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: propertiesWithDistributionChecksumOnly,
updatedDeps: [{ depName: 'maven' }],
config: { currentValue: '3.9.8', newValue: '3.9.9' },
});
expect(fs.deleteLocalFile).not.toHaveBeenCalled();
});
it('should update distribution checksum when maven version changes', async () => {
httpMock
.scope('https://repo.maven.apache.org')
.get(
'/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip',
)
.reply(200, Buffer.from('fake-maven-distribution-content'));
mockMavenFileChangedInGit();
// Mock readLocalFile to return content after first write (simulates reading back)
fs.readLocalFile.mockResolvedValueOnce(
propertiesWithDistributionChecksumOnly,
);
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: propertiesWithDistributionChecksumOnly,
updatedDeps: [{ depName: 'maven' }],
config: { currentValue: '3.9.8', newValue: '3.9.9' },
});
// Verify writeLocalFile was called twice (initial write, then checksum update)
expect(fs.writeLocalFile).toHaveBeenCalledTimes(2);
// Check the second (final) write contains updated checksum
const finalWrittenContent = vi.mocked(fs.writeLocalFile).mock.calls[1][1];
expect(finalWrittenContent).toContain('distributionSha256Sum=');
// Should not contain the old hash
expect(finalWrittenContent).not.toContain('oldhash123');
});
it('should use cached distribution checksum when available', async () => {
mockMavenFileChangedInGit();
fs.readLocalFile.mockResolvedValueOnce(
propertiesWithDistributionChecksumOnly,
);
const withCacheSpy = vi
.spyOn(withCacheModule, 'withCache')
.mockResolvedValueOnce('cachedhash123');
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: propertiesWithDistributionChecksumOnly,
updatedDeps: [{ depName: 'maven' }],
config: { currentValue: '3.9.8', newValue: '3.9.9' },
});
expect(withCacheSpy).toHaveBeenCalledWith(
expect.objectContaining({
namespace: 'url-sha256',
key: 'https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip',
ttlMinutes: 3 * 24 * 60,
}),
expect.any(Function),
);
expect(fs.writeLocalFile).toHaveBeenCalledTimes(2);
const finalWrittenContent = vi.mocked(fs.writeLocalFile).mock.calls[1][1];
expect(finalWrittenContent).toContain(
'distributionSha256Sum=cachedhash123',
);
});
it('should skip checksum update when current content is missing', async () => {
mockMavenFileChangedInGit();
fs.readLocalFile.mockResolvedValueOnce(null);
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: propertiesWithDistributionChecksumOnly,
updatedDeps: [{ depName: 'maven' }],
config: { currentValue: '3.9.8', newValue: '3.9.9' },
});
expect(fs.writeLocalFile).toHaveBeenCalledTimes(1);
});
it('should update both checksums when wrapper version changes', async () => {
const propertiesWithChecksums = codeBlock`
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
distributionSha256Sum=oldhash123
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
wrapperSha256Sum=oldwrapperhash456
`;
httpMock
.scope('https://repo.maven.apache.org')
.get(
'/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip',
)
.reply(200, Buffer.from('fake-maven-content'))
.get(
'/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar',
)
.reply(200, Buffer.from('fake-wrapper-content'));
mockMavenFileChangedInGit();
mockExecAll({ stdout: '', stderr: '' });
// Mock readLocalFile to return content after wrapper:wrapper runs
fs.readLocalFile.mockResolvedValueOnce(propertiesWithChecksums);
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: propertiesWithChecksums,
updatedDeps: [{ depName: 'maven-wrapper', newValue: '3.3.2' }],
config: { currentValue: '3.3.1', newValue: '3.3.2' },
});
// writeLocalFile called twice: initial write, then after checksum update
expect(fs.writeLocalFile).toHaveBeenCalledTimes(2);
const finalWrittenContent = vi.mocked(fs.writeLocalFile).mock.calls[1][1];
expect(finalWrittenContent).not.toContain('oldhash123');
expect(finalWrittenContent).not.toContain('oldwrapperhash456');
expect(fs.deleteLocalFile).toHaveBeenCalledWith(
'.mvn/wrapper/maven-wrapper.jar',
);
});
it('should not attempt to delete old JAR file if doing a Maven Wrapper update', async () => {
const propertiesWithChecksums = codeBlock`
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
distributionSha256Sum=oldhash123
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
wrapperSha256Sum=oldwrapperhash456
`;
httpMock
.scope('https://repo.maven.apache.org')
.get(
'/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip',
)
.reply(200, Buffer.from('fake-maven-content'))
.get(
'/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar',
)
.reply(200, Buffer.from('fake-wrapper-content'));
mockMavenFileChangedInGit();
mockExecAll({ stdout: '', stderr: '' });
// Mock readLocalFile to return content after wrapper:wrapper runs
fs.readLocalFile.mockResolvedValueOnce(propertiesWithChecksums);
await updateArtifacts({
packageFileName: 'mvnw',
newPackageFileContent: propertiesWithChecksums,
updatedDeps: [{ depName: 'maven-wrapper', newValue: '3.3.2' }],
config: { currentValue: '3.3.1', newValue: '3.3.2' },
});
expect(fs.deleteLocalFile).not.toHaveBeenCalled();
});
it('should preserve old checksum when fetch fails', async () => {
httpMock
.scope('https://repo.maven.apache.org')
.get(
'/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip',
)
.replyWithError('Network error');
mockMavenFileChangedInGit();
// Mock readLocalFile to return content for checksum update
fs.readLocalFile.mockResolvedValueOnce(
propertiesWithDistributionChecksumOnly,
);
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: propertiesWithDistributionChecksumOnly,
updatedDeps: [{ depName: 'maven' }],
config: { currentValue: '3.9.8', newValue: '3.9.9' },
});
// writeLocalFile called twice: initial write, then checksum update (even if fetch fails, it still writes)
expect(fs.writeLocalFile).toHaveBeenCalledTimes(2);
// On fetch failure, checksum is not updated (original value preserved)
const finalWrittenContent = vi.mocked(fs.writeLocalFile).mock.calls[1][1];
expect(finalWrittenContent).toContain('distributionSha256Sum=oldhash123');
});
it('should restore distribution checksum when fetch fails after stripping', async () => {
const propertiesWithoutDistChecksum = `distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip`;
httpMock
.scope('https://repo.maven.apache.org')
.get(
'/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip',
)
.replyWithError('Network error');
mockMavenFileChangedInGit();
mockExecAll({ stdout: '', stderr: '' });
fs.readLocalFile.mockResolvedValueOnce(propertiesWithoutDistChecksum);
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: propertiesWithDistributionChecksumOnly,
updatedDeps: [{ depName: 'maven-wrapper', newValue: '3.3.2' }],
config: { currentValue: '3.3.1', newValue: '3.3.2' },
});
expect(fs.writeLocalFile).toHaveBeenCalledTimes(2);
const finalWrittenContent = vi.mocked(fs.writeLocalFile).mock.calls[1][1];
expect(finalWrittenContent).toContain('distributionSha256Sum=oldhash123');
});
it('should skip HTTP when no checksums in properties file', async () => {
const propertiesWithoutChecksums = codeBlock`
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
`;
mockMavenFileChangedInGit();
mockExecAll({ stdout: '', stderr: '' });
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: propertiesWithoutChecksums,
updatedDeps: [{ depName: 'maven-wrapper' }],
config: { currentValue: '3.3.1', newValue: '3.3.2' },
});
// File should still be written (for wrapper update)
expect(fs.writeLocalFile).toHaveBeenCalled();
// No HTTP calls should have been made (no mocked endpoints were hit)
});
it('should return null when only maven is updated without checksums', async () => {
const propertiesWithoutChecksums = `distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip`;
const result = await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: propertiesWithoutChecksums,
updatedDeps: [{ depName: 'maven' }],
config: { currentValue: '3.9.8', newValue: '3.9.9' },
});
expect(result).toBeNull();
});
it('should construct wrapper URL from version when wrapperUrl is missing', async () => {
const propertiesWithWrapperVersion = codeBlock`
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
wrapperVersion=3.3.2
wrapperSha256Sum=oldwrapperhash456
`;
httpMock
.scope('https://repo.maven.apache.org')
.get(
'/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar',
)
.reply(200, Buffer.from('fake-wrapper-content'));
mockMavenFileChangedInGit();
mockExecAll({ stdout: '', stderr: '' });
// Mock readLocalFile to return content after wrapper:wrapper runs
fs.readLocalFile.mockResolvedValueOnce(propertiesWithWrapperVersion);
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: propertiesWithWrapperVersion,
updatedDeps: [{ depName: 'maven-wrapper', newValue: '3.3.2' }],
config: { currentValue: '3.3.1', newValue: '3.3.2' },
});
// writeLocalFile called twice: initial write, then after checksum update
expect(fs.writeLocalFile).toHaveBeenCalledTimes(2);
const finalWrittenContent = vi.mocked(fs.writeLocalFile).mock.calls[1][1];
expect(finalWrittenContent).not.toContain('oldwrapperhash456');
});
it('should add distribution checksum when it does not exist', async () => {
const propertiesWithoutDistChecksum = `distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip`;
httpMock
.scope('https://repo.maven.apache.org')
.get(
'/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip',
)
.reply(200, Buffer.from('fake-maven-distribution-content'));
mockMavenFileChangedInGit();
mockExecAll({ stdout: '', stderr: '' });
// Mock readLocalFile to return content after wrapper:wrapper runs
fs.readLocalFile.mockResolvedValueOnce(propertiesWithoutDistChecksum);
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: codeBlock`
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
distributionSha256Sum=oldhash123
`,
updatedDeps: [{ depName: 'maven-wrapper' }],
config: { currentValue: '3.3.1', newValue: '3.3.2' },
});
// writeLocalFile called twice: initial write (stripped), then after checksum update
expect(fs.writeLocalFile).toHaveBeenCalledTimes(2);
const finalWrittenContent = vi.mocked(fs.writeLocalFile).mock.calls[1][1];
// Should have added the checksum after distributionUrl
expect(finalWrittenContent).toContain('distributionSha256Sum=');
});
it('should add wrapper checksum when it does not exist', async () => {
const propertiesWithoutWrapperChecksum = codeBlock`
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
`;
httpMock
.scope('https://repo.maven.apache.org')
.get(
'/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar',
)
.reply(200, Buffer.from('fake-wrapper-content'));
mockMavenFileChangedInGit();
mockExecAll({ stdout: '', stderr: '' });
// Mock readLocalFile to return content after wrapper:wrapper runs
fs.readLocalFile.mockResolvedValueOnce(propertiesWithoutWrapperChecksum);
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: codeBlock`
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
wrapperSha256Sum=oldwrapperhash456
`,
updatedDeps: [{ depName: 'maven-wrapper', newValue: '3.3.2' }],
config: { currentValue: '3.3.1', newValue: '3.3.2' },
});
// writeLocalFile called twice: initial write (stripped), then after checksum update
expect(fs.writeLocalFile).toHaveBeenCalledTimes(2);
const finalWrittenContent = vi.mocked(fs.writeLocalFile).mock.calls[1][1];
// Should have added the checksum after wrapperUrl
expect(finalWrittenContent).toContain('wrapperSha256Sum=');
});
it('should preserve wrapper checksum when fetch fails', async () => {
const propertiesWithWrapperChecksumOnly = codeBlock`
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
wrapperSha256Sum=oldwrapperhash456
`;
httpMock
.scope('https://repo.maven.apache.org')
.get(
'/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar',
)
.replyWithError('Network error');
mockMavenFileChangedInGit();
mockExecAll({ stdout: '', stderr: '' });
fs.readLocalFile.mockResolvedValueOnce(propertiesWithWrapperChecksumOnly);
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: propertiesWithWrapperChecksumOnly,
updatedDeps: [{ depName: 'maven-wrapper', newValue: '3.3.2' }],
config: { currentValue: '3.3.1', newValue: '3.3.2' },
});
expect(fs.writeLocalFile).toHaveBeenCalledTimes(2);
const finalWrittenContent = vi.mocked(fs.writeLocalFile).mock.calls[1][1];
expect(finalWrittenContent).toContain(
'wrapperSha256Sum=oldwrapperhash456',
);
});
it('should restore wrapper checksum when fetch fails after stripping', async () => {
const propertiesWithoutWrapperChecksum = `wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar`;
const propertiesWithWrapperChecksumOnly = codeBlock`
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
wrapperSha256Sum=oldwrapperhash456
`;
httpMock
.scope('https://repo.maven.apache.org')
.get(
'/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar',
)
.replyWithError('Network error');
mockMavenFileChangedInGit();
mockExecAll({ stdout: '', stderr: '' });
fs.readLocalFile.mockResolvedValueOnce(propertiesWithoutWrapperChecksum);
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: propertiesWithWrapperChecksumOnly,
updatedDeps: [{ depName: 'maven-wrapper', newValue: '3.3.2' }],
config: { currentValue: '3.3.1', newValue: '3.3.2' },
});
expect(fs.writeLocalFile).toHaveBeenCalledTimes(2);
const finalWrittenContent = vi.mocked(fs.writeLocalFile).mock.calls[1][1];
expect(finalWrittenContent).toContain(
'wrapperSha256Sum=oldwrapperhash456',
);
});
it('should unescape distributionUrl, honor wrapperVersion, and keep distributionType', async () => {
const propertiesWithEscapedUrlsAndType = codeBlock`
distributionUrl=https\\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
distributionSha256Sum=oldhash123
distributionType=bin
wrapperVersion=3.3.2
wrapperSha256Sum=oldwrapperhash456
`;
httpMock
.scope('https://repo.maven.apache.org')
.get(
'/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip',
)
.reply(200, Buffer.from('fake-maven-content'))
.get(
'/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar',
)
.reply(200, Buffer.from('fake-wrapper-content'));
mockMavenFileChangedInGit();
const execSnapshots = mockExecAll({ stdout: '', stderr: '' });
fs.readLocalFile.mockResolvedValueOnce(propertiesWithEscapedUrlsAndType);
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: propertiesWithEscapedUrlsAndType,
updatedDeps: [{ depName: 'maven-wrapper', newValue: '3.3.2' }],
config: { currentValue: '3.3.1', newValue: '3.3.2' },
});
expect(execSnapshots[0].cmd).toContain('-Dtype=bin');
expect(fs.writeLocalFile).toHaveBeenCalledTimes(2);
const finalWrittenContent = vi.mocked(fs.writeLocalFile).mock.calls[1][1];
expect(finalWrittenContent).not.toContain('oldhash123');
expect(finalWrittenContent).not.toContain('oldwrapperhash456');
});
it('should skip distribution checksum update when distributionUrl is missing', async () => {
const propertiesWithoutDistributionUrl = `distributionSha256Sum=oldhash123`;
mockMavenFileChangedInGit();
fs.readLocalFile.mockResolvedValueOnce(propertiesWithoutDistributionUrl);
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: propertiesWithoutDistributionUrl,
updatedDeps: [{ depName: 'maven' }],
config: { currentValue: '3.9.8', newValue: '3.9.9' },
});
expect(fs.writeLocalFile).toHaveBeenCalledTimes(2);
const finalWrittenContent = vi.mocked(fs.writeLocalFile).mock.calls[1][1];
expect(finalWrittenContent).toContain('distributionSha256Sum=oldhash123');
});
it('should skip wrapper checksum update when wrapperVersion is missing', async () => {
const propertiesWithoutWrapperInfo = `wrapperSha256Sum=oldwrapperhash456`;
mockMavenFileChangedInGit();
mockExecAll({ stdout: '', stderr: '' });
fs.readLocalFile.mockResolvedValueOnce(propertiesWithoutWrapperInfo);
await updateArtifacts({
packageFileName: '.mvn/wrapper/maven-wrapper.properties',
newPackageFileContent: propertiesWithoutWrapperInfo,
updatedDeps: [{ depName: 'maven-wrapper', newValue: '3.3.2' }],
config: { currentValue: '3.3.1', newValue: '3.3.2' },
});
expect(fs.writeLocalFile).toHaveBeenCalledTimes(2);
const finalWrittenContent = vi.mocked(fs.writeLocalFile).mock.calls[1][1];
expect(finalWrittenContent).toContain(
'wrapperSha256Sum=oldwrapperhash456',
);
});
});
});