mbkauthe 4.9.0 → 5.0.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/test.spec.js CHANGED
@@ -1,37 +1,186 @@
1
1
  import request from 'supertest';
2
+ import express from 'express';
3
+ import { engine } from 'express-handlebars';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { jest } from '@jest/globals';
2
7
 
3
- const BASE_URL = "http://localhost:5555";
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ process.env.test = 'dev';
12
+ process.env.env = 'dev';
13
+ process.env.dbLogs = 'true';
14
+ process.env.dbLogsCallsite = 'false';
15
+
16
+ const { default: router } = await import('./lib/main.js');
17
+ const { packageJson } = await import('./lib/config/index.js');
18
+ const { dblogin } = await import('./lib/pool.js');
19
+ const {
20
+ attachDevQueryLogger,
21
+ resetQueryCount,
22
+ resetQueryLog,
23
+ runWithRequestContext
24
+ } = await import('./lib/utils/dbQueryLogger.js');
25
+
26
+ const viewsPath = path.join(__dirname, 'views');
27
+
28
+ const handlebarsHelpers = {
29
+ eq: (a, b) => a === b,
30
+ encodeURIComponent: (str) => encodeURIComponent(str),
31
+ formatTimestamp: (timestamp) => new Date(timestamp).toLocaleString(),
32
+ jsonStringify: (context) => JSON.stringify(context),
33
+ json: (obj) => JSON.stringify(obj, null, 2),
34
+ objectEntries: (obj) => {
35
+ if (!obj || typeof obj !== 'object') return [];
36
+ return Object.entries(obj).map(([key, value]) => ({ key, value }));
37
+ },
38
+ cacheBuster: () => `?v=${packageJson.version}`
39
+ };
40
+
41
+ const app = express();
42
+ app.set('views', [
43
+ viewsPath,
44
+ path.join(__dirname, 'node_modules/mbkauthe/views')
45
+ ]);
46
+ app.engine('handlebars', engine({
47
+ defaultLayout: false,
48
+ cache: true,
49
+ partialsDir: [
50
+ viewsPath,
51
+ path.join(__dirname, 'node_modules/mbkauthe/views'),
52
+ path.join(__dirname, 'node_modules/mbkauthe/views/Error'),
53
+ ],
54
+ helpers: handlebarsHelpers
55
+ }));
56
+ app.set('view engine', 'handlebars');
57
+ app.use(router);
58
+
59
+ const shouldSilenceConsole = (args) => {
60
+ const [firstArg = ''] = args;
61
+ const text = typeof firstArg === 'string' ? firstArg : '';
62
+
63
+ return text.includes('[mbkauthe]');
64
+ };
65
+
66
+ const originalConsoleLog = console.log;
67
+ const originalConsoleWarn = console.warn;
68
+ const originalConsoleError = console.error;
69
+
70
+ beforeAll(() => {
71
+ jest.spyOn(console, 'log').mockImplementation((...args) => {
72
+ if (!shouldSilenceConsole(args)) {
73
+ originalConsoleLog(...args);
74
+ }
75
+ });
76
+
77
+ jest.spyOn(console, 'warn').mockImplementation((...args) => {
78
+ if (!shouldSilenceConsole(args)) {
79
+ originalConsoleWarn(...args);
80
+ }
81
+ });
82
+
83
+ jest.spyOn(console, 'error').mockImplementation((...args) => {
84
+ if (!shouldSilenceConsole(args)) {
85
+ originalConsoleError(...args);
86
+ }
87
+ });
88
+ });
89
+
90
+ afterAll(async () => {
91
+ jest.restoreAllMocks();
92
+ await dblogin.end().catch(() => {});
93
+ });
4
94
 
5
95
  // Helper to get CSRF token and cookies
