light-sentry-sdk/tests/PluginManager.test.ts

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();
});
});
});