Files
stack/packages/mosaic/src/credentials/gitea-provider.spec.ts
T

208 lines
7.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { GiteaCredentialProviderAdapter, GiteaTeamGrantProviderAdapter } from './gitea-provider.js';
import type { ResolvedCredential } from './credential-provider.dto.js';
const credential: ResolvedCredential = Object.freeze({
identity: 'seat-name',
estate: 'homelab',
host: 'git.example.invalid',
resolutionId: 'resolution-1',
secret: new TextEncoder().encode('seeded-secret-canary'),
});
function jsonResponse(body: object, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { 'content-type': 'application/json;charset=utf-8' },
});
}
describe('Gitea credential provider transport', (): void => {
it('reads the provider identity with the fixed transport and no secret in the URL', async (): Promise<void> => {
const calls: Array<{ readonly input: string; readonly init?: RequestInit }> = [];
const adapter = new GiteaCredentialProviderAdapter(
'https://git.example.invalid',
async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
calls.push({ input: String(input), ...(init === undefined ? {} : { init }) });
return jsonResponse({ id: 21, login: 'seat-name' });
},
);
const evidence = await adapter.readIdentity(credential);
expect(evidence).toEqual({
login: 'seat-name',
endpoint: 'GET /api/v1/user',
contentType: 'application/json;charset=utf-8',
});
expect(calls[0]?.input).toBe('https://git.example.invalid/api/v1/user');
expect(calls[0]?.input).not.toContain('seeded-secret-canary');
expect(new Headers(calls[0]?.init?.headers).get('user-agent')).toBe('mosaic-cred/1');
});
it('maps the authenticated provider repository object to effective permission', async (): Promise<void> => {
const adapter = new GiteaCredentialProviderAdapter(
'https://git.example.invalid',
async (): Promise<Response> =>
jsonResponse({
id: 99,
full_name: 'owner/repo',
permissions: { admin: false, push: true, pull: true },
}),
);
const evidence = await adapter.readRepositoryPermission(credential, 'owner/repo');
expect(evidence.effective).toBe('write');
expect(evidence.endpoint).toBe('GET /api/v1/repos/owner/repo');
});
it('binds an authenticated receive-pack advertisement to the supplied credential handle', async (): Promise<void> => {
const adapter = new GiteaCredentialProviderAdapter(
'https://git.example.invalid',
async (): Promise<Response> =>
new Response('001f# service=git-receive-pack\n0000', {
status: 200,
headers: {
'content-type': 'application/x-git-receive-pack-advertisement',
},
}),
);
const evidence = await adapter.probeReceivePack(credential, 'owner/repo');
expect(evidence).toEqual({
state: 'advertised',
principal: 'seat-name',
resolutionId: 'resolution-1',
contentType: 'application/x-git-receive-pack-advertisement',
});
});
it('reports authenticated and unauthenticated receive-pack refusals without inventing success', async (): Promise<void> => {
const adapter = new GiteaCredentialProviderAdapter(
'https://git.example.invalid',
async (): Promise<Response> =>
new Response('denied', { status: 403, headers: { 'content-type': 'text/plain' } }),
);
await expect(adapter.probeReceivePack(credential, 'owner/repo')).resolves.toMatchObject({
state: 'refused',
principal: 'seat-name',
resolutionId: 'resolution-1',
});
await expect(adapter.probeReceivePack(undefined, 'owner/repo')).resolves.toMatchObject({
state: 'refused',
principal: null,
resolutionId: null,
});
});
it('does not call a scope-forbidden identity read a dead credential', async (): Promise<void> => {
const adapter = new GiteaCredentialProviderAdapter(
'https://git.example.invalid',
async (): Promise<Response> => jsonResponse({ message: 'forbidden' }, 403),
);
await expect(adapter.readIdentity(credential)).rejects.toMatchObject({
code: 'identity-read-forbidden',
});
});
it('classifies only the supplied credential as rejected without inferring identity absence', async (): Promise<void> => {
let calls = 0;
const adapter = new GiteaCredentialProviderAdapter(
'https://git.example.invalid',
async (): Promise<Response> => {
calls += 1;
return jsonResponse({ message: 'unauthorized' }, 401);
},
);
await expect(adapter.readIdentity(credential)).rejects.toMatchObject({
code: 'credential-rejected',
});
expect(calls).toBe(1);
});
it('classifies a rejected credential separately when the declared identity exists', async (): Promise<void> => {
let call = 0;
const adapter = new GiteaCredentialProviderAdapter(
'https://git.example.invalid',
async (): Promise<Response> => {
call += 1;
if (call === 1) return jsonResponse({ message: 'unauthorized' }, 401);
return jsonResponse({ id: 21, login: 'seat-name' });
},
);
await expect(adapter.readIdentity(credential)).rejects.toMatchObject({
code: 'credential-rejected',
});
});
it('rejects a 200 HTML identity response as unexpected content type', async (): Promise<void> => {
const adapter = new GiteaCredentialProviderAdapter(
'https://git.example.invalid',
async (): Promise<Response> =>
new Response('<html>not an API object</html>', {
status: 200,
headers: { 'content-type': 'text/html' },
}),
);
await expect(adapter.readIdentity(credential)).rejects.toMatchObject({
code: 'unexpected-content-type',
});
});
it('reads team permission, member attachment, and repository attachment separately', async (): Promise<void> => {
const adapter = new GiteaTeamGrantProviderAdapter(
'https://git.example.invalid',
async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
const url = String(input);
if (url.endsWith('/api/v1/orgs/owner/teams')) {
return jsonResponse([{ id: 7, name: 'writers', permission: 'write' }]);
}
if (init?.method === 'PUT') return new Response(null, { status: 204 });
if (url.includes('/members/seat-name')) {
return jsonResponse({ id: 21, login: 'seat-name' });
}
if (url.includes('/repos/owner/repo')) {
return jsonResponse({ id: 4, full_name: 'owner/repo' });
}
return jsonResponse({ message: 'unexpected' }, 500);
},
);
const team = await adapter.resolveTeam(credential, 'owner', 'writers');
await adapter.addTeamMember(credential, team.id, 'seat-name');
await adapter.attachTeamRepository(credential, team.id, 'owner/repo');
await expect(adapter.readTeamMember(credential, team.id, 'seat-name')).resolves.toMatchObject({
state: 'present',
});
await expect(
adapter.readTeamRepository(credential, team.id, 'owner/repo'),
).resolves.toMatchObject({ state: 'present' });
expect(team).toMatchObject({ id: 7, name: 'writers', permission: 'write' });
});
it('never includes seeded secret material in provider error messages', async (): Promise<void> => {
const adapter = new GiteaCredentialProviderAdapter(
'https://git.example.invalid',
async (): Promise<Response> => {
throw new Error('connection reset');
},
);
let message = '';
try {
await adapter.readIdentity(credential);
} catch (error: unknown) {
message = error instanceof Error ? error.message : String(error);
}
expect(message).not.toContain('seeded-secret-canary');
expect(message).toContain('provider-unavailable');
});
});