Skip to content

Commit 483fe3c

Browse files
author
Lasim
committed
Add unit tests for global settings schemas, main routes, and user routes
- Implement comprehensive tests for GlobalSettingSchema, CreateGlobalSettingSchema, UpdateGlobalSettingSchema, BulkGlobalSettingsSchema, SearchGlobalSettingsSchema, and CategoryFilterSchema. - Validate various scenarios including valid and invalid inputs for global settings. - Test route registration and health check endpoint in main routes. - Add tests for user routes including CRUD operations, permission checks, and error handling. - Mock dependencies and ensure proper middleware configuration in user routes.
1 parent a2c36b0 commit 483fe3c

File tree

11 files changed

+4543
-0
lines changed

11 files changed

+4543
-0
lines changed

services/backend/tests/unit/routes/auth/loginEmail.test.ts

Lines changed: 449 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
import { describe, it, expect, vi, beforeEach, type MockedFunction } from 'vitest';
2+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
3+
import logoutRoute from '../../../../src/routes/auth/logout';
4+
import { getLucia } from '../../../../src/lib/lucia';
5+
import { getDb, getSchema, getDbStatus } from '../../../../src/db';
6+
7+
// Mock dependencies
8+
vi.mock('../../../../src/lib/lucia');
9+
vi.mock('../../../../src/db');
10+
11+
// Type the mocked functions
12+
const mockGetLucia = getLucia as MockedFunction<typeof getLucia>;
13+
const mockGetDb = getDb as MockedFunction<typeof getDb>;
14+
const mockGetSchema = getSchema as MockedFunction<typeof getSchema>;
15+
const mockGetDbStatus = getDbStatus as MockedFunction<typeof getDbStatus>;
16+
17+
describe('Logout Route', () => {
18+
let mockFastify: Partial<FastifyInstance>;
19+
let mockRequest: Partial<FastifyRequest>;
20+
let mockReply: Partial<FastifyReply>;
21+
let mockLucia: any;
22+
let mockDb: any;
23+
let mockSchema: any;
24+
let routeHandlers: Record<string, any>;
25+
26+
beforeEach(() => {
27+
// Reset all mocks
28+
vi.clearAllMocks();
29+
30+
// Setup mock Lucia
31+
mockLucia = {
32+
readSessionCookie: vi.fn(),
33+
invalidateSession: vi.fn(),
34+
createBlankSessionCookie: vi.fn().mockReturnValue({
35+
name: 'session',
36+
value: '',
37+
attributes: { httpOnly: true, secure: true, maxAge: 0 },
38+
}),
39+
};
40+
41+
// Setup mock database
42+
const mockQuery = {
43+
delete: vi.fn().mockReturnThis(),
44+
where: vi.fn().mockResolvedValue(undefined),
45+
};
46+
47+
mockDb = {
48+
delete: vi.fn().mockReturnValue(mockQuery),
49+
};
50+
51+
// Setup mock schema
52+
mockSchema = {
53+
authSession: {
54+
id: 'id',
55+
user_id: 'user_id',
56+
expires_at: 'expires_at',
57+
},
58+
};
59+
60+
mockGetLucia.mockReturnValue(mockLucia);
61+
mockGetDb.mockReturnValue(mockDb);
62+
mockGetSchema.mockReturnValue(mockSchema);
63+
mockGetDbStatus.mockReturnValue({ configured: true, initialized: true, dialect: 'sqlite' });
64+
65+
// Setup route handlers storage
66+
routeHandlers = {};
67+
68+
// Setup mock Fastify instance
69+
mockFastify = {
70+
post: vi.fn((path, options, handler) => {
71+
routeHandlers[`POST ${path}`] = handler;
72+
return mockFastify as FastifyInstance;
73+
}),
74+
log: {
75+
error: vi.fn(),
76+
info: vi.fn(),
77+
debug: vi.fn(),
78+
warn: vi.fn(),
79+
},
80+
} as any;
81+
82+
// Setup mock request
83+
mockRequest = {
84+
session: {
85+
id: 'session-123',
86+
userId: 'user-123',
87+
expiresAt: new Date(),
88+
fresh: false,
89+
},
90+
headers: {
91+
cookie: 'session=session-cookie-value',
92+
},
93+
};
94+
95+
// Setup mock reply
96+
mockReply = {
97+
status: vi.fn().mockReturnThis(),
98+
send: vi.fn().mockReturnThis(),
99+
setCookie: vi.fn().mockReturnThis(),
100+
};
101+
});
102+
103+
describe('Route Registration', () => {
104+
it('should register logout route', async () => {
105+
await logoutRoute(mockFastify as FastifyInstance);
106+
107+
expect(mockFastify.post).toHaveBeenCalledWith('/logout', expect.any(Object), expect.any(Function));
108+
});
109+
});
110+
111+
describe('POST /logout', () => {
112+
beforeEach(async () => {
113+
await logoutRoute(mockFastify as FastifyInstance);
114+
});
115+
116+
it('should logout successfully with valid session', async () => {
117+
mockLucia.invalidateSession.mockResolvedValue(undefined);
118+
119+
const handler = routeHandlers['POST /logout'];
120+
await handler(mockRequest, mockReply);
121+
122+
expect(mockFastify.log!.info).toHaveBeenCalledWith('Logout attempt - Session exists: true, Session ID: session-123');
123+
expect(mockFastify.log!.info).toHaveBeenCalledWith('Attempting to invalidate session: session-123');
124+
expect(mockLucia.invalidateSession).toHaveBeenCalledWith('session-123');
125+
expect(mockFastify.log!.info).toHaveBeenCalledWith('Session session-123 invalidated successfully');
126+
expect(mockLucia.createBlankSessionCookie).toHaveBeenCalled();
127+
expect(mockReply.setCookie).toHaveBeenCalledWith('session', '', expect.any(Object));
128+
expect(mockReply.status).toHaveBeenCalledWith(200);
129+
expect(mockReply.send).toHaveBeenCalledWith({
130+
success: true,
131+
message: 'Logged out successfully.',
132+
});
133+
});
134+
135+
it('should handle logout when no session exists', async () => {
136+
mockRequest.session = null;
137+
mockLucia.readSessionCookie.mockReturnValue(null);
138+
139+
const handler = routeHandlers['POST /logout'];
140+
await handler(mockRequest, mockReply);
141+
142+
expect(mockFastify.log!.info).toHaveBeenCalledWith('Logout attempt - Session exists: false, Session ID: none');
143+
expect(mockFastify.log!.info).toHaveBeenCalledWith('No active session to logout - sending blank cookie');
144+
expect(mockLucia.createBlankSessionCookie).toHaveBeenCalled();
145+
expect(mockReply.setCookie).toHaveBeenCalledWith('session', '', expect.any(Object));
146+
expect(mockReply.status).toHaveBeenCalledWith(200);
147+
expect(mockReply.send).toHaveBeenCalledWith({
148+
success: true,
149+
message: 'No active session to logout or already logged out.',
150+
});
151+
});
152+
153+
it('should handle logout when no session exists but cookie is present', async () => {
154+
mockRequest.session = null;
155+
mockLucia.readSessionCookie.mockReturnValue('invalid-session-123');
156+
157+
const mockQuery = {
158+
delete: vi.fn().mockReturnThis(),
159+
where: vi.fn().mockResolvedValue(undefined),
160+
};
161+
mockDb.delete.mockReturnValue(mockQuery);
162+
163+
const handler = routeHandlers['POST /logout'];
164+
await handler(mockRequest, mockReply);
165+
166+
expect(mockFastify.log!.info).toHaveBeenCalledWith('Found session cookie invalid-session-123 but authHook couldn\'t validate it - attempting manual cleanup');
167+
expect(mockDb.delete).toHaveBeenCalledWith(mockSchema.authSession);
168+
expect(mockFastify.log!.info).toHaveBeenCalledWith('Manually deleted session invalid-session-123 from database');
169+
expect(mockReply.status).toHaveBeenCalledWith(200);
170+
expect(mockReply.send).toHaveBeenCalledWith({
171+
success: true,
172+
message: 'No active session to logout or already logged out.',
173+
});
174+
});
175+
176+
it('should handle manual cleanup when authSession table is missing', async () => {
177+
mockRequest.session = null;
178+
mockLucia.readSessionCookie.mockReturnValue('invalid-session-123');
179+
mockGetSchema.mockReturnValue({
180+
authSession: null, // Missing table
181+
});
182+
183+
const handler = routeHandlers['POST /logout'];
184+
await handler(mockRequest, mockReply);
185+
186+
expect(mockFastify.log!.warn).toHaveBeenCalledWith('authSession table or id column not found in schema');
187+
expect(mockReply.status).toHaveBeenCalledWith(200);
188+
});
189+
190+
it('should handle database errors during manual cleanup', async () => {
191+
mockRequest.session = null;
192+
mockLucia.readSessionCookie.mockReturnValue('invalid-session-123');
193+
194+
const mockQuery = {
195+
delete: vi.fn().mockReturnThis(),
196+
where: vi.fn().mockRejectedValue(new Error('Database error')),
197+
};
198+
mockDb.delete.mockReturnValue(mockQuery);
199+
200+
const handler = routeHandlers['POST /logout'];
201+
await handler(mockRequest, mockReply);
202+
203+
expect(mockFastify.log!.error).toHaveBeenCalledWith(expect.any(Error), 'Failed to manually delete session from database');
204+
expect(mockReply.status).toHaveBeenCalledWith(200);
205+
});
206+
207+
it('should handle Lucia invalidation errors with fallback cleanup', async () => {
208+
const luciaError = new Error('Lucia invalidation failed');
209+
mockLucia.invalidateSession.mockRejectedValue(luciaError);
210+
211+
const mockQuery = {
212+
delete: vi.fn().mockReturnThis(),
213+
where: vi.fn().mockResolvedValue(undefined),
214+
};
215+
mockDb.delete.mockReturnValue(mockQuery);
216+
217+
const handler = routeHandlers['POST /logout'];
218+
await handler(mockRequest, mockReply);
219+
220+
expect(mockFastify.log!.error).toHaveBeenCalledWith(luciaError, 'Error during logout (invalidating session from authHook):');
221+
expect(mockDb.delete).toHaveBeenCalledWith(mockSchema.authSession);
222+
expect(mockFastify.log!.info).toHaveBeenCalledWith('Manually deleted session session-123 after Lucia invalidation failed');
223+
expect(mockLucia.createBlankSessionCookie).toHaveBeenCalled();
224+
expect(mockReply.setCookie).toHaveBeenCalledWith('session', '', expect.any(Object));
225+
expect(mockReply.status).toHaveBeenCalledWith(200);
226+
expect(mockReply.send).toHaveBeenCalledWith({
227+
success: true,
228+
message: 'Logged out successfully (with fallback cleanup).',
229+
});
230+
});
231+
232+
it('should handle both Lucia and database errors gracefully', async () => {
233+
const luciaError = new Error('Lucia invalidation failed');
234+
const dbError = new Error('Database deletion failed');
235+
236+
mockLucia.invalidateSession.mockRejectedValue(luciaError);
237+
238+
const mockQuery = {
239+
delete: vi.fn().mockReturnThis(),
240+
where: vi.fn().mockRejectedValue(dbError),
241+
};
242+
mockDb.delete.mockReturnValue(mockQuery);
243+
244+
const handler = routeHandlers['POST /logout'];
245+
await handler(mockRequest, mockReply);
246+
247+
expect(mockFastify.log!.error).toHaveBeenCalledWith(luciaError, 'Error during logout (invalidating session from authHook):');
248+
expect(mockFastify.log!.error).toHaveBeenCalledWith(dbError, 'Failed to manually delete session after Lucia error');
249+
expect(mockLucia.createBlankSessionCookie).toHaveBeenCalled();
250+
expect(mockReply.setCookie).toHaveBeenCalledWith('session', '', expect.any(Object));
251+
expect(mockReply.status).toHaveBeenCalledWith(200);
252+
expect(mockReply.send).toHaveBeenCalledWith({
253+
success: true,
254+
message: 'Logged out successfully (with fallback cleanup).',
255+
});
256+
});
257+
258+
it('should handle fallback cleanup when authSession table is missing', async () => {
259+
const luciaError = new Error('Lucia invalidation failed');
260+
mockLucia.invalidateSession.mockRejectedValue(luciaError);
261+
mockGetSchema.mockReturnValue({
262+
authSession: null, // Missing table
263+
});
264+
265+
const handler = routeHandlers['POST /logout'];
266+
await handler(mockRequest, mockReply);
267+
268+
expect(mockFastify.log!.error).toHaveBeenCalledWith(luciaError, 'Error during logout (invalidating session from authHook):');
269+
expect(mockFastify.log!.warn).toHaveBeenCalledWith('authSession table or id column not found in schema');
270+
expect(mockReply.status).toHaveBeenCalledWith(200);
271+
expect(mockReply.send).toHaveBeenCalledWith({
272+
success: true,
273+
message: 'Logged out successfully (with fallback cleanup).',
274+
});
275+
});
276+
277+
it('should handle non-sqlite database dialect', async () => {
278+
mockRequest.session = null;
279+
mockLucia.readSessionCookie.mockReturnValue('invalid-session-123');
280+
mockGetDbStatus.mockReturnValue({ configured: true, initialized: true, dialect: null });
281+
282+
const handler = routeHandlers['POST /logout'];
283+
await handler(mockRequest, mockReply);
284+
285+
// Should not attempt database deletion for non-sqlite
286+
expect(mockDb.delete).not.toHaveBeenCalled();
287+
expect(mockReply.status).toHaveBeenCalledWith(200);
288+
});
289+
290+
it('should handle missing cookie headers', async () => {
291+
mockRequest.session = null;
292+
mockRequest.headers = {}; // No cookie header
293+
mockLucia.readSessionCookie.mockReturnValue(null);
294+
295+
const handler = routeHandlers['POST /logout'];
296+
await handler(mockRequest, mockReply);
297+
298+
expect(mockLucia.readSessionCookie).toHaveBeenCalledWith('');
299+
expect(mockReply.status).toHaveBeenCalledWith(200);
300+
expect(mockReply.send).toHaveBeenCalledWith({
301+
success: true,
302+
message: 'No active session to logout or already logged out.',
303+
});
304+
});
305+
});
306+
});

0 commit comments

Comments
 (0)