Files
setup-java/__tests__/auth.test.ts
Bruno Borges df4305dee7 Support Maven mirrors in generated settings.xml
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 59df028a-0d1b-49cb-8126-ab26408663e4
2026-10-16 09:39:48 -04:00

1586 lines
48 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([
'*',
'central',
'external:*',
'external:http:*',
'*,!internal',
'external:*,!internal',
'!internal,external:http:*',
'external:*,!external:http:*'
])('preserves the mirror selector %s and URL colons', mirrorOf => {
expect(
auth.parseMavenMirrors([
'',
' \t ',
` private : https://[2001:db8::1]:8443/maven : ${mirrorOf} `,
'public:https://repo.example.com:443/releases:central'
])
).toEqual([
{
id: 'private',
url: 'https://[2001:db8::1]:8443/maven',
mirrorOf
},
{
id: 'public',
url: 'https://repo.example.com:443/releases',
mirrorOf: 'central'
}
]);
});
it.each([
'private',
'private:https://repo.example.com',
':https://repo.example.com:*',
'private::*',
'private:https://repo.example.com:',
'private: :*',
'private:https://repo.example.com: '
])('rejects malformed mirror entry %s with a line number', entry => {
expect(() => auth.parseMavenMirrors(['', entry])).toThrow(
'Invalid mvn-mirrors entry at line 2.'
);
});
it('rejects duplicate mirror IDs after trimming', () => {
expect(() =>
auth.parseMavenMirrors([
'private:https://first.example.com:*',
' private :https://second.example.com:central'
])
).toThrow("Duplicate mirror-id 'private' in mvn-mirrors input at line 2");
});
it('leaves settings unchanged for absent or blank mirrors', () => {
const servers = credentials('github', 'USERNAME', 'PASSWORD');
expect(auth.parseMavenMirrors(['', ' ', '\t'])).toEqual([]);
expect(auth.generate(servers, undefined, undefined, [])).toBe(
auth.generate(servers)
);
expect(auth.generate(servers)).not.toContain('<mirrors>');
});
it('generates public mirrors without credentials or repository profiles', () => {
const mirrors = auth.parseMavenMirrors([
'public:https://repo.example.com:external:*',
'fallback:https://other.example.com:central'
]);
const xml = auth.generate([], undefined, undefined, mirrors);
expect(parseXmlObject(xml)).toMatchObject({
settings: {mirrors: {mirror: mirrors}}
});
expect(xml).not.toContain('<profiles>');
expect(xml).not.toContain('<activeProfiles>');
});
it('escapes all mirror fields as XML text', () => {
const mirror = {
id: 'private<&>',
url: 'https://repo.example.com/maven?a=1&b=<value>',
mirrorOf: '*,!internal<&>'
};
const xml = auth.generate([], undefined, undefined, [mirror]);
expect(xml).toContain('<id>private&lt;&amp;&gt;</id>');
expect(xml).toContain('a=1&amp;b=&lt;value&gt;');
expect(xml).toContain('<mirrorOf>*,!internal&lt;&amp;&gt;</mirrorOf>');
expect(parseXmlObject(xml)).toMatchObject({
settings: {mirrors: {mirror}}
});
});
it.each([
{combined: false, overwrite: true},
{combined: true, overwrite: true},
{combined: true, overwrite: false}
])(
'configures mirrors with combined=$combined and overwrite=$overwrite',
async ({combined, overwrite}) => {
const directory = path.join(m2Dir, 'mirrors');
const location = path.join(directory, MVN_SETTINGS_FILE);
fs.mkdirSync(directory, {recursive: true});
fs.writeFileSync(location, 'EXISTING SETTINGS');
const url = 'https://maven.example.com/releases';
const inputs: Record<string, string> = {
'settings-path': directory,
'overwrite-settings': String(overwrite),
'mvn-repositories-include-central': 'false',
...(combined ? {'gpg-passphrase-env-var': 'GPG_PASSPHRASE'} : {})
};
const multilineInputs: Record<string, string[]> = {
'mvn-server-credentials': ['private:PRIVATE_USERNAME:PRIVATE_TOKEN'],
'mvn-mirrors': [`private:${url}:*`],
...(combined
? {
'mvn-repositories': [`private:${url}:false`],
'mvn-plugin-repositories': [`private:${url}:false`]
}
: {})
};
jest.mocked(core.getInput).mockImplementation(name => inputs[name] ?? '');
jest
.mocked(core.getMultilineInput)
.mockImplementation(name => multilineInputs[name] ?? []);
await auth.configureAuthentication();
const xml = fs.readFileSync(location, 'utf8');
if (!overwrite) {
expect(xml).toBe('EXISTING SETTINGS');
return;
}
expect(parseXmlObject(xml)).toMatchObject({
settings: {
mirrors: {mirror: {id: 'private', url, mirrorOf: '*'}},
servers: {
server: {
id: 'private',
username: '${env.PRIVATE_USERNAME}',
password: '${env.PRIVATE_TOKEN}'
}
}
}
});
if (combined) {
expect(xml).toContain('<repositories>');
expect(xml).toContain('<pluginRepositories>');
expect(xml).toContain('<gpg.passphraseEnvName>GPG_PASSPHRASE');
} else {
expect(xml).not.toContain('<profiles>');
expect(core.getInput).not.toHaveBeenCalledWith(
'mvn-repositories-include-central'
);
}
}
);
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();
});
});