diff --git a/src/commands/issue.ts b/src/commands/issue.ts index 15d61d6..9314549 100644 --- a/src/commands/issue.ts +++ b/src/commands/issue.ts @@ -31,13 +31,13 @@ function extractRkey(uri: string): string { * Resolve issue number or rkey to full AT-URI * @param input - User input: number ("1"), hash ("#1"), or rkey ("3mef...") * @param client - API client - * @param repoAtUri - Repository AT-URI + * @param repoId - Repository identifier (repoDid or AT-URI fallback) * @returns Object with full issue AT-URI and display identifier */ async function resolveIssueUri( input: string, client: TangledApiClient, - repoAtUri: string + repoId: string ): Promise<{ uri: string; displayId: string }> { // Strip # prefix if present const normalized = input.startsWith('#') ? input.slice(1) : input; @@ -53,7 +53,7 @@ async function resolveIssueUri( // Query all issues for this repo const { issues } = await listIssues({ client, - repoAtUri, + repoId, limit: 100, // Adjust if needed for large repos }); @@ -122,13 +122,13 @@ function createViewCommand(): Command { } // 3. Build repo AT-URI - const repoAtUri = await resolveRepoId(context.owner, context.name); + const repoId = await resolveRepoId(context.owner, context.name); // 4. Resolve issue ID to URI - const { uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoAtUri); + const { uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoId); // 5. Fetch complete issue data (record, sequential number, state) - const issueData = await getCompleteIssueData(client, issueUri, displayId, repoAtUri); + const issueData = await getCompleteIssueData(client, issueUri, displayId, repoId); // 6. Output result if (options.json !== undefined) { @@ -195,10 +195,10 @@ function createEditCommand(): Command { } // 4. Build repo AT-URI - const repoAtUri = await resolveRepoId(context.owner, context.name); + const repoId = await resolveRepoId(context.owner, context.name); // 5. Resolve issue ID to URI - const { uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoAtUri); + const { uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoId); // 6. Handle body input const body = await readBodyInput(options.body, options.bodyFile); @@ -218,7 +218,7 @@ function createEditCommand(): Command { // 9. Output result if (options.json !== undefined) { const [number, state] = await Promise.all([ - resolveSequentialNumber(displayId, updatedIssue.uri, client, repoAtUri), + resolveSequentialNumber(displayId, updatedIssue.uri, client, repoId), getIssueState({ client, issueUri: updatedIssue.uri }), ]); const issueData: IssueData = { @@ -275,19 +275,13 @@ function createCloseCommand(): Command { } // 3. Build repo AT-URI - const repoAtUri = await resolveRepoId(context.owner, context.name); + const repoId = await resolveRepoId(context.owner, context.name); // 4. Resolve issue ID to URI - const { uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoAtUri); + const { uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoId); // 5. Fetch complete issue data (state will be 'closed' after operation) - const issueData = await getCompleteIssueData( - client, - issueUri, - displayId, - repoAtUri, - 'closed' - ); + const issueData = await getCompleteIssueData(client, issueUri, displayId, repoId, 'closed'); // 6. Close issue await closeIssue({ client, issueUri }); @@ -332,19 +326,13 @@ function createReopenCommand(): Command { } // 3. Build repo AT-URI - const repoAtUri = await resolveRepoId(context.owner, context.name); + const repoId = await resolveRepoId(context.owner, context.name); // 4. Resolve issue ID to URI - const { uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoAtUri); + const { uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoId); // 5. Fetch complete issue data (state will be 'open' after operation) - const issueData = await getCompleteIssueData( - client, - issueUri, - displayId, - repoAtUri, - 'open' - ); + const issueData = await getCompleteIssueData(client, issueUri, displayId, repoId, 'open'); // 6. Reopen issue await reopenIssue({ client, issueUri }); @@ -421,7 +409,7 @@ function createCreateCommand(): Command { } // 5. Build repo AT-URI - const repoAtUri = await resolveRepoId(context.owner, context.name); + const repoId = await resolveRepoId(context.owner, context.name); // 6. Create issue (suppress progress message in JSON mode) if (options.json === undefined) { @@ -429,13 +417,13 @@ function createCreateCommand(): Command { } const issue = await createIssue({ client, - repoAtUri, + repoId, title: validTitle, body, }); // 7. Compute sequential number - const { issues: allIssues } = await listIssues({ client, repoAtUri, limit: 100 }); + const { issues: allIssues } = await listIssues({ client, repoId, limit: 100 }); const sortedAll = allIssues.sort( (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() ); @@ -496,7 +484,7 @@ function createListCommand(): Command { } // 3. Build repo AT-URI - const repoAtUri = await resolveRepoId(context.owner, context.name); + const repoId = await resolveRepoId(context.owner, context.name); // 4. Fetch issues const limit = Number.parseInt(options.limit, 10); @@ -507,7 +495,7 @@ function createListCommand(): Command { const { issues } = await listIssues({ client, - repoAtUri, + repoId, limit, }); diff --git a/src/lib/issues-api.ts b/src/lib/issues-api.ts index 8e0fd7f..990c4ec 100644 --- a/src/lib/issues-api.ts +++ b/src/lib/issues-api.ts @@ -32,7 +32,7 @@ export interface IssueWithMetadata extends IssueRecord { */ export interface CreateIssueParams { client: TangledApiClient; - repoAtUri: string; + repoId: string; title: string; body?: string; } @@ -42,7 +42,7 @@ export interface CreateIssueParams { */ export interface ListIssuesParams { client: TangledApiClient; - repoAtUri: string; + repoId: string; limit?: number; cursor?: string; } @@ -115,7 +115,7 @@ function parseIssueUri(issueUri: string): { * Create a new issue */ export async function createIssue(params: CreateIssueParams): Promise { - const { client, repoAtUri, title, body } = params; + const { client, repoId, title, body } = params; // Validate authentication const session = await requireAuth(client); @@ -123,7 +123,7 @@ export async function createIssue(params: CreateIssueParams): Promise { - const { client, repoAtUri, limit = 50, cursor } = params; + const { client, repoId, limit = 50, cursor } = params; // Validate authentication await requireAuth(client); try { // Query constellation for all issues that reference this repo across all PDSs - const backlinks = await getBacklinks( - repoAtUri, - 'sh.tangled.repo.issue', - '.repo', - limit, - cursor - ); + const backlinks = await getBacklinks(repoId, 'sh.tangled.repo.issue', '.repo', limit, cursor); // Fetch each issue record individually (constellation only gives us the AT-URI components) const issuePromises = backlinks.records.map(async ({ did, collection, rkey }) => { @@ -387,12 +381,12 @@ export async function resolveSequentialNumber( displayId: string, issueUri: string, client: TangledApiClient, - repoAtUri: string + repoId: string ): Promise { const match = displayId.match(/^#(\d+)$/); if (match) return Number.parseInt(match[1], 10); - const { issues } = await listIssues({ client, repoAtUri, limit: 100 }); + const { issues } = await listIssues({ client, repoId, limit: 100 }); const sorted = issues.sort( (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() ); @@ -424,12 +418,12 @@ export async function getCompleteIssueData( client: TangledApiClient, issueUri: string, displayId: string, - repoAtUri: string, + repoId: string, stateOverride?: 'open' | 'closed' ): Promise { const [issue, number] = await Promise.all([ getIssue({ client, issueUri }), - resolveSequentialNumber(displayId, issueUri, client, repoAtUri), + resolveSequentialNumber(displayId, issueUri, client, repoId), ]); const state = stateOverride ?? (await getIssueState({ client, issueUri })); return { diff --git a/src/utils/at-uri.ts b/src/utils/at-uri.ts index 320c6a5..35627b7 100644 --- a/src/utils/at-uri.ts +++ b/src/utils/at-uri.ts @@ -1,6 +1,7 @@ import { AtpAgent } from '@atproto/api'; -import { IdResolver } from '@atproto/identity'; -import { resolvePdsFromIdentifier } from './pds-resolver.js'; +import { resolveHandleToDid, resolvePdsFromIdentifier } from './pds-resolver.js'; + +export { resolveHandleToDid }; /** * Parse an AT-URI into its components @@ -29,39 +30,6 @@ export function parseAtUri(uri: string): { }; } -/** - * Resolve a handle to a DID using AT Protocol identity resolution. - * Uses @atproto/identity directly rather than routing through the authenticated - * agent, so the result is independent of which PDS the caller is logged into. - * - * @param handle - Handle string (e.g., "mark.bsky.social" or "@mark.bsky.social") - * @returns DID string (e.g., "did:plc:abc123") - * @throws Error if handle cannot be resolved - */ -export async function resolveHandleToDid(handle: string): Promise { - // Strip leading @ if present - const cleanHandle = handle.startsWith('@') ? handle.slice(1) : handle; - - try { - const resolver = new IdResolver(); - const did = await resolver.handle.resolve(cleanHandle); - - if (!did) { - throw new Error(`No DID found for handle: ${cleanHandle}`); - } - - return did; - } catch (error) { - if (error instanceof Error && error.message.startsWith('No DID found')) { - throw error; - } - if (error instanceof Error) { - throw new Error(`Failed to resolve handle '${cleanHandle}': ${error.message}`); - } - throw new Error(`Failed to resolve handle '${cleanHandle}': Unknown error`); - } -} - /** * Resolve a repository identifier to its repo DID (the identifier used in issue records). * Resolves the owner's PDS independently of the caller's authenticated session, @@ -87,10 +55,11 @@ export async function resolveRepoId(ownerDidOrHandle: string, repoName: string): const ownerAgent = new AtpAgent({ service: ownerPds }); // Query for sh.tangled.repo records + // TODO: does not paginate; will silently fail to find a repo for users with >100 repos. const response = await ownerAgent.com.atproto.repo.listRecords({ repo: did, collection: 'sh.tangled.repo', - limit: 100, // Reasonable limit for most users + limit: 100, }); // Find the record matching the repo name. diff --git a/src/utils/pds-resolver.ts b/src/utils/pds-resolver.ts index 2fd9aa6..096b476 100644 --- a/src/utils/pds-resolver.ts +++ b/src/utils/pds-resolver.ts @@ -1,5 +1,38 @@ import { ensureAtpDocument, IdResolver } from '@atproto/identity'; +/** + * Resolve a handle to a DID using AT Protocol identity resolution. + * Uses @atproto/identity directly rather than routing through the authenticated + * agent, so the result is independent of which PDS the caller is logged into. + * + * @param handle - Handle string (e.g., "mark.bsky.social" or "@mark.bsky.social") + * @returns DID string (e.g., "did:plc:abc123") + * @throws Error if handle cannot be resolved + */ +export async function resolveHandleToDid(handle: string): Promise { + // Strip leading @ if present + const cleanHandle = handle.startsWith('@') ? handle.slice(1) : handle; + + try { + const resolver = new IdResolver(); + const did = await resolver.handle.resolve(cleanHandle); + + if (!did) { + throw new Error(`No DID found for handle: ${cleanHandle}`); + } + + return did; + } catch (error) { + if (error instanceof Error && error.message.startsWith('No DID found')) { + throw error; + } + if (error instanceof Error) { + throw new Error(`Failed to resolve handle '${cleanHandle}': ${error.message}`); + } + throw new Error(`Failed to resolve handle '${cleanHandle}': Unknown error`); + } +} + export async function resolvePdsFromIdentifier(identifier: string): Promise { const resolver = new IdResolver(); @@ -14,7 +47,11 @@ export async function resolvePdsFromIdentifier(identifier: string): Promise[0]); return atpData.pds; } diff --git a/tests/commands/issue.test.ts b/tests/commands/issue.test.ts index 79e431c..6682dc4 100644 --- a/tests/commands/issue.test.ts +++ b/tests/commands/issue.test.ts @@ -48,10 +48,8 @@ describe('issue create command', () => { protocol: 'ssh', }); - // Mock AT-URI builder - vi.mocked(atUri.resolveRepoId).mockResolvedValue( - 'at://did:plc:abc123/sh.tangled.repo/test-repo' - ); + // Mock repo ID resolver + vi.mocked(atUri.resolveRepoId).mockResolvedValue('did:plc:repodid111'); // Mock body input vi.mocked(bodyInput.readBodyInput).mockResolvedValue(undefined); @@ -86,7 +84,7 @@ describe('issue create command', () => { expect(issuesApi.createIssue).toHaveBeenCalledWith({ client: mockClient, - repoAtUri: 'at://did:plc:abc123/sh.tangled.repo/test-repo', + repoId: 'did:plc:repodid111', title: 'Test Issue', body: 'Test body', }); @@ -126,7 +124,7 @@ describe('issue create command', () => { expect(bodyInput.readBodyInput).toHaveBeenCalledWith(undefined, '/tmp/body.txt'); expect(issuesApi.createIssue).toHaveBeenCalledWith({ client: mockClient, - repoAtUri: 'at://did:plc:abc123/sh.tangled.repo/test-repo', + repoId: 'did:plc:repodid111', title: 'Test Issue', body: 'Body from file', }); @@ -154,7 +152,7 @@ describe('issue create command', () => { expect(issuesApi.createIssue).toHaveBeenCalledWith({ client: mockClient, - repoAtUri: 'at://did:plc:abc123/sh.tangled.repo/test-repo', + repoId: 'did:plc:repodid111', title: 'Test Issue', body: undefined, }); @@ -341,7 +339,7 @@ describe('issue list command', () => { }); // Mock AT-URI builder - vi.mocked(atUri.resolveRepoId).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); + vi.mocked(atUri.resolveRepoId).mockResolvedValue('did:plc:repodid111'); }); afterEach(() => { @@ -353,7 +351,7 @@ describe('issue list command', () => { const mockIssues: IssueWithMetadata[] = [ { $type: 'sh.tangled.repo.issue', - repo: 'at://did:plc:abc123/sh.tangled.repo/xyz789', + repo: 'did:plc:repodid111', title: 'First Issue', createdAt: new Date('2024-01-01').toISOString(), uri: 'at://did:plc:abc123/sh.tangled.repo.issue/issue1', @@ -362,7 +360,7 @@ describe('issue list command', () => { }, { $type: 'sh.tangled.repo.issue', - repo: 'at://did:plc:abc123/sh.tangled.repo/xyz789', + repo: 'did:plc:repodid111', title: 'Second Issue', createdAt: new Date('2024-01-02').toISOString(), uri: 'at://did:plc:abc123/sh.tangled.repo.issue/issue2', @@ -382,7 +380,7 @@ describe('issue list command', () => { expect(issuesApi.listIssues).toHaveBeenCalledWith({ client: mockClient, - repoAtUri: 'at://did:plc:abc123/sh.tangled.repo/xyz789', + repoId: 'did:plc:repodid111', limit: 50, }); @@ -402,7 +400,7 @@ describe('issue list command', () => { expect(issuesApi.listIssues).toHaveBeenCalledWith({ client: mockClient, - repoAtUri: 'at://did:plc:abc123/sh.tangled.repo/xyz789', + repoId: 'did:plc:repodid111', limit: 25, }); }); @@ -503,7 +501,7 @@ describe('issue list command', () => { const mockIssues: IssueWithMetadata[] = [ { $type: 'sh.tangled.repo.issue', - repo: 'at://did:plc:abc123/sh.tangled.repo/xyz789', + repo: 'did:plc:repodid111', title: 'First Issue', body: 'First body', createdAt: new Date('2024-01-01').toISOString(), @@ -513,7 +511,7 @@ describe('issue list command', () => { }, { $type: 'sh.tangled.repo.issue', - repo: 'at://did:plc:abc123/sh.tangled.repo/xyz789', + repo: 'did:plc:repodid111', title: 'Second Issue', createdAt: new Date('2024-01-02').toISOString(), uri: 'at://did:plc:abc123/sh.tangled.repo.issue/issue2', @@ -574,7 +572,7 @@ describe('issue view command', () => { const mockIssue: IssueWithMetadata = { $type: 'sh.tangled.repo.issue', - repo: 'at://did:plc:abc123/sh.tangled.repo/xyz789', + repo: 'did:plc:repodid111', title: 'Test Issue', body: 'Issue body', createdAt: new Date('2024-01-01').toISOString(), @@ -604,7 +602,7 @@ describe('issue view command', () => { protocol: 'ssh', }); - vi.mocked(atUri.resolveRepoId).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); + vi.mocked(atUri.resolveRepoId).mockResolvedValue('did:plc:repodid111'); vi.mocked(authHelpers.requireAuth).mockResolvedValue({ did: 'did:plc:abc123', @@ -639,7 +637,7 @@ describe('issue view command', () => { mockClient, mockIssue.uri, '#1', - 'at://did:plc:abc123/sh.tangled.repo/xyz789' + 'did:plc:repodid111' ); expect(consoleLogSpy).toHaveBeenCalledWith('\nIssue #1 [OPEN]'); expect(consoleLogSpy).toHaveBeenCalledWith('Title: Test Issue'); @@ -666,7 +664,7 @@ describe('issue view command', () => { mockClient, 'at://did:plc:abc123/sh.tangled.repo.issue/issue1', 'issue1', - 'at://did:plc:abc123/sh.tangled.repo/xyz789' + 'did:plc:repodid111' ); expect(consoleLogSpy).toHaveBeenCalledWith('\nIssue issue1 [CLOSED]'); }); @@ -801,7 +799,7 @@ describe('issue edit command', () => { const mockIssue: IssueWithMetadata = { $type: 'sh.tangled.repo.issue', - repo: 'at://did:plc:abc123/sh.tangled.repo/xyz789', + repo: 'did:plc:repodid111', title: 'Original Title', createdAt: new Date('2024-01-01').toISOString(), uri: 'at://did:plc:abc123/sh.tangled.repo.issue/issue1', @@ -830,7 +828,7 @@ describe('issue edit command', () => { protocol: 'ssh', }); - vi.mocked(atUri.resolveRepoId).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); + vi.mocked(atUri.resolveRepoId).mockResolvedValue('did:plc:repodid111'); vi.mocked(bodyInput.readBodyInput).mockResolvedValue(undefined); vi.mocked(authHelpers.requireAuth).mockResolvedValue({ @@ -975,7 +973,7 @@ describe('issue close command', () => { const mockIssue: IssueWithMetadata = { $type: 'sh.tangled.repo.issue', - repo: 'at://did:plc:abc123/sh.tangled.repo/xyz789', + repo: 'did:plc:repodid111', title: 'Test Issue', createdAt: new Date('2024-01-01').toISOString(), uri: 'at://did:plc:abc123/sh.tangled.repo.issue/issue1', @@ -1004,7 +1002,7 @@ describe('issue close command', () => { protocol: 'ssh', }); - vi.mocked(atUri.resolveRepoId).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); + vi.mocked(atUri.resolveRepoId).mockResolvedValue('did:plc:repodid111'); vi.mocked(issuesApi.getCompleteIssueData).mockResolvedValue({ number: 1, title: mockIssue.title, @@ -1092,7 +1090,7 @@ describe('issue reopen command', () => { const mockIssue: IssueWithMetadata = { $type: 'sh.tangled.repo.issue', - repo: 'at://did:plc:abc123/sh.tangled.repo/xyz789', + repo: 'did:plc:repodid111', title: 'Test Issue', createdAt: new Date('2024-01-01').toISOString(), uri: 'at://did:plc:abc123/sh.tangled.repo.issue/issue1', @@ -1121,7 +1119,7 @@ describe('issue reopen command', () => { protocol: 'ssh', }); - vi.mocked(atUri.resolveRepoId).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); + vi.mocked(atUri.resolveRepoId).mockResolvedValue('did:plc:repodid111'); vi.mocked(issuesApi.getCompleteIssueData).mockResolvedValue({ number: 1, title: mockIssue.title, diff --git a/tests/index.test.ts b/tests/index.test.ts index a57ae79..605d9f6 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -27,6 +27,6 @@ describe('Tangled CLI', () => { }); it('package.json should have correct name', () => { - expect(packageJson.name).toBe('tangled-cli'); + expect(packageJson.name).toBe('@markbennett/tang'); }); }); diff --git a/tests/lib/issues-api.test.ts b/tests/lib/issues-api.test.ts index 4f6d66b..4c7b206 100644 --- a/tests/lib/issues-api.test.ts +++ b/tests/lib/issues-api.test.ts @@ -67,7 +67,7 @@ describe('createIssue', () => { const result = await createIssue({ client: mockClient, - repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', + repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Bug: Login fails', body: 'Detailed description of the bug', }); @@ -114,7 +114,7 @@ describe('createIssue', () => { const result = await createIssue({ client: mockClient, - repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', + repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Simple issue', }); @@ -128,7 +128,7 @@ describe('createIssue', () => { await expect( createIssue({ client: mockClient, - repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', + repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Test', }) ).rejects.toThrow('Must be authenticated'); @@ -150,7 +150,7 @@ describe('createIssue', () => { await expect( createIssue({ client: mockClient, - repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', + repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Test', }) ).rejects.toThrow('Failed to create issue: API error'); @@ -208,7 +208,7 @@ describe('listIssues', () => { const result = await listIssues({ client: mockClient, - repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', + repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', }); expect(result.issues).toHaveLength(2); @@ -238,7 +238,7 @@ describe('listIssues', () => { const result = await listIssues({ client: mockClient, - repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', + repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', }); expect(result.issues).toEqual([]); @@ -249,7 +249,7 @@ describe('listIssues', () => { const result = await listIssues({ client: mockClient, - repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', + repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', }); expect(result.cursor).toBe('nextpage'); @@ -261,7 +261,7 @@ describe('listIssues', () => { await expect( listIssues({ client: mockClient, - repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', + repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', }) ).rejects.toThrow('Must be authenticated'); }); diff --git a/tests/utils/at-uri.test.ts b/tests/utils/at-uri.test.ts index f38dde0..5b145a3 100644 --- a/tests/utils/at-uri.test.ts +++ b/tests/utils/at-uri.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { parseAtUri, resolveHandleToDid, resolveRepoId } from '../../src/utils/at-uri.js'; +import { parseAtUri, resolveRepoId } from '../../src/utils/at-uri.js'; const { mockHandleResolve, mockListRecords, mockResolvePds } = vi.hoisted(() => ({ mockHandleResolve: vi.fn(), @@ -7,14 +7,9 @@ const { mockHandleResolve, mockListRecords, mockResolvePds } = vi.hoisted(() => mockResolvePds: vi.fn().mockResolvedValue('https://pds.example.com'), })); -vi.mock('@atproto/identity', () => ({ - IdResolver: vi.fn().mockImplementation(() => ({ - handle: { resolve: mockHandleResolve }, - })), -})); - vi.mock('../../src/utils/pds-resolver.js', () => ({ resolvePdsFromIdentifier: mockResolvePds, + resolveHandleToDid: mockHandleResolve, })); vi.mock('@atproto/api', () => ({ @@ -75,45 +70,6 @@ describe('parseAtUri', () => { }); }); -describe('resolveHandleToDid', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should resolve handle to DID', async () => { - mockHandleResolve.mockResolvedValue('did:plc:abc123'); - - const result = await resolveHandleToDid('mark.bsky.social'); - - expect(result).toBe('did:plc:abc123'); - expect(mockHandleResolve).toHaveBeenCalledWith('mark.bsky.social'); - }); - - it('should strip leading @ from handle', async () => { - mockHandleResolve.mockResolvedValue('did:plc:abc123'); - - await resolveHandleToDid('@mark.bsky.social'); - - expect(mockHandleResolve).toHaveBeenCalledWith('mark.bsky.social'); - }); - - it('should throw error when handle resolves to nothing', async () => { - mockHandleResolve.mockResolvedValue(undefined); - - await expect(resolveHandleToDid('nonexistent.bsky.social')).rejects.toThrow( - 'No DID found for handle: nonexistent.bsky.social' - ); - }); - - it('should throw error on resolution failure', async () => { - mockHandleResolve.mockRejectedValue(new Error('Network error')); - - await expect(resolveHandleToDid('mark.bsky.social')).rejects.toThrow( - "Failed to resolve handle 'mark.bsky.social': Network error" - ); - }); -}); - describe('resolveRepoId', () => { beforeEach(() => { vi.clearAllMocks(); @@ -237,7 +193,9 @@ describe('resolveRepoId', () => { }); it('should throw error when handle resolution fails', async () => { - mockHandleResolve.mockRejectedValue(new Error('Resolution failed')); + mockHandleResolve.mockRejectedValue( + new Error("Failed to resolve handle 'mark.bsky.social': Resolution failed") + ); await expect(resolveRepoId('mark.bsky.social', 'my-repo')).rejects.toThrow( "Failed to resolve handle 'mark.bsky.social': Resolution failed" diff --git a/tests/utils/pds-resolver.test.ts b/tests/utils/pds-resolver.test.ts index 87a2cf0..67d01b6 100644 --- a/tests/utils/pds-resolver.test.ts +++ b/tests/utils/pds-resolver.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { resolvePdsFromIdentifier } from '../../src/utils/pds-resolver.js'; +import { resolveHandleToDid, resolvePdsFromIdentifier } from '../../src/utils/pds-resolver.js'; const PLC_DID = 'did:plc:abc123'; const WEB_DID = 'did:web:example.com'; @@ -125,3 +125,50 @@ describe('resolvePdsFromIdentifier', () => { }); }); }); + +describe('resolveHandleToDid', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should resolve handle to DID', async () => { + const { resolveHandle } = await getMocks(); + resolveHandle.mockResolvedValue(PLC_DID); + + const result = await resolveHandleToDid('mark.bsky.social'); + + expect(result).toBe(PLC_DID); + expect(resolveHandle).toHaveBeenCalledWith('mark.bsky.social'); + }); + + it('should strip leading @ from handle', async () => { + const { resolveHandle } = await getMocks(); + resolveHandle.mockResolvedValue(PLC_DID); + + await resolveHandleToDid('@mark.bsky.social'); + + expect(resolveHandle).toHaveBeenCalledWith('mark.bsky.social'); + }); + + it('should throw error when handle resolves to nothing', async () => { + const { resolveHandle } = await getMocks(); + resolveHandle.mockResolvedValue(undefined); + + await expect(resolveHandleToDid('nonexistent.bsky.social')).rejects.toThrow( + 'No DID found for handle: nonexistent.bsky.social' + ); + }); + + it('should throw error on resolution failure', async () => { + const { resolveHandle } = await getMocks(); + resolveHandle.mockRejectedValue(new Error('Network error')); + + await expect(resolveHandleToDid('mark.bsky.social')).rejects.toThrow( + "Failed to resolve handle 'mark.bsky.social': Network error" + ); + }); +});