@rapidraptor/auth-server 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +44 -0
- package/dist/config.js.map +1 -0
- package/dist/firebase/admin.d.ts +18 -0
- package/dist/firebase/admin.d.ts.map +1 -0
- package/dist/firebase/admin.js +96 -0
- package/dist/firebase/admin.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/authMiddleware.d.ts +9 -0
- package/dist/middleware/authMiddleware.d.ts.map +1 -0
- package/dist/middleware/authMiddleware.js +241 -0
- package/dist/middleware/authMiddleware.js.map +1 -0
- package/dist/middleware/authMiddleware.test.d.ts +2 -0
- package/dist/middleware/authMiddleware.test.d.ts.map +1 -0
- package/dist/middleware/authMiddleware.test.js +691 -0
- package/dist/middleware/authMiddleware.test.js.map +1 -0
- package/dist/middleware/logoutHandler.d.ts +9 -0
- package/dist/middleware/logoutHandler.d.ts.map +1 -0
- package/dist/middleware/logoutHandler.js +54 -0
- package/dist/middleware/logoutHandler.js.map +1 -0
- package/dist/middleware/logoutHandler.test.d.ts +2 -0
- package/dist/middleware/logoutHandler.test.d.ts.map +1 -0
- package/dist/middleware/logoutHandler.test.js +103 -0
- package/dist/middleware/logoutHandler.test.js.map +1 -0
- package/dist/session/firestoreSync.d.ts +37 -0
- package/dist/session/firestoreSync.d.ts.map +1 -0
- package/dist/session/firestoreSync.js +88 -0
- package/dist/session/firestoreSync.js.map +1 -0
- package/dist/session/firestoreSync.test.d.ts +2 -0
- package/dist/session/firestoreSync.test.d.ts.map +1 -0
- package/dist/session/firestoreSync.test.js +142 -0
- package/dist/session/firestoreSync.test.js.map +1 -0
- package/dist/session/sessionCache.d.ts +37 -0
- package/dist/session/sessionCache.d.ts.map +1 -0
- package/dist/session/sessionCache.js +63 -0
- package/dist/session/sessionCache.js.map +1 -0
- package/dist/session/sessionCache.test.d.ts +2 -0
- package/dist/session/sessionCache.test.d.ts.map +1 -0
- package/dist/session/sessionCache.test.js +117 -0
- package/dist/session/sessionCache.test.js.map +1 -0
- package/dist/session/sessionService.d.ts +97 -0
- package/dist/session/sessionService.d.ts.map +1 -0
- package/dist/session/sessionService.js +311 -0
- package/dist/session/sessionService.js.map +1 -0
- package/dist/session/sessionService.test.d.ts +2 -0
- package/dist/session/sessionService.test.d.ts.map +1 -0
- package/dist/session/sessionService.test.js +426 -0
- package/dist/session/sessionService.test.js.map +1 -0
- package/dist/session/types.d.ts +7 -0
- package/dist/session/types.d.ts.map +1 -0
- package/dist/session/types.js +2 -0
- package/dist/session/types.js.map +1 -0
- package/dist/tokenVerifier/errors.d.ts +23 -0
- package/dist/tokenVerifier/errors.d.ts.map +1 -0
- package/dist/tokenVerifier/errors.js +34 -0
- package/dist/tokenVerifier/errors.js.map +1 -0
- package/dist/tokenVerifier/joseTokenVerifier.d.ts +24 -0
- package/dist/tokenVerifier/joseTokenVerifier.d.ts.map +1 -0
- package/dist/tokenVerifier/joseTokenVerifier.js +157 -0
- package/dist/tokenVerifier/joseTokenVerifier.js.map +1 -0
- package/dist/tokenVerifier/types.d.ts +41 -0
- package/dist/tokenVerifier/types.d.ts.map +1 -0
- package/dist/tokenVerifier/types.js +2 -0
- package/dist/tokenVerifier/types.js.map +1 -0
- package/dist/types/middleware.d.ts +33 -0
- package/dist/types/middleware.d.ts.map +1 -0
- package/dist/types/middleware.js +2 -0
- package/dist/types/middleware.js.map +1 -0
- package/dist/types/session.d.ts +7 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +2 -0
- package/dist/types/session.js.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { decodeJwt } from 'jose';
|
|
3
|
+
import { createAuthMiddleware } from './authMiddleware.js';
|
|
4
|
+
import { SessionService, TokenRevokedError } from '../session/sessionService.js';
|
|
5
|
+
import { SessionCache } from '../session/sessionCache.js';
|
|
6
|
+
import { FirestoreSync } from '../session/firestoreSync.js';
|
|
7
|
+
import { ERROR_CODES, SessionValidationStatus } from '@rapidraptor/auth-shared';
|
|
8
|
+
// Mock jose
|
|
9
|
+
vi.mock('jose', () => ({
|
|
10
|
+
decodeJwt: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
describe('createAuthMiddleware', () => {
|
|
13
|
+
let authMiddleware;
|
|
14
|
+
let mockUserTokenVerifier;
|
|
15
|
+
let sessionService;
|
|
16
|
+
let mockFirestore;
|
|
17
|
+
let mockLogger;
|
|
18
|
+
let mockRequest;
|
|
19
|
+
let mockResponse;
|
|
20
|
+
let mockNext;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
// Setup SessionService
|
|
24
|
+
const cache = new SessionCache(24 * 60 * 60 * 1000);
|
|
25
|
+
const firestoreSync = new FirestoreSync({
|
|
26
|
+
collection: vi.fn(),
|
|
27
|
+
batch: vi.fn(),
|
|
28
|
+
}, 5 * 60 * 1000, 'user_sessions');
|
|
29
|
+
mockFirestore = {
|
|
30
|
+
collection: vi.fn(() => ({
|
|
31
|
+
doc: vi.fn(() => ({
|
|
32
|
+
get: vi.fn(),
|
|
33
|
+
set: vi.fn(),
|
|
34
|
+
delete: vi.fn(),
|
|
35
|
+
})),
|
|
36
|
+
})),
|
|
37
|
+
};
|
|
38
|
+
sessionService = new SessionService(cache, firestoreSync, mockFirestore, 24 * 60 * 60 * 1000);
|
|
39
|
+
// Setup mocks
|
|
40
|
+
mockUserTokenVerifier = {
|
|
41
|
+
verify: vi.fn(),
|
|
42
|
+
};
|
|
43
|
+
mockLogger = {
|
|
44
|
+
debug: vi.fn(),
|
|
45
|
+
info: vi.fn(),
|
|
46
|
+
warn: vi.fn(),
|
|
47
|
+
error: vi.fn(),
|
|
48
|
+
};
|
|
49
|
+
mockRequest = {
|
|
50
|
+
headers: {},
|
|
51
|
+
correlationId: 'test-correlation-id',
|
|
52
|
+
};
|
|
53
|
+
mockResponse = {
|
|
54
|
+
status: vi.fn().mockReturnThis(),
|
|
55
|
+
json: vi.fn().mockReturnThis(),
|
|
56
|
+
};
|
|
57
|
+
mockNext = vi.fn();
|
|
58
|
+
authMiddleware = createAuthMiddleware(mockUserTokenVerifier, sessionService, mockLogger);
|
|
59
|
+
});
|
|
60
|
+
describe('Authorization Header Validation', () => {
|
|
61
|
+
it('should return 401 when authorization header is missing', async () => {
|
|
62
|
+
mockRequest.headers = {};
|
|
63
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
64
|
+
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
|
65
|
+
expect(mockResponse.json).toHaveBeenCalledWith({
|
|
66
|
+
error: {
|
|
67
|
+
code: ERROR_CODES.AUTH_FAILED,
|
|
68
|
+
message: 'Authorization header required',
|
|
69
|
+
requiresLogout: false,
|
|
70
|
+
sessionExpired: false,
|
|
71
|
+
timestamp: expect.any(String),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
it('should return 401 when authorization header does not start with "Bearer "', async () => {
|
|
77
|
+
mockRequest.headers = {
|
|
78
|
+
authorization: 'Invalid token',
|
|
79
|
+
};
|
|
80
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
81
|
+
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
|
82
|
+
expect(mockResponse.json).toHaveBeenCalledWith({
|
|
83
|
+
error: {
|
|
84
|
+
code: ERROR_CODES.AUTH_FAILED,
|
|
85
|
+
message: 'Authorization header required',
|
|
86
|
+
requiresLogout: false,
|
|
87
|
+
sessionExpired: false,
|
|
88
|
+
timestamp: expect.any(String),
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
it('should return 401 when authorization header is empty string', async () => {
|
|
94
|
+
mockRequest.headers = {
|
|
95
|
+
authorization: '',
|
|
96
|
+
};
|
|
97
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
98
|
+
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
|
99
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('JWT Decoding', () => {
|
|
103
|
+
it('should return 401 when JWT cannot be decoded', async () => {
|
|
104
|
+
mockRequest.headers = {
|
|
105
|
+
authorization: 'Bearer invalid-token',
|
|
106
|
+
};
|
|
107
|
+
vi.mocked(decodeJwt).mockImplementation(() => {
|
|
108
|
+
throw new Error('Invalid JWT');
|
|
109
|
+
});
|
|
110
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
111
|
+
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
|
112
|
+
expect(mockResponse.json).toHaveBeenCalledWith({
|
|
113
|
+
error: {
|
|
114
|
+
code: ERROR_CODES.AUTH_FAILED,
|
|
115
|
+
message: 'Invalid token format',
|
|
116
|
+
requiresLogout: false,
|
|
117
|
+
sessionExpired: false,
|
|
118
|
+
timestamp: expect.any(String),
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
expect(mockLogger.warn).toHaveBeenCalledWith('Failed to decode JWT', {
|
|
122
|
+
event: 'jwt_decode_failed',
|
|
123
|
+
error: 'Invalid JWT',
|
|
124
|
+
correlationId: 'test-correlation-id',
|
|
125
|
+
});
|
|
126
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
it('should extract tokenIssuedAt from JWT iat claim', async () => {
|
|
129
|
+
const token = 'valid-token';
|
|
130
|
+
const iat = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago
|
|
131
|
+
mockRequest.headers = {
|
|
132
|
+
authorization: `Bearer ${token}`,
|
|
133
|
+
};
|
|
134
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat, sub: 'user123' });
|
|
135
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
136
|
+
sub: 'user123',
|
|
137
|
+
email: 'user@example.com',
|
|
138
|
+
});
|
|
139
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.VALID);
|
|
140
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
141
|
+
expect(decodeJwt).toHaveBeenCalledWith(token);
|
|
142
|
+
expect(mockNext).toHaveBeenCalledWith();
|
|
143
|
+
});
|
|
144
|
+
it('should handle JWT with missing iat claim (defaults to 0)', async () => {
|
|
145
|
+
const token = 'valid-token';
|
|
146
|
+
mockRequest.headers = {
|
|
147
|
+
authorization: `Bearer ${token}`,
|
|
148
|
+
};
|
|
149
|
+
vi.mocked(decodeJwt).mockReturnValue({ sub: 'user123' }); // No iat
|
|
150
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
151
|
+
sub: 'user123',
|
|
152
|
+
email: 'user@example.com',
|
|
153
|
+
});
|
|
154
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.VALID);
|
|
155
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
156
|
+
expect(decodeJwt).toHaveBeenCalledWith(token);
|
|
157
|
+
expect(mockNext).toHaveBeenCalledWith();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe('JWT Verification', () => {
|
|
161
|
+
beforeEach(() => {
|
|
162
|
+
mockRequest.headers = {
|
|
163
|
+
authorization: 'Bearer valid-token',
|
|
164
|
+
};
|
|
165
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat: Math.floor(Date.now() / 1000), sub: 'user123' });
|
|
166
|
+
});
|
|
167
|
+
it('should return 401 when token verification fails (invalid signature)', async () => {
|
|
168
|
+
const error = new Error('Invalid signature');
|
|
169
|
+
vi.mocked(mockUserTokenVerifier.verify).mockRejectedValue(error);
|
|
170
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
171
|
+
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
|
172
|
+
expect(mockResponse.json).toHaveBeenCalledWith({
|
|
173
|
+
error: {
|
|
174
|
+
code: ERROR_CODES.AUTH_FAILED,
|
|
175
|
+
message: 'Invalid signature',
|
|
176
|
+
requiresLogout: false,
|
|
177
|
+
sessionExpired: false,
|
|
178
|
+
timestamp: expect.any(String),
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
expect(mockLogger.warn).toHaveBeenCalledWith('JWT verification failed', {
|
|
182
|
+
event: 'jwt_verification_failed',
|
|
183
|
+
error: 'Invalid signature',
|
|
184
|
+
isExpired: false,
|
|
185
|
+
correlationId: 'test-correlation-id',
|
|
186
|
+
});
|
|
187
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
it('should return 401 with TOKEN_EXPIRED when token is expired', async () => {
|
|
190
|
+
const error = new Error('Token expired');
|
|
191
|
+
error.isExpired = true;
|
|
192
|
+
vi.mocked(mockUserTokenVerifier.verify).mockRejectedValue(error);
|
|
193
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
194
|
+
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
|
195
|
+
expect(mockResponse.json).toHaveBeenCalledWith({
|
|
196
|
+
error: {
|
|
197
|
+
code: ERROR_CODES.TOKEN_EXPIRED,
|
|
198
|
+
message: 'Token expired',
|
|
199
|
+
requiresLogout: true,
|
|
200
|
+
sessionExpired: false,
|
|
201
|
+
timestamp: expect.any(String),
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
expect(mockLogger.warn).toHaveBeenCalledWith('JWT verification failed', {
|
|
205
|
+
event: 'jwt_verification_failed',
|
|
206
|
+
error: 'Token expired',
|
|
207
|
+
isExpired: true,
|
|
208
|
+
correlationId: 'test-correlation-id',
|
|
209
|
+
});
|
|
210
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
211
|
+
});
|
|
212
|
+
it('should return 401 with AUTH_FAILED for other verification errors', async () => {
|
|
213
|
+
const error = new Error('Verification failed');
|
|
214
|
+
vi.mocked(mockUserTokenVerifier.verify).mockRejectedValue(error);
|
|
215
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
216
|
+
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
|
217
|
+
expect(mockResponse.json).toHaveBeenCalledWith({
|
|
218
|
+
error: {
|
|
219
|
+
code: ERROR_CODES.AUTH_FAILED,
|
|
220
|
+
message: 'Verification failed',
|
|
221
|
+
requiresLogout: false,
|
|
222
|
+
sessionExpired: false,
|
|
223
|
+
timestamp: expect.any(String),
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
227
|
+
});
|
|
228
|
+
it('should pass correlationId to token verifier', async () => {
|
|
229
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
230
|
+
sub: 'user123',
|
|
231
|
+
email: 'user@example.com',
|
|
232
|
+
});
|
|
233
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.VALID);
|
|
234
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
235
|
+
expect(mockUserTokenVerifier.verify).toHaveBeenCalledWith('valid-token', 'test-correlation-id');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
describe('Session Validation - VALID', () => {
|
|
239
|
+
beforeEach(() => {
|
|
240
|
+
mockRequest.headers = {
|
|
241
|
+
authorization: 'Bearer valid-token',
|
|
242
|
+
};
|
|
243
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat: Math.floor(Date.now() / 1000), sub: 'user123' });
|
|
244
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
245
|
+
sub: 'user123',
|
|
246
|
+
email: 'user@example.com',
|
|
247
|
+
name: 'Test User',
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
it('should attach user to request and call next() when session is valid', async () => {
|
|
251
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.VALID);
|
|
252
|
+
vi.spyOn(sessionService, 'updateLastActivity').mockResolvedValue(undefined);
|
|
253
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
254
|
+
expect(mockRequest.user).toEqual({
|
|
255
|
+
sub: 'user123',
|
|
256
|
+
email: 'user@example.com',
|
|
257
|
+
name: 'Test User',
|
|
258
|
+
});
|
|
259
|
+
expect(mockNext).toHaveBeenCalledWith();
|
|
260
|
+
expect(mockResponse.status).not.toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
it('should update last activity asynchronously (not wait)', async () => {
|
|
263
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.VALID);
|
|
264
|
+
const updateSpy = vi.spyOn(sessionService, 'updateLastActivity').mockResolvedValue(undefined);
|
|
265
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
266
|
+
expect(updateSpy).toHaveBeenCalledWith('user123');
|
|
267
|
+
expect(mockNext).toHaveBeenCalledWith();
|
|
268
|
+
});
|
|
269
|
+
it('should log error but continue when activity update fails', async () => {
|
|
270
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.VALID);
|
|
271
|
+
const updateError = new Error('Update failed');
|
|
272
|
+
vi.spyOn(sessionService, 'updateLastActivity').mockRejectedValue(updateError);
|
|
273
|
+
// Wait a bit for the async catch to execute
|
|
274
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
275
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
276
|
+
expect(mockNext).toHaveBeenCalledWith();
|
|
277
|
+
// Error should be logged (but we can't easily test the async catch timing)
|
|
278
|
+
});
|
|
279
|
+
it('should use request logger when available, fallback to provided logger', async () => {
|
|
280
|
+
const requestLogger = {
|
|
281
|
+
warn: vi.fn(),
|
|
282
|
+
error: vi.fn(),
|
|
283
|
+
};
|
|
284
|
+
mockRequest.logger = requestLogger;
|
|
285
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.VALID);
|
|
286
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
287
|
+
// Request logger should be used instead of provided logger
|
|
288
|
+
expect(mockNext).toHaveBeenCalledWith();
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
describe('Session Validation - EXPIRED', () => {
|
|
292
|
+
beforeEach(() => {
|
|
293
|
+
mockRequest.headers = {
|
|
294
|
+
authorization: 'Bearer valid-token',
|
|
295
|
+
};
|
|
296
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat: Math.floor(Date.now() / 1000), sub: 'user123' });
|
|
297
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
298
|
+
sub: 'user123',
|
|
299
|
+
email: 'user@example.com',
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
it('should return 401 with SESSION_EXPIRED when session is expired', async () => {
|
|
303
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.EXPIRED);
|
|
304
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
305
|
+
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
|
306
|
+
expect(mockResponse.json).toHaveBeenCalledWith({
|
|
307
|
+
error: {
|
|
308
|
+
code: ERROR_CODES.SESSION_EXPIRED,
|
|
309
|
+
message: 'Session has expired due to inactivity',
|
|
310
|
+
requiresLogout: true,
|
|
311
|
+
sessionExpired: true,
|
|
312
|
+
timestamp: expect.any(String),
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
expect(mockLogger.warn).toHaveBeenCalledWith('Session expired', {
|
|
316
|
+
event: 'session_expired',
|
|
317
|
+
userId: 'user123',
|
|
318
|
+
correlationId: 'test-correlation-id',
|
|
319
|
+
});
|
|
320
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
describe('Session Validation - NOT_FOUND', () => {
|
|
324
|
+
beforeEach(() => {
|
|
325
|
+
mockRequest.headers = {
|
|
326
|
+
authorization: 'Bearer valid-token',
|
|
327
|
+
};
|
|
328
|
+
const iat = Math.floor(Date.now() / 1000);
|
|
329
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat, sub: 'user123' });
|
|
330
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
331
|
+
sub: 'user123',
|
|
332
|
+
email: 'user@example.com',
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
it('should create new session when session does not exist', async () => {
|
|
336
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.NOT_FOUND);
|
|
337
|
+
const ensureSessionSpy = vi.spyOn(sessionService, 'ensureSession').mockResolvedValue(true);
|
|
338
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
339
|
+
expect(ensureSessionSpy).toHaveBeenCalledWith('user123', expect.any(Date));
|
|
340
|
+
expect(mockNext).toHaveBeenCalledWith();
|
|
341
|
+
expect(mockRequest.user).toBeDefined();
|
|
342
|
+
});
|
|
343
|
+
it('should log info when session is created', async () => {
|
|
344
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.NOT_FOUND);
|
|
345
|
+
vi.spyOn(sessionService, 'ensureSession').mockResolvedValue(true);
|
|
346
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
347
|
+
expect(mockLogger.info).toHaveBeenCalledWith('Session created', {
|
|
348
|
+
event: 'session_created',
|
|
349
|
+
userId: 'user123',
|
|
350
|
+
correlationId: 'test-correlation-id',
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
it('should attach user and call next() after creating session', async () => {
|
|
354
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.NOT_FOUND);
|
|
355
|
+
vi.spyOn(sessionService, 'ensureSession').mockResolvedValue(true);
|
|
356
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
357
|
+
expect(mockRequest.user).toEqual({
|
|
358
|
+
sub: 'user123',
|
|
359
|
+
email: 'user@example.com',
|
|
360
|
+
});
|
|
361
|
+
expect(mockNext).toHaveBeenCalledWith();
|
|
362
|
+
});
|
|
363
|
+
it('should return 401 with token revoked error when token was issued before logout', async () => {
|
|
364
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.NOT_FOUND);
|
|
365
|
+
vi.spyOn(sessionService, 'ensureSession').mockRejectedValue(new TokenRevokedError());
|
|
366
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
367
|
+
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
|
368
|
+
expect(mockResponse.json).toHaveBeenCalledWith({
|
|
369
|
+
error: {
|
|
370
|
+
code: ERROR_CODES.SESSION_EXPIRED,
|
|
371
|
+
message: 'This token was issued before logout. Please log in again.',
|
|
372
|
+
requiresLogout: true,
|
|
373
|
+
sessionExpired: true,
|
|
374
|
+
timestamp: expect.any(String),
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
expect(mockLogger.warn).toHaveBeenCalledWith('Token revoked (issued before logout)', expect.objectContaining({
|
|
378
|
+
event: 'token_revoked',
|
|
379
|
+
userId: 'user123',
|
|
380
|
+
correlationId: 'test-correlation-id',
|
|
381
|
+
}));
|
|
382
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
383
|
+
});
|
|
384
|
+
it('should return 503 when Firestore is unavailable during session creation', async () => {
|
|
385
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.NOT_FOUND);
|
|
386
|
+
const firestoreError = { code: 'unavailable', message: 'Firestore unavailable' };
|
|
387
|
+
vi.spyOn(sessionService, 'ensureSession').mockRejectedValue(firestoreError);
|
|
388
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
389
|
+
expect(mockResponse.status).toHaveBeenCalledWith(503);
|
|
390
|
+
expect(mockResponse.json).toHaveBeenCalledWith({
|
|
391
|
+
error: {
|
|
392
|
+
code: ERROR_CODES.SERVICE_UNAVAILABLE,
|
|
393
|
+
message: 'User sessions could not be created',
|
|
394
|
+
requiresLogout: false,
|
|
395
|
+
sessionExpired: false,
|
|
396
|
+
timestamp: expect.any(String),
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Firestore unavailable for session creation', {
|
|
400
|
+
event: 'firestore_unavailable',
|
|
401
|
+
error: 'Firestore unavailable',
|
|
402
|
+
userId: 'user123',
|
|
403
|
+
correlationId: 'test-correlation-id',
|
|
404
|
+
});
|
|
405
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
406
|
+
});
|
|
407
|
+
it('should return 503 when Firestore deadline is exceeded during session creation', async () => {
|
|
408
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.NOT_FOUND);
|
|
409
|
+
const firestoreError = { code: 'deadline-exceeded', message: 'Deadline exceeded' };
|
|
410
|
+
vi.spyOn(sessionService, 'ensureSession').mockRejectedValue(firestoreError);
|
|
411
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
412
|
+
expect(mockResponse.status).toHaveBeenCalledWith(503);
|
|
413
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
414
|
+
});
|
|
415
|
+
it('should re-throw unexpected errors from ensureSession', async () => {
|
|
416
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.NOT_FOUND);
|
|
417
|
+
const unexpectedError = new Error('Unexpected error');
|
|
418
|
+
vi.spyOn(sessionService, 'ensureSession').mockRejectedValue(unexpectedError);
|
|
419
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
420
|
+
expect(mockNext).toHaveBeenCalledWith(unexpectedError);
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
describe('Session Validation - DATA_INTEGRITY_ERROR', () => {
|
|
424
|
+
beforeEach(() => {
|
|
425
|
+
mockRequest.headers = {
|
|
426
|
+
authorization: 'Bearer valid-token',
|
|
427
|
+
};
|
|
428
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat: Math.floor(Date.now() / 1000), sub: 'user123' });
|
|
429
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
430
|
+
sub: 'user123',
|
|
431
|
+
email: 'user@example.com',
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
it('should return 500 when session data integrity error is detected', async () => {
|
|
435
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.DATA_INTEGRITY_ERROR);
|
|
436
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
437
|
+
expect(mockResponse.status).toHaveBeenCalledWith(500);
|
|
438
|
+
expect(mockResponse.json).toHaveBeenCalledWith({
|
|
439
|
+
error: {
|
|
440
|
+
code: ERROR_CODES.INTERNAL_ERROR,
|
|
441
|
+
message: 'Session data integrity error',
|
|
442
|
+
requiresLogout: true,
|
|
443
|
+
sessionExpired: false,
|
|
444
|
+
timestamp: expect.any(String),
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Session data integrity error', {
|
|
448
|
+
event: 'session_data_integrity_error',
|
|
449
|
+
userId: 'user123',
|
|
450
|
+
correlationId: 'test-correlation-id',
|
|
451
|
+
});
|
|
452
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
describe('Firestore Unavailability During Validation', () => {
|
|
456
|
+
beforeEach(() => {
|
|
457
|
+
mockRequest.headers = {
|
|
458
|
+
authorization: 'Bearer valid-token',
|
|
459
|
+
};
|
|
460
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat: Math.floor(Date.now() / 1000), sub: 'user123' });
|
|
461
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
462
|
+
sub: 'user123',
|
|
463
|
+
email: 'user@example.com',
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
it('should return 503 when Firestore is unavailable during validateSession', async () => {
|
|
467
|
+
const firestoreError = { code: 'unavailable', message: 'Firestore unavailable' };
|
|
468
|
+
vi.spyOn(sessionService, 'validateSession').mockRejectedValue(firestoreError);
|
|
469
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
470
|
+
expect(mockResponse.status).toHaveBeenCalledWith(503);
|
|
471
|
+
expect(mockResponse.json).toHaveBeenCalledWith({
|
|
472
|
+
error: {
|
|
473
|
+
code: ERROR_CODES.SERVICE_UNAVAILABLE,
|
|
474
|
+
message: 'User sessions could not be validated',
|
|
475
|
+
requiresLogout: false,
|
|
476
|
+
sessionExpired: false,
|
|
477
|
+
timestamp: expect.any(String),
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Firestore unavailable for session validation', {
|
|
481
|
+
event: 'firestore_unavailable',
|
|
482
|
+
error: 'Firestore unavailable',
|
|
483
|
+
userId: 'user123',
|
|
484
|
+
correlationId: 'test-correlation-id',
|
|
485
|
+
});
|
|
486
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
487
|
+
});
|
|
488
|
+
it('should return 503 when Firestore deadline is exceeded during validateSession', async () => {
|
|
489
|
+
const firestoreError = { code: 'deadline-exceeded', message: 'Deadline exceeded' };
|
|
490
|
+
vi.spyOn(sessionService, 'validateSession').mockRejectedValue(firestoreError);
|
|
491
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
492
|
+
expect(mockResponse.status).toHaveBeenCalledWith(503);
|
|
493
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
494
|
+
});
|
|
495
|
+
it('should re-throw non-Firestore errors from validateSession', async () => {
|
|
496
|
+
const unexpectedError = new Error('Unexpected error');
|
|
497
|
+
vi.spyOn(sessionService, 'validateSession').mockRejectedValue(unexpectedError);
|
|
498
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
499
|
+
expect(mockNext).toHaveBeenCalledWith(unexpectedError);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
describe('Error Handling', () => {
|
|
503
|
+
it('should log error and call next(error) for unexpected errors', async () => {
|
|
504
|
+
mockRequest.headers = {
|
|
505
|
+
authorization: 'Bearer valid-token',
|
|
506
|
+
};
|
|
507
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat: Math.floor(Date.now() / 1000), sub: 'user123' });
|
|
508
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
509
|
+
sub: 'user123',
|
|
510
|
+
});
|
|
511
|
+
const unexpectedError = new Error('Unexpected error');
|
|
512
|
+
vi.spyOn(sessionService, 'validateSession').mockRejectedValue(unexpectedError);
|
|
513
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
514
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Authentication middleware error', {
|
|
515
|
+
event: 'auth_middleware_error',
|
|
516
|
+
error: {
|
|
517
|
+
name: 'Error',
|
|
518
|
+
message: 'Unexpected error',
|
|
519
|
+
},
|
|
520
|
+
correlationId: 'test-correlation-id',
|
|
521
|
+
});
|
|
522
|
+
expect(mockNext).toHaveBeenCalledWith(unexpectedError);
|
|
523
|
+
});
|
|
524
|
+
it('should include error name and message in error log', async () => {
|
|
525
|
+
mockRequest.headers = {
|
|
526
|
+
authorization: 'Bearer valid-token',
|
|
527
|
+
};
|
|
528
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat: Math.floor(Date.now() / 1000), sub: 'user123' });
|
|
529
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
530
|
+
sub: 'user123',
|
|
531
|
+
});
|
|
532
|
+
const customError = new Error('Custom error');
|
|
533
|
+
customError.name = 'CustomError';
|
|
534
|
+
vi.spyOn(sessionService, 'validateSession').mockRejectedValue(customError);
|
|
535
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
536
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Authentication middleware error', {
|
|
537
|
+
event: 'auth_middleware_error',
|
|
538
|
+
error: {
|
|
539
|
+
name: 'CustomError',
|
|
540
|
+
message: 'Custom error',
|
|
541
|
+
},
|
|
542
|
+
correlationId: 'test-correlation-id',
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
it('should include correlationId in error log', async () => {
|
|
546
|
+
mockRequest.headers = {
|
|
547
|
+
authorization: 'Bearer valid-token',
|
|
548
|
+
};
|
|
549
|
+
mockRequest.correlationId = 'custom-correlation-id';
|
|
550
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat: Math.floor(Date.now() / 1000), sub: 'user123' });
|
|
551
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
552
|
+
sub: 'user123',
|
|
553
|
+
});
|
|
554
|
+
const error = new Error('Test error');
|
|
555
|
+
vi.spyOn(sessionService, 'validateSession').mockRejectedValue(error);
|
|
556
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
557
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Authentication middleware error', expect.objectContaining({
|
|
558
|
+
correlationId: 'custom-correlation-id',
|
|
559
|
+
}));
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
describe('Success Path', () => {
|
|
563
|
+
it('should process complete flow: valid JWT + valid session', async () => {
|
|
564
|
+
mockRequest.headers = {
|
|
565
|
+
authorization: 'Bearer valid-token',
|
|
566
|
+
};
|
|
567
|
+
const iat = Math.floor(Date.now() / 1000);
|
|
568
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat, sub: 'user123' });
|
|
569
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
570
|
+
sub: 'user123',
|
|
571
|
+
email: 'user@example.com',
|
|
572
|
+
name: 'Test User',
|
|
573
|
+
});
|
|
574
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.VALID);
|
|
575
|
+
vi.spyOn(sessionService, 'updateLastActivity').mockResolvedValue(undefined);
|
|
576
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
577
|
+
expect(mockRequest.user).toEqual({
|
|
578
|
+
sub: 'user123',
|
|
579
|
+
email: 'user@example.com',
|
|
580
|
+
name: 'Test User',
|
|
581
|
+
});
|
|
582
|
+
expect(mockNext).toHaveBeenCalledWith();
|
|
583
|
+
expect(mockResponse.status).not.toHaveBeenCalled();
|
|
584
|
+
});
|
|
585
|
+
it('should extract user information from verified token', async () => {
|
|
586
|
+
mockRequest.headers = {
|
|
587
|
+
authorization: 'Bearer valid-token',
|
|
588
|
+
};
|
|
589
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat: Math.floor(Date.now() / 1000), sub: 'user123' });
|
|
590
|
+
const userInfo = {
|
|
591
|
+
sub: 'user123',
|
|
592
|
+
email: 'test@example.com',
|
|
593
|
+
name: 'Test Name',
|
|
594
|
+
};
|
|
595
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue(userInfo);
|
|
596
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.VALID);
|
|
597
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
598
|
+
expect(mockRequest.user).toEqual(userInfo);
|
|
599
|
+
});
|
|
600
|
+
it('should attach user object to request with correct structure', async () => {
|
|
601
|
+
mockRequest.headers = {
|
|
602
|
+
authorization: 'Bearer valid-token',
|
|
603
|
+
};
|
|
604
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat: Math.floor(Date.now() / 1000), sub: 'user123' });
|
|
605
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
606
|
+
sub: 'user123',
|
|
607
|
+
email: 'user@example.com',
|
|
608
|
+
});
|
|
609
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.VALID);
|
|
610
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
611
|
+
expect(mockRequest.user).toHaveProperty('sub', 'user123');
|
|
612
|
+
expect(mockRequest.user).toHaveProperty('email', 'user@example.com');
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
describe('Logger Usage', () => {
|
|
616
|
+
it('should use req.logger when available', async () => {
|
|
617
|
+
const requestLogger = {
|
|
618
|
+
warn: vi.fn(),
|
|
619
|
+
error: vi.fn(),
|
|
620
|
+
info: vi.fn(),
|
|
621
|
+
};
|
|
622
|
+
mockRequest.logger = requestLogger;
|
|
623
|
+
mockRequest.headers = {
|
|
624
|
+
authorization: 'Bearer valid-token',
|
|
625
|
+
};
|
|
626
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat: Math.floor(Date.now() / 1000), sub: 'user123' });
|
|
627
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
628
|
+
sub: 'user123',
|
|
629
|
+
});
|
|
630
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.VALID);
|
|
631
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
632
|
+
// Request logger should be used, not the provided logger
|
|
633
|
+
expect(mockNext).toHaveBeenCalledWith();
|
|
634
|
+
});
|
|
635
|
+
it('should fallback to provided logger when req.logger is not available', async () => {
|
|
636
|
+
mockRequest.headers = {
|
|
637
|
+
authorization: 'Bearer valid-token',
|
|
638
|
+
};
|
|
639
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat: Math.floor(Date.now() / 1000), sub: 'user123' });
|
|
640
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
641
|
+
sub: 'user123',
|
|
642
|
+
});
|
|
643
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.VALID);
|
|
644
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
645
|
+
expect(mockNext).toHaveBeenCalledWith();
|
|
646
|
+
});
|
|
647
|
+
it('should handle undefined logger gracefully', async () => {
|
|
648
|
+
const middlewareWithoutLogger = createAuthMiddleware(mockUserTokenVerifier, sessionService);
|
|
649
|
+
mockRequest.headers = {
|
|
650
|
+
authorization: 'Bearer valid-token',
|
|
651
|
+
};
|
|
652
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat: Math.floor(Date.now() / 1000), sub: 'user123' });
|
|
653
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
654
|
+
sub: 'user123',
|
|
655
|
+
});
|
|
656
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.VALID);
|
|
657
|
+
await middlewareWithoutLogger(mockRequest, mockResponse, mockNext);
|
|
658
|
+
expect(mockNext).toHaveBeenCalledWith();
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
describe('Correlation ID', () => {
|
|
662
|
+
it('should use req.correlationId when available', async () => {
|
|
663
|
+
mockRequest.headers = {
|
|
664
|
+
authorization: 'Bearer valid-token',
|
|
665
|
+
};
|
|
666
|
+
mockRequest.correlationId = 'custom-correlation-id';
|
|
667
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat: Math.floor(Date.now() / 1000), sub: 'user123' });
|
|
668
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
669
|
+
sub: 'user123',
|
|
670
|
+
});
|
|
671
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.VALID);
|
|
672
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
673
|
+
expect(mockUserTokenVerifier.verify).toHaveBeenCalledWith('valid-token', 'custom-correlation-id');
|
|
674
|
+
});
|
|
675
|
+
it('should handle missing correlationId gracefully', async () => {
|
|
676
|
+
mockRequest.headers = {
|
|
677
|
+
authorization: 'Bearer valid-token',
|
|
678
|
+
};
|
|
679
|
+
delete mockRequest.correlationId;
|
|
680
|
+
vi.mocked(decodeJwt).mockReturnValue({ iat: Math.floor(Date.now() / 1000), sub: 'user123' });
|
|
681
|
+
vi.mocked(mockUserTokenVerifier.verify).mockResolvedValue({
|
|
682
|
+
sub: 'user123',
|
|
683
|
+
});
|
|
684
|
+
vi.spyOn(sessionService, 'validateSession').mockResolvedValue(SessionValidationStatus.VALID);
|
|
685
|
+
await authMiddleware(mockRequest, mockResponse, mockNext);
|
|
686
|
+
expect(mockUserTokenVerifier.verify).toHaveBeenCalledWith('valid-token', undefined);
|
|
687
|
+
expect(mockNext).toHaveBeenCalledWith();
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
//# sourceMappingURL=authMiddleware.test.js.map
|