import { describe, it, expect } from 'vitest'; import { generateWebFingerJRD } from '../src/generators/webfinger.js'; import type { WebFingerConfig } from '../src/types.js'; describe('generateWebFingerJRD', () => { const testURL = new URL('https://example.com'); describe('Static Resources', () => { it('returns null for unknown resource', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', subject: 'acct:alice@example.com', }, ], }; const result = await generateWebFingerJRD( config, 'acct:bob@example.com', undefined, testURL ); expect(result).toBeNull(); }); it('generates basic JRD for known resource', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', subject: 'acct:alice@example.com', }, ], }; const result = await generateWebFingerJRD( config, 'acct:alice@example.com', undefined, testURL ); expect(result).toBeTruthy(); const jrd = JSON.parse(result!); expect(jrd.subject).toBe('acct:alice@example.com'); }); it('uses resource as subject when subject not provided', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', }, ], }; const result = await generateWebFingerJRD( config, 'acct:alice@example.com', undefined, testURL ); const jrd = JSON.parse(result!); expect(jrd.subject).toBe('acct:alice@example.com'); }); it('includes aliases when provided', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', aliases: [ 'https://example.com/~alice', 'https://example.com/users/alice', ], }, ], }; const result = await generateWebFingerJRD( config, 'acct:alice@example.com', undefined, testURL ); const jrd = JSON.parse(result!); expect(jrd.aliases).toEqual([ 'https://example.com/~alice', 'https://example.com/users/alice', ]); }); it('includes properties when provided', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', properties: { 'http://schema.org/name': 'Alice Smith', 'http://example.com/role': 'Developer', }, }, ], }; const result = await generateWebFingerJRD( config, 'acct:alice@example.com', undefined, testURL ); const jrd = JSON.parse(result!); expect(jrd.properties).toEqual({ 'http://schema.org/name': 'Alice Smith', 'http://example.com/role': 'Developer', }); }); it('includes links when provided', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', links: [ { rel: 'http://webfinger.net/rel/profile-page', href: 'https://example.com/~alice', type: 'text/html', }, { rel: 'http://webfinger.net/rel/avatar', href: 'https://example.com/avatars/alice.jpg', type: 'image/jpeg', }, ], }, ], }; const result = await generateWebFingerJRD( config, 'acct:alice@example.com', undefined, testURL ); const jrd = JSON.parse(result!); expect(jrd.links).toHaveLength(2); expect(jrd.links[0].rel).toBe('http://webfinger.net/rel/profile-page'); expect(jrd.links[0].href).toBe('https://example.com/~alice'); expect(jrd.links[1].rel).toBe('http://webfinger.net/rel/avatar'); }); it('filters links by rel when requested', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', links: [ { rel: 'http://webfinger.net/rel/profile-page', href: 'https://example.com/~alice', }, { rel: 'http://webfinger.net/rel/avatar', href: 'https://example.com/avatars/alice.jpg', }, { rel: 'http://openid.net/specs/connect/1.0/issuer', href: 'https://auth.example.com', }, ], }, ], }; const result = await generateWebFingerJRD( config, 'acct:alice@example.com', ['http://webfinger.net/rel/profile-page'], testURL ); const jrd = JSON.parse(result!); expect(jrd.links).toHaveLength(1); expect(jrd.links[0].rel).toBe('http://webfinger.net/rel/profile-page'); }); it('filters links by multiple rels', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', links: [ { rel: 'profile', href: 'https://example.com/~alice' }, { rel: 'avatar', href: 'https://example.com/avatars/alice.jpg' }, { rel: 'blog', href: 'https://blog.example.com/alice' }, ], }, ], }; const result = await generateWebFingerJRD( config, 'acct:alice@example.com', ['profile', 'blog'], testURL ); const jrd = JSON.parse(result!); expect(jrd.links).toHaveLength(2); expect(jrd.links[0].rel).toBe('profile'); expect(jrd.links[1].rel).toBe('blog'); }); it('includes link titles when provided', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', links: [ { rel: 'http://webfinger.net/rel/profile-page', href: 'https://example.com/~alice', titles: { en: 'Alice Smith Profile', es: 'Perfil de Alice Smith', }, }, ], }, ], }; const result = await generateWebFingerJRD( config, 'acct:alice@example.com', undefined, testURL ); const jrd = JSON.parse(result!); expect(jrd.links[0].titles).toEqual({ en: 'Alice Smith Profile', es: 'Perfil de Alice Smith', }); }); it('generates complete JRD with all fields', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', subject: 'acct:alice@example.com', aliases: ['https://example.com/~alice'], properties: { 'http://schema.org/name': 'Alice Smith', }, links: [ { rel: 'http://webfinger.net/rel/profile-page', href: 'https://example.com/~alice', type: 'text/html', titles: { en: 'Profile' }, }, ], }, ], }; const result = await generateWebFingerJRD( config, 'acct:alice@example.com', undefined, testURL ); const jrd = JSON.parse(result!); expect(jrd.subject).toBe('acct:alice@example.com'); expect(jrd.aliases).toEqual(['https://example.com/~alice']); expect(jrd.properties).toEqual({ 'http://schema.org/name': 'Alice Smith', }); expect(jrd.links).toHaveLength(1); expect(jrd.links[0].rel).toBe('http://webfinger.net/rel/profile-page'); }); }); describe('HTTP Resource URIs', () => { it('supports https:// resource URIs', async () => { const config: WebFingerConfig = { resources: [ { resource: 'https://example.com/user/alice', links: [ { rel: 'http://webfinger.net/rel/profile-page', href: 'https://example.com/user/alice', }, ], }, ], }; const result = await generateWebFingerJRD( config, 'https://example.com/user/alice', undefined, testURL ); expect(result).toBeTruthy(); const jrd = JSON.parse(result!); expect(jrd.subject).toBe('https://example.com/user/alice'); }); }); describe('ActivityPub / Fediverse', () => { it('supports ActivityPub profile discovery', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', aliases: ['https://example.com/@alice'], links: [ { rel: 'self', type: 'application/activity+json', href: 'https://example.com/users/alice', }, { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: 'https://example.com/@alice', }, ], }, ], }; const result = await generateWebFingerJRD( config, 'acct:alice@example.com', undefined, testURL ); const jrd = JSON.parse(result!); expect(jrd.links).toHaveLength(2); expect(jrd.links[0].type).toBe('application/activity+json'); expect(jrd.links[0].rel).toBe('self'); }); }); describe('OpenID Connect', () => { it('supports OpenID Connect issuer discovery', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', links: [ { rel: 'http://openid.net/specs/connect/1.0/issuer', href: 'https://auth.example.com', }, ], }, ], }; const result = await generateWebFingerJRD( config, 'acct:alice@example.com', ['http://openid.net/specs/connect/1.0/issuer'], testURL ); const jrd = JSON.parse(result!); expect(jrd.links).toHaveLength(1); expect(jrd.links[0].rel).toBe( 'http://openid.net/specs/connect/1.0/issuer' ); expect(jrd.links[0].href).toBe('https://auth.example.com'); }); }); describe('Edge Cases', () => { it('returns valid JSON for resource with no links', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', }, ], }; const result = await generateWebFingerJRD( config, 'acct:alice@example.com', undefined, testURL ); const jrd = JSON.parse(result!); expect(jrd.subject).toBe('acct:alice@example.com'); expect(jrd.links).toBeUndefined(); }); it('omits empty aliases array', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', aliases: [], }, ], }; const result = await generateWebFingerJRD( config, 'acct:alice@example.com', undefined, testURL ); const jrd = JSON.parse(result!); expect(jrd.aliases).toBeUndefined(); }); it('omits empty properties object', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', properties: {}, }, ], }; const result = await generateWebFingerJRD( config, 'acct:alice@example.com', undefined, testURL ); const jrd = JSON.parse(result!); expect(jrd.properties).toBeUndefined(); }); it('returns null when all links filtered out by rel', async () => { const config: WebFingerConfig = { resources: [ { resource: 'acct:alice@example.com', links: [ { rel: 'profile', href: 'https://example.com/~alice' }, ], }, ], }; const result = await generateWebFingerJRD( config, 'acct:alice@example.com', ['avatar'], // Requesting rel that doesn't exist testURL ); const jrd = JSON.parse(result!); expect(jrd.links).toBeUndefined(); }); }); });