UNPKG

@iocium/favicon-fetcher

Version:

Favicon and BIMI logo fetcher for Cloudflare Workers and browser-compatible environments

229 lines (204 loc) 7.95 kB
import { FaviconFetcher, Service } from './fetcher'; describe('FaviconFetcher (mocked)', () => { /** * BEHAVIOUR */ it('throws if hostname is missing', () => { expect(() => new FaviconFetcher('' as any)).toThrow('Hostname is required'); }); it('prepends CORS proxy URL when useCorsProxy is true', async () => { const mockFetch = jest.fn(() => Promise.resolve(new Response(new ArrayBuffer(10), { status: 200, headers: { 'Content-Type': 'image/png' } })) ); global.fetch = mockFetch as jest.Mock; const fetcher = new FaviconFetcher('github.com', { useCorsProxy: true }); await fetcher.fetchFavicon('google'); expect(mockFetch).toHaveBeenCalledWith( expect.stringMatching(/^https:\/\/corsproxy\.io\/\?https:\/\/www\.google\.com/), expect.any(Object) ); }); it('prepends custom CORS proxy URL when useCorsProxy is a string', async () => { const mockFetch = jest.fn(() => Promise.resolve(new Response(new ArrayBuffer(10), { status: 200, headers: { 'Content-Type': 'image/png' } })) ); global.fetch = mockFetch as jest.Mock; const fetcher = new FaviconFetcher('duckduckgo.com', { useCorsProxy: 'https://my-cors-proxy/' }); await fetcher.fetchFavicon('duckduckgo'); expect(mockFetch).toHaveBeenCalledWith( expect.stringMatching(/^https:\/\/my-cors-proxy\/https:\/\/icons\.duckduckgo\.com/), expect.any(Object) ); }); /** * SERVICE SUCCESS */ it.each([ 'google', 'duckduckgo', 'bitwarden', 'yandex', 'fastmail', 'iconHorse', 'nextdns', 'iocium', 'faviconis', 'faviconim' ])('fetchFavicon covers serviceUrls[%s]', async (service) => { global.fetch = jest.fn(() => Promise.resolve(new Response(new ArrayBuffer(10), { status: 200, headers: { 'Content-Type': 'image/png' } })) ) as jest.Mock; const fetcher = new FaviconFetcher('example.com'); const result = await fetcher.fetchFavicon(service as Service); expect(result.status).toBe(200); }); /** * REQUEST INTEGRITY */ it('enforces iconHorseApiKey over headers.X-API-Key', async () => { const mockFetch = jest.fn(() => Promise.resolve(new Response(new ArrayBuffer(1), { status: 200, headers: { 'Content-Type': 'image/png' } })) ); global.fetch = mockFetch as jest.Mock; const fetcher = new FaviconFetcher('example.com', { iconHorseApiKey: 'REAL_KEY', headers: { 'User-Agent': 'TestAgent', 'X-API-Key': 'BAD_HEADER_SHOULD_BE_OVERRIDDEN' } }); await fetcher.fetchFavicon('iconHorse'); expect(mockFetch).toHaveBeenCalledWith( 'https://icon.horse/icon/example.com', expect.objectContaining({ headers: expect.objectContaining({ 'User-Agent': 'TestAgent', 'X-API-Key': 'REAL_KEY' // this must override the user-provided one }) }) ); }); /** * SERVICE FAILURE */ it('throws error if favicon fetch fails', async () => { global.fetch = jest.fn(() => { return Promise.resolve(new Response('Not Found', { status: 404, statusText: 'Not Found' })); }) as jest.Mock; const fetcher = new FaviconFetcher('example.com'); await expect(fetcher.fetchFavicon('iconHorse')).rejects.toThrow('Failed to fetch favicon from iconHorse'); }); /** * BIMI */ it('successfully fetches BIMI logo using default DoH resolver', async () => { const dummySvg = '<svg></svg>'; const dummyArrayBuffer = new TextEncoder().encode(dummySvg).buffer; global.fetch = jest.fn((url: RequestInfo) => { if (typeof url === 'string' && url.includes('dns-query')) { return Promise.resolve(new Response(JSON.stringify({ Answer: [ { data: '"v=BIMI1; l=https://example.com/logo.svg;"' } ] }), { status: 200 })); } else if (typeof url === 'string' && url.includes('logo.svg')) { return Promise.resolve(new Response(dummyArrayBuffer, { status: 200, headers: { 'Content-Type': 'image/svg+xml' } })); } return Promise.reject(new Error('Unexpected fetch')); }) as jest.Mock; const fetcher = new FaviconFetcher('example.com'); const result = await fetcher.fetchFavicon('bimi'); expect(result.url).toBe('https://example.com/logo.svg'); expect(result.contentType).toBe('image/svg+xml'); expect(result.status).toBe(200); }); it('successfully fetches BIMI logo using custom DoH resolver', async () => { const dummySvg = '<svg></svg>'; const dummyArrayBuffer = new TextEncoder().encode(dummySvg).buffer; global.fetch = jest.fn((url: RequestInfo) => { if (typeof url === 'string' && url.includes('dns.google')) { return Promise.resolve(new Response(JSON.stringify({ Answer: [ { data: '"v=BIMI1; l=https://cdn.example.org/logo.svg;"' } ] }), { status: 200 })); } else if (typeof url === 'string' && url.includes('logo.svg')) { return Promise.resolve(new Response(dummyArrayBuffer, { status: 200, headers: { 'Content-Type': 'image/svg+xml' } })); } return Promise.reject(new Error('Unexpected fetch')); }) as jest.Mock; const fetcher = new FaviconFetcher('example.com', { dohServerUrl: 'https://dns.google/dns-query' }); const result = await fetcher.fetchFavicon('bimi'); expect(result.url).toBe('https://cdn.example.org/logo.svg'); expect(result.contentType).toBe('image/svg+xml'); expect(result.status).toBe(200); }); it('throws error if no BIMI TXT record is found', async () => { global.fetch = jest.fn((url: RequestInfo) => { if (typeof url === 'string' && url.includes('dns-query')) { return Promise.resolve(new Response(JSON.stringify({ Answer: [] }), { status: 200 })); } return Promise.reject(new Error('Unexpected fetch')); }) as jest.Mock; const fetcher = new FaviconFetcher('example.com'); await expect(fetcher.fetchFavicon('bimi')).rejects.toThrow('No BIMI l= logo URL found in TXT record'); }); it('throws error if DNS response is malformed', async () => { global.fetch = jest.fn((url: RequestInfo) => { if (typeof url === 'string' && url.includes('dns-query')) { return Promise.resolve(new Response('{}', { status: 200 })); } return Promise.reject(new Error('Unexpected fetch')); }) as jest.Mock; const fetcher = new FaviconFetcher('example.com'); await expect(fetcher.fetchFavicon('bimi')).rejects.toThrow('No BIMI l= logo URL found in TXT record'); }); it('throws error if BIMI logo fetch fails', async () => { global.fetch = jest.fn((url: RequestInfo) => { if (typeof url === 'string' && url.includes('dns-query')) { return Promise.resolve(new Response(JSON.stringify({ Answer: [ { data: '"v=BIMI1; l=https://cdn.example.com/logo.svg;"' } ] }), { status: 200 })); } return Promise.resolve(new Response(null, { status: 404 })); }) as jest.Mock; const fetcher = new FaviconFetcher('example.com'); await expect(fetcher.fetchFavicon('bimi')).rejects.toThrow('Failed to fetch BIMI logo'); }); it('throws error if BIMI DNS query fails', async () => { global.fetch = jest.fn((url: RequestInfo) => { if (typeof url === 'string' && url.includes('dns-query')) { return Promise.resolve(new Response('Server error', { status: 503, statusText: 'Service Unavailable' })); } return Promise.reject(new Error('Unexpected fetch')); }) as jest.Mock; const fetcher = new FaviconFetcher('example.com'); await expect(fetcher.fetchFavicon('bimi')).rejects.toThrow('BIMI DNS query failed'); }); });