import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { claimDeployment } from '../src/agent-claim.js'; import { loadAgentCredentials, saveAgentCredentials } from '../src/agent-credentials.js'; vi.mock('../src/agent-credentials.js', () => ({ loadAgentCredentials: vi.fn(), saveAgentCredentials: vi.fn(), })); const issuer = 'https://auth.example.com'; const apiUrl = 'https://api.example.com'; const options = { apiUrl, directory: '/docs', code: 'ABCDEFGH' }; const original = { registrationId: 'reg', issuer, assertion: 'old-assertion', claimToken: 'secret-claim', accessToken: 'anonymous-token', accessTokenExpiresAt: Date.now() + 300_000, }; const discovery = { issuer, token_endpoint: `${issuer}/oauth2/token`, agent_auth: { identity_endpoint: `${issuer}/agent/identity`, claim_endpoint: `${issuer}/agent/identity/claim`, identity_types_supported: ['anonymous'], }, }; const response = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status }); const fetchMock = vi.fn(); beforeEach(() => { vi.resetAllMocks(); vi.stubGlobal('fetch', fetchMock); vi.mocked(loadAgentCredentials).mockResolvedValue({ ...original }); vi.mocked(saveAgentCredentials).mockResolvedValue(undefined); fetchMock.mockImplementation(async (url: string) => { if (url.endsWith('oauth-protected-resource')) return response({ authorization_servers: [issuer] }); if (url.endsWith('oauth-authorization-server')) return response(discovery); if (url.endsWith('/complete')) return response({ identity: { assertion: 'claimed-assertion' } }); if (url.endsWith('/oauth2/token')) return response({ access_token: 'claimed-token', expires_in: 300, token_type: 'Bearer' }); if (url.endsWith('/signup')) return response({ orgId: 'org', claimed: true }); if (url.endsWith('/claim')) return response({ attempt: { verification_uri: `${issuer}/verify?token=attempt` } }); throw new Error('Unexpected endpoint'); }); }); afterEach(() => vi.unstubAllGlobals()); describe('claim command', () => { it('completes the existing claim, saves the new identity, and signs up with a fresh token', async () => { expect(await claimDeployment(options, vi.fn())).toContain('successfully'); expect(fetchMock).toHaveBeenCalledWith( `${issuer}/agent/identity/claim/complete`, expect.objectContaining({ body: JSON.stringify({ claim_token: 'secret-claim', user_code: options.code }), }) ); expect(fetchMock).not.toHaveBeenCalledWith(`${issuer}/agent/identity/claim`, expect.anything()); expect(fetchMock).toHaveBeenCalledWith( `${apiUrl}/api/agent/signup`, expect.objectContaining({ headers: { Authorization: 'Bearer claimed-token' } }) ); expect(vi.mocked(saveAgentCredentials).mock.calls[0]?.[2]).toMatchObject({ assertion: 'claimed-assertion', claimVerified: true, accessToken: undefined, }); expect(vi.mocked(saveAgentCredentials).mock.invocationCallOrder[0]).toBeLessThan( fetchMock.mock.invocationCallOrder[3]! ); }); it('resumes after signup fails without consuming the code again', async () => { const implementation = fetchMock.getMockImplementation()!; fetchMock.mockImplementation((url, init) => url.endsWith('/signup') ? response({ code: 'unavailable' }, 503) : implementation(url, init) ); await expect(claimDeployment(options, vi.fn())).rejects.toThrow('503'); const saved = vi.mocked(saveAgentCredentials).mock.calls[0]![2]; vi.mocked(loadAgentCredentials).mockResolvedValue(saved); fetchMock.mockClear().mockImplementation(implementation); expect(await claimDeployment(options, vi.fn())).toContain('successfully'); expect(fetchMock.mock.calls.some(([url]) => url.endsWith('/complete'))).toBe(false); }); it('does not save or sign up after an invalid code', async () => { const implementation = fetchMock.getMockImplementation()!; fetchMock.mockImplementation((url, init) => url.endsWith('/complete') ? response({ code: 'invalid_user_code' }, 400) : implementation(url, init) ); await expect(claimDeployment(options, vi.fn())).rejects.toThrow('invalid_user_code'); expect(saveAgentCredentials).not.toHaveBeenCalled(); expect(fetchMock.mock.calls.some(([url]) => url.endsWith('/signup'))).toBe(false); }); it('warns on storage failure and continues signup without exposing secrets', async () => { vi.mocked(saveAgentCredentials).mockRejectedValue(new Error('keyring unavailable')); const warn = vi.fn(); const output = await claimDeployment(options, warn); expect(output).toContain('successfully'); expect(warn).toHaveBeenCalled(); expect(JSON.stringify([output, warn.mock.calls])).not.toContain('secret-claim'); expect(JSON.stringify([output, warn.mock.calls])).not.toContain('claimed-assertion'); }); it('creates only a claim link for --email', async () => { const output = await claimDeployment( { apiUrl, directory: '/docs', email: 'person@example.com' }, vi.fn() ); expect(output).toContain('mint claim --code'); expect( fetchMock.mock.calls.some(([url]) => url.endsWith('/signup') || url.endsWith('/complete')) ).toBe(false); }); it('fails before network calls when credentials are missing', async () => { vi.mocked(loadAgentCredentials).mockResolvedValue(null); await expect(claimDeployment(options, vi.fn())).rejects.toThrow('No saved agent registration'); expect(fetchMock).not.toHaveBeenCalled(); }); it.each([{ code: undefined }, { code: ' ', email: undefined }, { email: 'person@example.com' }])( 'rejects missing or conflicting arguments: %o', async (override) => { await expect(claimDeployment({ ...options, ...override }, vi.fn())).rejects.toThrow( 'Provide either' ); expect(fetchMock).not.toHaveBeenCalled(); } ); it('rejects a different issuer without sending the claim token', async () => { vi.mocked(loadAgentCredentials).mockResolvedValue({ ...original, issuer: 'https://other.example.com', }); await expect(claimDeployment(options, vi.fn())).rejects.toThrow('another issuer'); expect(fetchMock.mock.calls).toHaveLength(2); }); it('does not report success if signup returns an anonymous identity', async () => { const implementation = fetchMock.getMockImplementation()!; fetchMock.mockImplementation((url, init) => url.endsWith('/signup') ? response({ orgId: 'org', claimed: false }) : implementation(url, init) ); await expect(claimDeployment(options, vi.fn())).rejects.toThrow('unexpected response'); }); });