diff --git a/tests/webfinger.test.ts b/tests/webfinger.test.ts new file mode 100644 index 0000000..34e4e68 --- /dev/null +++ b/tests/webfinger.test.ts @@ -0,0 +1,480 @@ +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(); + }); + }); +});