Files
setup-java/__tests__/auth.test.ts
Bruno Borges dbe35f84c5 Add Maven plugin repository support
Generate plugin repositories alongside dependency repositories and share Central inclusion and ordering controls. Cover repository parsing, credentials, explicit Central overrides, plugin ordering, and effective Maven models; document the compatibility change and rebuild runtime bundles.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 52a8a8e2-7965-42d3-88d9-d66f64d0d60d
2026-10-14 15:26:18 -04:00

1430 lines
42 KiB
TypeScript

import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import {fileURLToPath} from 'url';
import * as io from '@actions/io';
import * as fs from 'fs';
import * as path from 'path';
import os from 'os';
import {XMLParser} from 'fast-xml-parser';
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(() => []),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
jest.unstable_mockModule('../src/gpg.js', () => ({
importKey: jest.fn(),
removeGpgHome: jest.fn(),
toGpgPath: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const gpg = await import('../src/gpg.js');
const auth = await import('../src/auth.js');
const {M2_DIR, MVN_SETTINGS_FILE, STATE_GPG_HOME} =
await import('../src/constants.js');
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const m2Dir = path.join(__dirname, M2_DIR);
const settingsFile = path.join(m2Dir, MVN_SETTINGS_FILE);
const credentials = (id: string, username: string, password: string) => [
{id, usernameEnvVar: username, passwordEnvVar: password}
];
const repositorySettings = (
repositories: auth.MavenRepository[],
includeCentral = true,
prioritizeCentral = true,
pluginRepositories: auth.MavenRepository[] = []
): auth.MavenRepositorySettings => ({
repositories,
pluginRepositories,
includeCentral,
prioritizeCentral
});
const repositoryInputs = ['mvn-repositories', 'mvn-plugin-repositories'];
const privateRepository = {
id: 'private',
url: 'https://repo.example.com/maven',
snapshotsEnabled: true
};
const privateRepositoryXml = {
id: privateRepository.id,
url: privateRepository.url,
snapshots: {enabled: 'true'}
};
const centralRepositoryXml = {
id: 'central',
url: 'https://repo.maven.apache.org/maven2',
snapshots: {enabled: 'false'}
};
const disabledCentralRepositoryXml = {
...centralRepositoryXml,
releases: {enabled: 'false'}
};
const repositoryConfigurations = [
{
name: 'dependency-only',
repositories: [privateRepository],
pluginRepositories: []
},
{
name: 'plugin-only',
repositories: [],
pluginRepositories: [privateRepository]
},
{
name: 'combined',
repositories: [privateRepository],
pluginRepositories: [privateRepository]
}
];
describe('auth tests', () => {
let spyOSHomedir: any;
let spyInfo: any;
beforeEach(async () => {
await io.rmRF(m2Dir);
spyOSHomedir = jest.spyOn(os, 'homedir');
spyOSHomedir.mockReturnValue(__dirname);
spyInfo = core.info as jest.Mock;
spyInfo.mockImplementation(() => null);
(gpg.toGpgPath as jest.Mock<any>).mockImplementation((p: string) => p);
}, 300000);
afterEach(() => {
(core.getInput as jest.Mock).mockReset();
(core.getMultilineInput as jest.Mock).mockReset();
(core.getMultilineInput as jest.Mock).mockReturnValue([]);
(core.warning as jest.Mock).mockReset();
(core.exportVariable as jest.Mock).mockReset();
(gpg.importKey as jest.Mock).mockReset();
(gpg.removeGpgHome as jest.Mock).mockReset();
(gpg.toGpgPath as jest.Mock).mockReset();
});
afterAll(async () => {
try {
await io.rmRF(m2Dir);
} catch {
console.log('Failed to remove test directories');
}
jest.resetAllMocks();
jest.clearAllMocks();
jest.restoreAllMocks();
}, 100000);
it('creates settings.xml in alternate locations', async () => {
const id = 'packages';
const username = 'UNAMI';
const password = 'TOLKIEN';
const altHome = path.join(__dirname, 'runner', 'settings');
const altSettingsFile = path.join(altHome, MVN_SETTINGS_FILE);
await io.rmRF(altHome); // ensure it doesn't already exist
await auth.createAuthenticationSettings(
credentials(id, username, password),
altHome,
true
);
expect(fs.existsSync(m2Dir)).toBe(false);
expect(fs.existsSync(settingsFile)).toBe(false);
expect(fs.existsSync(altHome)).toBe(true);
expect(fs.existsSync(altSettingsFile)).toBe(true);
expect(fs.readFileSync(altSettingsFile, 'utf-8')).toEqual(
auth.generate(credentials(id, username, password))
);
await io.rmRF(altHome);
}, 100000);
it('creates settings.xml with minimal configuration', async () => {
const id = 'packages';
const username = 'UNAME';
const password = 'TOKEN';
await auth.createAuthenticationSettings(
credentials(id, username, password),
m2Dir,
true
);
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(settingsFile)).toBe(true);
expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual(
auth.generate(credentials(id, username, password))
);
}, 100000);
it('creates settings.xml with additional configuration', async () => {
const id = 'packages';
const username = 'UNAME';
const password = 'TOKEN';
const gpgPassphrase = 'GPG';
await auth.createAuthenticationSettings(
credentials(id, username, password),
m2Dir,
true,
gpgPassphrase
);
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(settingsFile)).toBe(true);
expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual(
auth.generate(credentials(id, username, password), gpgPassphrase)
);
}, 100000);
it('exports a GPG-compatible path and persists the native GPG home', async () => {
const gpgHome = 'D:\\a\\_temp\\setup-java-gpg-1';
const exportedGpgHome = '/d/a/_temp/setup-java-gpg-1';
(gpg.importKey as jest.Mock<any>).mockResolvedValue(gpgHome);
(gpg.toGpgPath as jest.Mock<any>).mockReturnValue(exportedGpgHome);
(core.getInput as jest.Mock<any>).mockImplementation((name: string) => {
const inputs: Record<string, string> = {
'server-id': 'packages',
'server-username-env-var': 'USERNAME',
'server-password-env-var': 'PASSWORD',
'settings-path': m2Dir,
'gpg-private-key': 'KEY ONE\nKEY TWO'
};
return inputs[name] ?? '';
});
await auth.configureAuthentication();
expect(gpg.importKey).toHaveBeenCalledWith('KEY ONE\nKEY TWO');
expect(core.saveState).toHaveBeenCalledWith(STATE_GPG_HOME, gpgHome);
expect(gpg.toGpgPath).toHaveBeenCalledWith(gpgHome);
expect(core.exportVariable).toHaveBeenCalledWith(
'GNUPGHOME',
exportedGpgHome
);
});
it('removes the isolated GPG home when environment export fails', async () => {
const gpgHome = path.join(__dirname, 'runner', 'temp', 'setup-java-gpg-2');
(gpg.importKey as jest.Mock<any>).mockResolvedValue(gpgHome);
(core.exportVariable as jest.Mock<any>).mockImplementation(() => {
throw new Error('environment file unavailable');
});
(core.getInput as jest.Mock<any>).mockImplementation((name: string) => {
if (name === 'gpg-private-key') return 'KEY CONTENTS';
if (name === 'settings-path') return m2Dir;
return '';
});
await expect(auth.configureAuthentication()).rejects.toThrow(
'environment file unavailable'
);
expect(gpg.removeGpgHome).toHaveBeenCalledWith(gpgHome);
});
it('overwrites existing settings.xml files', async () => {
const id = 'packages';
const username = 'USERNAME';
const password = 'PASSWORD';
fs.mkdirSync(m2Dir, {recursive: true});
fs.writeFileSync(settingsFile, 'FAKE FILE');
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(settingsFile)).toBe(true);
await auth.createAuthenticationSettings(
credentials(id, username, password),
m2Dir,
true
);
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(settingsFile)).toBe(true);
expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual(
auth.generate(credentials(id, username, password))
);
}, 100000);
it('does not overwrite existing settings.xml files', async () => {
const id = 'packages';
const username = 'USERNAME';
const password = 'PASSWORD';
fs.mkdirSync(m2Dir, {recursive: true});
fs.writeFileSync(settingsFile, 'FAKE FILE');
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(settingsFile)).toBe(true);
await auth.createAuthenticationSettings(
credentials(id, username, password),
m2Dir,
false
);
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(settingsFile)).toBe(true);
expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual('FAKE FILE');
}, 100000);
it('generates valid settings.xml with minimal configuration', () => {
const id = 'packages';
const username = 'USER';
const password = '&<>"\'\'"><&';
const expectedSettings = `<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
<interactiveMode>false</interactiveMode>
<servers>
<server>
<id>${id}</id>
<username>\${env.${username}}</username>
<password>\${env.&amp;&lt;&gt;"''"&gt;&lt;&amp;}</password>
</server>
</servers>
</settings>`;
expect(auth.generate(credentials(id, username, password))).toEqual(
expectedSettings
);
});
it('generates valid settings.xml with additional configuration', () => {
const id = 'packages';
const username = 'USER';
const password = '&<>"\'\'"><&';
const gpgPassphrase = 'PASSPHRASE';
const expectedSettings = `<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
<interactiveMode>false</interactiveMode>
<servers>
<server>
<id>${id}</id>
<username>\${env.${username}}</username>
<password>\${env.&amp;&lt;&gt;"''"&gt;&lt;&amp;}</password>
</server>
</servers>
<profiles>
<profile>
<id>setup-java-gpg</id>
<properties>
<gpg.passphraseEnvName>${gpgPassphrase}</gpg.passphraseEnvName>
</properties>
</profile>
</profiles>
<activeProfiles>
<activeProfile>setup-java-gpg</activeProfile>
</activeProfiles>
</settings>`;
expect(
auth.generate(credentials(id, username, password), gpgPassphrase)
).toEqual(expectedSettings);
});
it('generates repository origins for a Maven server', () => {
const settings = auth.generate([
{
id: 'central',
usernameEnvVar: 'CENTRAL_USER',
passwordEnvVar: 'CENTRAL_PASS',
repositoryOrigins: ['https://central.sonatype.com']
}
]);
expect(settings).toContain(` <repositoryOrigins>
<repositoryOrigin>https://central.sonatype.com</repositoryOrigin>
</repositoryOrigins>`);
});
it('does not add a gpg profile when the passphrase env var is the maven-gpg-plugin default', () => {
const id = 'packages';
const username = 'USER';
const password = '&<>"\'\'"><&';
const gpgPassphrase = 'MAVEN_GPG_PASSPHRASE';
const expectedSettings = `<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
<interactiveMode>false</interactiveMode>
<servers>
<server>
<id>${id}</id>
<username>\${env.${username}}</username>
<password>\${env.&amp;&lt;&gt;"''"&gt;&lt;&amp;}</password>
</server>
</servers>
</settings>`;
expect(
auth.generate(credentials(id, username, password), gpgPassphrase)
).toEqual(expectedSettings);
});
it('escapes settings.xml values while preserving parsed semantics', () => {
const id = `packages&<>"'é`;
const username = `USER&<>"'é`;
const password = `TOKEN&<>"'é`;
const gpgPassphrase = `GPG&<>"'é`;
const xml = auth.generate(
credentials(id, username, password),
gpgPassphrase
);
const parsed = parseXmlObject(xml) as any;
expect(parsed.settings.interactiveMode).toBe('false');
expect(xmlElementText(xml, 'id')).toBe(id);
expect(xmlElementText(xml, 'username')).toBe(`\${env.${username}}`);
expect(xmlElementText(xml, 'password')).toBe(`\${env.${password}}`);
expect(xmlElementText(xml, 'gpg.passphraseEnvName')).toBe(gpgPassphrase);
expect(parsed.settings.activeProfiles.activeProfile).toBe('setup-java-gpg');
});
it('generates ordered settings.xml entries for multiple servers', () => {
const servers = [
{
id: 'releases',
usernameEnvVar: 'RELEASES_USERNAME',
passwordEnvVar: 'RELEASES_PASSWORD'
},
{
id: 'snapshots',
usernameEnvVar: 'SNAPSHOTS_USERNAME',
passwordEnvVar: 'SNAPSHOTS_PASSWORD'
}
];
const parsed = parseXmlObject(auth.generate(servers)) as any;
expect(parsed.settings.servers.server).toEqual([
{
id: 'releases',
username: '${env.RELEASES_USERNAME}',
password: '${env.RELEASES_PASSWORD}'
},
{
id: 'snapshots',
username: '${env.SNAPSHOTS_USERNAME}',
password: '${env.SNAPSHOTS_PASSWORD}'
}
]);
});
it('generates an active profile with Central before custom repositories by default', () => {
const parsed = parseXmlObject(
auth.generate(
credentials('packages', 'USERNAME', 'PASSWORD'),
undefined,
repositorySettings([
{
id: 'private',
url: 'https://repo.example.com/maven',
snapshotsEnabled: true
}
])
)
) as any;
expect(parsed.settings.profiles.profile).toEqual({
id: 'setup-java-repositories',
repositories: {
repository: [
{
id: 'central',
url: 'https://repo.maven.apache.org/maven2',
snapshots: {enabled: 'false'}
},
{
id: 'private',
url: 'https://repo.example.com/maven',
snapshots: {enabled: 'true'}
}
]
}
});
expect(parsed.settings.activeProfiles.activeProfile).toBe(
'setup-java-repositories'
);
});
it('places custom repositories before Central when configured', () => {
const parsed = parseXmlObject(
auth.generate(
credentials('packages', 'USERNAME', 'PASSWORD'),
undefined,
repositorySettings(
[
{
id: 'first',
url: 'https://first.example.com',
snapshotsEnabled: false
},
{
id: 'second',
url: 'https://second.example.com',
snapshotsEnabled: true
}
],
true,
false
)
)
) as any;
expect(
parsed.settings.profiles.profile.repositories.repository.map(
(repository: {id: string}) => repository.id
)
).toEqual(['first', 'second', 'central']);
});
it.each([
{
name: 'Central first by default',
prioritizeCentral: undefined,
expectedRepositories: [
{
id: 'central',
url: 'https://repo.maven.apache.org/maven2',
snapshots: {enabled: 'false'}
},
{
id: 'first',
url: 'https://first.example.com/plugins',
snapshots: {enabled: 'false'}
},
{
id: 'second',
url: 'https://second.example.com/plugins',
snapshots: {enabled: 'true'}
}
]
},
{
name: 'custom plugins before Central',
prioritizeCentral: false,
expectedRepositories: [
{
id: 'first',
url: 'https://first.example.com/plugins',
snapshots: {enabled: 'false'}
},
{
id: 'second',
url: 'https://second.example.com/plugins',
snapshots: {enabled: 'true'}
},
{
id: 'central',
url: 'https://repo.maven.apache.org/maven2',
snapshots: {enabled: 'false'}
}
]
}
])(
'generates plugin-only settings with $name',
({prioritizeCentral, expectedRepositories}) => {
const xml = auth.generate(
[],
undefined,
repositorySettings([], true, prioritizeCentral, [
{
id: 'first',
url: 'https://first.example.com/plugins',
snapshotsEnabled: false
},
{
id: 'second',
url: 'https://second.example.com/plugins',
snapshotsEnabled: true
}
])
);
expect(parseXmlObject(xml)).toEqual(
expect.objectContaining({
settings: expect.objectContaining({
profiles: {
profile: {
id: 'setup-java-repositories',
pluginRepositories: {
pluginRepository: expectedRepositories
}
}
},
activeProfiles: {activeProfile: 'setup-java-repositories'}
})
})
);
}
);
it('excludes Central from the generated repository profile when configured', () => {
const parsed = parseXmlObject(
auth.generate(
credentials('packages', 'USERNAME', 'PASSWORD'),
undefined,
repositorySettings(
[
{
id: 'private',
url: 'https://repo.example.com',
snapshotsEnabled: false
}
],
false
)
)
) as any;
expect(parsed.settings.profiles.profile.repositories.repository).toEqual([
{
id: 'private',
url: 'https://repo.example.com',
snapshots: {enabled: 'false'}
},
{
id: 'central',
url: 'https://repo.maven.apache.org/maven2',
releases: {enabled: 'false'},
snapshots: {enabled: 'false'}
}
]);
expect(
parsed.settings.profiles.profile.pluginRepositories.pluginRepository
).toEqual(disabledCentralRepositoryXml);
});
it('combines repository and GPG configuration in shared profile blocks', () => {
const parsed = parseXmlObject(
auth.generate(
credentials('packages', 'USERNAME', 'PASSWORD'),
'GPG_PASSPHRASE',
repositorySettings(
[
{
id: 'private',
url: 'https://repo.example.com',
snapshotsEnabled: false
}
],
false
)
)
) as any;
expect(parsed.settings.profiles.profile).toEqual([
{
id: 'setup-java-repositories',
repositories: {
repository: [
{
id: 'private',
url: 'https://repo.example.com',
snapshots: {enabled: 'false'}
},
{
id: 'central',
url: 'https://repo.maven.apache.org/maven2',
releases: {enabled: 'false'},
snapshots: {enabled: 'false'}
}
]
},
pluginRepositories: {
pluginRepository: disabledCentralRepositoryXml
}
},
{
id: 'setup-java-gpg',
properties: {['gpg.passphraseEnvName']: 'GPG_PASSPHRASE'}
}
]);
expect(parsed.settings.activeProfiles.activeProfile).toEqual([
'setup-java-repositories',
'setup-java-gpg'
]);
});
it.each(
repositoryConfigurations.flatMap(configuration =>
[
{includeCentral: true, prioritizeCentral: true},
{includeCentral: true, prioritizeCentral: false},
{includeCentral: false, prioritizeCentral: true},
{includeCentral: false, prioritizeCentral: false}
].map(controls => ({...configuration, ...controls}))
)
)(
'generates $name repositories with includeCentral=$includeCentral and prioritizeCentral=$prioritizeCentral',
({repositories, pluginRepositories, includeCentral, prioritizeCentral}) => {
const xml = auth.generate(
credentials('private', 'USERNAME', 'PASSWORD'),
undefined,
repositorySettings(
repositories,
includeCentral,
prioritizeCentral,
pluginRepositories
)
);
const expectedEntries = (configured: boolean) => {
const entries = includeCentral
? configured
? prioritizeCentral
? [centralRepositoryXml, privateRepositoryXml]
: [privateRepositoryXml, centralRepositoryXml]
: []
: configured
? [privateRepositoryXml, disabledCentralRepositoryXml]
: [disabledCentralRepositoryXml];
return entries.length === 1 ? entries[0] : entries;
};
const includeRepositories = repositories.length > 0 || !includeCentral;
const includePluginRepositories =
pluginRepositories.length > 0 || !includeCentral;
expect(parseXmlObject(xml)).toMatchObject({
settings: {
profiles: {
profile: {
id: 'setup-java-repositories',
...(includeRepositories
? {
repositories: {
repository: expectedEntries(repositories.length > 0)
}
}
: {}),
...(includePluginRepositories
? {
pluginRepositories: {
pluginRepository: expectedEntries(
pluginRepositories.length > 0
)
}
}
: {})
}
},
activeProfiles: {activeProfile: 'setup-java-repositories'}
}
});
if (!includeRepositories) {
expect(xml).not.toContain('<repositories>');
}
if (!includePluginRepositories) {
expect(xml).not.toContain('<pluginRepositories>');
}
}
);
it.each(repositoryConfigurations)(
'handles explicit Central independently for $name repositories',
({repositories, pluginRepositories}) => {
const central = {
id: 'central',
url: 'https://mirror.example.com/maven',
snapshotsEnabled: true
};
const explicitCentralXml = {
id: central.id,
url: central.url,
snapshots: {enabled: 'true'}
};
const xml = auth.generate(
[],
undefined,
repositorySettings(
repositories.length ? [central] : [],
false,
true,
pluginRepositories.length ? [central] : []
)
);
expect(parseXmlObject(xml)).toMatchObject({
settings: {
profiles: {
profile: {
repositories: {
repository: repositories.length
? explicitCentralXml
: disabledCentralRepositoryXml
},
pluginRepositories: {
pluginRepository: pluginRepositories.length
? explicitCentralXml
: disabledCentralRepositoryXml
}
}
}
}
});
expect(xml.match(/<id>central<\/id>/g)).toHaveLength(2);
}
);
it('escapes plugin repository values while preserving parsed semantics', () => {
const repository = {
id: `private&<>"'é`,
url: `https://repo.example.com:8443/a?x=1&y=<value>"'é`,
snapshotsEnabled: false
};
const xml = auth.generate(
credentials(repository.id, 'USERNAME', 'PASSWORD'),
undefined,
repositorySettings([], true, true, [repository])
);
expect(parseXmlObject(xml)).toMatchObject({
settings: {
servers: {server: {id: repository.id}},
profiles: {
profile: {
pluginRepositories: {
pluginRepository: [
centralRepositoryXml,
{
id: repository.id,
url: repository.url,
snapshots: {enabled: 'false'}
}
]
}
}
}
}
});
expect(xml).toContain('private&amp;&lt;&gt;');
expect(xml).toContain('x=1&amp;y=&lt;value&gt;');
expect(xml).not.toContain('<repositories>');
});
it('escapes repository values while preserving parsed semantics', () => {
const repository = {
id: `private&<>"'é`,
url: `https://repo.example.com/a?x=1&y=<value>"'é`,
snapshotsEnabled: true
};
const xml = auth.generate(
credentials('packages', 'USERNAME', 'PASSWORD'),
undefined,
repositorySettings([repository], false)
);
const parsed = parseXmlObject(xml) as any;
expect(
parsed.settings.profiles.profile.repositories.repository.find(
(entry: {id: string}) => entry.id === repository.id
)
).toEqual({
id: repository.id,
url: repository.url,
snapshots: {enabled: 'true'}
});
});
it('parses and trims multiline Maven server credentials', () => {
expect(
auth.parseMavenServerCredentials([
'',
' releases : RELEASES_USERNAME : RELEASES_PASSWORD ',
'snapshots:SNAPSHOTS_USERNAME:SNAPSHOTS_PASSWORD'
])
).toEqual([
{
id: 'releases',
usernameEnvVar: 'RELEASES_USERNAME',
passwordEnvVar: 'RELEASES_PASSWORD'
},
{
id: 'snapshots',
usernameEnvVar: 'SNAPSHOTS_USERNAME',
passwordEnvVar: 'SNAPSHOTS_PASSWORD'
}
]);
});
it.each(repositoryInputs)(
'parses %s while preserving colons in URLs',
inputName => {
expect(
auth.parseMavenRepositories(
[
'',
' private : https://repo.example.com:8443/maven : TRUE ',
'releases:https://repo.example.com/releases:false'
],
true,
inputName
)
).toEqual([
{
id: 'private',
url: 'https://repo.example.com:8443/maven',
snapshotsEnabled: true
},
{
id: 'releases',
url: 'https://repo.example.com/releases',
snapshotsEnabled: false
}
]);
}
);
it.each(
[
{
entries: ['private'],
error:
'Invalid mvn-repositories entry at line 1. Expected format: repository-id:repository-url:snapshots-enabled'
},
{
entries: ['private:https://repo.example.com'],
error:
"Invalid snapshots-enabled value '//repo.example.com' in mvn-repositories entry at line 1. Expected true or false"
},
{
entries: ['private::true'],
error:
'Invalid mvn-repositories entry at line 1. repository-id, repository URL, and snapshots-enabled are required'
},
{
entries: ['private:https://repo.example.com:'],
error:
'Invalid mvn-repositories entry at line 1. repository-id, repository URL, and snapshots-enabled are required'
},
{
entries: ['', 'private:https://repo.example.com:true', ' :url:false'],
error:
'Invalid mvn-repositories entry at line 3. repository-id, repository URL, and snapshots-enabled are required'
},
{
entries: ['private:https://repo.example.com:sometimes'],
error:
"Invalid snapshots-enabled value 'sometimes' in mvn-repositories entry at line 1. Expected true or false"
}
].flatMap(testCase =>
repositoryInputs.map(inputName => ({
...testCase,
inputName,
error: testCase.error.replace('mvn-repositories', inputName)
}))
)
)('rejects malformed $inputName: $entries', ({entries, error, inputName}) => {
expect(() => auth.parseMavenRepositories(entries, true, inputName)).toThrow(
error
);
});
it.each(repositoryInputs)('rejects duplicate IDs in %s', inputName => {
expect(() =>
auth.parseMavenRepositories(
[
'private:https://first.example.com:false',
'private:https://second.example.com:true'
],
true,
inputName
)
).toThrow(`Duplicate repository-id 'private' in ${inputName} input`);
});
it.each(repositoryInputs)(
'reserves the Central ID in %s only when automatic inclusion is enabled',
inputName => {
const central = 'central:https://custom.example.com/maven:false';
expect(() =>
auth.parseMavenRepositories([central], true, inputName)
).toThrow(
"Repository-id 'central' is reserved when mvn-repositories-include-central is enabled"
);
expect(auth.parseMavenRepositories([central], false, inputName)).toEqual([
{
id: 'central',
url: 'https://custom.example.com/maven',
snapshotsEnabled: false
}
]);
}
);
it('uses an explicit Central repository instead of adding a disabled override', () => {
const parsed = parseXmlObject(
auth.generate(
credentials('packages', 'USERNAME', 'PASSWORD'),
undefined,
repositorySettings(
auth.parseMavenRepositories(
['central:https://mirror.example.com/maven:true'],
false
),
false
)
)
) as any;
expect(parsed.settings.profiles.profile.repositories.repository).toEqual({
id: 'central',
url: 'https://mirror.example.com/maven',
snapshots: {enabled: 'true'}
});
});
it('reads Maven repository settings and Central controls', () => {
(core.getMultilineInput as jest.Mock).mockImplementation((name: string) =>
name === 'mvn-repositories'
? ['private:https://repo.example.com:true']
: []
);
(core.getInput as jest.Mock).mockImplementation((name: string) => {
const inputs: Record<string, string> = {
'mvn-repositories-include-central': 'false',
'mvn-repositories-prioritize-central': 'false'
};
return inputs[name] ?? '';
});
expect(auth.getMavenRepositorySettings()).toEqual({
repositories: [
{
id: 'private',
url: 'https://repo.example.com',
snapshotsEnabled: true
}
],
pluginRepositories: [],
includeCentral: false,
prioritizeCentral: false
});
});
it('does not read repository controls when no repositories are configured', () => {
(
core.getInput as jest.MockedFunction<typeof core.getInput>
).mockReturnValue('false');
expect(auth.getMavenRepositorySettings()).toBeUndefined();
expect(core.getInput).not.toHaveBeenCalled();
});
it('ignores blank dependency and plugin repository inputs', () => {
(
core.getMultilineInput as jest.MockedFunction<
typeof core.getMultilineInput
>
).mockReturnValue(['', ' ', '\t']);
expect(auth.getMavenRepositorySettings()).toBeUndefined();
expect(core.getInput).not.toHaveBeenCalled();
});
it.each(repositoryConfigurations)(
'reads $name inputs with default Central controls and shared IDs',
({repositories, pluginRepositories}) => {
(
core.getInput as jest.MockedFunction<typeof core.getInput>
).mockReturnValue('');
(
core.getMultilineInput as jest.MockedFunction<
typeof core.getMultilineInput
>
).mockImplementation(name => {
const configured =
name === 'mvn-repositories' ? repositories : pluginRepositories;
return configured.map(
repository =>
`${repository.id}:${repository.url}:${repository.snapshotsEnabled}`
);
});
expect(auth.getMavenRepositorySettings()).toEqual({
repositories,
pluginRepositories,
includeCentral: true,
prioritizeCentral: true
});
}
);
it.each([true, false])(
'configures plugin repositories with credentials, GPG, a custom path, and overwrite=%s',
async overwriteSettings => {
const directory = path.join(m2Dir, 'custom');
const location = path.join(directory, MVN_SETTINGS_FILE);
fs.mkdirSync(directory, {recursive: true});
fs.writeFileSync(location, 'EXISTING SETTINGS');
(
core.getInput as jest.MockedFunction<typeof core.getInput>
).mockImplementation(name => {
const inputs: Record<string, string> = {
'settings-path': directory,
'overwrite-settings': String(overwriteSettings),
'gpg-passphrase-env-var': 'GPG_PASSPHRASE',
'mvn-repositories-include-central': 'false'
};
return inputs[name] ?? '';
});
(
core.getMultilineInput as jest.MockedFunction<
typeof core.getMultilineInput
>
).mockImplementation(name => {
const inputs: Record<string, string[]> = {
'mvn-server-credentials': ['private:USERNAME:PASSWORD'],
'mvn-server-repository-origins': ['private:https://repo.example.com'],
'mvn-plugin-repositories': [
'private:https://repo.example.com/maven:true'
]
};
return inputs[name] ?? [];
});
await auth.configureAuthentication();
const xml = fs.readFileSync(location, 'utf8');
if (!overwriteSettings) {
expect(xml).toBe('EXISTING SETTINGS');
return;
}
expect(parseXmlObject(xml)).toMatchObject({
settings: {
servers: {
server: {
id: 'private',
username: '${env.USERNAME}',
password: '${env.PASSWORD}',
repositoryOrigins: {
repositoryOrigin: 'https://repo.example.com'
}
}
},
profiles: {
profile: [
{
id: 'setup-java-repositories',
repositories: {repository: disabledCentralRepositoryXml},
pluginRepositories: {
pluginRepository: [
privateRepositoryXml,
disabledCentralRepositoryXml
]
}
},
{
id: 'setup-java-gpg',
properties: {'gpg.passphraseEnvName': 'GPG_PASSPHRASE'}
}
]
},
activeProfiles: {
activeProfile: ['setup-java-repositories', 'setup-java-gpg']
}
}
});
expect(fs.existsSync(settingsFile)).toBe(false);
}
);
it.each([
{
entries: ['releases:RELEASES_USERNAME'],
error:
'Invalid mvn-server-credentials entry at line 1. Expected format: server-id:USERNAME_ENV:PASSWORD_ENV'
},
{
entries: ['', 'releases:RELEASES_USERNAME:RELEASES_PASSWORD:EXTRA'],
error:
'Invalid mvn-server-credentials entry at line 2. Expected format: server-id:USERNAME_ENV:PASSWORD_ENV'
},
{
entries: ['releases::RELEASES_PASSWORD'],
error:
'Invalid mvn-server-credentials entry at line 1. server-id, username environment variable, and password environment variable are required'
}
])(
'rejects malformed Maven server credentials: $entries',
({entries, error}) => {
expect(() => auth.parseMavenServerCredentials(entries)).toThrow(error);
}
);
it('rejects duplicate Maven server ids', () => {
expect(() =>
auth.parseMavenServerCredentials([
'releases:RELEASES_USERNAME:RELEASES_PASSWORD',
'releases:OTHER_USERNAME:OTHER_PASSWORD'
])
).toThrow("Duplicate server-id 'releases' in mvn-server-credentials input");
});
it('uses multiline Maven server credentials instead of single-server inputs', () => {
(core.getMultilineInput as jest.Mock).mockImplementation((name: string) =>
name === 'mvn-server-credentials'
? [
'releases:RELEASES_USERNAME:RELEASES_PASSWORD',
'snapshots:SNAPSHOTS_USERNAME:SNAPSHOTS_PASSWORD'
]
: name === 'mvn-server-repository-origins'
? ['releases:https://repo.example.com']
: []
);
expect(auth.getMavenServerSettings()).toEqual([
{
id: 'releases',
usernameEnvVar: 'RELEASES_USERNAME',
passwordEnvVar: 'RELEASES_PASSWORD',
repositoryOrigins: ['https://repo.example.com']
},
{
id: 'snapshots',
usernameEnvVar: 'SNAPSHOTS_USERNAME',
passwordEnvVar: 'SNAPSHOTS_PASSWORD'
}
]);
expect(core.getInput).not.toHaveBeenCalled();
});
it('uses the existing single-server inputs when multiline credentials are absent', () => {
(core.getInput as jest.Mock).mockImplementation((name: string) =>
name === 'server-id' ? 'github' : ''
);
expect(auth.getMavenServerSettings()).toEqual([
{
id: 'github',
usernameEnvVar: 'GITHUB_ACTOR',
passwordEnvVar: 'GITHUB_TOKEN'
}
]);
});
it('preserves deprecated single-server aliases as fallback inputs', () => {
(core.getInput as jest.Mock).mockImplementation((name: string) => {
const inputs: Record<string, string> = {
'server-id': 'legacy',
'server-username': 'LEGACY_USERNAME',
'server-password': 'LEGACY_PASSWORD'
};
return inputs[name] ?? '';
});
expect(auth.getMavenServerSettings()).toEqual([
{
id: 'legacy',
usernameEnvVar: 'LEGACY_USERNAME',
passwordEnvVar: 'LEGACY_PASSWORD'
}
]);
expect(core.warning).toHaveBeenCalledTimes(2);
});
it('adds normalized repository origins to matching Maven servers', () => {
expect(
auth.addMavenServerRepositoryOrigins(
[
{
id: 'central',
usernameEnvVar: 'CENTRAL_USER',
passwordEnvVar: 'CENTRAL_PASS'
},
{
id: 'packages',
usernameEnvVar: 'GITHUB_ACTOR',
passwordEnvVar: 'GITHUB_TOKEN'
}
],
[
'central:https://central.sonatype.com/',
'central:https://central.sonatype.com',
'central:https://central.sonatype.com:443',
'packages:https://maven.pkg.github.com',
'packages:ssh://packages.example.com:22',
'packages:ftp://packages.example.com:21'
]
)
).toEqual([
{
id: 'central',
usernameEnvVar: 'CENTRAL_USER',
passwordEnvVar: 'CENTRAL_PASS',
repositoryOrigins: ['https://central.sonatype.com']
},
{
id: 'packages',
usernameEnvVar: 'GITHUB_ACTOR',
passwordEnvVar: 'GITHUB_TOKEN',
repositoryOrigins: [
'https://maven.pkg.github.com',
'ssh://packages.example.com:22',
'ftp://packages.example.com:21'
]
}
]);
});
it.each([
{
entries: ['central'],
error:
'Invalid mvn-server-repository-origins entry at line 1. Expected format: server-id:repository-origin'
},
{
entries: ['other:https://repo.example.com'],
error:
"Unknown server-id 'other' in mvn-server-repository-origins at line 1"
},
{
entries: ['central:https://repo.example.com/path'],
error:
"Invalid repository origin 'https://repo.example.com/path' in mvn-server-repository-origins at line 1"
},
{
entries: ['central:https:repo.example.com'],
error:
"Invalid repository origin 'https:repo.example.com' in mvn-server-repository-origins at line 1"
},
{
entries: ['central:https:/repo.example.com'],
error:
"Invalid repository origin 'https:/repo.example.com' in mvn-server-repository-origins at line 1"
},
{
entries: ['central:https://repo.example.com?'],
error:
"Invalid repository origin 'https://repo.example.com?' in mvn-server-repository-origins at line 1"
},
{
entries: ['central:https://repo.example.com#'],
error:
"Invalid repository origin 'https://repo.example.com#' in mvn-server-repository-origins at line 1"
},
{
entries: ['central:https://@repo.example.com'],
error:
"Invalid repository origin 'https://@repo.example.com' in mvn-server-repository-origins at line 1"
}
])('rejects invalid Maven server repository origins', ({entries, error}) => {
expect(() =>
auth.addMavenServerRepositoryOrigins(
[
{
id: 'central',
usernameEnvVar: 'CENTRAL_USER',
passwordEnvVar: 'CENTRAL_PASS'
}
],
entries
)
).toThrow(error);
});
function xmlElementText(xml: string, tagName: string): string {
const match = new RegExp(`<${tagName}>([\\s\\S]*?)</${tagName}>`).exec(xml);
expect(match).not.toBeNull();
return (parseXmlObject(`<value>${match?.[1]}</value>`) as {value: string})
.value;
}
function parseXmlObject(xml: string): unknown {
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '@',
parseAttributeValue: false,
parseTagValue: false,
trimValues: true
});
return parser.parse(xml);
}
it('uses deprecated input aliases and warns', () => {
const mockGetInput = core.getInput as jest.MockedFunction<
typeof core.getInput
>;
const mockWarning = core.warning as jest.MockedFunction<
typeof core.warning
>;
mockGetInput.mockImplementation(name =>
name === 'server-username' ? 'LEGACY_USERNAME' : ''
);
expect(
auth.getInputWithDeprecatedAlias(
'server-username-env-var',
'server-username',
'GITHUB_ACTOR'
)
).toBe('LEGACY_USERNAME');
expect(mockWarning).toHaveBeenCalledWith(
"The 'server-username' input is deprecated and may be removed in a future release. Please use 'server-username-env-var' instead."
);
mockGetInput.mockReset();
mockWarning.mockReset();
});
it('prefers the replacement input over its deprecated alias', () => {
const mockGetInput = core.getInput as jest.MockedFunction<
typeof core.getInput
>;
mockGetInput.mockImplementation(name => {
const inputs: Record<string, string> = {
'server-password-env-var': 'NEW_PASSWORD',
'server-password': 'LEGACY_PASSWORD'
};
return inputs[name] || '';
});
expect(
auth.getInputWithDeprecatedAlias(
'server-password-env-var',
'server-password',
'GITHUB_TOKEN'
)
).toBe('NEW_PASSWORD');
expect(core.warning).toHaveBeenCalled();
mockGetInput.mockReset();
(core.warning as jest.Mock).mockReset();
});
});