auth-verify 1.4.0 → 1.5.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/package.json CHANGED
@@ -8,12 +8,11 @@
8
8
  "nodemailer": "^7.0.6",
9
9
  "qrcode": "^1.5.4",
10
10
  "redis": "^5.8.3",
11
- "twilio": "^5.10.3",
12
11
  "uuid": "^9.0.1"
13
12
  },
14
13
  "name": "auth-verify",
15
- "version": "1.4.0",
16
- "description": "A simple Node.js library for sending and verifying OTP via email, SMS and Telegram bot. And handling JWT with Cookies",
14
+ "version": "1.5.0",
15
+ "description": "A simple Node.js library for sending and verifying OTP via email, SMS and Telegram bot. And generating TOTP codes and QR codes. And handling JWT with Cookies",
17
16
  "main": "index.js",
18
17
  "scripts": {
19
18
  "test": "jest --runInBand"
@@ -42,7 +41,10 @@
42
41
  "redis",
43
42
  "cookie",
44
43
  "jwa",
45
- "jsonwebtoken"
44
+ "jsonwebtoken",
45
+ "totp",
46
+ "google-authenticator",
47
+ "signin"
46
48
  ],
47
49
  "author": "Jahongir Sobirov",
48
50
  "license": "MIT",
package/readme.md CHANGED
@@ -1,13 +1,16 @@
1
1
  # auth-verify
2
2
 
3
3
  **auth-verify** is a Node.js authentication utility that provides:
4
- - ✅ Secure OTP (one-time password) generation and verification
5
- - ✅ Sending OTPs via Email, SMS (pluggable helpers), and Telegram bot
6
- - ✅ TOTP (Time-based One Time Passwords) generation code and QR code and verification (Google Authenticator support)
7
- - ✅ JWT creation, verification and optional token revocation with memory/Redis storage
8
- - ✅ Session management (in-memory or Redis)
9
- - ✅ New: OAuth 2.0 integration for Google, Facebook, GitHub, X (Twitter) and Linkedin
10
- - ⚙️ Developer extensibility: custom senders and `auth.register.sender()` / `auth.use(name).send(...)`
4
+ - ✅ Secure OTP (one-time password) generation and verification.
5
+ - ✅ Sending OTPs via Email, SMS (pluggable helpers), and Telegram bot.
6
+ - ✅ TOTP (Time-based One Time Passwords) generation, QR code generation, and verification (Google Authenticator support).
7
+ - ✅ JWT creation, verification, optional token revocation with memory/Redis storage, and advanced middleware for protecting routes, custom cookie/header handling, role-based guards, and token extraction from custom sources.
8
+ - ✅ Session management (in-memory or Redis).
9
+ - ✅ OAuth 2.0 integration for Google, Facebook, GitHub, X (Twitter), Linkedin, and additional providers like Apple, Discord, Slack, Microsoft, Telegram,and WhatsApp.
10
+ - ⚙️ Developer extensibility: custom senders via auth.register.sender() and chainable sending via auth.use(name).send(...).
11
+ - ✅ Automatic JWT cookie handling for Express apps, supporting secure, HTTP-only cookies and optional auto-verification.
12
+ - ✅ Fully asynchronous/Promise-based API, with callback support where applicable.
13
+ - ✅ Chainable OTP workflow with cooldowns, max attempts, and resend functionality.
11
14
  ---
12
15
 
13
16
  ## 🧩 Installation
@@ -24,11 +27,12 @@ npm install auth-verify
24
27
 
25
28
  ## ⚙️ Quick overview
26
29
 
27
- - `AuthVerify` (entry): constructs and exposes `.jwt`, `.otp`, (optionally) `.session` and `.oauth` managers.
28
- - `JWTManager`: sign, verify, decode, revoke tokens. Supports `storeTokens: "memory" | "redis" | "none"`.
30
+ - `AuthVerify` (entry): constructs and exposes `.jwt`, `.otp`, (optionally) `.session`, `.totp` and `.oauth` managers.
31
+ - `JWTManager`: sign, verify, decode, revoke tokens. Supports `storeTokens: "memory" | "redis" | "none"` and middleware with custom cookie, header, and token extraction.
29
32
  - `OTPManager`: generate, store, send, verify, resend OTPs. Supports `storeTokens: "memory" | "redis" | "none"`. Supports email, SMS helper, Telegram bot, and custom dev senders.
