473 lines
13 KiB
TypeScript
473 lines
13 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { PluginManager } from '../src/core/PluginManager';
|
|
import type { LightPlugin, LightClient, SentryEvent } from '../src/types';
|
|
|
|
describe('PluginManager', () => {
|
|
let mockClient: LightClient;
|
|
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
beforeEach(() => {
|
|
mockClient = {
|
|
config: { dsn: 'https://abc@sentry.io/123' },
|
|
dsn: { protocol: 'https', publicKey: 'abc', host: 'sentry.io', projectId: '123' },
|
|
on: vi.fn(),
|
|
off: vi.fn(),
|
|
emit: vi.fn(),
|
|
captureException: vi.fn(),
|
|
captureMessage: vi.fn(),
|
|
captureEvent: vi.fn(),
|
|
setUser: vi.fn(),
|
|
setAnonymousId: vi.fn(),
|
|
setTag: vi.fn(),
|
|
setTags: vi.fn(),
|
|
setExtra: vi.fn(),
|
|
addBreadcrumb: vi.fn(),
|
|
startTransaction: vi.fn(),
|
|
getCurrentTransaction: vi.fn(),
|
|
getCurrentSpan: vi.fn(),
|
|
setSpan: vi.fn(),
|
|
startSpan: vi.fn(),
|
|
flush: vi.fn(),
|
|
disable: vi.fn(),
|
|
enable: vi.fn(),
|
|
use: vi.fn(),
|
|
init: vi.fn(),
|
|
destroy: vi.fn(),
|
|
} as unknown as LightClient;
|
|
|
|
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
it('should create a PluginManager instance', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
expect(manager).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('add', () => {
|
|
it('should add a plugin', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin: LightPlugin = {
|
|
name: 'test-plugin',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
};
|
|
|
|
manager.add(plugin);
|
|
expect(manager.has('test-plugin')).toBe(true);
|
|
});
|
|
|
|
it('should not add duplicate plugin', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const setup1 = vi.fn();
|
|
const setup2 = vi.fn();
|
|
const plugin1: LightPlugin = {
|
|
name: 'test-plugin',
|
|
version: '1.0.0',
|
|
setup: setup1,
|
|
};
|
|
const plugin2: LightPlugin = {
|
|
name: 'test-plugin',
|
|
version: '2.0.0',
|
|
setup: setup2,
|
|
};
|
|
|
|
manager.add(plugin1);
|
|
manager.add(plugin2);
|
|
|
|
expect(manager.has('test-plugin')).toBe(true);
|
|
expect(setup1).toHaveBeenCalledWith(mockClient);
|
|
expect(setup2).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should call plugin setup with client', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const setupFn = vi.fn();
|
|
const plugin: LightPlugin = {
|
|
name: 'setup-test',
|
|
version: '1.0.0',
|
|
setup: setupFn,
|
|
};
|
|
|
|
manager.add(plugin);
|
|
expect(setupFn).toHaveBeenCalledWith(mockClient);
|
|
});
|
|
|
|
it('should not throw if setup throws', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin: LightPlugin = {
|
|
name: 'error-plugin',
|
|
version: '1.0.0',
|
|
setup: () => {
|
|
throw new Error('Setup error');
|
|
},
|
|
};
|
|
|
|
expect(() => manager.add(plugin)).not.toThrow();
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('remove', () => {
|
|
it('should remove a plugin', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin: LightPlugin = {
|
|
name: 'removable',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
};
|
|
|
|
manager.add(plugin);
|
|
expect(manager.has('removable')).toBe(true);
|
|
|
|
manager.remove('removable');
|
|
expect(manager.has('removable')).toBe(false);
|
|
});
|
|
|
|
it('should call plugin destroy when removing', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const destroyFn = vi.fn();
|
|
const plugin: LightPlugin = {
|
|
name: 'destroyable',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
destroy: destroyFn,
|
|
};
|
|
|
|
manager.add(plugin);
|
|
manager.remove('destroyable');
|
|
|
|
expect(destroyFn).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not throw if destroy throws', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin: LightPlugin = {
|
|
name: 'destroy-error',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
destroy: () => {
|
|
throw new Error('Destroy error');
|
|
},
|
|
};
|
|
|
|
manager.add(plugin);
|
|
expect(() => manager.remove('destroy-error')).not.toThrow();
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should do nothing if plugin does not exist', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
manager.remove('non-existent');
|
|
expect(consoleSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should do nothing if plugin has no destroy method', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin: LightPlugin = {
|
|
name: 'no-destroy',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
};
|
|
|
|
manager.add(plugin);
|
|
expect(() => manager.remove('no-destroy')).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('get', () => {
|
|
it('should return plugin by name', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin: LightPlugin = {
|
|
name: 'gettable',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
};
|
|
|
|
manager.add(plugin);
|
|
const retrieved = manager.get('gettable');
|
|
expect(retrieved).toBe(plugin);
|
|
});
|
|
|
|
it('should return undefined for non-existent plugin', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const retrieved = manager.get('non-existent');
|
|
expect(retrieved).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('has', () => {
|
|
it('should return true for added plugin', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin: LightPlugin = {
|
|
name: 'checkable',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
};
|
|
|
|
manager.add(plugin);
|
|
expect(manager.has('checkable')).toBe(true);
|
|
});
|
|
|
|
it('should return false for non-existent plugin', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
expect(manager.has('non-existent')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('applyBeforeReport', () => {
|
|
it('should pass event through all plugins', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin1: LightPlugin = {
|
|
name: 'before-1',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
beforeReport: (event) => ({ ...event, extra: { ...event.extra, from1: true } }),
|
|
};
|
|
const plugin2: LightPlugin = {
|
|
name: 'before-2',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
beforeReport: (event) => ({ ...event, extra: { ...event.extra, from2: true } }),
|
|
};
|
|
|
|
manager.add(plugin1);
|
|
manager.add(plugin2);
|
|
|
|
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
|
|
const result = manager.applyBeforeReport(event);
|
|
|
|
expect(result.extra).toEqual({ from1: true, from2: true });
|
|
});
|
|
|
|
it('should return null if any plugin filters the event', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin1: LightPlugin = {
|
|
name: 'filter-1',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
beforeReport: (event) => event,
|
|
};
|
|
const plugin2: LightPlugin = {
|
|
name: 'filter-2',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
beforeReport: () => null,
|
|
};
|
|
|
|
manager.add(plugin1);
|
|
manager.add(plugin2);
|
|
|
|
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
|
|
const result = manager.applyBeforeReport(event);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should continue if plugin beforeReport throws', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin1: LightPlugin = {
|
|
name: 'error-before',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
beforeReport: () => {
|
|
throw new Error('BeforeReport error');
|
|
},
|
|
};
|
|
const plugin2: LightPlugin = {
|
|
name: 'ok-after',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
beforeReport: (event) => ({ ...event, extra: { passed: true } }),
|
|
};
|
|
|
|
manager.add(plugin1);
|
|
manager.add(plugin2);
|
|
|
|
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
|
|
const result = manager.applyBeforeReport(event);
|
|
|
|
expect(result?.extra).toEqual({ passed: true });
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should skip plugin without beforeReport', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin1: LightPlugin = {
|
|
name: 'no-before',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
};
|
|
const plugin2: LightPlugin = {
|
|
name: 'modify-after',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
beforeReport: (event) => ({ ...event, extra: { modified: true } }),
|
|
};
|
|
|
|
manager.add(plugin1);
|
|
manager.add(plugin2);
|
|
|
|
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
|
|
const result = manager.applyBeforeReport(event);
|
|
|
|
expect(result?.extra).toEqual({ modified: true });
|
|
});
|
|
|
|
it('should return event when no plugins', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
|
|
const result = manager.applyBeforeReport(event);
|
|
expect(result).toBe(event);
|
|
});
|
|
});
|
|
|
|
describe('applyAfterReport', () => {
|
|
it('should call afterReport on all plugins', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const after1 = vi.fn();
|
|
const after2 = vi.fn();
|
|
const plugin1: LightPlugin = {
|
|
name: 'after-1',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
afterReport: after1,
|
|
};
|
|
const plugin2: LightPlugin = {
|
|
name: 'after-2',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
afterReport: after2,
|
|
};
|
|
|
|
manager.add(plugin1);
|
|
manager.add(plugin2);
|
|
|
|
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
|
|
manager.applyAfterReport(event);
|
|
|
|
expect(after1).toHaveBeenCalledWith(event);
|
|
expect(after2).toHaveBeenCalledWith(event);
|
|
});
|
|
|
|
it('should continue if afterReport throws', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin1: LightPlugin = {
|
|
name: 'error-after',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
afterReport: () => {
|
|
throw new Error('AfterReport error');
|
|
},
|
|
};
|
|
const plugin2: LightPlugin = {
|
|
name: 'ok-after',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
afterReport: vi.fn(),
|
|
};
|
|
|
|
manager.add(plugin1);
|
|
manager.add(plugin2);
|
|
|
|
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
|
|
expect(() => manager.applyAfterReport(event)).not.toThrow();
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should skip plugin without afterReport', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin: LightPlugin = {
|
|
name: 'no-after',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
};
|
|
|
|
manager.add(plugin);
|
|
|
|
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
|
|
expect(() => manager.applyAfterReport(event)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('destroy', () => {
|
|
it('should call destroy on all plugins', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const destroy1 = vi.fn();
|
|
const destroy2 = vi.fn();
|
|
const plugin1: LightPlugin = {
|
|
name: 'destroy-1',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
destroy: destroy1,
|
|
};
|
|
const plugin2: LightPlugin = {
|
|
name: 'destroy-2',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
destroy: destroy2,
|
|
};
|
|
|
|
manager.add(plugin1);
|
|
manager.add(plugin2);
|
|
manager.destroy();
|
|
|
|
expect(destroy1).toHaveBeenCalled();
|
|
expect(destroy2).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should clear plugins after destroy', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin: LightPlugin = {
|
|
name: 'clear-me',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
};
|
|
|
|
manager.add(plugin);
|
|
expect(manager.has('clear-me')).toBe(true);
|
|
|
|
manager.destroy();
|
|
expect(manager.has('clear-me')).toBe(false);
|
|
});
|
|
|
|
it('should continue even if destroy throws', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin1: LightPlugin = {
|
|
name: 'destroy-error',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
destroy: () => {
|
|
throw new Error('Destroy error');
|
|
},
|
|
};
|
|
const plugin2: LightPlugin = {
|
|
name: 'destroy-ok',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
destroy: vi.fn(),
|
|
};
|
|
|
|
manager.add(plugin1);
|
|
manager.add(plugin2);
|
|
manager.destroy();
|
|
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
expect((plugin2 as any).destroy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not throw if plugin has no destroy method', () => {
|
|
const manager = new PluginManager(mockClient);
|
|
const plugin: LightPlugin = {
|
|
name: 'no-destroy',
|
|
version: '1.0.0',
|
|
setup: vi.fn(),
|
|
};
|
|
|
|
manager.add(plugin);
|
|
expect(() => manager.destroy()).not.toThrow();
|
|
});
|
|
});
|
|
});
|