Skip to content

Commit d182b3d

Browse files
committed
feat(tests): add unit tests for plugin system errors and plugin manager functionality
1 parent 9ee78fd commit d182b3d

File tree

3 files changed

+238
-0
lines changed

3 files changed

+238
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
PluginError,
4+
PluginLoadError,
5+
PluginInitializeError,
6+
PluginDuplicateError,
7+
PluginNotFoundError,
8+
} from '@src/plugin-system/errors';
9+
10+
describe('Plugin System Errors', () => {
11+
describe('PluginError', () => {
12+
it('should create a PluginError instance', () => {
13+
const error = new PluginError('Test base message');
14+
expect(error).toBeInstanceOf(Error);
15+
expect(error).toBeInstanceOf(PluginError);
16+
expect(error.name).toBe('PluginError');
17+
expect(error.message).toBe('Test base message');
18+
expect(error.cause).toBeUndefined();
19+
});
20+
});
21+
22+
describe('PluginLoadError', () => {
23+
it('should create a PluginLoadError instance', () => {
24+
const causeError = new Error('Underlying cause');
25+
const error = new PluginLoadError('test-plugin', causeError);
26+
expect(error).toBeInstanceOf(Error);
27+
expect(error).toBeInstanceOf(PluginError);
28+
expect(error).toBeInstanceOf(PluginLoadError);
29+
expect(error.name).toBe('PluginLoadError');
30+
expect(error.message).toBe('Failed to load plugin: test-plugin');
31+
expect(error.cause).toBe(causeError);
32+
});
33+
});
34+
35+
describe('PluginInitializeError', () => {
36+
it('should create a PluginInitializeError instance', () => {
37+
const causeError = new Error('Initialization failed');
38+
const error = new PluginInitializeError('test-plugin', causeError);
39+
expect(error).toBeInstanceOf(Error);
40+
expect(error).toBeInstanceOf(PluginError);
41+
expect(error).toBeInstanceOf(PluginInitializeError);
42+
expect(error.name).toBe('PluginInitializeError');
43+
expect(error.message).toBe('Failed to initialize plugin: test-plugin');
44+
expect(error.cause).toBe(causeError);
45+
});
46+
});
47+
48+
describe('PluginDuplicateError', () => {
49+
it('should create a PluginDuplicateError instance', () => {
50+
const error = new PluginDuplicateError('test-plugin');
51+
expect(error).toBeInstanceOf(Error);
52+
expect(error).toBeInstanceOf(PluginError);
53+
expect(error).toBeInstanceOf(PluginDuplicateError);
54+
expect(error.name).toBe('PluginDuplicateError');
55+
expect(error.message).toBe("Plugin with ID 'test-plugin' is already loaded");
56+
expect(error.cause).toBeUndefined();
57+
});
58+
});
59+
60+
describe('PluginNotFoundError', () => {
61+
it('should create a PluginNotFoundError instance', () => {
62+
const error = new PluginNotFoundError('test-plugin');
63+
expect(error).toBeInstanceOf(Error);
64+
expect(error).toBeInstanceOf(PluginError);
65+
expect(error).toBeInstanceOf(PluginNotFoundError);
66+
expect(error.name).toBe('PluginNotFoundError');
67+
expect(error.message).toBe("Plugin with ID 'test-plugin' not found");
68+
expect(error.cause).toBeUndefined();
69+
});
70+
});
71+
});
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from 'vitest';
2+
import path from 'node:path';
3+
import { PluginManager } from '@src/plugin-system/plugin-manager';
4+
import {
5+
PluginLoadError,
6+
PluginDuplicateError,
7+
PluginNotFoundError
8+
} from '@src/plugin-system/errors';
9+
import type {
10+
Plugin,
11+
PluginConfiguration,
12+
PluginPackage,
13+
GlobalSettingDefinitionForPlugin,
14+
GlobalSettingGroupForPlugin
15+
} from '@src/plugin-system/types';
16+
import type { FastifyInstance } from 'fastify';
17+
import type { AnyDatabase } from '@src/db';
18+
19+
// Mock modules
20+
vi.mock('node:fs');
21+
vi.mock('node:fs/promises');
22+
vi.mock('@src/services/globalSettingsService');
23+
24+
// Helper to create a mock plugin
25+
const createMockPlugin = (id: string, name: string, version = '1.0.0'): Mocked<Plugin> => ({
26+
meta: { id, name, version, description: `Mock plugin ${id}` },
27+
initialize: vi.fn().mockResolvedValue(undefined),
28+
shutdown: vi.fn().mockResolvedValue(undefined),
29+
reinitialize: vi.fn().mockResolvedValue(undefined),
30+
databaseExtension: undefined,
31+
globalSettingsExtension: undefined,
32+
});
33+
34+
describe('PluginManager', () => {
35+
let pluginManager: PluginManager;
36+
let mockApp: Mocked<FastifyInstance>;
37+
let mockDb: Mocked<AnyDatabase>;
38+
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
39+
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
40+
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
41+
42+
beforeEach(() => {
43+
vi.resetAllMocks();
44+
45+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
46+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
47+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
48+
49+
mockApp = {
50+
decorate: vi.fn(),
51+
addHook: vi.fn(),
52+
register: vi.fn(),
53+
} as unknown as Mocked<FastifyInstance>;
54+
55+
mockDb = {} as Mocked<AnyDatabase>;
56+
57+
pluginManager = new PluginManager();
58+
});
59+
60+
afterEach(() => {
61+
consoleLogSpy.mockRestore();
62+
consoleWarnSpy.mockRestore();
63+
consoleErrorSpy.mockRestore();
64+
});
65+
66+
describe('Constructor and Basic Configuration', () => {
67+
it('should initialize with default empty paths and options', () => {
68+
expect(pluginManager['pluginPaths']).toEqual([]);
69+
expect(pluginManager['pluginOptions'].size).toBe(0);
70+
});
71+
72+
it('should initialize with provided paths', () => {
73+
const config: PluginConfiguration = { paths: ['/path/to/plugins'] };
74+
const pm = new PluginManager(config);
75+
expect(pm['pluginPaths']).toEqual(['/path/to/plugins']);
76+
});
77+
78+
it('should set the Fastify app instance', () => {
79+
pluginManager.setApp(mockApp);
80+
expect(pluginManager['app']).toBe(mockApp);
81+
});
82+
83+
it('should set the database instance', () => {
84+
pluginManager.setDatabase(mockDb);
85+
expect(pluginManager['db']).toBe(mockDb);
86+
pluginManager.setDatabase(null);
87+
expect(pluginManager['db']).toBeNull();
88+
});
89+
});
90+
91+
describe('Plugin Registration and Retrieval', () => {
92+
it('should register a plugin', () => {
93+
const plugin = createMockPlugin('plugin1', 'Plugin One');
94+
pluginManager.registerPlugin(plugin);
95+
expect(pluginManager.getPlugin('plugin1')).toBe(plugin);
96+
expect(pluginManager.getAllPlugins()).toEqual([plugin]);
97+
});
98+
99+
it('should throw PluginDuplicateError when registering a duplicate plugin', () => {
100+
const plugin1 = createMockPlugin('plugin1', 'Plugin One');
101+
pluginManager.registerPlugin(plugin1);
102+
const plugin2 = createMockPlugin('plugin1', 'Plugin One Duplicate');
103+
104+
expect(() => {
105+
pluginManager.registerPlugin(plugin2);
106+
}).toThrow(PluginDuplicateError);
107+
});
108+
109+
it('should get a plugin by ID', () => {
110+
const plugin = createMockPlugin('plugin1', 'Plugin One');
111+
pluginManager.registerPlugin(plugin);
112+
expect(pluginManager.getPlugin('plugin1')).toBe(plugin);
113+
});
114+
115+
it('should throw PluginNotFoundError when getting a non-existent plugin', () => {
116+
expect(() => {
117+
pluginManager.getPlugin('non-existent');
118+
}).toThrow(PluginNotFoundError);
119+
});
120+
121+
it('should get all plugins', () => {
122+
const plugin1 = createMockPlugin('plugin1', 'Plugin One');
123+
const plugin2 = createMockPlugin('plugin2', 'Plugin Two');
124+
pluginManager.registerPlugin(plugin1);
125+
pluginManager.registerPlugin(plugin2);
126+
expect(pluginManager.getAllPlugins()).toEqual(expect.arrayContaining([plugin1, plugin2]));
127+
expect(pluginManager.getAllPlugins().length).toBe(2);
128+
});
129+
});
130+
131+
describe('Plugin Lifecycle', () => {
132+
let plugin1: Mocked<Plugin>, plugin2: Mocked<Plugin>;
133+
134+
beforeEach(() => {
135+
plugin1 = createMockPlugin('p1', 'Plugin1');
136+
plugin2 = createMockPlugin('p2', 'Plugin2');
137+
pluginManager.registerPlugin(plugin1);
138+
pluginManager.registerPlugin(plugin2);
139+
pluginManager.setApp(mockApp);
140+
pluginManager.setDatabase(mockDb);
141+
});
142+
143+
describe('initializePlugins', () => {
144+
it('should initialize all loaded plugins', async () => {
145+
await pluginManager.initializePlugins();
146+
expect(plugin1.initialize).toHaveBeenCalledWith(mockApp, mockDb);
147+
expect(plugin2.initialize).toHaveBeenCalledWith(mockApp, mockDb);
148+
expect(pluginManager['initialized']).toBe(true);
149+
});
150+
151+
it('should throw error if app is not set', async () => {
152+
pluginManager.setApp(null as any);
153+
await expect(pluginManager.initializePlugins()).rejects.toThrow('Cannot initialize plugins: Fastify app not set');
154+
});
155+
});
156+
157+
describe('shutdownPlugins', () => {
158+
it('should call shutdown on all plugins that have it', async () => {
159+
await pluginManager.shutdownPlugins();
160+
expect(plugin1.shutdown).toHaveBeenCalled();
161+
expect(plugin2.shutdown).toHaveBeenCalled();
162+
expect(pluginManager['initialized']).toBe(false);
163+
});
164+
});
165+
});
166+
});

services/backend/vitest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default defineConfig({
1515
'src/**/*.spec.ts',
1616
'src/test/**',
1717
'src/types/**',
18+
'src/plugins/example-plugin/**', // Exclude example plugin
1819
'src/index.ts', // Entry point
1920
'src/server.ts', // Server setup
2021
],

0 commit comments

Comments
 (0)