266 lines
9.7 KiB
TypeScript
266 lines
9.7 KiB
TypeScript
/*
|
|
* Copyright (C) 2026 Fluxer Contributors
|
|
*
|
|
* This file is part of Fluxer.
|
|
*
|
|
* Fluxer is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Fluxer is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import {BaseResolver} from '@fluxer/api/src/unfurler/resolvers/BaseResolver';
|
|
import {MockMediaService} from '@fluxer/api/src/unfurler/tests/ResolverTestUtils';
|
|
import type {MessageEmbedResponse} from '@fluxer/schema/src/domains/message/EmbedSchemas';
|
|
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
|
|
class TestableResolver extends BaseResolver {
|
|
match(_url: URL, _mimeType: string, _content: Uint8Array): boolean {
|
|
return true;
|
|
}
|
|
|
|
async resolve(_url: URL, _content: Uint8Array, _isNSFWAllowed?: boolean): Promise<Array<MessageEmbedResponse>> {
|
|
return [];
|
|
}
|
|
|
|
testResolveRelativeURL(baseUrl: string, relativeUrl?: string): string | null {
|
|
return this.resolveRelativeURL(baseUrl, relativeUrl);
|
|
}
|
|
|
|
async testResolveMediaURL(
|
|
url: URL,
|
|
mediaUrl?: string | null,
|
|
isNSFWAllowed: boolean = false,
|
|
): Promise<MessageEmbedResponse['image']> {
|
|
return this.resolveMediaURL(url, mediaUrl, isNSFWAllowed);
|
|
}
|
|
}
|
|
|
|
describe('BaseResolver', () => {
|
|
let mediaService: MockMediaService;
|
|
let resolver: TestableResolver;
|
|
|
|
beforeEach(() => {
|
|
mediaService = new MockMediaService();
|
|
resolver = new TestableResolver(mediaService);
|
|
});
|
|
|
|
afterEach(() => {
|
|
mediaService.reset();
|
|
});
|
|
|
|
describe('resolveRelativeURL', () => {
|
|
it('resolves relative paths correctly', () => {
|
|
const result = resolver.testResolveRelativeURL('https://example.com/page/', '../image.jpg');
|
|
expect(result).toBe('https://example.com/image.jpg');
|
|
});
|
|
|
|
it('resolves absolute paths correctly', () => {
|
|
const result = resolver.testResolveRelativeURL('https://example.com/page/', '/images/photo.jpg');
|
|
expect(result).toBe('https://example.com/images/photo.jpg');
|
|
});
|
|
|
|
it('preserves full URLs', () => {
|
|
const result = resolver.testResolveRelativeURL('https://example.com/page/', 'https://cdn.example.com/image.jpg');
|
|
expect(result).toBe('https://cdn.example.com/image.jpg');
|
|
});
|
|
|
|
it('handles protocol-relative URLs', () => {
|
|
const result = resolver.testResolveRelativeURL('https://example.com/page/', '//cdn.example.com/image.jpg');
|
|
expect(result).toBe('https://cdn.example.com/image.jpg');
|
|
});
|
|
|
|
it('returns null for undefined input', () => {
|
|
const result = resolver.testResolveRelativeURL('https://example.com/', undefined);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('returns null for empty string input', () => {
|
|
const result = resolver.testResolveRelativeURL('https://example.com/', '');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('handles URLs with query parameters', () => {
|
|
const result = resolver.testResolveRelativeURL('https://example.com/page/', 'image.jpg?v=123');
|
|
expect(result).toBe('https://example.com/page/image.jpg?v=123');
|
|
});
|
|
|
|
it('handles URLs with fragments', () => {
|
|
const result = resolver.testResolveRelativeURL('https://example.com/page/', 'image.jpg#section');
|
|
expect(result).toBe('https://example.com/page/image.jpg#section');
|
|
});
|
|
|
|
it('handles complex relative paths', () => {
|
|
const result = resolver.testResolveRelativeURL('https://example.com/a/b/c/', '../../d/image.jpg');
|
|
expect(result).toBe('https://example.com/a/d/image.jpg');
|
|
});
|
|
|
|
it('handles base URL without trailing slash', () => {
|
|
const result = resolver.testResolveRelativeURL('https://example.com/page', 'image.jpg');
|
|
expect(result).toBe('https://example.com/image.jpg');
|
|
});
|
|
|
|
it('handles data URLs by returning as-is', () => {
|
|
const dataUrl = 'data:image/png;base64,iVBORw0KGgo=';
|
|
const result = resolver.testResolveRelativeURL('https://example.com/', dataUrl);
|
|
expect(result).toBe(dataUrl);
|
|
});
|
|
|
|
it('returns original URL for invalid base URLs', () => {
|
|
const result = resolver.testResolveRelativeURL('not-a-url', 'image.jpg');
|
|
expect(result).toBe('image.jpg');
|
|
});
|
|
});
|
|
|
|
describe('resolveMediaURL', () => {
|
|
it('returns null for null media URL', async () => {
|
|
const url = new URL('https://example.com/page');
|
|
const result = await resolver.testResolveMediaURL(url, null);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('returns null for undefined media URL', async () => {
|
|
const url = new URL('https://example.com/page');
|
|
const result = await resolver.testResolveMediaURL(url, undefined);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('returns null for empty string media URL', async () => {
|
|
const url = new URL('https://example.com/page');
|
|
const result = await resolver.testResolveMediaURL(url, '');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('resolves valid external media URL', async () => {
|
|
const url = new URL('https://example.com/page');
|
|
const result = await resolver.testResolveMediaURL(url, 'https://example.com/image.jpg');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.url).toBe('https://example.com/image.jpg');
|
|
});
|
|
|
|
it('resolves relative media URL', async () => {
|
|
const url = new URL('https://example.com/page/');
|
|
const result = await resolver.testResolveMediaURL(url, 'image.jpg');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.url).toBe('https://example.com/page/image.jpg');
|
|
});
|
|
|
|
it('includes width and height in result', async () => {
|
|
const url = new URL('https://example.com/page');
|
|
mediaService.setMetadata('https://example.com/image.jpg', {
|
|
width: 800,
|
|
height: 600,
|
|
});
|
|
|
|
const result = await resolver.testResolveMediaURL(url, 'https://example.com/image.jpg');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.width).toBe(800);
|
|
expect(result!.height).toBe(600);
|
|
});
|
|
|
|
it('includes content_type in result', async () => {
|
|
const url = new URL('https://example.com/page');
|
|
mediaService.setMetadata('https://example.com/image.jpg', {
|
|
content_type: 'image/jpeg',
|
|
});
|
|
|
|
const result = await resolver.testResolveMediaURL(url, 'https://example.com/image.jpg');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.content_type).toBe('image/jpeg');
|
|
});
|
|
|
|
it('includes content_hash in result', async () => {
|
|
const url = new URL('https://example.com/page');
|
|
const result = await resolver.testResolveMediaURL(url, 'https://example.com/image.jpg');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.content_hash).toBeDefined();
|
|
});
|
|
|
|
it('includes placeholder in result when available', async () => {
|
|
const url = new URL('https://example.com/page');
|
|
mediaService.setMetadata('https://example.com/image.jpg', {
|
|
placeholder: 'data:image/jpeg;base64,/9j/4AAQ...',
|
|
});
|
|
|
|
const result = await resolver.testResolveMediaURL(url, 'https://example.com/image.jpg');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.placeholder).toBe('data:image/jpeg;base64,/9j/4AAQ...');
|
|
});
|
|
|
|
it('passes NSFW flag to media service', async () => {
|
|
const url = new URL('https://example.com/page');
|
|
const result = await resolver.testResolveMediaURL(url, 'https://example.com/image.jpg', true);
|
|
expect(result).not.toBeNull();
|
|
});
|
|
|
|
it('returns null when media service fails', async () => {
|
|
const url = new URL('https://example.com/page');
|
|
mediaService.markAsFailing('https://example.com/broken.jpg');
|
|
|
|
const result = await resolver.testResolveMediaURL(url, 'https://example.com/broken.jpg');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('handles URL-like strings that get resolved as relative paths', async () => {
|
|
const url = new URL('https://example.com/page/');
|
|
const result = await resolver.testResolveMediaURL(url, 'not a valid url at all');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.url).toBe('https://example.com/page/not%20a%20valid%20url%20at%20all');
|
|
});
|
|
|
|
it('handles URLs with special characters', async () => {
|
|
const url = new URL('https://example.com/page');
|
|
const result = await resolver.testResolveMediaURL(url, 'https://example.com/image%20name.jpg');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.url).toBe('https://example.com/image%20name.jpg');
|
|
});
|
|
|
|
it('handles URLs with unicode characters', async () => {
|
|
const url = new URL('https://example.com/page');
|
|
const result = await resolver.testResolveMediaURL(url, 'https://example.com/%E7%94%BB%E5%83%8F.jpg');
|
|
expect(result).not.toBeNull();
|
|
});
|
|
|
|
it('handles duration in result for video/audio', async () => {
|
|
const url = new URL('https://example.com/page');
|
|
mediaService.setMetadata('https://example.com/video.mp4', {
|
|
duration: 120.5,
|
|
});
|
|
|
|
const result = await resolver.testResolveMediaURL(url, 'https://example.com/video.mp4');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.duration).toBe(120.5);
|
|
});
|
|
|
|
it('sets animated flag in result', async () => {
|
|
const url = new URL('https://example.com/page');
|
|
mediaService.setMetadata('https://example.com/animation.gif', {
|
|
animated: true,
|
|
});
|
|
|
|
const result = await resolver.testResolveMediaURL(url, 'https://example.com/animation.gif');
|
|
expect(result).not.toBeNull();
|
|
expect((result!.flags ?? 0) & 32).toBe(32);
|
|
});
|
|
|
|
it('sets NSFW flag in result when content is NSFW', async () => {
|
|
const url = new URL('https://example.com/page');
|
|
mediaService.setMetadata('https://example.com/nsfw.jpg', {
|
|
nsfw: true,
|
|
});
|
|
|
|
const result = await resolver.testResolveMediaURL(url, 'https://example.com/nsfw.jpg', true);
|
|
expect(result).not.toBeNull();
|
|
expect((result!.flags ?? 0) & 16).toBe(16);
|
|
});
|
|
});
|
|
});
|