Skip to content

Commit a31fe39

Browse files
committed
feat(tests): add unit tests for database service and mock dependencies
1 parent 0d18fe2 commit a31fe39

File tree

3 files changed

+472
-1
lines changed

3 files changed

+472
-1
lines changed

services/backend/src/db/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { sqliteTable, text as sqliteText, integer as sqliteInteger } from 'drizz
1717
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1818
export type AnyDatabase = BetterSQLite3Database<any>;
1919
// eslint-disable-next-line @typescript-eslint/no-explicit-any
20-
type AnySchema = Record<string, any>; // Represents the schema object Drizzle uses
20+
export type AnySchema = Record<string, any>; // Represents the schema object Drizzle uses
2121

2222
// Global state for database instance and schema
2323
let dbInstance: AnyDatabase | null = null;
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach, type MockedFunction } from 'vitest';
2+
import path from 'node:path';
3+
import { getDbConfig, saveDbConfig, deleteDbConfig, type DbConfig, type SQLiteConfig } from '../../../src/db/config';
4+
5+
// Create mock functions using vi.hoisted
6+
const { mockMkdir, mockReadFile, mockWriteFile, mockUnlink } = vi.hoisted(() => ({
7+
mockMkdir: vi.fn(),
8+
mockReadFile: vi.fn(),
9+
mockWriteFile: vi.fn(),
10+
mockUnlink: vi.fn(),
11+
}));
12+
13+
// Mock the fs/promises module
14+
vi.mock('node:fs/promises', () => ({
15+
default: {
16+
mkdir: mockMkdir,
17+
readFile: mockReadFile,
18+
writeFile: mockWriteFile,
19+
unlink: mockUnlink,
20+
},
21+
mkdir: mockMkdir,
22+
readFile: mockReadFile,
23+
writeFile: mockWriteFile,
24+
unlink: mockUnlink,
25+
}));
26+
27+
const TEST_CONFIG_DIR = path.join(__dirname, '..', '..', '..', 'src', 'db', '..', '..', 'persistent_data');
28+
const TEST_DB_SELECTION_FILE_NAME = 'db.selection.test.json';
29+
const TEST_CONFIG_FILE_PATH = path.join(TEST_CONFIG_DIR, TEST_DB_SELECTION_FILE_NAME);
30+
31+
describe('Database Configuration', () => {
32+
let originalNodeEnv: string | undefined;
33+
34+
beforeEach(() => {
35+
vi.resetAllMocks();
36+
originalNodeEnv = process.env.NODE_ENV;
37+
process.env.NODE_ENV = 'test'; // Ensure test mode for consistent file naming
38+
39+
// Default mock for mkdir, can be overridden in specific tests if needed
40+
mockMkdir.mockResolvedValue(TEST_CONFIG_DIR as any);
41+
});
42+
43+
afterEach(() => {
44+
process.env.NODE_ENV = originalNodeEnv;
45+
});
46+
47+
describe('getDbConfig', () => {
48+
it('should read and parse an existing configuration file', async () => {
49+
const mockConfig: SQLiteConfig = { type: 'sqlite', dbPath: 'test.db' };
50+
mockReadFile.mockResolvedValue(JSON.stringify(mockConfig));
51+
52+
const config = await getDbConfig();
53+
expect(config).toEqual(mockConfig);
54+
expect(mockMkdir).toHaveBeenCalledWith(TEST_CONFIG_DIR, { recursive: true });
55+
expect(mockReadFile).toHaveBeenCalledWith(TEST_CONFIG_FILE_PATH, 'utf-8');
56+
});
57+
58+
it('should return null if the configuration file does not exist (ENOENT)', async () => {
59+
const error = new Error('File not found') as NodeJS.ErrnoException;
60+
error.code = 'ENOENT';
61+
mockReadFile.mockRejectedValue(error);
62+
63+
const config = await getDbConfig();
64+
expect(config).toBeNull();
65+
expect(mockMkdir).toHaveBeenCalledWith(TEST_CONFIG_DIR, { recursive: true });
66+
});
67+
68+
it('should log an error and return null for other readFile errors', async () => {
69+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
70+
const error = new Error('Read permission denied');
71+
mockReadFile.mockRejectedValue(error);
72+
73+
const config = await getDbConfig();
74+
expect(config).toBeNull();
75+
expect(mockMkdir).toHaveBeenCalledWith(TEST_CONFIG_DIR, { recursive: true });
76+
expect(consoleErrorSpy).toHaveBeenCalledWith('[ERROR] Failed to read database configuration:', error);
77+
consoleErrorSpy.mockRestore();
78+
});
79+
80+
it('should log an error and return null if JSON parsing fails', async () => {
81+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
82+
mockReadFile.mockResolvedValue('invalid json');
83+
84+
// We expect JSON.parse to throw, which should be caught by getDbConfig
85+
// For this test, we don't mock JSON.parse itself, but rely on it failing.
86+
// The function should catch this and return null.
87+
const config = await getDbConfig();
88+
expect(config).toBeNull();
89+
// The error logged would be the SyntaxError from JSON.parse
90+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('[ERROR] Failed to read database configuration:'), expect.any(SyntaxError));
91+
consoleErrorSpy.mockRestore();
92+
});
93+
});
94+
95+
describe('saveDbConfig', () => {
96+
const mockConfig: SQLiteConfig = { type: 'sqlite', dbPath: 'test.db' };
97+
98+
it('should save the configuration file successfully', async () => {
99+
process.env.NODE_ENV = 'development'; // Set to non-test mode to enable logging
100+
mockWriteFile.mockResolvedValue(undefined);
101+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
102+
103+
await saveDbConfig(mockConfig);
104+
105+
expect(mockMkdir).toHaveBeenCalledWith(TEST_CONFIG_DIR, { recursive: true });
106+
expect(mockWriteFile).toHaveBeenCalledWith(TEST_CONFIG_FILE_PATH, JSON.stringify(mockConfig, null, 2), 'utf-8');
107+
expect(consoleLogSpy).toHaveBeenCalledWith(`[INFO] Database configuration saved to ${TEST_CONFIG_FILE_PATH}`);
108+
consoleLogSpy.mockRestore();
109+
});
110+
111+
it('should log an error and re-throw if writeFile fails', async () => {
112+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
113+
const error = new Error('Write permission denied');
114+
mockWriteFile.mockRejectedValue(error);
115+
116+
await expect(saveDbConfig(mockConfig)).rejects.toThrow(error);
117+
expect(mockMkdir).toHaveBeenCalledWith(TEST_CONFIG_DIR, { recursive: true });
118+
expect(consoleErrorSpy).toHaveBeenCalledWith('[ERROR] Failed to save database configuration:', error);
119+
consoleErrorSpy.mockRestore();
120+
});
121+
122+
it('should log an error and re-throw if mkdir fails', async () => {
123+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
124+
const error = new Error('Cannot create directory');
125+
mockMkdir.mockRejectedValue(error); // Mock mkdir to fail
126+
127+
await expect(saveDbConfig(mockConfig)).rejects.toThrow(error);
128+
// writeFile should not be called if mkdir fails
129+
expect(mockWriteFile).not.toHaveBeenCalled();
130+
// The error from mkdir is caught by the try-catch in saveDbConfig
131+
expect(consoleErrorSpy).toHaveBeenCalledWith('[ERROR] Failed to save database configuration:', error);
132+
consoleErrorSpy.mockRestore();
133+
});
134+
});
135+
136+
describe('deleteDbConfig', () => {
137+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
138+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
139+
140+
beforeEach(() => {
141+
consoleLogSpy.mockClear();
142+
consoleErrorSpy.mockClear();
143+
})
144+
145+
it('should delete the configuration file successfully', async () => {
146+
process.env.NODE_ENV = 'development'; // Set to non-test mode to enable logging
147+
mockUnlink.mockResolvedValue(undefined);
148+
149+
await deleteDbConfig();
150+
expect(mockUnlink).toHaveBeenCalledWith(TEST_CONFIG_FILE_PATH);
151+
expect(consoleLogSpy).toHaveBeenCalledWith(`[INFO] Database configuration deleted from ${TEST_CONFIG_FILE_PATH}`);
152+
});
153+
154+
it('should log info and not throw if the file does not exist (ENOENT)', async () => {
155+
process.env.NODE_ENV = 'development'; // Set to non-test mode to enable logging
156+
const error = new Error('File not found') as NodeJS.ErrnoException;
157+
error.code = 'ENOENT';
158+
mockUnlink.mockRejectedValue(error);
159+
160+
await deleteDbConfig();
161+
expect(mockUnlink).toHaveBeenCalledWith(TEST_CONFIG_FILE_PATH);
162+
expect(consoleLogSpy).toHaveBeenCalledWith('[INFO] Database configuration file not found, nothing to delete.');
163+
expect(consoleErrorSpy).not.toHaveBeenCalled(); // No error should be logged
164+
});
165+
166+
it('should log an error and re-throw for other unlink errors', async () => {
167+
const error = new Error('Delete permission denied');
168+
mockUnlink.mockRejectedValue(error);
169+
170+
await expect(deleteDbConfig()).rejects.toThrow(error);
171+
expect(mockUnlink).toHaveBeenCalledWith(TEST_CONFIG_FILE_PATH);
172+
expect(consoleErrorSpy).toHaveBeenCalledWith('[ERROR] Failed to delete database configuration:', error);
173+
expect(consoleLogSpy).not.toHaveBeenCalledWith(expect.stringContaining('nothing to delete'));
174+
});
175+
});
176+
177+
describe('NODE_ENV handling for logging', () => {
178+
const mockConfig: SQLiteConfig = { type: 'sqlite', dbPath: 'test.db' };
179+
180+
it('should not call console.log when NODE_ENV is "test" for saveDbConfig', async () => {
181+
process.env.NODE_ENV = 'test';
182+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
183+
mockWriteFile.mockResolvedValue(undefined);
184+
185+
await saveDbConfig(mockConfig);
186+
expect(consoleLogSpy).not.toHaveBeenCalled(); // logInfo should prevent this
187+
consoleLogSpy.mockRestore();
188+
});
189+
190+
it('should call console.log when NODE_ENV is not "test" for saveDbConfig', async () => {
191+
process.env.NODE_ENV = 'development'; // Not 'test'
192+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
193+
mockWriteFile.mockResolvedValue(undefined);
194+
195+
await saveDbConfig(mockConfig);
196+
expect(consoleLogSpy).toHaveBeenCalledWith(`[INFO] Database configuration saved to ${TEST_CONFIG_FILE_PATH}`); // Use the actual path that will be used
197+
consoleLogSpy.mockRestore();
198+
});
199+
200+
201+
it('should not call console.log when NODE_ENV is "test" for deleteDbConfig (success)', async () => {
202+
process.env.NODE_ENV = 'test';
203+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
204+
mockUnlink.mockResolvedValue(undefined);
205+
206+
await deleteDbConfig();
207+
expect(consoleLogSpy).not.toHaveBeenCalled();
208+
consoleLogSpy.mockRestore();
209+
});
210+
211+
it('should call console.log when NODE_ENV is not "test" for deleteDbConfig (success)', async () => {
212+
process.env.NODE_ENV = 'development';
213+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
214+
mockUnlink.mockResolvedValue(undefined);
215+
216+
await deleteDbConfig();
217+
expect(consoleLogSpy).toHaveBeenCalledWith(`[INFO] Database configuration deleted from ${TEST_CONFIG_FILE_PATH}`);
218+
consoleLogSpy.mockRestore();
219+
});
220+
221+
it('should not call console.log when NODE_ENV is "test" for deleteDbConfig (ENOENT)', async () => {
222+
process.env.NODE_ENV = 'test';
223+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
224+
const error = new Error('File not found') as NodeJS.ErrnoException;
225+
error.code = 'ENOENT';
226+
mockUnlink.mockRejectedValue(error);
227+
228+
await deleteDbConfig();
229+
expect(consoleLogSpy).not.toHaveBeenCalled();
230+
consoleLogSpy.mockRestore();
231+
});
232+
233+
it('should call console.log when NODE_ENV is not "test" for deleteDbConfig (ENOENT)', async () => {
234+
process.env.NODE_ENV = 'development';
235+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
236+
const error = new Error('File not found') as NodeJS.ErrnoException;
237+
error.code = 'ENOENT';
238+
mockUnlink.mockRejectedValue(error);
239+
240+
await deleteDbConfig();
241+
expect(consoleLogSpy).toHaveBeenCalledWith('[INFO] Database configuration file not found, nothing to delete.');
242+
consoleLogSpy.mockRestore();
243+
});
244+
});
245+
});

0 commit comments

Comments
 (0)