Files
login-action/__tests__/context.test.ts
CrazyMax 4e12eabf92 use registry hosts for scoped credentials
Parse registry URLs so scoped ECR credentials use the same config directory
for login, logout, and Buildx lookup. Preserve explicit default ports.

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
2026-10-17 12:24:46 +02:00

211 lines
6.3 KiB
TypeScript

import {afterEach, expect, test, vi} from 'vitest';
import * as path from 'path';
import {Buildx} from '@docker/actions-toolkit/lib/buildx/buildx.js';
import {getAuthList, getInputs, scopeToConfigDir} from '../src/context.js';
afterEach(() => {
vi.restoreAllMocks();
for (const key of Object.keys(process.env)) {
if (key.startsWith('INPUT_')) {
delete process.env[key];
}
}
});
test('with password and username getInputs does not throw error', async () => {
process.env['INPUT_USERNAME'] = 'dbowie';
process.env['INPUT_PASSWORD'] = 'groundcontrol';
process.env['INPUT_LOGOUT'] = 'true';
expect(() => {
getInputs();
}).not.toThrow();
});
test('getAuthList uses the default Docker Hub registry when computing scoped config dir', async () => {
process.env['INPUT_USERNAME'] = 'dbowie';
process.env['INPUT_PASSWORD'] = 'groundcontrol';
process.env['INPUT_SCOPE'] = 'myscope';
process.env['INPUT_LOGOUT'] = 'false';
const [auth] = getAuthList(getInputs());
expect(auth).toMatchObject({
registry: 'docker.io',
configDir: path.join(Buildx.configDir, 'config', 'registry-1.docker.io', 'myscope')
});
});
test('getAuthList supports @ scopes appended to the registry config dir', async () => {
process.env['INPUT_USERNAME'] = 'dbowie';
process.env['INPUT_PASSWORD'] = 'groundcontrol';
process.env['INPUT_SCOPE'] = '@push';
process.env['INPUT_LOGOUT'] = 'false';
const [auth] = getAuthList(getInputs());
expect(auth).toMatchObject({
configDir: path.join(Buildx.configDir, 'config', 'registry-1.docker.io') + '@push'
});
});
test('getAuthList supports repository scopes with appended actions', async () => {
process.env['INPUT_USERNAME'] = 'dbowie';
process.env['INPUT_PASSWORD'] = 'groundcontrol';
process.env['INPUT_SCOPE'] = 'docker/buildx-bin@push';
process.env['INPUT_LOGOUT'] = 'false';
const [auth] = getAuthList(getInputs());
expect(auth).toMatchObject({
configDir: path.join(Buildx.configDir, 'config', 'registry-1.docker.io', 'docker', 'buildx-bin@push')
});
});
test('getAuthList supports comma-separated scope actions', async () => {
process.env['INPUT_USERNAME'] = 'dbowie';
process.env['INPUT_PASSWORD'] = 'groundcontrol';
process.env['INPUT_SCOPE'] = 'docker/buildx-bin@pull,push';
process.env['INPUT_LOGOUT'] = 'false';
const [auth] = getAuthList(getInputs());
expect(auth).toMatchObject({
configDir: path.join(Buildx.configDir, 'config', 'registry-1.docker.io', 'docker', 'buildx-bin@pull,push')
});
});
// prettier-ignore
test.each([
'../../../../work/leaked',
'..',
'foo/../../../../etc',
'@../../../leaked',
'foo/bar@../../../leaked',
'@push@pull',
'foo/bar@push@pull',
'@push,',
'@,push',
'@pull,,push',
'@Push',
path.join(path.parse(Buildx.configDir).root, 'work', 'leaked')
])('getAuthList rejects unsafe or unsupported scope path: %s', async scope => {
expect(() => {
getAuthList({
registry: '',
username: 'dbowie',
password: 'groundcontrol',
scope,
ecr: '',
logout: false,
registryAuth: ''
});
}).toThrow(/Invalid scope/);
});
// prettier-ignore
test.each([
'../../../../work/leaked',
'..',
'foo/../../../../etc',
path.join(path.parse(Buildx.configDir).root, 'work', 'leaked')
])('getAuthList rejects unsafe registry path: %s', async registry => {
expect(() => {
getAuthList({
registry,
username: 'dbowie',
password: 'groundcontrol',
scope: '@push',
ecr: '',
logout: false,
registryAuth: ''
});
}).toThrow(/Invalid registry/);
});
test('getAuthList skips secret masking when registry-auth password is absent', async () => {
const stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
const [auth] = getAuthList({
registry: '',
username: '',
password: '',
scope: '',
ecr: '',
logout: true,
registryAuth: '- registry: public.ecr.aws\n'
});
expect(stdoutWriteSpy.mock.calls.map(call => call[0]).join('')).not.toContain('::add-mask::');
expect(auth).toMatchObject({
registry: 'public.ecr.aws',
ecr: 'auto'
});
});
test('getAuthList supports password-less Docker Hub registry-auth for OIDC', async () => {
const [auth] = getAuthList({
registry: '',
username: '',
password: '',
scope: '',
ecr: '',
logout: true,
registryAuth: '- username: docker-org\n'
});
expect(auth).toMatchObject({
registry: 'docker.io',
username: 'docker-org',
password: undefined,
ecr: 'auto'
});
});
test('getAuthList masks registry-auth password when present', async () => {
const stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
getAuthList({
registry: '',
username: '',
password: '',
scope: '',
ecr: '',
logout: true,
registryAuth: '- registry: ghcr.io\n username: dbowie\n password: groundcontrol\n'
});
expect(stdoutWriteSpy.mock.calls.map(call => call[0]).join('')).toContain('::add-mask::groundcontrol');
});
// prettier-ignore
test.each([
['false', 'false'],
['"false"', 'false'],
['true', 'true'],
['auto', 'auto'],
['""', 'auto']
])('getAuthList normalizes registry-auth ecr: %s', (ecr, expected) => {
const [auth] = getAuthList({
registry: '',
username: '',
password: '',
scope: '',
ecr: '',
logout: true,
registryAuth: `- registry: 012345678910.dkr.ecr.eu-west-3.amazonaws.com\n username: AWS\n password: token\n ecr: ${ecr}\n`
});
expect(auth.ecr).toBe(expected);
});
// prettier-ignore
test.each([
['ghcr.io', 'ghcr.io'],
['ghcr.io/', 'ghcr.io'],
['https://ghcr.io', 'ghcr.io'],
['https://ghcr.io/', 'ghcr.io'],
['https://ghcr.io/v2/', 'ghcr.io'],
['http://localhost:5000', 'localhost:5000'],
['http://localhost:80', 'localhost:80'],
['https://ghcr.io:443', 'ghcr.io:443'],
['https://012345678910.dkr.ecr.eu-west-3.amazonaws.com', '012345678910.dkr.ecr.eu-west-3.amazonaws.com'],
['https://docker.io', 'registry-1.docker.io']
])('scopeToConfigDir uses the registry host for %s', (registry, host) => {
expect(scopeToConfigDir(registry, 'myorg/myimage@push')).toBe(path.join(Buildx.configDir, 'config', host, 'myorg', 'myimage') + '@push');
});
test('scopeToConfigDir rejects a URL whose host escapes the config directory', () => {
expect(() => scopeToConfigDir('https://../', '@push')).toThrow(/Invalid registry/);
});