6
96
  const getCSRFTokenAndCookies = async () => {
7
- const response = await request(BASE_URL).get('/mbkauthe/login');
97
+ const response = await request(app).get('/mbkauthe/login');
8
98
  const html = response.text;
9
- const csrfMatch = html.match(/name="_csrf".*?value="([^"]+)"/i) ||
10
- html.match(/content="([^"]+)".*?name="_csrf"/i);
11
-
99
+ const csrfMatch = html.match(/name="_csrf".*?value="([^"]+)"/i) ||
100
+ html.match(/content="([^"]+)".*?name="_csrf"/i);
101
+
12
102
  return {
13
103
  csrfToken: csrfMatch?.[1] || '',
14
104
  cookies: response.headers['set-cookie'] || []
15
105
  };
16
106
  };
17
107
 
108
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
109
+
110
+ const createFakePool = ({ name = 'fake-db-pool' } = {}) => {
111
+ const pool = {
112
+ totalCount: 1,
113
+ idleCount: 1,
114
+ waitingCount: 0,
115
+ options: { application_name: name },
116
+ async connect() {
117
+ pool.idleCount = 0;
118
+
119
+ return {
120
+ async query(configOrText, maybeValues) {
121
+ const text = typeof configOrText === 'string' ? configOrText : configOrText?.text || '';
122
+ const values = Array.isArray(maybeValues)
123
+ ? maybeValues
124
+ : Array.isArray(configOrText?.values)
125
+ ? configOrText.values
126
+ : [];
127
+
128
+ if (text.includes('slow_table')) {
129
+ await sleep(6);
130
+ }
131
+
132
+ if (text.includes('broken_table')) {
133
+ const error = new Error('broken query');
134
+ error.code = 'FAKE_ERR';
135
+ throw error;
136
+ }
137
+
138
+ return {
139
+ command: 'SELECT',
140
+ rowCount: values.length ? 1 : 0,
141
+ rows: values.length ? [{ value: values[0] }] : [],
142
+ };
143
+ },
144
+ release() {
145
+ pool.idleCount = 1;
146
+ }
147
+ };
148
+ },
149
+ async query(configOrText, maybeValues) {
150
+ const client = await this.connect();
151
+ try {
152
+ return await client.query(configOrText, maybeValues);
153
+ } finally {
154
+ client.release();
155
+ }
156
+ }
157
+ };
158
+
159
+ attachDevQueryLogger(pool);
160
+ return pool;
161
+ };
162
+
18
163
  describe('mbkauthe Routes', () => {
19
-
164
+ beforeEach(() => {
165
+ resetQueryCount();
166
+ resetQueryLog();
167
+ });
168
+
20
169
  describe('Redirect Routes', () => {
21
170
  test('GET /login redirects to /mbkauthe/login', async () => {
22
- const response = await request(BASE_URL)
171
+ const response = await request(app)
23
172
  .get('/login')
24
173
  .redirects(0);
25
-
174
+
26
175
  expect(response.status).toBe(302);
27
176
  expect(response.headers.location).toContain('/mbkauthe/login');
28
177
  });
29
178
 
30
179
  test('GET /signin redirects to /mbkauthe/login', async () => {
31
- const response = await request(BASE_URL)
180
+ const response = await request(app)
32
181
  .get('/signin')
33
182
  .redirects(0);
34
-
183
+
35
184
  expect(response.status).toBe(302);
36
185
  expect(response.headers.location).toContain('/mbkauthe/login');
37
186
  });
@@ -39,17 +188,24 @@ describe('mbkauthe Routes', () => {
39
188
 
40
189
  describe('Authentication Pages', () => {
41
190
  test('GET /mbkauthe/login contains loginUsername input', async () => {
42
- const response = await request(BASE_URL).get('/mbkauthe/login');
43
-
191
+ const response = await request(app).get('/mbkauthe/login');
192
+
44
193
  expect(response.status).toBe(200);
45
194
  expect(response.text).toMatch(/id\s*=\s*["']loginUsername["']/i);
46
195
  });
47
196
 
197
+ test('GET /mbkauthe/login renders csrf token field', async () => {
198
+ const response = await request(app).get('/mbkauthe/login');
199
+
200
+ expect(response.status).toBe(200);
201
+ expect(response.text).toMatch(/name\s*=\s*["']_csrf["']/i);
202
+ });
203
+
48
204
  test('GET /mbkauthe/2fa contains token input or redirects', async () => {
49
- const response = await request(BASE_URL)
205
+ const response = await request(app)
50
206
  .get('/mbkauthe/2fa')
51
207
  .redirects(0);
52
-
208
+
53
209
  if (response.status === 302) {
54
210
  expect(response.headers.location).toContain('/mbkauthe/login');
55
211
  } else {
@@ -63,40 +219,51 @@ describe('mbkauthe Routes', () => {
63
219
  test.each([
64
220
  ['/mbkauthe/info', 'info page'],
65
221
  ['/mbkauthe/i', 'info short page'],
66
- ])('GET %s contains CurrentVersion div', async (path, desc) => {
67
- const response = await request(BASE_URL).get(path);
68
-
222
+ ])('GET %s contains CurrentVersion div', async (routePath) => {
223
+ const response = await request(app).get(routePath);
224
+
69
225
  expect(response.status).toBe(200);
70
226
  expect(response.text).toMatch(/id\s*=\s*["']CurrentVersion["']/i);
71
227
  });
72
228
 
73
229
  test('GET /mbkauthe/ErrorCode contains error-603 div', async () => {
74
- const response = await request(BASE_URL).get('/mbkauthe/ErrorCode');
75
-
230
+ const response = await request(app).get('/mbkauthe/ErrorCode');
231
+
76
232
  expect(response.status).toBe(200);
77
233
  expect(response.text).toMatch(/id\s*=\s*["']error-603["']/i);
78
234
  });
235
+
236
+ test('GET /mbkauthe/db renders DB monitor filters and summary sections', async () => {
237
+ const response = await request(app).get('/mbkauthe/db');
238
+
239
+ expect(response.status).toBe(200);
240
+ expect(response.text).toContain('DB Query Monitor');
241
+ expect(response.text).toContain('Top Repeated Query Shapes');
242
+ expect(response.text).toContain('Slowest Recent Queries');
243
+ expect(response.text).toMatch(/name="username"/i);
244
+ expect(response.text).toMatch(/name="url"/i);
245
+ expect(response.text).toMatch(/name="success"/i);
246
+ });
79
247
  });
80
248
 
81
249
  describe('Static Assets', () => {
82
250
  test('GET /mbkauthe/main.js returns JavaScript', async () => {
83
- const response = await request(BASE_URL).get('/mbkauthe/main.js');
84
-
251
+ const response = await request(app).get('/mbkauthe/main.js');
252
+
85
253
  expect(response.status).toBe(200);
86
254
  expect(response.headers['content-type']).toContain('javascript');
87
255
  });
88
256
 
89
257
  test('GET /icon.svg returns SVG content', async () => {
90
- const response = await request(BASE_URL).get('/icon.svg');
91
-
258
+ const response = await request(app).get('/icon.svg');
259
+
92
260
  expect(response.status).toBe(200);
93
261
  expect(response.headers['content-type']).toContain('image/png');
94
-
95
262
  });
96
263
 
97
264
  test('GET /mbkauthe/bg.webp returns WEBP content', async () => {
98
- const response = await request(BASE_URL).get('/mbkauthe/bg.webp');
99
-
265
+ const response = await request(app).get('/mbkauthe/bg.webp');
266
+
100
267
  expect(response.status).toBe(200);
101
268
  expect(response.headers['content-type']).toContain('image/webp');
102
269
  });
@@ -104,12 +271,12 @@ describe('mbkauthe Routes', () => {
104
271
 
105
272
  describe('Protected Routes', () => {
106
273
  test('GET /mbkauthe/test responds appropriately', async () => {
107
- const response = await request(BASE_URL).get('/mbkauthe/test');
274
+ const response = await request(app).get('/mbkauthe/test');
108
275
  expect([200, 302, 401, 403, 429]).toContain(response.status);
109
276
  });
110
277
 
111
278
  test('GET /mbkauthe/test with curl UA returns JSON 401', async () => {
112
- const response = await request(BASE_URL)
279
+ const response = await request(app)
113
280
  .get('/mbkauthe/test')
114
281
  .set('User-Agent', 'curl/8.0.1')
115
282
  .set('Accept', '*/*');
@@ -119,16 +286,28 @@ describe('mbkauthe Routes', () => {
119
286
  expect(response.body).toHaveProperty('success', false);
120
287
  expect(response.body).toHaveProperty('errorCode');
121
288
  });
289
+
290
+ test('GET /mbkauthe/test with browser accept redirects to login when unauthenticated', async () => {
291
+ const response = await request(app)
292
+ .get('/mbkauthe/test')
293
+ .set('User-Agent', 'Mozilla/5.0')
294
+ .set('Accept', 'text/html')
295
+ .redirects(0);
296
+
297
+ expect(response.status).toBe(302);
298
+ expect(response.headers.location).toContain('/mbkauthe/login');
299
+ expect(response.headers.location).toContain('reason=logged_out');
300
+ });
122
301
  });
123
302
 
124
303
  describe('OAuth Routes', () => {
125
304
  test('GET /mbkauthe/api/github/login handles GitHub App flow', async () => {
126
- const response = await request(BASE_URL)
305
+ const response = await request(app)
127
306
  .get('/mbkauthe/api/github/login')
128
307
  .redirects(0);
129
-
308
+
130
309
  expect([200, 302, 403, 500, 429]).toContain(response.status);
131
-
310
+
132
311
  if (response.status === 302) {
133
312
  const location = response.headers.location;
134
313
  expect(location).toMatch(/github\.com|login/);
@@ -136,17 +315,17 @@ describe('mbkauthe Routes', () => {
136
315
  });
137
316
 
138
317
  test('GET /mbkauthe/api/github/login/callback handles GitHub App callback', async () => {
139
- const response = await request(BASE_URL).get('/mbkauthe/api/github/login/callback');
318
+ const response = await request(app).get('/mbkauthe/api/github/login/callback');
140
319
  expect([200, 302, 400, 401, 403, 429]).toContain(response.status);
141
320
  });
142
321
 
143
322
  test('GET /mbkauthe/api/google/login handles Google OAuth', async () => {
144
- const response = await request(BASE_URL)
323
+ const response = await request(app)
145
324
  .get('/mbkauthe/api/google/login')
146
325
  .redirects(0);
147
-
326
+
148
327
  expect([200, 302, 403, 500, 429]).toContain(response.status);
149
-
328
+
150
329
  if (response.status === 302) {
151
330
  const location = response.headers.location;
152
331
  expect(location).toMatch(/accounts\.google\.com|login/);
@@ -154,43 +333,247 @@ describe('mbkauthe Routes', () => {
154
333
  });
155
334
 
156
335
  test('GET /mbkauthe/api/google/login/callback handles callback', async () => {
157
- const response = await request(BASE_URL).get('/mbkauthe/api/google/login/callback');
336
+ const response = await request(app).get('/mbkauthe/api/google/login/callback');
158
337
  expect([200, 302, 400, 401, 403, 429]).toContain(response.status);
159
338
  });
339
+
340
+ test('GET /mbkauthe/api/github/login rejects unsafe redirect targets', async () => {
341
+ const response = await request(app)
342
+ .get('/mbkauthe/api/github/login?redirect=https://evil.example')
343
+ .redirects(0);
344
+
345
+ expect([200, 302, 403, 500, 429]).toContain(response.status);
346
+ if (response.status === 302) {
347
+ expect(response.headers.location).not.toContain('evil.example');
348
+ }
349
+ });
160
350
  });
161
351
 
162
352
  describe('API Endpoints', () => {
353
+ test('GET /mbkauthe/db.json returns newest-first logs with summary stats and fingerprints', async () => {
354
+ const fakePool = createFakePool({ name: 'reporting-db' });
355
+
356
+ await runWithRequestContext(
357
+ {
358
+ method: 'POST',
359
+ originalUrl: '/mbkauthe/api/login',
360
+ url: '/mbkauthe/api/login',
361
+ ip: '127.0.0.1',
362
+ session: { user: { id: '1', username: 'support' } }
363
+ },
364
+ () => fakePool.query({ text: 'SELECT * FROM users WHERE id = $1', values: [1], name: 'userLookup' })
365
+ );
366
+
367
+ await sleep(4);
368
+
369
+ await runWithRequestContext(
370
+ {
371
+ method: 'POST',
372
+ originalUrl: '/mbkauthe/api/login',
373
+ url: '/mbkauthe/api/login',
374
+ ip: '127.0.0.1',
375
+ session: { user: { id: '1', username: 'support' } }
376
+ },
377
+ () => fakePool.query({ text: 'SELECT * FROM users WHERE id = $1', values: [2], name: 'userLookup' })
378
+ );
379
+
380
+ await sleep(4);
381
+
382
+ await runWithRequestContext(
383
+ {
384
+ method: 'GET',
385
+ originalUrl: '/mbkauthe/api/audit',
386
+ url: '/mbkauthe/api/audit',
387
+ ip: '127.0.0.1',
388
+ session: { user: { id: '2', username: 'auditor' } }
389
+ },
390
+ () => fakePool.query({ text: 'SELECT * FROM slow_table WHERE team_id = $1', values: [55], name: 'slowAudit' })
391
+ );
392
+
393
+ await sleep(4);
394
+
395
+ await runWithRequestContext(
396
+ {
397
+ method: 'GET',
398
+ originalUrl: '/mbkauthe/api/admin',
399
+ url: '/mbkauthe/api/admin',
400
+ ip: '127.0.0.1',
401
+ session: { user: { id: '3', username: 'admin' } }
402
+ },
403
+ async () => {
404
+ await fakePool.query({ text: 'SELECT * FROM broken_table WHERE id = $1', values: [99], name: 'brokenLookup' })
405
+ .catch(() => {});
406
+ }
407
+ );
408
+
409
+ const response = await request(app).get('/mbkauthe/db.json?limit=10');
410
+
411
+ expect(response.status).toBe(200);
412
+ expect(response.body.isDev).toBe(true);
413
+ expect(response.body.queryCount).toBe(4);
414
+ expect(response.body.summary.totalVisible).toBe(4);
415
+ expect(response.body.summary.errorCount).toBe(1);
416
+ expect(response.body.queryLog).toHaveLength(4);
417
+ expect(response.body.queryLog[0].query).toContain('broken_table');
418
+ expect(response.body.queryLog[1].query).toContain('slow_table');
419
+ expect(response.body.queryLog[0].fingerprint).toMatch(/^[0-9a-f]{12}$/);
420
+ expect(response.body.queryLog[0].poolWait).toMatchObject({
421
+ source: 'pool.query',
422
+ captured: true
423
+ });
424
+ expect(response.body.queryLog[0].trigger).toMatchObject({
425
+ type: 'request',
426
+ source: 'route'
427
+ });
428
+ expect(response.body.summary.slowestQueries[0].query).toContain('slow_table');
429
+ expect(response.body.summary.repeatedGroups[0]).toMatchObject({
430
+ count: 2,
431
+ sampleName: 'userLookup'
432
+ });
433
+ expect(response.body.summary.repeatedGroups[0].fingerprint).toMatch(/^[0-9a-f]{12}$/);
434
+ });
435
+
436
+ test('GET /mbkauthe/db.json filters by username, url, and success', async () => {
437
+ const fakePool = createFakePool({ name: 'filter-db' });
438
+
439
+ await runWithRequestContext(
440
+ {
441
+ method: 'POST',
442
+ originalUrl: '/mbkauthe/api/login',
443
+ url: '/mbkauthe/api/login',
444
+ ip: '127.0.0.1',
445
+ session: { user: { id: '1', username: 'support' } }
446
+ },
447
+ () => fakePool.query({ text: 'SELECT * FROM users WHERE id = $1', values: [7], name: 'loginLookup' })
448
+ );
449
+
450
+ await runWithRequestContext(
451
+ {
452
+ method: 'GET',
453
+ originalUrl: '/mbkauthe/api/reports',
454
+ url: '/mbkauthe/api/reports',
455
+ ip: '127.0.0.1',
456
+ session: { user: { id: '2', username: 'auditor' } }
457
+ },
458
+ async () => {
459
+ await fakePool.query({ text: 'SELECT * FROM broken_table WHERE id = $1', values: [8], name: 'badLookup' })
460
+ .catch(() => {});
461
+ }
462
+ );
463
+
464
+ const usernameResponse = await request(app).get('/mbkauthe/db.json?username=support');
465
+ expect(usernameResponse.status).toBe(200);
466
+ expect(usernameResponse.body.queryLog).toHaveLength(1);
467
+ expect(usernameResponse.body.queryLog[0].request.username).toBe('support');
468
+
469
+ const urlResponse = await request(app).get('/mbkauthe/db.json?url=/mbkauthe/api/reports');
470
+ expect(urlResponse.status).toBe(200);
471
+ expect(urlResponse.body.queryLog).toHaveLength(1);
472
+ expect(urlResponse.body.queryLog[0].request.url).toBe('/mbkauthe/api/reports');
473
+ expect(urlResponse.body.queryLog[0].trigger.label).toContain('/mbkauthe/api/reports');
474
+
475
+ const successResponse = await request(app).get('/mbkauthe/db.json?success=false');
476
+ expect(successResponse.status).toBe(200);
477
+ expect(successResponse.body.queryLog).toHaveLength(1);
478
+ expect(successResponse.body.queryLog[0].success).toBe(false);
479
+ expect(successResponse.body.summary.errorCount).toBe(1);
480
+ });
481
+
482
+ test('session-store-shaped queries are labeled as session-store triggers during a request', async () => {
483
+ const fakePool = createFakePool({ name: 'session-db' });
484
+
485
+ await runWithRequestContext(
486
+ {
487
+ method: 'GET',
488
+ originalUrl: '/mbkauthe/db.json?limit=50',
489
+ url: '/mbkauthe/db.json?limit=50',
490
+ ip: '127.0.0.1',
491
+ session: {}
492
+ },
493
+ () => fakePool.query({
494
+ text: 'SELECT sess FROM "session" WHERE sid = $1 AND expire >= to_timestamp($2)',
495
+ values: ['abc', 123],
496
+ name: 'sessionLookup'
497
+ })
498
+ );
499
+
500
+ const response = await request(app).get('/mbkauthe/db.json?limit=10');
501
+
502
+ expect(response.status).toBe(200);
503
+ expect(response.body.queryLog[0].trigger).toMatchObject({
504
+ type: 'request',
505
+ source: 'session-store',
506
+ route: 'GET /mbkauthe/db.json?limit=50'
507
+ });
508
+ expect(response.body.queryLog[0].trigger.label).toContain('Session store during GET /mbkauthe/db.json?limit=50');
509
+ });
510
+
163
511
  test('POST /mbkauthe/api/login handles login API', async () => {
164
- const response = await request(BASE_URL)
512
+ const response = await request(app)
165
513
  .post('/mbkauthe/api/login')
166
514
  .send({ username: 'test', password: 'test' });
167
-
515
+
168
516
  expect([200, 400, 401, 403, 429]).toContain(response.status);
169
517
  expect(response.headers['content-type']).toContain('application/json');
170
518
  });
171
519
 
520
+ test('POST /mbkauthe/api/login rejects missing credentials', async () => {
521
+ const response = await request(app)
522
+ .post('/mbkauthe/api/login')
523
+ .send({});
524
+
525
+ expect(response.status).toBe(400);
526
+ expect(response.headers['content-type']).toContain('application/json');
527
+ expect(response.body).toHaveProperty('errorCode', 1001);
528
+ });
529
+
530
+ test('POST /mbkauthe/api/login rejects short passwords before DB auth', async () => {
531
+ const response = await request(app)
532
+ .post('/mbkauthe/api/login')
533
+ .send({ username: 'tester', password: 'short' });
534
+
535
+ expect(response.status).toBe(400);
536
+ expect(response.headers['content-type']).toContain('application/json');
537
+ expect(response.body).toHaveProperty('errorCode', 1003);
538
+ });
539
+
172
540
  test('POST /mbkauthe/api/verify-2fa handles 2FA API', async () => {
173
541
  const { csrfToken, cookies } = await getCSRFTokenAndCookies();
174
-
175
- const response = await request(BASE_URL)
542
+
543
+ const response = await request(app)
176
544
  .post('/mbkauthe/api/verify-2fa')
177
545
  .set('Cookie', cookies)
178
546
  .send({ token: '123456', _csrf: csrfToken });
179
-
547
+
180
548
  expect([200, 400, 401, 403, 429]).toContain(response.status);
181
549
  expect(response.headers['content-type']).toContain('application/json');
182
550
  });
183
551
 
552
+ test('POST /mbkauthe/api/verify-2fa rejects malformed tokens', async () => {
553
+ const { csrfToken, cookies } = await getCSRFTokenAndCookies();
554
+
555
+ const response = await request(app)
556
+ .post('/mbkauthe/api/verify-2fa')
557
+ .set('Cookie', cookies)
558
+ .send({ token: '12ab', _csrf: csrfToken });
559
+
560
+ expect([400, 401]).toContain(response.status);
561
+ expect(response.headers['content-type']).toContain('application/json');
562
+ if (response.status === 400) {
563
+ expect(response.body).toHaveProperty('errorCode', 1004);
564
+ }
565
+ });
566
+
184
567
  test('POST /mbkauthe/api/terminateAllSessions handles session termination', async () => {
185
568
  const { csrfToken, cookies } = await getCSRFTokenAndCookies();
186
-
187
- const response = await request(BASE_URL)
569
+
570
+ const response = await request(app)
188
571
  .post('/mbkauthe/api/terminateAllSessions')
189
572
  .set('Cookie', cookies)
190
573
  .send({ _csrf: csrfToken });
191
-
574
+
192
575
  expect([200, 400, 401, 403, 429]).toContain(response.status);
193
-
576
+
194
577
  if (response.status === 401) {
195
578
  expect(response.headers['content-type']).not.toContain('application/json');
196
579
  } else {
@@ -199,18 +582,102 @@ describe('mbkauthe Routes', () => {
199
582
  });
200
583
 
201
584
  test('GET /mbkauthe/api/checkSession handles session check', async () => {
202
- const response = await request(BASE_URL).get('/mbkauthe/api/checkSession');
585
+ const response = await request(app).get('/mbkauthe/api/checkSession');
203
586
  expect(response.status).toBe(200);
204
587
  expect(response.headers['content-type']).toContain('application/json');
205
588
  expect(response.body).toHaveProperty('sessionValid');
206
589
  });
207
590
 
591
+ test('POST /mbkauthe/api/checkSession rejects missing session ids', async () => {
592
+ const response = await request(app)
593
+ .post('/mbkauthe/api/checkSession')
594
+ .send({});
595
+
596
+ expect(response.status).toBe(400);
597
+ expect(response.headers['content-type']).toContain('application/json');
598
+ expect(response.body).toHaveProperty('errorCode', 1001);
599
+ });
600
+
601
+ test('POST /mbkauthe/api/checkSession rejects non-uuid session ids', async () => {
602
+ const response = await request(app)
603
+ .post('/mbkauthe/api/checkSession')
604
+ .send({ sessionId: 'bad-session-id' });
605
+
606
+ expect(response.status).toBe(400);
607
+ expect(response.headers['content-type']).toContain('application/json');
608
+ expect(response.body).toHaveProperty('errorCode', 802);
609
+ });
610
+
611
+ test('POST /mbkauthe/api/verifySession rejects missing session ids', async () => {
612
+ const response = await request(app)
613
+ .post('/mbkauthe/api/verifySession')
614
+ .send({});
615
+
616
+ expect(response.status).toBe(400);
617
+ expect(response.headers['content-type']).toContain('application/json');
618
+ expect(response.body).toHaveProperty('errorCode', 1001);
619
+ });
620
+
621
+ test('POST /mbkauthe/api/verifySession rejects invalid encrypted session ids', async () => {
622
+ const response = await request(app)
623
+ .post('/mbkauthe/api/verifySession')
624
+ .send({ sessionId: 'definitely-not-encrypted', isEncrypt: true });
625
+
626
+ expect(response.status).toBe(400);
627
+ expect(response.headers['content-type']).toContain('application/json');
628
+ expect(response.body).toHaveProperty('errorCode', 802);
629
+ });
630
+
208
631
  test('POST /mbkauthe/api/logout handles logout', async () => {
209
- const response = await request(BASE_URL).post('/mbkauthe/api/logout').send();
632
+ const response = await request(app).post('/mbkauthe/api/logout').send();
210
633
  expect([200, 400, 401, 403, 429]).toContain(response.status);
211
634
  if (response.status === 200) {
212
635
  expect(response.headers['content-type']).toContain('application/json');
213
636
  }
214
637
  });
638
+
639
+ test('GET /mbkauthe/api/account-sessions handles remembered account listing', async () => {
640
+ const response = await request(app).get('/mbkauthe/api/account-sessions');
641
+ expect([200, 401, 403, 429]).toContain(response.status);
642
+ if (response.status === 200) {
643
+ expect(response.headers['content-type']).toContain('application/json');
644
+ expect(response.body).toHaveProperty('accounts');
645
+ }
646
+ });
647
+
648
+ test('GET /mbkauthe/api/account-sessions tolerates malformed remembered-account cookies', async () => {
649
+ const response = await request(app)
650
+ .get('/mbkauthe/api/account-sessions')
651
+ .set('Cookie', ['mbkauthe_accounts=not-json']);
652
+
653
+ expect([200, 401, 403, 429]).toContain(response.status);
654
+ if (response.status === 200) {
655
+ expect(Array.isArray(response.body.accounts)).toBe(true);
656
+ }
657
+ });
658
+
659
+ test('POST /mbkauthe/api/switch-session rejects invalid session ids', async () => {
660
+ const response = await request(app)
661
+ .post('/mbkauthe/api/switch-session')
662
+ .send({ sessionId: 'not-a-uuid' });
663
+
664
+ expect([400, 429]).toContain(response.status);
665
+ expect(response.headers['content-type']).toContain('application/json');
666
+ if (response.status === 400) {
667
+ expect(response.body).toHaveProperty('errorCode');
668
+ }
669
+ });
670
+
671
+ test('POST /mbkauthe/api/logout-all handles no-session callers safely', async () => {
672
+ const response = await request(app)
673
+ .post('/mbkauthe/api/logout-all')
674
+ .send({});
675
+
676
+ expect([200, 500, 429]).toContain(response.status);
677
+ if (response.status === 200) {
678
+ expect(response.headers['content-type']).toContain('application/json');
679
+ expect(response.body).toHaveProperty('success', true);
680
+ }
681
+ });
215
682
  });
216
- });
683
+ });