mirror of
https://gh-proxy.org/https://github.com/docker/login-action.git
synced 2026-07-23 03:13:00 +08:00
14d6a7934e
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
134 lines
5.6 KiB
TypeScript
134 lines
5.6 KiB
TypeScript
import * as core from '@actions/core';
|
|
import * as httpm from '@actions/http-client';
|
|
import {beforeEach, describe, expect, test, vi} from 'vitest';
|
|
|
|
import * as dockerhub from '../src/dockerhub.js';
|
|
|
|
vi.mock('@actions/core', () => ({
|
|
getIDToken: vi.fn(),
|
|
info: vi.fn(),
|
|
setSecret: vi.fn()
|
|
}));
|
|
|
|
const validConnectionID = '123e4567-e89b-42d3-a456-426614174000';
|
|
|
|
const httpResponse = (statusCode: number, body: string, headers: Record<string, string> = {}): httpm.HttpClientResponse => {
|
|
return {
|
|
message: {
|
|
statusCode,
|
|
headers
|
|
},
|
|
readBody: vi.fn(async () => body)
|
|
} as unknown as httpm.HttpClientResponse;
|
|
};
|
|
|
|
describe('isDockerHubOIDC', () => {
|
|
beforeEach(() => {
|
|
delete process.env.DOCKERHUB_OIDC_CONNECTIONID;
|
|
});
|
|
|
|
test.each(['', 'docker.io', 'registry-1.docker.io', 'registry-1-stage.docker.io'])('detects Docker Hub registry %p with empty password', registry => {
|
|
process.env.DOCKERHUB_OIDC_CONNECTIONID = validConnectionID;
|
|
expect(dockerhub.isDockerHubOIDC(registry, '')).toBe(true);
|
|
});
|
|
|
|
test('requires connection ID env var', () => {
|
|
expect(dockerhub.isDockerHubOIDC('docker.io', '')).toBe(false);
|
|
});
|
|
|
|
test('requires empty password', () => {
|
|
process.env.DOCKERHUB_OIDC_CONNECTIONID = validConnectionID;
|
|
expect(dockerhub.isDockerHubOIDC('docker.io', 'groundcontrol')).toBe(false);
|
|
});
|
|
|
|
test('ignores non-Docker Hub registries', () => {
|
|
process.env.DOCKERHUB_OIDC_CONNECTIONID = validConnectionID;
|
|
expect(dockerhub.isDockerHubOIDC('ghcr.io', '')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getOIDCToken', () => {
|
|
const getIDTokenMock = vi.mocked(core.getIDToken);
|
|
const setSecretMock = vi.mocked(core.setSecret);
|
|
let postSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
beforeEach(() => {
|
|
process.env.DOCKERHUB_OIDC_CONNECTIONID = validConnectionID;
|
|
delete process.env.DOCKERHUB_OIDC_EXPIREIN;
|
|
getIDTokenMock.mockResolvedValue('github-id-token');
|
|
postSpy = vi.spyOn(httpm.HttpClient.prototype, 'post').mockResolvedValue(httpResponse(200, JSON.stringify({access_token: 'hub-token'})));
|
|
});
|
|
|
|
test('exchanges GitHub OIDC token for Docker Hub token', async () => {
|
|
const credentials = await dockerhub.getOIDCToken('docker.io', 'dbowie');
|
|
|
|
expect(credentials).toEqual({
|
|
username: 'dbowie',
|
|
token: 'hub-token'
|
|
});
|
|
expect(getIDTokenMock).toHaveBeenCalledWith('https://identity.docker.com');
|
|
expect(postSpy).toHaveBeenCalledTimes(1);
|
|
expect(postSpy.mock.calls[0][0]).toBe('https://identity.docker.com/oauth/token');
|
|
|
|
const http = postSpy.mock.contexts[0] as httpm.HttpClient;
|
|
expect(http.userAgent).toBe('github.com/docker/login-action');
|
|
expect(http.requestOptions?.headers).toEqual({
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
});
|
|
|
|
const body = new URLSearchParams(postSpy.mock.calls[0][1]);
|
|
expect(body.get('grant_type')).toBe('urn:ietf:params:oauth:grant-type:token-exchange');
|
|
expect(body.get('subject_token_type')).toBe('urn:ietf:params:oauth:token-type:id_token');
|
|
expect(body.get('subject_token')).toBe('github-id-token');
|
|
expect(body.get('connection_id')).toBe(validConnectionID);
|
|
expect(body.get('expires_in')).toBe('300');
|
|
expect(setSecretMock).toHaveBeenCalledWith('hub-token');
|
|
});
|
|
|
|
test('uses custom token expiration', async () => {
|
|
process.env.DOCKERHUB_OIDC_EXPIREIN = '900';
|
|
await dockerhub.getOIDCToken('docker.io', 'dbowie');
|
|
const body = new URLSearchParams(postSpy.mock.calls[0][1]);
|
|
expect(body.get('expires_in')).toBe('900');
|
|
});
|
|
|
|
test('uses stage identity host for stage registry', async () => {
|
|
await dockerhub.getOIDCToken('registry-1-stage.docker.io', 'dbowie');
|
|
expect(getIDTokenMock).toHaveBeenCalledWith('https://identity-stage.docker.com');
|
|
expect(postSpy.mock.calls[0][0]).toBe('https://identity-stage.docker.com/oauth/token');
|
|
});
|
|
|
|
test('requires connection ID env var', async () => {
|
|
delete process.env.DOCKERHUB_OIDC_CONNECTIONID;
|
|
await expect(dockerhub.getOIDCToken('docker.io', 'dbowie')).rejects.toThrow('DOCKERHUB_OIDC_CONNECTIONID is required for Docker Hub OIDC login');
|
|
expect(getIDTokenMock).not.toHaveBeenCalled();
|
|
expect(postSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('validates connection ID', async () => {
|
|
process.env.DOCKERHUB_OIDC_CONNECTIONID = 'not-a-uuid';
|
|
await expect(dockerhub.getOIDCToken('docker.io', 'dbowie')).rejects.toThrow('Invalid DOCKERHUB_OIDC_CONNECTIONID. Must be a valid UUID.');
|
|
expect(getIDTokenMock).not.toHaveBeenCalled();
|
|
expect(postSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test.each(['not-a-number', '299', '3601'])('validates token expiration %p', async expiresIn => {
|
|
process.env.DOCKERHUB_OIDC_EXPIREIN = expiresIn;
|
|
await expect(dockerhub.getOIDCToken('docker.io', 'dbowie')).rejects.toThrow(`Invalid DOCKERHUB_OIDC_EXPIREIN: ${expiresIn}. Must be between 300 and 3600`);
|
|
expect(getIDTokenMock).not.toHaveBeenCalled();
|
|
expect(postSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('retries rate limited token requests with Retry-After', async () => {
|
|
postSpy.mockResolvedValueOnce(httpResponse(429, '', {'retry-after': '0'})).mockResolvedValueOnce(httpResponse(200, JSON.stringify({access_token: 'hub-token'})));
|
|
await dockerhub.getOIDCToken('docker.io', 'dbowie');
|
|
expect(postSpy).toHaveBeenCalledTimes(2);
|
|
expect(core.info).toHaveBeenCalledWith('Docker Hub OIDC token request rate limited, retrying in 0ms (attempt 1/5)');
|
|
});
|
|
|
|
test('throws Docker Hub API errors', async () => {
|
|
postSpy.mockResolvedValue(httpResponse(400, JSON.stringify({description: 'bad connection'})));
|
|
await expect(dockerhub.getOIDCToken('docker.io', 'dbowie')).rejects.toThrow('Docker Hub API: bad status code 400: bad connection');
|
|
});
|
|
});
|