Files
webmail/lib/__tests__/plugin-hook-result.test.ts

97 lines
4.0 KiB
TypeScript

// @vitest-environment node
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { clearAllHooks, uiHooks, pluginErrorTracker } from '../plugin-hooks';
import { interceptWithResult, acceptLinkRewrite, resolveExternalLink } from '../plugin-hook-result';
beforeEach(() => {
clearAllHooks();
pluginErrorTracker.resetAll();
});
describe('interceptWithResult', () => {
it('lets the action through unchanged without handlers', async () => {
const ctx = { href: 'https://a.example/' };
expect(await interceptWithResult(uiHooks.onBeforeExternalLink, ctx, acceptLinkRewrite))
.toEqual({ allowed: true, value: ctx });
});
it('stops on false and does not ask later handlers', async () => {
const later = vi.fn();
uiHooks.onBeforeExternalLink.register('a', () => false);
uiHooks.onBeforeExternalLink.register('b', later);
const out = await interceptWithResult(uiHooks.onBeforeExternalLink, { href: 'https://a.example/' }, acceptLinkRewrite);
expect(out).toEqual({ allowed: false });
expect(later).not.toHaveBeenCalled();
});
it('takes a returned { href } and hands it to the next handler', async () => {
const second = vi.fn(() => undefined);
uiHooks.onBeforeExternalLink.register('a', () => ({ href: 'https://proxy.example/?u=1' }));
uiHooks.onBeforeExternalLink.register('b', second);
const out = await interceptWithResult(uiHooks.onBeforeExternalLink, { href: 'https://a.example/', emailId: 'e1' }, acceptLinkRewrite);
expect(out).toEqual({ allowed: true, value: { href: 'https://proxy.example/?u=1', emailId: 'e1' } });
expect(second).toHaveBeenCalledWith({ href: 'https://proxy.example/?u=1', emailId: 'e1' });
});
it('calls every handler once', async () => {
const handler = vi.fn(() => ({ href: 'https://b.example/' }));
uiHooks.onBeforeExternalLink.register('a', handler);
await interceptWithResult(uiHooks.onBeforeExternalLink, { href: 'https://a.example/' }, acceptLinkRewrite);
expect(handler).toHaveBeenCalledTimes(1);
});
it('skips a handler that throws (fails open)', async () => {
uiHooks.onBeforeExternalLink.register('a', () => { throw new Error('boom'); });
const out = await interceptWithResult(uiHooks.onBeforeExternalLink, { href: 'https://a.example/' }, acceptLinkRewrite);
expect(out).toEqual({ allowed: true, value: { href: 'https://a.example/' } });
});
});
describe('acceptLinkRewrite', () => {
const cur = { href: 'https://a.example/' };
it.each([
'javascript:alert(1)',
'data:text/html,hi',
'/settings',
'mailto:x@example.com',
'https://',
])('ignores %s', (href) => {
expect(acceptLinkRewrite({ href }, cur)).toBeUndefined();
});
it('ignores anything but an object with a string href', () => {
expect(acceptLinkRewrite('https://b.example/', cur)).toBeUndefined();
expect(acceptLinkRewrite({ href: 42 }, cur)).toBeUndefined();
});
it('takes an http(s) address', () => {
expect(acceptLinkRewrite({ href: ' HTTP://b.example/x ' }, cur)).toEqual({ href: 'HTTP://b.example/x' });
});
});
describe('resolveExternalLink (a link clicked in a message)', () => {
const ctx = { href: 'https://a.example/', emailId: 'e1' };
it('opens the original address without plugins', async () => {
expect(await resolveExternalLink(ctx)).toBe('https://a.example/');
});
it('opens the address a plugin returns', async () => {
const handler = vi.fn(() => ({ href: 'https://proxy.example/?u=a' }));
uiHooks.onBeforeExternalLink.register('p', handler);
expect(await resolveExternalLink(ctx)).toBe('https://proxy.example/?u=a');
expect(handler).toHaveBeenCalledWith(ctx);
});
it('opens nothing when a plugin returns false', async () => {
uiHooks.onBeforeExternalLink.register('p', () => false);
expect(await resolveExternalLink(ctx)).toBeNull();
});
it('keeps the original when the rewrite is not a web address', async () => {
uiHooks.onBeforeExternalLink.register('p', () => ({ href: 'javascript:alert(1)' }));
expect(await resolveExternalLink(ctx)).toBe('https://a.example/');
});
});