33
+ - `TOTPManager`: generate, verify uri, codes and QR codes
30
34
  - `SessionManager`: simple session creation/verification/destroy with memory or Redis backend.
31
- - `OAuthManager`: Handle OAuth 2.0 logins for Google, Facebook, GitHub, X and Linkedin
35
+ - `OAuthManager`: Handle OAuth 2.0 logins for Google, Facebook, GitHub, X, Linkedin, Apple, Discord, Slack, Microsoft, Telegram and WhatsApp
32
36
  ---
33
37
 
34
38
  ## 🚀 Example: Initialize library (CommonJS)
@@ -49,6 +53,108 @@ const auth = new AuthVerify({
49
53
 
50
54
  ## 🔐 JWT Usage
51
55
 
56
+ ### JWT Middleware (`protect`) (New in v1.5.0)
57
+
58
+ auth-verify comes with a fully customizable JWT middleware, making it easy to **protect routes**, **attach decoded data to the request**, and **check user roles**.
59
+
60
+ #### ⚙️ `protect` Method Overview
61
+
62
+ Function signature:
63
+ ```js
64
+ protect(options = {})
65
+ ```
66
+ **Description**:
67
+ - Returns an Express-style middleware.
68
+ - Automatically reads JWT from cookie, header, or custom extractor.
69
+ - Verifies the token and optionally checks for roles.
70
+ - Attaches the decoded payload to req (default property: req.user).
71
+
72
+ | Option | Type | Default | Description |
73
+ | ---------------- | -------- | ----------------- | -------------------------------------------------------------------------------------- |
74
+ | `onError` | Function | `null` | Custom error handler. `(err, req, res)` |
75
+ | `attachProperty` | String | `"user"` | Where to attach decoded token payload on `req` |
76
+ | `requiredRole` | String | `null` | Optional role check. Throws error if decoded role does not match |
77
+ | `cookieName` | String | `this.cookieName` | Name of the cookie to read JWT from |
78
+ | `headerName` | String | `"authorization"` | Header name to read JWT from. `"authorization"` splits `Bearer TOKEN` automatically |
79
+ | `extractor` | Function | `null` | Custom function to extract token. Receives `req` as argument and must return the token |
80
+
81
+ #### Middleware Behavior
82
+
83
+ 1. **Token extraction order:**
84
+ - First: `extractor(req)` if provided
85
+ - Second: `req.headers[headerName]` (for `authorization`, splits `Bearer TOKEN`)
86
+ - Third: `cookieName` from request cookies
87
+ 2. **Verification**:
88
+ - Calls `this.verify(token)`
89
+ - Throws `NO_TOKEN` if no token is found
90
+ - Throws `ROLE_NOT_ALLOWED` if `requiredRole`is provided but decoded role does not match
91
+ 3. **Attachment**:
92
+ - Decoded token is attached to `req[attachProperty]`
93
+ 4. **Error handling**:
94
+ - Default: responds with `401` and JSON `{ success: false, error: err.message }`
95
+ - Custom: if `onError` is provided, it is called instead of default behavior
96
+
97
+ #### Example Usage
98
+ ##### Basic Usage
99
+ ```js
100
+ const express = require("express");
101
+ const AuthVerify = require("auth-verify");
102
+ const app = express();
103
+
104
+ const auth = new AuthVerify({ jwtSecret: "supersecret" });
105
+
106
+ // Protect route
107
+ app.get("/dashboard", auth.jwt.protect(), (req, res) => {
108
+ // req.user contains decoded JWT payload
109
+ res.json({ message: `Welcome, ${req.user.userId}` });
110
+ });
111
+
112
+ app.listen(3000, () => console.log("Server running on port 3000"));
113
+ ```
114
+
115
+ ##### Custom Cookie & Header
116
+ ```js
117
+ app.get("/profile", auth.jwt.protect({
118
+ cookieName: "myToken",
119
+ headerName: "x-access-token"
120
+ }), (req, res) => {
121
+ res.json({ user: req.user });
122
+ });
123
+ ```
124
+ - JWT will be read from the cookie named `"myToken"` or from the header `"x-access-token"`.
125
+
126
+ #### Role-based Guard
127
+ ```js
128
+ app.get("/admin", auth.jwt.protect({
129
+ requiredRole: "admin"
130
+ }), (req, res) => {
131
+ res.json({ message: "Welcome Admin" });
132
+ });
133
+ ```
134
+ - Throws error if decoded token does not have role: `"admin"`.
135
+
136
+ #### Custom Token Extractor
137
+ ```js
138
+ app.get("/custom", auth.jwt.protect({
139
+ extractor: (req) => req.query.token
140
+ }), (req, res) => {
141
+ res.json({ user: req.user });
142
+ });
143
+ ```
144
+ - Allows you to read token from any custom location (e.g., query params).
145
+
146
+ #### Custom Error Handler
147
+ ```js
148
+ app.get("/custom-error", auth.jwt.protect({
149
+ onError: (err, req, res) => {
150
+ res.status(403).json({ error: "Access denied", details: err.message });
151
+ }
152
+ }), (req, res) => {
153
+ res.json({ user: req.user });
154
+ });
155
+ ```
156
+ - Overrides default `401` response with custom logic.
157
+
52
158
  ### JWA Handling (v1.3.0+)
53
159
 
54
160
  You can choose json web algorithm for signing jwt
@@ -144,8 +250,26 @@ auth.otp.setSender({
144
250
  auth.otp.setSender({
145
251
  via: 'sms',
146
252
  provider: 'infobip',
147
- apiKey: 'xxx',
148
- apiSecret: 'yyy',
253
+ apiKey: 'API_KEY',
254
+ apiSecret: 'API_SECRET',
255
+ sender: 'SENDER_NAME',
256
+ mock: true // in dev prints message instead of sending
257
+ });
258
+
259
+ auth.otp.setSender({
260
+ via: 'sms',
261
+ provider: 'twilio',
262
+ apiKey: 'ACCOUNT_SID',
263
+ apiSecret: 'AUTH_TOKEN',
264
+ sender: 'SENDER_NAME',
265
+ mock: true // in dev prints message instead of sending
266
+ });
267
+
268
+ auth.otp.setSender({
269
+ via: 'sms',
270
+ provider: 'vonage',
271
+ apiKey: 'API_KEY',
272
+ apiSecret: 'API_SECRET',
149
273
  sender: 'SENDER_NAME',
150
274
  mock: true // in dev prints message instead of sending
151
275
  });
@@ -175,7 +299,20 @@ auth.otp.generate(6).set('user@example.com', (err) => {
175
299
  else console.log('sent', info && info.messageId);
176
300
  });
177
301
  });
302
+
303
+ // Sending OTP with SMS
304
+ auth.otp.generate(6).set('+1234567890', (err) => {
305
+ if (err) throw err;
306
+ auth.otp.message({
307
+ to: '+1234567890',
308
+ text: `Your code: <b>${auth.otp.code}</b>`
309
+ }, (err, info) => {
310
+ if (err) console.error('send error', err);
311
+ else console.log('sent', info && info.messageId);
312
+ });
313
+ });
178
314
  ```
315
+ `+1234567890` is reciever number
179
316
 
180
317
  Async/await style:
181
318
 
@@ -218,7 +355,7 @@ auth.otp.verify({ check: 'user@example.com', code: '123456' }, (err, isValid)=>{
218
355
 
219
356
  ---
220
357
 
221
- ## ✅ TOTP (Time-based One Time Passwords) — Google Authenticator support (New in v1.4.0)
358
+ ## ✅ TOTP (Time-based One Time Passwords) — Google Authenticator support (v1.4.0+)
222
359
  ```js
223
360
  const AuthVerify = require("auth-verify");
224
361
  const auth = new AuthVerify();
@@ -521,11 +658,12 @@ auth-verify/
521
658
  │ ├─ /session/index.js
522
659
  | ├─ /oauth/index.js
523
660
  │ └─ helpers/helper.js
524
- ├─ test/
661
+ ├─ tests/
525
662
  │ ├─ jwa.test.js
526
663
  │ ├─ jwtmanager.multitab.test.js
527
664
  │ ├─ jwtmanager.test.js
528
665
  │ ├─ otpmanager.test.js
666
+ │ ├─ oauth.test.js
529
667
  │ ├─ totpmanager.test.js
530
668
  ├─ babel.config.js
531
669
  ```
package/src/jwt/index.js CHANGED
@@ -391,6 +391,76 @@ class JWTManager {
391
391
  await this.redis.set(token, JSON.stringify(data));
392
392
  }
393
393
  }
394
+
395
+ // JWT middleware part
396
+ readCookie(req, name) {
397
+ const cookieHeader = req.headers.cookie;
398
+ if (!cookieHeader) return null;
399
+
400
+ const cookies = cookieHeader.split(';').map(v => v.trim());
401
+ for (const c of cookies) {
402
+ const [key, val] = c.split('=');
403
+ if (key === name) return val;
404
+ }
405
+ return null;
406
+ }
407
+
408
+ protect(options = {}) {
409
+ // customizable messages / behavior
410
+ const {
411
+ onError,
412
+ attachProperty = "user", // where to put decoded data on req
413
+ requiredRole = null, // optional: role check
414
+ cookieName = this.cookieName,
415
+ headerName = "authorization",
416
+ extractor = null
417
+ } = options;
418
+
419
+ return async (req, res, next) => {
420
+ try {
421
+ // read token (cookie → header)
422
+ let token = null;
423
+
424
+ if (extractor && typeof extractor === "function") {
425
+ token = extractor(req);
426
+ }
427
+
428
+ if(!token && req.headers[headerName]){
429
+ if(headerName == "authorization"){
430
+ token = req.headers.authorization.split(" ")[1];
431
+ }else{
432
+ token = req.headers[headerName]; // custom header — return raw
433
+ }
434
+ }
435
+
436
+ if (!token) {
437
+ token = this.readCookie(req, cookieName);
438
+ }
439
+
440
+ if (!token) throw new Error("NO_TOKEN");
441
+
442
+ const decoded = await this.verify(token);
443
+
444
+ // role guard?
445
+ if (requiredRole && decoded.role !== requiredRole) {
446
+ throw new Error("ROLE_NOT_ALLOWED");
447
+ }
448
+
449
+ // attach user
450
+ req[attachProperty] = decoded;
451
+
452
+ next();
453
+ } catch (err) {
454
+ if (onError) return onError(err, req, res);
455
+
456
+ return res.status(401).json({
457
+ success: false,
458
+ error: err.message
459
+ });
460
+ }
461
+ };
462
+ }
463
+
394
464
  }
395
465
 
396
466
  module.exports = JWTManager;
@@ -235,6 +235,184 @@ class OAuthManager {
235
235
  };
236
236
  }
237
237
 
238
+ // --- APPLE LOGIN ---
239
+ apple({ clientId, clientSecret, redirectUri }) {
240
+ return {
241
+ redirect: (res) => {
242
+ const url =
243
+ "https://appleid.apple.com/auth/authorize?" +
244
+ new URLSearchParams({
245
+ response_type: "code",
246
+ client_id: clientId,
247
+ redirect_uri: redirectUri,
248
+ scope: "name email",
249
+ response_mode: "form_post",
250
+ });
251
+ res.redirect(url);
252
+ },
253
+ callback: async (code) => {
254
+ const tokenRes = await fetch("https://appleid.apple.com/auth/token", {
255
+ method: "POST",
256
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
257
+ body: new URLSearchParams({
258
+ grant_type: "authorization_code",
259
+ code,
260
+ client_id: clientId,
261
+ client_secret: clientSecret,
262
+ redirect_uri: redirectUri,
263
+ }),
264
+ });
265
+ const tokenData = await tokenRes.json();
266
+ if (tokenData.error) throw new Error("OAuth Error: " + tokenData.error);
267
+ return tokenData;
268
+ },
269
+ };
270
+ }
271
+
272
+ // --- DISCORD LOGIN ---
273
+ discord({ clientId, clientSecret, redirectUri }) {
274
+ return {
275
+ redirect: (res) => {
276
+ const url =
277
+ "https://discord.com/api/oauth2/authorize?" +
278
+ new URLSearchParams({
279
+ client_id: clientId,
280
+ redirect_uri: redirectUri,
281
+ response_type: "code",
282
+ scope: "identify email",
283
+ });
284
+ res.redirect(url);
285
+ },
286
+ callback: async (code) => {
287
+ const tokenRes = await fetch("https://discord.com/api/oauth2/token", {
288
+ method: "POST",
289
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
290
+ body: new URLSearchParams({
291
+ client_id: clientId,
292
+ client_secret: clientSecret,
293
+ code,
294
+ grant_type: "authorization_code",
295
+ redirect_uri: redirectUri,
296
+ }),
297
+ });
298
+ const tokenData = await tokenRes.json();
299
+ if (tokenData.error) throw new Error("OAuth Error: " + tokenData.error);
300
+ const userRes = await fetch("https://discord.com/api/users/@me", {
301
+ headers: { Authorization: `Bearer ${tokenData.access_token}` },
302
+ });
303
+ const user = await userRes.json();
304
+ return { ...user, access_token: tokenData.access_token };
305
+ },
306
+ };
307
+ }
308
+
309
+ // --- SLACK LOGIN ---
310
+ slack({ clientId, clientSecret, redirectUri }) {
311
+ return {
312
+ redirect: (res) => {
313
+ const url =
314
+ "https://slack.com/oauth/v2/authorize?" +
315
+ new URLSearchParams({
316
+ client_id: clientId,
317
+ redirect_uri: redirectUri,
318
+ scope: "identity.basic identity.email",
319
+ });
320
+ res.redirect(url);
321
+ },
322
+ callback: async (code) => {
323
+ const tokenRes = await fetch("https://slack.com/api/oauth.v2.access?" +
324
+ new URLSearchParams({
325
+ client_id: clientId,
326
+ client_secret: clientSecret,
327
+ code,
328
+ redirect_uri: redirectUri,
329
+ }));
330
+ const tokenData = await tokenRes.json();
331
+ if (!tokenData.ok) throw new Error("OAuth Error: " + tokenData.error);
332
+ return { ...tokenData, access_token: tokenData.access_token };
333
+ },
334
+ };
335
+ }
336
+
337
+ // --- MICROSOFT LOGIN ---
338
+ microsoft({ clientId, clientSecret, redirectUri }) {
339
+ return {
340
+ redirect: (res) => {
341
+ const url =
342
+ "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?" +
343
+ new URLSearchParams({
344
+ client_id: clientId,
345
+ redirect_uri: redirectUri,
346
+ response_type: "code",
347
+ scope: "User.Read",
348
+ });
349
+ res.redirect(url);
350
+ },
351
+ callback: async (code) => {
352
+ const tokenRes = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
353
+ method: "POST",
354
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
355
+ body: new URLSearchParams({
356
+ client_id: clientId,
357
+ client_secret: clientSecret,
358
+ code,
359
+ redirect_uri: redirectUri,
360
+ grant_type: "authorization_code",
361
+ }),
362
+ });
363
+ const tokenData = await tokenRes.json();
364
+ if (tokenData.error) throw new Error("OAuth Error: " + tokenData.error);
365
+ return tokenData;
366
+ },
367
+ };
368
+ }
369
+
370
+ // --- TELEGRAM LOGIN ---
371
+ telegram({ botId, redirectUri }) {
372
+ return {
373
+ redirect: (res) => {
374
+ const url =
375
+ "https://t.me/" + botId + "?start=auth&redirect_uri=" + encodeURIComponent(redirectUri);
376
+ res.redirect(url);
377
+ },
378
+ callback: async (code) => {
379
+ // Telegram uses bot login; typically handled via deep links
380
+ return { code, message: "Telegram login uses deep link auth" };
381
+ },
382
+ };
383
+ }
384
+
385
+ // --- WHATSAPP LOGIN ---
386
+ whatsapp({ phoneNumberId, redirectUri }) {
387
+ return {
388
+ redirect: (res) => {
389
+ const url =
390
+ "https://api.whatsapp.com/send?" +
391
+ new URLSearchParams({
392
+ phone: phoneNumberId,
393
+ text: "Please authorize: " + redirectUri,
394
+ });
395
+ res.redirect(url);
396
+ },
397
+ callback: async (code) => {
398
+ // WhatsApp login usually handled via QR / deep link
399
+ return { code, message: "WhatsApp login uses QR/deep link auth" };
400
+ },
401
+ };
402
+ }
403
+
404
+ // --- CUSTOM PROVIDER ---
405
+ register(name, fn) {
406
+ if (!name || typeof fn !== "function") throw new Error("Provider registration requires a name and function");
407
+ this.providers[name] = fn;
408
+ }
409
+
410
+ use(name, options) {
411
+ const provider = this.providers[name];
412
+ if (!provider) throw new Error(`Provider "${name}" not found`);
413
+ return provider(options);
414
+ }
415
+
238
416
  }
239
417
 
240
418
  module.exports = OAuthManager;
@@ -0,0 +1,89 @@
1
+ const request = require("supertest");
2
+ const express = require("express");
3
+ const AuthVerify = require("../index"); // path to your AuthVerify file
4
+
5
+ describe("AuthVerify protect() with custom options", () => {
6
+ let app;
7
+ let auth;
8
+
9
+ beforeEach(() => {
10
+ auth = new AuthVerify({ jwtSecret: "test_secret" });
11
+
12
+ app = express();
13
+
14
+ // route that signs a token + sets cookie
15
+ app.get("/login", async (req, res) => {
16
+ const token = await auth.jwt.sign({ id: 123 }, "30m", { res });
17
+ res.json({ token });
18
+ });
19
+
20
+ // standard protected route
21
+ app.get("/profile", auth.jwt.protect(), (req, res) => {
22
+ res.json({ ok: true, user: req.user });
23
+ });
24
+
25
+ // route with custom cookie
26
+ app.get(
27
+ "/cookie",
28
+ auth.jwt.protect({ cookieName: "my_cookie" }),
29
+ (req, res) => {
30
+ res.json({ ok: true, user: req.user });
31
+ }
32
+ );
33
+
34
+ // route with custom header
35
+ app.get(
36
+ "/header",
37
+ auth.jwt.protect({ headerName: "x-access-token" }),
38
+ (req, res) => {
39
+ res.json({ ok: true, user: req.user });
40
+ }
41
+ );
42
+
43
+ // route with custom extractor
44
+ app.get(
45
+ "/query",
46
+ auth.jwt.protect({
47
+ extractor: (req) => req.query.token
48
+ }),
49
+ (req, res) => {
50
+ res.json({ ok: true, user: req.user });
51
+ }
52
+ );
53
+ });
54
+
55
+ test("custom cookie name works", async () => {
56
+ const token = await auth.jwt.sign({ id: 555 });
57
+ const res = await request(app)
58
+ .get("/cookie")
59
+ .set("Cookie", `my_cookie=${token}`);
60
+ expect(res.status).toBe(200);
61
+ expect(res.body.user.id).toBe(555);
62
+ });
63
+
64
+ test("custom header name works", async () => {
65
+ const token = await auth.jwt.sign({ id: 777 });
66
+ const res = await request(app)
67
+ .get("/header")
68
+ .set("x-access-token", token);
69
+ expect(res.status).toBe(200);
70
+ expect(res.body.user.id).toBe(777);
71
+ });
72
+
73
+ test("custom extractor works (query param)", async () => {
74
+ const token = await auth.jwt.sign({ id: 999 });
75
+ const res = await request(app)
76
+ .get(`/query?token=${token}`);
77
+ expect(res.status).toBe(200);
78
+ expect(res.body.user.id).toBe(999);
79
+ });
80
+
81
+ test("default fallback still works", async () => {
82
+ const token = await auth.jwt.sign({ id: 123 });
83
+ const res = await request(app)
84
+ .get("/profile")
85
+ .set("Authorization", `Bearer ${token}`);
86
+ expect(res.status).toBe(200);
87
+ expect(res.body.user.id).toBe(123);
88
+ });
89
+ });
@@ -0,0 +1,77 @@
1
+ // __tests__/authVerify.oauth.direct.test.js
2
+ const AuthVerify = require("../index"); // your wrapper
3
+
4
+ describe("AuthVerify OAuth - Direct Provider Tests", () => {
5
+ let auth;
6
+ let res;
7
+
8
+ beforeEach(() => {
9
+ auth = new AuthVerify();
10
+ res = { redirect: jest.fn() }; // mock Express response
11
+ });
12
+
13
+ beforeAll(() => {
14
+ global.fetch = jest.fn(); // mock fetch
15
+ });
16
+
17
+ afterEach(() => {
18
+ jest.clearAllMocks();
19
+ });
20
+
21
+ test("Apple redirect and callback", async () => {
22
+ const apple = auth.oauth.apple({
23
+ clientId: "apple_id",
24
+ clientSecret: "apple_secret",
25
+ redirectUri: "http://localhost/callback",
26
+ });
27
+
28
+ apple.redirect(res);
29
+ expect(res.redirect).toHaveBeenCalled();
30
+ expect(res.redirect.mock.calls[0][0]).toContain("https://appleid.apple.com/auth/authorize");
31
+
32
+ global.fetch.mockResolvedValueOnce({ json: async () => ({ access_token: "apple_token" }) });
33
+ const tokenData = await apple.callback("code123");
34
+ expect(tokenData.access_token).toBe("apple_token");
35
+ });
36
+
37
+ test("Discord redirect and callback", async () => {
38
+ const discord = auth.oauth.discord({
39
+ clientId: "discord_id",
40
+ clientSecret: "discord_secret",
41
+ redirectUri: "http://localhost/callback",
42
+ });
43
+
44
+ discord.redirect(res);
45
+ expect(res.redirect).toHaveBeenCalled();
46
+
47
+ global.fetch
48
+ .mockResolvedValueOnce({ json: async () => ({ access_token: "discord_token" }) })
49
+ .mockResolvedValueOnce({ json: async () => ({ id: "discord_123", username: "user" }) });
50
+
51
+ const userData = await discord.callback("code123");
52
+ expect(userData.access_token).toBe("discord_token");
53
+ expect(userData.id).toBe("discord_123");
54
+ });
55
+
56
+ test("Telegram callback returns message", async () => {
57
+ const telegram = auth.oauth.telegram({ botId: "bot123", redirectUri: "http://localhost/callback" });
58
+ const tResult = await telegram.callback("code123");
59
+ expect(tResult.message).toContain("Telegram login");
60
+ });
61
+
62
+ test("WhatsApp callback returns message", async () => {
63
+ const whatsapp = auth.oauth.whatsapp({ phoneNumberId: "12345", redirectUri: "http://localhost/callback" });
64
+ const wResult = await whatsapp.callback("code456");
65
+ expect(wResult.message).toContain("WhatsApp login");
66
+ });
67
+
68
+ test("Slack and Microsoft redirect URLs", () => {
69
+ const slack = auth.oauth.slack({ clientId: "slack_id", redirectUri: "http://localhost/callback" });
70
+ slack.redirect(res);
71
+ expect(res.redirect).toHaveBeenCalled();
72
+
73
+ const ms = auth.oauth.microsoft({ clientId: "ms_id", redirectUri: "http://localhost/callback" });
74
+ ms.redirect(res);
75
+ expect(res.redirect).toHaveBeenCalledTimes(2);
76
+ });
77
+ });