Skip to content

Commit f5ccdf4

Browse files
author
Lasim
committed
Add unit tests for Global Settings module and related types
- Implement tests for GlobalSettingsInitService covering methods like getAllSettings, getSettingsByGroup, loadSettingsDefinitions, and configuration getters. - Create tests for settings modules including SMTP, GitHub OAuth, and Global settings to validate structure, required settings, and descriptions. - Introduce type validation tests for GlobalSettingType, GlobalSettingDefinition, GlobalSettingGroup, GlobalSettingsModule, SmtpConfig, GitHubOAuthConfig, GlobalConfig, ValidationResult, and InitializationResult. - Ensure cross-module validation for unique group IDs and setting keys, consistent sort order, and appropriate default values.
1 parent d182b3d commit f5ccdf4

File tree

9 files changed

+2822
-0
lines changed

9 files changed

+2822
-0
lines changed
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach, type MockedFunction } from 'vitest';
2+
import { generateMigrations, applyMigrations } from '../../../src/db/migrations';
3+
4+
// Create mock functions using vi.hoisted
5+
const { mockExec, mockDatabase, mockDrizzle, mockMigrate, mockAccess, mockClose } = vi.hoisted(() => ({
6+
mockExec: vi.fn(),
7+
mockDatabase: vi.fn(),
8+
mockDrizzle: vi.fn(),
9+
mockMigrate: vi.fn(),
10+
mockAccess: vi.fn(),
11+
mockClose: vi.fn(),
12+
}));
13+
14+
// Mock the child_process module - we need to mock the callback version since it gets promisified
15+
vi.mock('node:child_process', () => ({
16+
exec: vi.fn(),
17+
}));
18+
19+
// Mock node:util to control the promisify behavior
20+
vi.mock('node:util', () => ({
21+
promisify: vi.fn(() => mockExec),
22+
}));
23+
24+
// Mock better-sqlite3
25+
vi.mock('better-sqlite3', () => ({
26+
default: mockDatabase,
27+
}));
28+
29+
// Mock drizzle-orm
30+
vi.mock('drizzle-orm/better-sqlite3', () => ({
31+
drizzle: mockDrizzle,
32+
}));
33+
34+
vi.mock('drizzle-orm/better-sqlite3/migrator', () => ({
35+
migrate: mockMigrate,
36+
}));
37+
38+
// Mock fs/promises
39+
vi.mock('node:fs/promises', () => ({
40+
default: {
41+
access: mockAccess,
42+
},
43+
access: mockAccess,
44+
}));
45+
46+
describe('Database Migrations', () => {
47+
beforeEach(() => {
48+
vi.resetAllMocks();
49+
50+
// Setup default mock implementations
51+
mockDatabase.mockReturnValue({
52+
close: mockClose,
53+
});
54+
55+
mockDrizzle.mockReturnValue({});
56+
mockMigrate.mockResolvedValue(undefined);
57+
mockAccess.mockResolvedValue(undefined);
58+
mockClose.mockReturnValue(undefined);
59+
});
60+
61+
afterEach(() => {
62+
vi.clearAllMocks();
63+
});
64+
65+
describe('generateMigrations', () => {
66+
const testSchemaPath = 'src/db/schema.ts';
67+
const testOutDir = 'drizzle/migrations';
68+
69+
it('should execute drizzle-kit generate command successfully', async () => {
70+
const mockStdout = 'Migration generated successfully';
71+
const mockStderr = '';
72+
73+
mockExec.mockResolvedValue({
74+
stdout: mockStdout,
75+
stderr: mockStderr,
76+
});
77+
78+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
79+
80+
await generateMigrations(testSchemaPath, testOutDir);
81+
82+
expect(mockExec).toHaveBeenCalledWith(
83+
`npx drizzle-kit generate:sqlite --schema=${testSchemaPath} --out=${testOutDir}`
84+
);
85+
expect(consoleLogSpy).toHaveBeenCalledWith(`Migration stdout: ${mockStdout}`);
86+
87+
consoleLogSpy.mockRestore();
88+
});
89+
90+
it('should log stderr output when present', async () => {
91+
const mockStdout = 'Migration generated';
92+
const mockStderr = 'Warning: deprecated option used';
93+
94+
mockExec.mockResolvedValue({
95+
stdout: mockStdout,
96+
stderr: mockStderr,
97+
});
98+
99+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
100+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
101+
102+
await generateMigrations(testSchemaPath, testOutDir);
103+
104+
expect(consoleErrorSpy).toHaveBeenCalledWith(`Migration stderr: ${mockStderr}`);
105+
expect(consoleLogSpy).toHaveBeenCalledWith(`Migration stdout: ${mockStdout}`);
106+
107+
consoleErrorSpy.mockRestore();
108+
consoleLogSpy.mockRestore();
109+
});
110+
111+
it('should handle and re-throw exec errors', async () => {
112+
const mockError = new Error('Command failed');
113+
mockExec.mockRejectedValue(mockError);
114+
115+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
116+
117+
await expect(generateMigrations(testSchemaPath, testOutDir)).rejects.toThrow(mockError);
118+
119+
expect(consoleErrorSpy).toHaveBeenCalledWith('Migration generation error:', mockError);
120+
121+
consoleErrorSpy.mockRestore();
122+
});
123+
124+
it('should handle exec errors with stderr', async () => {
125+
const mockError = new Error('drizzle-kit not found');
126+
mockExec.mockRejectedValue(mockError);
127+
128+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
129+
130+
await expect(generateMigrations(testSchemaPath, testOutDir)).rejects.toThrow(mockError);
131+
132+
expect(mockExec).toHaveBeenCalledWith(
133+
`npx drizzle-kit generate:sqlite --schema=${testSchemaPath} --out=${testOutDir}`
134+
);
135+
expect(consoleErrorSpy).toHaveBeenCalledWith('Migration generation error:', mockError);
136+
137+
consoleErrorSpy.mockRestore();
138+
});
139+
});
140+
141+
describe('applyMigrations', () => {
142+
const testDbPath = 'test.db';
143+
const testMigrationsDir = 'drizzle/migrations';
144+
let mockDbInstance: any;
145+
146+
beforeEach(() => {
147+
mockDbInstance = {
148+
close: mockClose,
149+
};
150+
mockDatabase.mockReturnValue(mockDbInstance);
151+
});
152+
153+
it('should apply migrations successfully', async () => {
154+
const mockDrizzleInstance = {};
155+
mockDrizzle.mockReturnValue(mockDrizzleInstance);
156+
mockAccess.mockResolvedValue(undefined);
157+
mockMigrate.mockResolvedValue(undefined);
158+
159+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
160+
161+
await applyMigrations(testDbPath, testMigrationsDir);
162+
163+
expect(mockDatabase).toHaveBeenCalledWith(testDbPath);
164+
expect(mockDrizzle).toHaveBeenCalledWith(mockDbInstance);
165+
expect(mockAccess).toHaveBeenCalledWith(testMigrationsDir);
166+
expect(mockMigrate).toHaveBeenCalledWith(mockDrizzleInstance, { migrationsFolder: testMigrationsDir });
167+
expect(mockClose).toHaveBeenCalled();
168+
expect(consoleLogSpy).toHaveBeenCalledWith('Migrations applied successfully');
169+
170+
consoleLogSpy.mockRestore();
171+
});
172+
173+
it('should handle missing migrations directory', async () => {
174+
const accessError = new Error('ENOENT: no such file or directory') as NodeJS.ErrnoException;
175+
accessError.code = 'ENOENT';
176+
mockAccess.mockRejectedValue(accessError);
177+
178+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
179+
180+
await expect(applyMigrations(testDbPath, testMigrationsDir)).rejects.toThrow(accessError);
181+
182+
expect(mockDatabase).toHaveBeenCalledWith(testDbPath);
183+
expect(mockAccess).toHaveBeenCalledWith(testMigrationsDir);
184+
expect(mockMigrate).not.toHaveBeenCalled();
185+
expect(mockClose).toHaveBeenCalled(); // Should still close the database
186+
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to apply migrations:', accessError);
187+
188+
consoleErrorSpy.mockRestore();
189+
});
190+
191+
it('should handle migration application errors', async () => {
192+
const mockDrizzleInstance = {};
193+
const migrationError = new Error('Migration failed: syntax error');
194+
195+
mockDrizzle.mockReturnValue(mockDrizzleInstance);
196+
mockAccess.mockResolvedValue(undefined);
197+
mockMigrate.mockRejectedValue(migrationError);
198+
199+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
200+
201+
await expect(applyMigrations(testDbPath, testMigrationsDir)).rejects.toThrow(migrationError);
202+
203+
expect(mockDatabase).toHaveBeenCalledWith(testDbPath);
204+
expect(mockDrizzle).toHaveBeenCalledWith(mockDbInstance);
205+
expect(mockAccess).toHaveBeenCalledWith(testMigrationsDir);
206+
expect(mockMigrate).toHaveBeenCalledWith(mockDrizzleInstance, { migrationsFolder: testMigrationsDir });
207+
expect(mockClose).toHaveBeenCalled(); // Should still close the database
208+
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to apply migrations:', migrationError);
209+
210+
consoleErrorSpy.mockRestore();
211+
});
212+
213+
it('should handle database connection errors', async () => {
214+
const dbError = new Error('Database connection failed');
215+
mockDatabase.mockImplementation(() => {
216+
throw dbError;
217+
});
218+
219+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
220+
221+
await expect(applyMigrations(testDbPath, testMigrationsDir)).rejects.toThrow(dbError);
222+
223+
expect(mockDatabase).toHaveBeenCalledWith(testDbPath);
224+
expect(mockDrizzle).not.toHaveBeenCalled();
225+
expect(mockAccess).not.toHaveBeenCalled();
226+
expect(mockMigrate).not.toHaveBeenCalled();
227+
// mockClose should not be called since database creation failed
228+
229+
consoleErrorSpy.mockRestore();
230+
});
231+
232+
it('should ensure database is closed even if migration fails', async () => {
233+
const mockDrizzleInstance = {};
234+
const migrationError = new Error('Migration failed');
235+
236+
mockDrizzle.mockReturnValue(mockDrizzleInstance);
237+
mockAccess.mockResolvedValue(undefined);
238+
mockMigrate.mockRejectedValue(migrationError);
239+
240+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
241+
242+
await expect(applyMigrations(testDbPath, testMigrationsDir)).rejects.toThrow(migrationError);
243+
244+
// Verify database is closed in finally block
245+
expect(mockClose).toHaveBeenCalled();
246+
247+
consoleErrorSpy.mockRestore();
248+
});
249+
250+
it('should ensure database is closed even if access check fails', async () => {
251+
const accessError = new Error('Permission denied');
252+
mockAccess.mockRejectedValue(accessError);
253+
254+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
255+
256+
await expect(applyMigrations(testDbPath, testMigrationsDir)).rejects.toThrow(accessError);
257+
258+
// Verify database is closed in finally block
259+
expect(mockClose).toHaveBeenCalled();
260+
261+
consoleErrorSpy.mockRestore();
262+
});
263+
});
264+
265+
describe('Integration scenarios', () => {
266+
it('should handle complete migration workflow', async () => {
267+
// Test generateMigrations followed by applyMigrations
268+
const schemaPath = 'src/db/schema.ts';
269+
const outDir = 'drizzle/migrations';
270+
const dbPath = 'test.db';
271+
272+
// Mock successful generation
273+
mockExec.mockResolvedValue({
274+
stdout: 'Migration files generated',
275+
stderr: '',
276+
});
277+
278+
// Mock successful application
279+
const mockDrizzleInstance = {};
280+
mockDrizzle.mockReturnValue(mockDrizzleInstance);
281+
mockAccess.mockResolvedValue(undefined);
282+
mockMigrate.mockResolvedValue(undefined);
283+
284+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
285+
286+
// Generate migrations
287+
await generateMigrations(schemaPath, outDir);
288+
289+
// Apply migrations
290+
await applyMigrations(dbPath, outDir);
291+
292+
expect(mockExec).toHaveBeenCalledWith(
293+
`npx drizzle-kit generate:sqlite --schema=${schemaPath} --out=${outDir}`
294+
);
295+
expect(mockMigrate).toHaveBeenCalledWith(mockDrizzleInstance, { migrationsFolder: outDir });
296+
expect(consoleLogSpy).toHaveBeenCalledWith('Migration stdout: Migration files generated');
297+
expect(consoleLogSpy).toHaveBeenCalledWith('Migrations applied successfully');
298+
299+
consoleLogSpy.mockRestore();
300+
});
301+
});
302+
});

0 commit comments

Comments
 (0)