mbkauthe 3.0.0 โ†’ 3.1.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/README.md CHANGED
@@ -110,6 +110,30 @@ app.get('/admin', validateSession, checkRolePermission(['SuperAdmin']), (req, re
110
110
  app.listen(3000);
111
111
  ```
112
112
 
113
+ ## ๐Ÿงช Testing
114
+
115
+ MBKAuthe includes comprehensive test coverage for all authentication features:
116
+
117
+ ```bash
118
+ # Run all tests
119
+ npm test
120
+
121
+ # Run tests in watch mode (auto-rerun on changes)
122
+ npm run test:watch
123
+
124
+ # Run with development flags
125
+ npm run dev
126
+ ```
127
+
128
+ **Test Coverage:**
129
+ - โœ… Authentication flows (login, 2FA, logout)
130
+ - โœ… OAuth integration (GitHub)
131
+ - โœ… Session management and security
132
+ - โœ… Role-based access control
133
+ - โœ… API endpoints and error handling
134
+ - โœ… CSRF protection and rate limiting
135
+ - โœ… Static asset serving
136
+
113
137
  ## ๐Ÿ“‚ Architecture (v3.0)
114
138
 
115
139
  ```
@@ -148,14 +172,31 @@ app.post('/api/data', authenticate(process.env.API_TOKEN), handler);
148
172
 
149
173
  ### Built-in Routes
150
174
 
151
- - `GET /mbkauthe/login` - Login page
175
+ **Authentication Routes:**
176
+ - `GET /login`, `/signin` - Redirect to main login page
177
+ - `GET /mbkauthe/login` - Login page (8/min rate limit)
152
178
  - `POST /mbkauthe/api/login` - Login endpoint (8/min rate limit)
153
179
  - `POST /mbkauthe/api/logout` - Logout endpoint (10/min rate limit)
154
- - `GET /mbkauthe/2fa` - 2FA page (if enabled)
155
- - `POST /mbkauthe/api/verify-2fa` - 2FA verification (5/min rate limit)
156
- - `GET /mbkauthe/api/github/login` - GitHub OAuth
157
- - `GET /mbkauthe/info` - Version & config info
158
- - `GET /mbkauthe/ErrorCode` - Error documentation
180
+ - `GET /mbkauthe/2fa` - 2FA verification page (if enabled)
181
+ - `POST /mbkauthe/api/verify-2fa` - 2FA verification API (5/min rate limit)
182
+
183
+ **OAuth Routes:**
184
+ - `GET /mbkauthe/api/github/login` - GitHub OAuth initiation (10/5min rate limit)
185
+ - `GET /mbkauthe/api/github/login/callback` - GitHub OAuth callback
186
+
187
+ **Information & Utility Routes:**
188
+ - `GET /mbkauthe/info`, `/mbkauthe/i` - Version & config info (8/min rate limit)
189
+ - `GET /mbkauthe/ErrorCode` - Error code documentation
190
+ - `GET /mbkauthe/test` - Test authentication status (8/min rate limit)
191
+
192
+ **Static Asset Routes:**
193
+ - `GET /mbkauthe/main.js` - Client-side JavaScript utilities
194
+ - `GET /mbkauthe/bg.webp` - Background image for auth pages
195
+ - `GET /icon.svg` - Application SVG icon (root level)
196
+ - `GET /favicon.ico`, `/icon.ico` - Application favicon
197
+
198
+ **Admin API Routes:**
199
+ - `POST /mbkauthe/api/terminateAllSessions` - Terminate all sessions (admin only)
159
200
 
160
201
  ## ๐Ÿ” Security Features
161
202
 
@@ -245,8 +286,9 @@ const result = await dblogin.query('SELECT * FROM "Users"');
245
286
  ## ๐Ÿ“š Documentation
246
287
 
247
288
  - [API Documentation](docs/api.md) - Complete API reference
248
- - [Database Guide](docs/db.md) - Schema details
289
+ - [Database Guide](docs/db.md) - Schema details
249
290
  - [Environment Config](docs/env.md) - Configuration options
291
+ - [Error Messages](docs/error-messages.md) - Error code reference
250
292
 
251
293
  ## ๐Ÿ“ License
252
294
 
package/docs/api.md CHANGED
@@ -55,14 +55,65 @@ When a user logs in, MBKAuthe creates a session and sets the following cookies:
55
55
 
56
56
  ### Public Endpoints
57
57
 
58
+ #### `GET /login`
59
+
60
+ Redirect route that forwards to the main login page.
61
+
62
+ **Rate Limit:** No rate limiting applied
63
+
64
+ **Query Parameters:**
65
+ - All query parameters are preserved and forwarded
66
+
67
+ **Response:** 302 redirect to `/mbkauthe/login`
68
+
69
+ **Example:**
70
+ ```
71
+ GET /login?redirect=/dashboard
72
+ โ†’ Redirects to: /mbkauthe/login?redirect=/dashboard
73
+ ```
74
+
75
+ ---
76
+
77
+ #### `GET /signin`
78
+
79
+ Alias redirect route that forwards to the main login page.
80
+
81
+ **Rate Limit:** No rate limiting applied
82
+
83
+ **Query Parameters:**
84
+ - All query parameters are preserved and forwarded
85
+
86
+ **Response:** 302 redirect to `/mbkauthe/login`
87
+
88
+ **Example:**
89
+ ```
90
+ GET /signin
91
+ โ†’ Redirects to: /mbkauthe/login
92
+ ```
93
+
94
+ ---
95
+
58
96
  #### `GET /mbkauthe/login`
59
97
 
60
- Renders the login page.
98
+ Renders the main login page.
99
+
100
+ **Rate Limit:** 8 requests per minute (exempt for logged-in users)
101
+
102
+ **CSRF Protection:** Required (token included in form)
61
103
 
62
104
  **Query Parameters:**
63
105
  - `redirect` (optional) - URL to redirect after successful login
64
106
 
65
- **Response:** HTML page
107
+ **Response:** HTML page with login form
108
+
109
+ **Template Variables:**
110
+ - `githubLoginEnabled` - Whether GitHub OAuth is enabled
111
+ - `customURL` - Redirect URL after login
112
+ - `userLoggedIn` - Whether user is already authenticated
113
+ - `username` - Current username if logged in
114
+ - `version` - MBKAuthe version
115
+ - `appName` - Application name
116
+ - `csrfToken` - CSRF protection token
66
117
 
67
118
  **Example:**
68
119
  ```
@@ -275,13 +326,17 @@ fetch('/mbkauthe/api/logout', {
275
326
 
276
327
  Terminates all active sessions across all users (admin only).
277
328
 
278
- **Authentication:** API token required
329
+ **Authentication:** API token required (via `authenticate` middleware)
330
+
331
+ **Rate Limit:** No explicit rate limiting applied
279
332
 
280
333
  **Headers:**
281
334
  ```
282
- Authorization: your-api-token
335
+ Authorization: your-main-secret-token
283
336
  ```
284
337
 
338
+ **Request Body:** None required
339
+
285
340
  **Success Response (200 OK):**
286
341
  ```json
287
342
  {
@@ -298,16 +353,28 @@ Authorization: your-api-token
298
353
  | 500 | Failed to terminate sessions |
299
354
  | 500 | Internal Server Error |
300
355
 
356
+ **Implementation Details:**
357
+ - Clears all user session IDs in the database (`Users` table)
358
+ - Deletes all session records from the `session` table
359
+ - Destroys the current request session
360
+ - Clears session cookies
361
+ - Runs database operations in parallel for better performance
362
+
301
363
  **Example Request:**
302
364
  ```javascript
303
365
  fetch('/mbkauthe/api/terminateAllSessions', {
304
366
  method: 'POST',
305
367
  headers: {
306
- 'Authorization': 'your-secret-token',
368
+ 'Authorization': process.env.MAIN_SECRET_TOKEN,
369
+ 'Content-Type': 'application/json'
307
370
  }
308
371
  })
309
372
  .then(response => response.json())
310
- .then(data => console.log(data));
373
+ .then(data => {
374
+ if (data.success) {
375
+ console.log('All sessions terminated successfully');
376
+ }
377
+ });
311
378
  ```
312
379
 
313
380
  ---
@@ -363,6 +430,10 @@ GET /mbkauthe/ErrorCode
363
430
 
364
431
  Serves the client-side JavaScript file containing helper functions for authentication operations.
365
432
 
433
+ **Rate Limit:** No rate limiting applied
434
+
435
+ **Cache:** Cached for 1 year (max-age=31536000)
436
+
366
437
  **Purpose:** Provides frontend JavaScript utilities including:
367
438
  - `logout()` - Logout function with confirmation dialog and cache clearing
368
439
  - `logoutuser()` - Alias for logout function
@@ -396,46 +467,20 @@ Serves the client-side JavaScript file containing helper functions for authentic
396
467
  - Clears cookies
397
468
  - Forces page reload
398
469
 
399
-
400
- ---
401
-
402
- #### `GET /mbkauthe/ErrorCode`
403
-
404
- Displays comprehensive error code documentation with descriptions and user messages.
405
-
406
- **Response:** HTML page showing:
407
- - All error codes organized by category
408
- - Error code ranges (Authentication, 2FA, Session, Authorization, etc.)
409
- - User-friendly messages and hints for each error
410
- - Dynamically generated from `lib/utils/errors.js`
411
-
412
- **Categories:**
413
- - **600-699**: Authentication Errors
414
- - **700-799**: Two-Factor Authentication Errors
415
- - **800-899**: Session Management Errors
416
- - **900-999**: Authorization Errors
417
- - **1000-1099**: Input Validation Errors
418
- - **1100-1199**: Rate Limiting Errors
419
- - **1200-1299**: Server Errors
420
- - **1300-1399**: OAuth Errors
421
-
422
- **Note:** This page is automatically synchronized with the error definitions in the codebase. Adding new errors only requires updating `lib/utils/errors.js` and the category code array.
423
-
424
- **Usage:**
425
- ```
426
- GET /mbkauthe/ErrorCode
427
- ```
428
-
429
470
  ---
430
471
 
431
472
  #### `GET /icon.svg`
432
473
 
433
- Serves the application's SVG icon file.
474
+ Serves the application's SVG icon file from the root level.
475
+
476
+ **Rate Limit:** No rate limiting applied
434
477
 
435
478
  **Response:** SVG image file (Content-Type: image/svg+xml)
436
479
 
437
480
  **Cache:** Cached for 1 year (max-age=31536000)
438
481
 
482
+ **Note:** This route is mounted at the root level (not under `/mbkauthe`)
483
+
439
484
  **Usage:**
440
485
  ```html
441
486
  <img src="/icon.svg" alt="App Icon">
@@ -710,64 +755,6 @@ Authorization: your-secret-token
710
755
 
711
756
  ---
712
757
 
713
- ### `authapi(requiredRole)`
714
-
715
- Advanced API authentication with role-based access control.
716
-
717
- **Parameters:**
718
- - `requiredRole` (array, optional) - Array of allowed roles
719
-
720
- **Usage:**
721
- ```javascript
722
- import { authapi } from 'mbkauthe';
723
-
724
- // Any authenticated API user
725
- app.get('/api/data', authapi(), (req, res) => {
726
- console.log(req.user); // { username, UserName, role, Role }
727
- res.json({ data: 'API data' });
728
- });
729
-
730
- // Only SuperAdmin via API
731
- app.post('/api/admin/action', authapi(['SuperAdmin']), (req, res) => {
732
- res.json({ success: true });
733
- });
734
- ```
735
-
736
- **Headers Required:**
737
- ```
738
- Authorization: user-api-key-64-char-hex
739
- ```
740
-
741
- **Behavior:**
742
- - Validates API key from `Authorization` header
743
- - Looks up user associated with API key
744
- - Checks if user account is active
745
- - Verifies user role if `requiredRole` is specified
746
- - Blocks demo users from accessing endpoints
747
- - Populates `req.user` with user information
748
- - Returns 401 if unauthorized, 403 if insufficient permissions
749
-
750
- **req.user Object:**
751
- ```javascript
752
- req.user = {
753
- username: "john.doe",
754
- UserName: "john.doe",
755
- role: "NormalUser",
756
- Role: "NormalUser"
757
- }
758
- ```
759
-
760
- **Database Requirement:**
761
- Requires `UserAuthApiKey` table:
762
- ```sql
763
- CREATE TABLE "UserAuthApiKey" (
764
- username VARCHAR(50) PRIMARY KEY REFERENCES "Users"("UserName"),
765
- "key" VARCHAR(64) UNIQUE NOT NULL
766
- );
767
- ```
768
-
769
- ---
770
-
771
758
  ## Error Codes
772
759
 
773
760
  ### HTTP Status Codes
@@ -876,7 +863,7 @@ app.get('/moderator',
876
863
  ### API Authentication
877
864
 
878
865
  ```javascript
879
- import { authenticate, authapi } from 'mbkauthe';
866
+ import { authenticate } from 'mbkauthe';
880
867
 
881
868
  // Simple token authentication
882
869
  app.post('/api/webhook',
@@ -887,24 +874,27 @@ app.post('/api/webhook',
887
874
  }
888
875
  );
889
876
 
890
- // API key with user validation
891
- app.get('/api/user/profile', authapi(), async (req, res) => {
892
- const { username } = req.user;
893
-
894
- // Fetch user profile
895
- const profile = await getUserProfile(username);
896
-
897
- res.json({ success: true, profile });
898
- });
899
-
900
- // API key with role requirement
901
- app.delete('/api/admin/users/:id',
902
- authapi(['SuperAdmin']),
877
+ // Admin API with token authentication
878
+ app.post('/api/admin/terminate-sessions',
879
+ authenticate(process.env.MAIN_SECRET_TOKEN),
903
880
  async (req, res) => {
904
- await deleteUser(req.params.id);
881
+ // Terminate all sessions
905
882
  res.json({ success: true });
906
883
  }
907
884
  );
885
+
886
+ // Protected API endpoint (requires session)
887
+ app.get('/api/user/profile',
888
+ validateSession,
889
+ async (req, res) => {
890
+ const { username } = req.session.user;
891
+
892
+ // Fetch user profile
893
+ const profile = await getUserProfile(username);
894
+
895
+ res.json({ success: true, profile });
896
+ }
897
+ );
908
898
  ```
909
899
 
910
900
  ---
package/index.d.ts CHANGED
@@ -5,6 +5,43 @@
5
5
  import { Request, Response, NextFunction, Router } from 'express';
6
6
  import { Pool } from 'pg';
7
7
 
8
+ // Global augmentations must be at the top level, outside any declare module blocks
9
+ declare global {
10
+ namespace Express {
11
+ interface Request {
12
+ user?: {
13
+ username: string;
14
+ UserName: string;
15
+ role: 'SuperAdmin' | 'NormalUser' | 'Guest';
16
+ Role: 'SuperAdmin' | 'NormalUser' | 'Guest';
17
+ };
18
+ userRole?: 'SuperAdmin' | 'NormalUser' | 'Guest';
19
+ }
20
+
21
+ interface Session {
22
+ user?: {
23
+ id: number;
24
+ username: string;
25
+ UserName: string;
26
+ role: 'SuperAdmin' | 'NormalUser' | 'Guest';
27
+ Role: 'SuperAdmin' | 'NormalUser' | 'Guest';
28
+ sessionId: string;
29
+ allowedApps?: string[];
30
+ };
31
+ preAuthUser?: {
32
+ id: number;
33
+ username: string;
34
+ UserName?: string;
35
+ role: 'SuperAdmin' | 'NormalUser' | 'Guest';
36
+ Role?: 'SuperAdmin' | 'NormalUser' | 'Guest';
37
+ loginMethod?: 'password' | 'github';
38
+ redirectUrl?: string | null;
39
+ };
40
+ oauthRedirect?: string;
41
+ }
42
+ }
43
+ }
44
+
8
45
  declare module 'mbkauthe' {
9
46
  // Configuration Types
10
47
  export interface MBKAuthConfig {
@@ -90,26 +127,6 @@ declare module 'mbkauthe' {
90
127
  updated_at: Date;
91
128
  }
92
129
 
93
- // Request Extensions
94
- declare global {
95
- namespace Express {
96
- interface Request {
97
- user?: {
98
- username: string;
99
- UserName: string;
100
- role: UserRole;
101
- Role: UserRole;
102
- };
103
- }
104
-
105
- interface Session {
106
- user?: SessionUser;
107
- preAuthUser?: PreAuthUser;
108
- oauthRedirect?: string;
109
- }
110
- }
111
- }
112
-
113
130
  // API Response Types
114
131
  export interface LoginResponse {
115
132
  success: boolean;
@@ -177,8 +194,6 @@ declare module 'mbkauthe' {
177
194
 
178
195
  export function authenticate(token: string): AuthMiddleware;
179
196
 
180
- export function authapi(requiredRole?: UserRole[]): AuthMiddleware;
181
-
182
197
  // Utility Functions
183
198
  export function renderError(
184
199
  res: Response,
package/lib/main.js CHANGED
@@ -2,7 +2,6 @@ import express from "express";
2
2
  import session from "express-session";
3
3
  import cookieParser from "cookie-parser";
4
4
  import passport from 'passport';
5
- import { packageJson } from "./config/index.js";
6
5
  import {
7
6
  sessionConfig,
8
7
  corsMiddleware,
@@ -12,6 +11,11 @@ import {
12
11
  import authRoutes from "./routes/auth.js";
13
12
  import oauthRoutes from "./routes/oauth.js";
14
13
  import miscRoutes from "./routes/misc.js";
14
+ import { fileURLToPath } from "url";
15
+ import path from "path";
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+
15
19
 
16
20
  const router = express.Router();
17
21
 
@@ -48,5 +52,15 @@ router.get(["/login", "/signin"], async (req, res) => {
48
52
  return res.redirect(redirectUrl);
49
53
  });
50
54
 
55
+ router.get('/icon.svg', (req, res) => {
56
+ res.setHeader('Cache-Control', 'public, max-age=31536000');
57
+ res.sendFile(path.join(__dirname, '..', 'public', 'icon.svg'));
58
+ });
59
+
60
+ router.get(['/favicon.ico', '/icon.ico'], (req, res) => {
61
+ res.setHeader('Cache-Control', 'public, max-age=31536000');
62
+ res.sendFile(path.join(__dirname, '..', 'public', 'icon.ico'));
63
+ });
64
+
51
65
  export { getLatestVersion } from "./routes/misc.js";
52
66
  export default router;
@@ -1,19 +1,20 @@
1
1
  import express from "express";
2
2
  import fetch from 'node-fetch';
3
3
  import rateLimit from 'express-rate-limit';
4
- import { fileURLToPath } from "url";
5
- import fs from "fs";
6
- import path from "path";
7
4
  import { mbkautheVar, packageJson, appVersion } from "../config/index.js";
8
5
  import { renderError } from "../utils/response.js";
9
6
  import { authenticate, validateSession } from "../middleware/auth.js";
10
7
  import { ErrorCodes, ErrorMessages } from "../utils/errors.js";
11
8
  import { dblogin } from "../database/pool.js";
12
9
  import { clearSessionCookies } from "../config/cookies.js";
10
+ import { fileURLToPath } from "url";
11
+ import path from "path";
12
+ import fs from "fs";
13
13
 
14
- const router = express.Router();
15
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
15
 
16
+
17
+ const router = express.Router();
17
18
  // Rate limiter for info/test routes
18
19
  const LoginLimit = rateLimit({
19
20
  windowMs: 1 * 60 * 1000,
@@ -30,16 +31,6 @@ router.get('/main.js', (req, res) => {
30
31
  res.sendFile(path.join(__dirname, '..', '..', 'public', 'main.js'));
31
32
  });
32
33
 
33
- router.get('/icon.svg', (req, res) => {
34
- res.setHeader('Cache-Control', 'public, max-age=31536000');
35
- res.sendFile(path.join(__dirname, '..', '..', 'public', 'icon.svg'));
36
- });
37
-
38
- router.get(['/favicon.ico', '/icon.ico'], (req, res) => {
39
- res.setHeader('Cache-Control', 'public, max-age=31536000');
40
- res.sendFile(path.join(__dirname, '..', '..', 'public', 'icon.ico'));
41
- });
42
-
43
34
  router.get("/bg.webp", (req, res) => {
44
35
  const imgPath = path.join(__dirname, "..", "..", "public", "bg.webp");
45
36
  res.setHeader('Content-Type', 'image/webp');
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "mbkauthe",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "MBKTech's reusable authentication system for Node.js applications.",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "types": "index.d.ts",
8
8
  "scripts": {
9
- "dev": "cross-env test=dev nodemon index.js"
9
+ "dev": "cross-env test=dev nodemon index.js",
10
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
11
+ "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch"
10
12
  },
11
13
  "repository": {
12
14
  "type": "git",
@@ -30,8 +32,6 @@
30
32
  "registry": "https://registry.npmjs.org/"
31
33
  },
32
34
  "dependencies": {
33
- "bcrypt": "^6.0.0",
34
- "cheerio": "^1.0.0",
35
35
  "connect-pg-simple": "^10.0.0",
36
36
  "cookie-parser": "^1.4.7",
37
37
  "csurf": "^1.2.2",
@@ -40,19 +40,26 @@
40
40
  "express-handlebars": "^8.0.1",
41
41
  "express-rate-limit": "^7.5.0",
42
42
  "express-session": "^1.18.1",
43
- "marked": "^15.0.11",
44
43
  "node-fetch": "^3.3.2",
45
44
  "passport": "^0.7.0",
46
45
  "passport-github2": "^0.1.12",
47
- "path": "^0.12.7",
48
46
  "pg": "^8.14.1",
49
- "speakeasy": "^2.0.0",
50
- "url": "^0.11.4"
47
+ "speakeasy": "^2.0.0"
51
48
  },
52
49
  "devDependencies": {
53
50
  "@types/express": "^5.0.6",
54
- "@types/node": "^22.19.1",
55
51
  "cross-env": "^7.0.3",
56
- "nodemon": "^3.1.11"
52
+ "jest": "^30.2.0",
53
+ "nodemon": "^3.1.11",
54
+ "supertest": "^7.1.4"
55
+ },
56
+ "jest": {
57
+ "testEnvironment": "node",
58
+ "testMatch": [
59
+ "**/*.spec.js",
60
+ "**/*.test.js"
61
+ ],
62
+ "testTimeout": 10000,
63
+ "transform": {}
57
64
  }
58
- }
65
+ }
package/test.spec.js ADDED
@@ -0,0 +1,178 @@
1
+ import request from 'supertest';
2
+
3
+ const BASE_URL = "http://localhost:5555";
4
+
5
+ // Helper to get CSRF token and cookies
6
+ const getCSRFTokenAndCookies = async () => {
7
+ const response = await request(BASE_URL).get('/mbkauthe/login');
8
+ const html = response.text;
9
+ const csrfMatch = html.match(/name="_csrf".*?value="([^"]+)"/i) ||
10
+ html.match(/content="([^"]+)".*?name="_csrf"/i);
11
+
12
+ return {
13
+ csrfToken: csrfMatch?.[1] || '',
14
+ cookies: response.headers['set-cookie'] || []
15
+ };
16
+ };
17
+
18
+ describe('mbkauthe Routes', () => {
19
+
20
+ describe('Redirect Routes', () => {
21
+ test('GET /login redirects to /mbkauthe/login', async () => {
22
+ const response = await request(BASE_URL)
23
+ .get('/login')
24
+ .redirects(0);
25
+
26
+ expect(response.status).toBe(302);
27
+ expect(response.headers.location).toContain('/mbkauthe/login');
28
+ });
29
+
30
+ test('GET /signin redirects to /mbkauthe/login', async () => {
31
+ const response = await request(BASE_URL)
32
+ .get('/signin')
33
+ .redirects(0);
34
+
35
+ expect(response.status).toBe(302);
36
+ expect(response.headers.location).toContain('/mbkauthe/login');
37
+ });
38
+ });
39
+
40
+ describe('Authentication Pages', () => {
41
+ test('GET /mbkauthe/login contains loginUsername input', async () => {
42
+ const response = await request(BASE_URL).get('/mbkauthe/login');
43
+
44
+ expect(response.status).toBe(200);
45
+ expect(response.text).toMatch(/id\s*=\s*["']loginUsername["']/i);
46
+ });
47
+
48
+ test('GET /mbkauthe/2fa contains token input or redirects', async () => {
49
+ const response = await request(BASE_URL)
50
+ .get('/mbkauthe/2fa')
51
+ .redirects(0);
52
+
53
+ if (response.status === 302) {
54
+ expect(response.headers.location).toContain('/mbkauthe/login');
55
+ } else {
56
+ expect(response.status).toBe(200);
57
+ expect(response.text).toMatch(/id\s*=\s*["']token["']/i);
58
+ }
59
+ });
60
+ });
61
+
62
+ describe('Info Pages', () => {
63
+ test.each([
64
+ ['/mbkauthe/info', 'info page'],
65
+ ['/mbkauthe/i', 'info short page'],
66
+ ])('GET %s contains CurrentVersion div', async (path, desc) => {
67
+ const response = await request(BASE_URL).get(path);
68
+
69
+ expect(response.status).toBe(200);
70
+ expect(response.text).toMatch(/id\s*=\s*["']CurrentVersion["']/i);
71
+ });
72
+
73
+ test('GET /mbkauthe/ErrorCode contains error-603 div', async () => {
74
+ const response = await request(BASE_URL).get('/mbkauthe/ErrorCode');
75
+
76
+ expect(response.status).toBe(200);
77
+ expect(response.text).toMatch(/id\s*=\s*["']error-603["']/i);
78
+ });
79
+ });
80
+
81
+ describe('Static Assets', () => {
82
+ test('GET /mbkauthe/main.js returns JavaScript', async () => {
83
+ const response = await request(BASE_URL).get('/mbkauthe/main.js');
84
+
85
+ expect(response.status).toBe(200);
86
+ expect(response.headers['content-type']).toContain('javascript');
87
+ });
88
+
89
+ test('GET /icon.svg returns SVG content', async () => {
90
+ const response = await request(BASE_URL).get('/icon.svg');
91
+
92
+ expect(response.status).toBe(200);
93
+ expect(response.text || response.body.toString()).toMatch(/<svg|SVG/);
94
+ });
95
+
96
+ test.each([
97
+ ['/favicon.ico', 'favicon'],
98
+ ['/icon.ico', 'icon'],
99
+ ])('GET %s returns ICO content', async (path, desc) => {
100
+ const response = await request(BASE_URL).get(path);
101
+ expect(response.status).toBe(200);
102
+ });
103
+
104
+ test('GET /mbkauthe/bg.webp returns WEBP content', async () => {
105
+ const response = await request(BASE_URL).get('/mbkauthe/bg.webp');
106
+
107
+ expect(response.status).toBe(200);
108
+ expect(response.headers['content-type']).toContain('image/webp');
109
+ });
110
+ });
111
+
112
+ describe('Protected Routes', () => {
113
+ test('GET /mbkauthe/test responds appropriately', async () => {
114
+ const response = await request(BASE_URL).get('/mbkauthe/test');
115
+ expect([200, 302, 401, 403, 429]).toContain(response.status);
116
+ });
117
+ });
118
+
119
+ describe('OAuth Routes', () => {
120
+ test('GET /mbkauthe/api/github/login handles GitHub OAuth', async () => {
121
+ const response = await request(BASE_URL)
122
+ .get('/mbkauthe/api/github/login')
123
+ .redirects(0);
124
+
125
+ expect([200, 302, 500, 429]).toContain(response.status);
126
+
127
+ if (response.status === 302) {
128
+ const location = response.headers.location;
129
+ expect(location).toMatch(/github\.com|login/);
130
+ }
131
+ });
132
+
133
+ test('GET /mbkauthe/api/github/login/callback handles callback', async () => {
134
+ const response = await request(BASE_URL).get('/mbkauthe/api/github/login/callback');
135
+ expect([200, 302, 400, 401, 429]).toContain(response.status);
136
+ });
137
+ });
138
+
139
+ describe('API Endpoints', () => {
140
+ test('POST /mbkauthe/api/login handles login API', async () => {
141
+ const response = await request(BASE_URL)
142
+ .post('/mbkauthe/api/login')
143
+ .send({ username: 'test', password: 'test' });
144
+
145
+ expect([200, 400, 401, 403, 429]).toContain(response.status);
146
+ expect(response.headers['content-type']).toContain('application/json');
147
+ });
148
+
149
+ test('POST /mbkauthe/api/verify-2fa handles 2FA API', async () => {
150
+ const { csrfToken, cookies } = await getCSRFTokenAndCookies();
151
+
152
+ const response = await request(BASE_URL)
153
+ .post('/mbkauthe/api/verify-2fa')
154
+ .set('Cookie', cookies)
155
+ .send({ token: '123456', _csrf: csrfToken });
156
+
157
+ expect([200, 400, 401, 403, 429]).toContain(response.status);
158
+ expect(response.headers['content-type']).toContain('application/json');
159
+ });
160
+
161
+ test('POST /mbkauthe/api/terminateAllSessions handles session termination', async () => {
162
+ const { csrfToken, cookies } = await getCSRFTokenAndCookies();
163
+
164
+ const response = await request(BASE_URL)
165
+ .post('/mbkauthe/api/terminateAllSessions')
166
+ .set('Cookie', cookies)
167
+ .send({ _csrf: csrfToken });
168
+
169
+ expect([200, 400, 401, 403, 429]).toContain(response.status);
170
+
171
+ if (response.status === 401) {
172
+ expect(response.headers['content-type']).not.toContain('application/json');
173
+ } else {
174
+ expect(response.headers['content-type']).toContain('application/json');
175
+ }
176
+ });
177
+ });
178
+ });