145 lines
4.8 KiB
JavaScript
145 lines
4.8 KiB
JavaScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { CONFIG } from '../../src/config/index.js';
|
|
import {
|
|
fetchToken,
|
|
getScopeFromUrl,
|
|
handleDockerAuth,
|
|
normalizeRegistryApiPath,
|
|
parseAuthenticate,
|
|
readRegistryTokenResponse
|
|
} from '../../src/protocols/docker.js';
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('Docker helper coverage', () => {
|
|
it('throws on malformed authenticate headers', () => {
|
|
expect(() => parseAuthenticate('Bearer service="registry.docker.io"')).toThrow(
|
|
/invalid WWW-Authenticate/
|
|
);
|
|
});
|
|
|
|
it('includes authorization when fetching registry tokens', async () => {
|
|
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
new Response('{}', {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})
|
|
);
|
|
|
|
await fetchToken(
|
|
{ realm: 'https://auth.example.com/token', service: 'registry.example.com' },
|
|
'repository:demo/app:pull',
|
|
'Bearer registry-secret'
|
|
);
|
|
|
|
const upstreamHeaders = new Headers(fetchSpy.mock.calls[0][1]?.headers);
|
|
expect(String(fetchSpy.mock.calls[0][0])).toContain('scope=repository%3Ademo%2Fapp%3Apull');
|
|
expect(upstreamHeaders.get('Authorization')).toBe('Bearer registry-secret');
|
|
});
|
|
|
|
it('reads both token formats and rejects malformed token payloads', async () => {
|
|
await expect(
|
|
readRegistryTokenResponse(
|
|
new Response(JSON.stringify({ token: 'abc123' }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})
|
|
)
|
|
).resolves.toBe('abc123');
|
|
|
|
await expect(
|
|
readRegistryTokenResponse(
|
|
new Response(JSON.stringify({ access_token: 'def456' }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})
|
|
)
|
|
).resolves.toBe('def456');
|
|
|
|
await expect(
|
|
readRegistryTokenResponse(
|
|
new Response(JSON.stringify('invalid-shape'), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})
|
|
)
|
|
).resolves.toBeNull();
|
|
|
|
await expect(
|
|
readRegistryTokenResponse(
|
|
new Response('{not-json', {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})
|
|
)
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('derives catalog and empty scopes from registry paths', () => {
|
|
const catalogUrl = new URL('https://example.com/cr/ghcr/v2/_catalog');
|
|
const unsupportedUrl = new URL('https://example.com/cr/ghcr/v2');
|
|
|
|
expect(getScopeFromUrl(catalogUrl, catalogUrl.pathname, 'cr-ghcr')).toBe('registry:catalog:*');
|
|
expect(getScopeFromUrl(unsupportedUrl, unsupportedUrl.pathname, 'cr-ghcr')).toBe('');
|
|
});
|
|
|
|
it('leaves normalized registry paths untouched when no library prefix is needed', () => {
|
|
expect(normalizeRegistryApiPath('cr-ghcr', '/v2/org/app/manifests/latest')).toBe(
|
|
'/v2/org/app/manifests/latest'
|
|
);
|
|
expect(normalizeRegistryApiPath('cr-docker', '/v2/library/nginx/manifests/latest')).toBe(
|
|
'/v2/library/nginx/manifests/latest'
|
|
);
|
|
expect(normalizeRegistryApiPath('cr-docker', '/v2/_catalog')).toBe('/v2/_catalog');
|
|
});
|
|
|
|
it('returns a generic error for unsupported Docker auth scopes', async () => {
|
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
const request = new Request(
|
|
'https://example.com/v2/auth?scope=repository:cr/unknown/private:pull'
|
|
);
|
|
|
|
const response = await handleDockerAuth(request, new URL(request.url), CONFIG);
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(await response.text()).toBe('Invalid Docker authentication request');
|
|
expect(errorSpy).toHaveBeenCalledWith(
|
|
'Failed to resolve Docker auth target:',
|
|
expect.any(Error)
|
|
);
|
|
});
|
|
|
|
it('forwards upstream auth responses that are not challenges', async () => {
|
|
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
new Response('already-authorized', {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'text/plain' }
|
|
})
|
|
);
|
|
|
|
const request = new Request('https://example.com/cr/ghcr/v2/auth?service=Xget');
|
|
const response = await handleDockerAuth(request, new URL(request.url), CONFIG);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(await response.text()).toBe('already-authorized');
|
|
});
|
|
|
|
it('forwards 401 responses without authenticate headers from the upstream root probe', async () => {
|
|
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
new Response('missing-authenticate', {
|
|
status: 401,
|
|
headers: { 'Content-Type': 'text/plain' }
|
|
})
|
|
);
|
|
|
|
const request = new Request('https://example.com/cr/ghcr/v2/auth?service=Xget');
|
|
const response = await handleDockerAuth(request, new URL(request.url), CONFIG);
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(await response.text()).toBe('missing-authenticate');
|
|
});
|
|
});
|