@igxjs/node-components 1.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/.github/workflows/node.js.yml +31 -0
- package/.github/workflows/npm-publish.yml +33 -0
- package/LICENSE +201 -0
- package/README.md +378 -0
- package/components/http-handlers.js +167 -0
- package/components/redis.js +76 -0
- package/components/router.js +54 -0
- package/components/session.js +376 -0
- package/index.d.ts +289 -0
- package/index.js +8 -0
- package/package.json +43 -0
- package/tests/http-handlers.test.js +21 -0
- package/tests/redis.test.js +50 -0
- package/tests/router.test.js +50 -0
- package/tests/session.test.js +116 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import 'express-session';
|
|
2
|
+
import '@types/express';
|
|
3
|
+
|
|
4
|
+
import { AxiosError } from 'axios';
|
|
5
|
+
import { RedisClientType } from '@redis/client';
|
|
6
|
+
import { Application, RequestHandler, Request, Response, NextFunction, Router } from '@types/express';
|
|
7
|
+
|
|
8
|
+
// Session Configuration
|
|
9
|
+
export interface SessionConfig {
|
|
10
|
+
SSO_ENDPOINT_URL?: string;
|
|
11
|
+
SSO_CLIENT_ID?: string;
|
|
12
|
+
SSO_CLIENT_SECRET?: string;
|
|
13
|
+
SSO_SUCCESS_URL?: string;
|
|
14
|
+
SSO_FAILURE_URL?: string;
|
|
15
|
+
|
|
16
|
+
SESSION_AGE?: number;
|
|
17
|
+
SESSION_COOKIE_PATH?: string;
|
|
18
|
+
SESSION_SECRET?: string;
|
|
19
|
+
SESSION_PREFIX?: string;
|
|
20
|
+
|
|
21
|
+
REDIS_URL?: string;
|
|
22
|
+
REDIS_CERT_PATH?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SessionUserAttributes {
|
|
26
|
+
/** @type {string} Identity Provider ID */
|
|
27
|
+
idp: string;
|
|
28
|
+
/** @type {string} User ID */
|
|
29
|
+
sub: string;
|
|
30
|
+
/** @type {number} Local timeout timestamp */
|
|
31
|
+
expires_at: number;
|
|
32
|
+
/** @type {number} Remote timeout timestamp */
|
|
33
|
+
expires_rt: number;
|
|
34
|
+
/** @type {string} Access token */
|
|
35
|
+
access_token: string;
|
|
36
|
+
/** @type {string} Refresh token */
|
|
37
|
+
refresh_token: string;
|
|
38
|
+
/** @type {Array<string>} Groups of the user */
|
|
39
|
+
groups: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SessionUser {
|
|
43
|
+
/** @type {string} First name */
|
|
44
|
+
first_name: string;
|
|
45
|
+
/** @type {string} Last name */
|
|
46
|
+
last_name: string;
|
|
47
|
+
/** @type {string} Full name */
|
|
48
|
+
name: string;
|
|
49
|
+
/** @type {string} Email address */
|
|
50
|
+
email: string;
|
|
51
|
+
/** @type {SessionUserAttributes} User attributes */
|
|
52
|
+
attributes: SessionUserAttributes;
|
|
53
|
+
/** @type {boolean} User is authorized */
|
|
54
|
+
authorized: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Session Manager
|
|
58
|
+
export class SessionManager {
|
|
59
|
+
/**
|
|
60
|
+
* Check if the email has a session refresh lock
|
|
61
|
+
* @param email Email address
|
|
62
|
+
* @returns Returns true if the email has a session refresh lock
|
|
63
|
+
*/
|
|
64
|
+
hasLock(email: string): boolean;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Lock the email for session refresh
|
|
68
|
+
* @param email Email address
|
|
69
|
+
*/
|
|
70
|
+
lock(email: string): void;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Clear session refresh locks
|
|
74
|
+
*/
|
|
75
|
+
clearLocks(): NodeJS.Timeout;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the Redis Manager
|
|
79
|
+
*/
|
|
80
|
+
redisManager(): RedisManager;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Initialize the session configurations
|
|
84
|
+
* @param app Express application
|
|
85
|
+
* @param config Session configurations
|
|
86
|
+
* @param updateUser Process user object to compute attributes like permissions, avatar URL, etc.
|
|
87
|
+
*/
|
|
88
|
+
setup(
|
|
89
|
+
app: Application,
|
|
90
|
+
config: SessionConfig,
|
|
91
|
+
updateUser: (user: SessionUser | undefined) => any
|
|
92
|
+
): Promise<void>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get session RequestHandler
|
|
96
|
+
* @returns Returns RequestHandler instance of Express
|
|
97
|
+
*/
|
|
98
|
+
sessionHandler(): Promise<RequestHandler>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resource protection middleware
|
|
102
|
+
* @param isDebugging Debugging flag (default: false)
|
|
103
|
+
* @param redirectUrl Redirect URL (default: '')
|
|
104
|
+
* @returns Returns express Request Handler
|
|
105
|
+
*/
|
|
106
|
+
authenticate(isDebugging?: boolean, redirectUrl?: string): RequestHandler;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* SSO callback for successful login
|
|
110
|
+
* @param initUser Initialize user object function
|
|
111
|
+
* @returns Returns express Request Handler
|
|
112
|
+
*/
|
|
113
|
+
callback(initUser: (user: SessionUser) => SessionUser): RequestHandler;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get Identity Providers
|
|
117
|
+
* @returns Returns express Request Handler
|
|
118
|
+
*/
|
|
119
|
+
identityProviders(): RequestHandler;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Application logout (NOT SSO)
|
|
123
|
+
* @returns Returns express Request Handler
|
|
124
|
+
*/
|
|
125
|
+
logout(): RequestHandler;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Refresh user session
|
|
129
|
+
* @param initUser Initialize user object function
|
|
130
|
+
* @returns Returns express Request Handler
|
|
131
|
+
*/
|
|
132
|
+
refresh(initUser: (user: SessionUser) => SessionUser): RequestHandler;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Custom Error class
|
|
136
|
+
export class CustomError extends Error {
|
|
137
|
+
code: number;
|
|
138
|
+
object;
|
|
139
|
+
error: object;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Construct a custom error
|
|
143
|
+
* @param code Error code
|
|
144
|
+
* @param message Message
|
|
145
|
+
* @param error Error object (optional)
|
|
146
|
+
* @param data Additional data (optional)
|
|
147
|
+
*/
|
|
148
|
+
constructor(code: number, message: string, error?: object, data?: object);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Singleton session instance
|
|
152
|
+
export const session: SessionManager;
|
|
153
|
+
|
|
154
|
+
// FlexRouter class for Express routing
|
|
155
|
+
export class FlexRouter {
|
|
156
|
+
context: string;
|
|
157
|
+
router: Router;
|
|
158
|
+
handlers: RequestHandler[];
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Constructor
|
|
162
|
+
* @param context Context path
|
|
163
|
+
* @param router Router instance
|
|
164
|
+
* @param handlers Request handlers (optional)
|
|
165
|
+
*/
|
|
166
|
+
constructor(context: string, router: Router, handlers?: RequestHandler[]);
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Mount router to Express app
|
|
170
|
+
* @param app Express application
|
|
171
|
+
* @param basePath Base path
|
|
172
|
+
*/
|
|
173
|
+
mount(app: Application, basePath: string): void;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// RedisManager class for Redis connection management
|
|
177
|
+
export class RedisManager {
|
|
178
|
+
/**
|
|
179
|
+
* Connect with Redis
|
|
180
|
+
* @param redisUrl Redis connection URL
|
|
181
|
+
* @param certPath Certificate path for TLS connections
|
|
182
|
+
* @returns Returns true if Redis server is connected
|
|
183
|
+
*/
|
|
184
|
+
connect(redisUrl: string, certPath: string): Promise<boolean>;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get Redis client
|
|
188
|
+
* @returns Returns Redis client instance
|
|
189
|
+
*/
|
|
190
|
+
getClient(): RedisClientType;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Determine if the Redis server is connected
|
|
194
|
+
* @returns Returns true if Redis server is connected
|
|
195
|
+
*/
|
|
196
|
+
isConnected(): Promise<boolean>;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Disconnect from Redis
|
|
200
|
+
* @returns Returns nothing
|
|
201
|
+
*/
|
|
202
|
+
disConnect(): Promise<void>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// HTTP status code keys (exposed for type safety)
|
|
206
|
+
export const httpCodes: {
|
|
207
|
+
OK: number;
|
|
208
|
+
CREATED: number;
|
|
209
|
+
NO_CONTENT: number;
|
|
210
|
+
BAD_REQUEST: number;
|
|
211
|
+
UNAUTHORIZED: number;
|
|
212
|
+
FORBIDDEN: number;
|
|
213
|
+
NOT_FOUND: number;
|
|
214
|
+
NOT_ACCEPTABLE: number;
|
|
215
|
+
CONFLICT: number;
|
|
216
|
+
SYSTEM_FAILURE: number;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// HTTP message keys (exposed for type safety)
|
|
220
|
+
export const httpMessages: {
|
|
221
|
+
OK: string;
|
|
222
|
+
CREATED: string;
|
|
223
|
+
NO_CONTENT: string;
|
|
224
|
+
BAD_REQUEST: string;
|
|
225
|
+
UNAUTHORIZED: string;
|
|
226
|
+
FORBIDDEN: string;
|
|
227
|
+
NOT_FOUND: string;
|
|
228
|
+
NOT_ACCEPTABLE: string;
|
|
229
|
+
CONFLICT: string;
|
|
230
|
+
SYSTEM_FAILURE: string;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* HTTP Helper utilities
|
|
235
|
+
*/
|
|
236
|
+
export const httpHelper: {
|
|
237
|
+
/**
|
|
238
|
+
* Format a string with placeholders
|
|
239
|
+
* @param str String with {0}, {1}, etc. placeholders
|
|
240
|
+
* @param args Values to replace placeholders
|
|
241
|
+
* @returns Formatted string
|
|
242
|
+
*/
|
|
243
|
+
format(str: string, ...args: any[]): string;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Generate friendly Zod validation error message
|
|
247
|
+
* @param error Zod validation error
|
|
248
|
+
* @returns Formatted error message
|
|
249
|
+
*/
|
|
250
|
+
toZodMessage(error: any): string;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Analyze and convert Axios/HTTP errors to CustomError
|
|
254
|
+
* @param error Error object
|
|
255
|
+
* @param defaultMessage Default error message
|
|
256
|
+
* @returns CustomError instance
|
|
257
|
+
*/
|
|
258
|
+
handleAxiosError(error: Error | AxiosError, defaultMessage?: string): CustomError;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Custom error handler middleware
|
|
263
|
+
* @param err Error object
|
|
264
|
+
* @param req Express Request
|
|
265
|
+
* @param res Express Response
|
|
266
|
+
* @param next Next function
|
|
267
|
+
*/
|
|
268
|
+
export function httpErrorHandler(
|
|
269
|
+
err: CustomError | Error | any,
|
|
270
|
+
req: Request,
|
|
271
|
+
res: Response,
|
|
272
|
+
next: NextFunction
|
|
273
|
+
): void;
|
|
274
|
+
|
|
275
|
+
declare global {
|
|
276
|
+
namespace Express {
|
|
277
|
+
export interface Request {
|
|
278
|
+
user?: SessionUser;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Augment Express Session with custom user property
|
|
284
|
+
declare module 'express-session' {
|
|
285
|
+
interface SessionData {
|
|
286
|
+
[key: string]: any;
|
|
287
|
+
user?: SessionUser;
|
|
288
|
+
}
|
|
289
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SessionManager } from './components/session.js';
|
|
2
|
+
|
|
3
|
+
export { SessionConfig, SessionManager } from './components/session.js';
|
|
4
|
+
export { httpCodes, httpMessages, httpErrorHandler, CustomError, httpHelper } from './components/http-handlers.js';
|
|
5
|
+
export { RedisManager } from './components/redis.js';
|
|
6
|
+
export { FlexRouter } from './components/router.js';
|
|
7
|
+
|
|
8
|
+
export const session = new SessionManager();
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@igxjs/node-components",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Node components for igxjs",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "mocha tests/**/*.test.js --timeout 5000"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/igxjs/node-components.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"igxjs"
|
|
16
|
+
],
|
|
17
|
+
"author": "Michael",
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/igxjs/node-components/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/igxjs/node-components#readme",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@redis/client": "^5.11.0",
|
|
25
|
+
"@types/express": "^5.0.6",
|
|
26
|
+
"axios": "^1.13.6",
|
|
27
|
+
"connect-redis": "^9.0.0",
|
|
28
|
+
"express-session": "^1.19.0",
|
|
29
|
+
"jose": "^6.1.3",
|
|
30
|
+
"memorystore": "^1.6.7"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"chai": "^6.2.2",
|
|
34
|
+
"express": "^5.2.1",
|
|
35
|
+
"mocha": "^11.0.1",
|
|
36
|
+
"sinon": "^21.0.2",
|
|
37
|
+
"supertest": "^7.0.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"types": "./index.d.ts"
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it } from 'mocha';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import { httpCodes, httpMessages, CustomError } from '../components/http-handlers.js';
|
|
4
|
+
|
|
5
|
+
describe('HTTP Handlers', () => {
|
|
6
|
+
describe('httpCodes', () => {
|
|
7
|
+
it('should have correct HTTP status codes', () => {
|
|
8
|
+
expect(httpCodes.OK).to.equal(200);
|
|
9
|
+
expect(httpCodes.BAD_REQUEST).to.equal(400);
|
|
10
|
+
expect(httpCodes.NOT_FOUND).to.equal(404);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('CustomError', () => {
|
|
15
|
+
it('should create a CustomError', () => {
|
|
16
|
+
const error = new CustomError(404, 'Not found');
|
|
17
|
+
expect(error.code).to.equal(404);
|
|
18
|
+
expect(error.message).to.equal('Not found');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from 'mocha';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import sinon from 'sinon';
|
|
4
|
+
import { RedisManager } from '../components/redis.js';
|
|
5
|
+
|
|
6
|
+
describe('RedisManager', () => {
|
|
7
|
+
let redisManager;
|
|
8
|
+
let consoleStubs;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
redisManager = new RedisManager();
|
|
12
|
+
consoleStubs = {
|
|
13
|
+
info: sinon.stub(console, 'info'),
|
|
14
|
+
warn: sinon.stub(console, 'warn'),
|
|
15
|
+
error: sinon.stub(console, 'error')
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
consoleStubs.info.restore();
|
|
21
|
+
consoleStubs.warn.restore();
|
|
22
|
+
consoleStubs.error.restore();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('connect', () => {
|
|
26
|
+
it('should return false if redisUrl is empty', async () => {
|
|
27
|
+
const result = await redisManager.connect('', null);
|
|
28
|
+
expect(result).to.be.false;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should return false if redisUrl is null', async () => {
|
|
32
|
+
const result = await redisManager.connect(null, null);
|
|
33
|
+
expect(result).to.be.false;
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('getClient', () => {
|
|
38
|
+
it('should return null when not connected', () => {
|
|
39
|
+
const client = redisManager.getClient();
|
|
40
|
+
expect(client).to.be.null;
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('isConnected', () => {
|
|
45
|
+
it('should return false when client is null', async () => {
|
|
46
|
+
const result = await redisManager.isConnected();
|
|
47
|
+
expect(result).to.be.false;
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from 'mocha';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import sinon from 'sinon';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import { FlexRouter } from '../components/router.js';
|
|
6
|
+
|
|
7
|
+
describe('FlexRouter', () => {
|
|
8
|
+
let app, router, middleware1, middleware2;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
app = express();
|
|
12
|
+
router = express.Router();
|
|
13
|
+
middleware1 = sinon.stub().callsFake((req, res, next) => next());
|
|
14
|
+
middleware2 = sinon.stub().callsFake((req, res, next) => next());
|
|
15
|
+
sinon.spy(app, 'use');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('constructor', () => {
|
|
19
|
+
it('should create FlexRouter with context and router', () => {
|
|
20
|
+
const flexRouter = new FlexRouter('/api', router);
|
|
21
|
+
expect(flexRouter.context).to.equal('/api');
|
|
22
|
+
expect(flexRouter.router).to.equal(router);
|
|
23
|
+
expect(flexRouter.handlers).to.deep.equal([]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should create FlexRouter with handlers', () => {
|
|
27
|
+
const handlers = [middleware1, middleware2];
|
|
28
|
+
const flexRouter = new FlexRouter('/api', router, handlers);
|
|
29
|
+
expect(flexRouter.handlers).to.deep.equal(handlers);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('mount', () => {
|
|
34
|
+
it('should mount router to app with correct path', () => {
|
|
35
|
+
const flexRouter = new FlexRouter('/users', router);
|
|
36
|
+
flexRouter.mount(app, '/api/v1');
|
|
37
|
+
expect(app.use.calledOnce).to.be.true;
|
|
38
|
+
expect(app.use.firstCall.args[0]).to.equal('/api/v1/users');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should mount router with handlers', () => {
|
|
42
|
+
const handlers = [middleware1, middleware2];
|
|
43
|
+
const flexRouter = new FlexRouter('/protected', router, handlers);
|
|
44
|
+
flexRouter.mount(app, '/api');
|
|
45
|
+
expect(app.use.calledOnce).to.be.true;
|
|
46
|
+
expect(app.use.firstCall.args[0]).to.equal('/api/protected');
|
|
47
|
+
expect(app.use.firstCall.args[1]).to.deep.equal(handlers);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from 'mocha';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import sinon from 'sinon';
|
|
4
|
+
import { SessionManager, SessionConfig } from '../components/session.js';
|
|
5
|
+
import { CustomError, httpCodes } from '../components/http-handlers.js';
|
|
6
|
+
|
|
7
|
+
describe('SessionManager', () => {
|
|
8
|
+
let sessionManager;
|
|
9
|
+
let clock;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
sessionManager = new SessionManager();
|
|
13
|
+
clock = sinon.useFakeTimers();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
clock.restore();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('SessionConfig', () => {
|
|
21
|
+
it('should create SessionConfig instance with properties', () => {
|
|
22
|
+
const config = new SessionConfig();
|
|
23
|
+
expect(config).to.be.instanceOf(SessionConfig);
|
|
24
|
+
expect(config).to.have.property('SSO_ENDPOINT_URL');
|
|
25
|
+
expect(config).to.have.property('SESSION_SECRET');
|
|
26
|
+
expect(config).to.have.property('REDIS_URL');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('Lock Management', () => {
|
|
31
|
+
describe('hasLock', () => {
|
|
32
|
+
it('should return false for email without lock', () => {
|
|
33
|
+
const result = sessionManager.hasLock('test@example.com');
|
|
34
|
+
expect(result).to.be.false;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should return true for email with active lock', () => {
|
|
38
|
+
sessionManager.lock('test@example.com');
|
|
39
|
+
const result = sessionManager.hasLock('test@example.com');
|
|
40
|
+
expect(result).to.be.true;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return false for expired lock', () => {
|
|
44
|
+
sessionManager.lock('test@example.com');
|
|
45
|
+
clock.tick(61000);
|
|
46
|
+
const result = sessionManager.hasLock('test@example.com');
|
|
47
|
+
expect(result).to.be.false;
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('lock', () => {
|
|
52
|
+
it('should create a lock for given email', () => {
|
|
53
|
+
sessionManager.lock('test@example.com');
|
|
54
|
+
expect(sessionManager.hasLock('test@example.com')).to.be.true;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should not create lock for empty email', () => {
|
|
58
|
+
sessionManager.lock('');
|
|
59
|
+
sessionManager.lock(null);
|
|
60
|
+
expect(sessionManager.hasLock('')).to.be.false;
|
|
61
|
+
expect(sessionManager.hasLock(null)).to.be.false;
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('authenticate', () => {
|
|
67
|
+
let req, res, next;
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
req = { user: null };
|
|
71
|
+
res = { redirect: sinon.stub() };
|
|
72
|
+
next = sinon.stub();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should call next() if user is authorized', () => {
|
|
76
|
+
req.user = { authorized: true };
|
|
77
|
+
const middleware = sessionManager.authenticate();
|
|
78
|
+
middleware(req, res, next);
|
|
79
|
+
expect(next.calledOnce).to.be.true;
|
|
80
|
+
expect(next.firstCall.args).to.be.empty;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should call next with error if user is not authorized', () => {
|
|
84
|
+
req.user = { authorized: false };
|
|
85
|
+
const middleware = sessionManager.authenticate();
|
|
86
|
+
middleware(req, res, next);
|
|
87
|
+
expect(next.calledOnce).to.be.true;
|
|
88
|
+
const error = next.firstCall.args[0];
|
|
89
|
+
expect(error).to.be.instanceOf(CustomError);
|
|
90
|
+
expect(error.code).to.equal(httpCodes.UNAUTHORIZED);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should redirect if redirectUrl is provided', () => {
|
|
94
|
+
req.user = { authorized: false };
|
|
95
|
+
const middleware = sessionManager.authenticate(false, '/login');
|
|
96
|
+
middleware(req, res, next);
|
|
97
|
+
expect(res.redirect.calledWith('/login')).to.be.true;
|
|
98
|
+
expect(next.called).to.be.false;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should allow access in debug mode', () => {
|
|
102
|
+
req.user = null;
|
|
103
|
+
const middleware = sessionManager.authenticate(true);
|
|
104
|
+
middleware(req, res, next);
|
|
105
|
+
expect(next.calledOnce).to.be.true;
|
|
106
|
+
expect(next.firstCall.args).to.be.empty;
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('redisManager', () => {
|
|
111
|
+
it('should return null before initialization', () => {
|
|
112
|
+
const manager = sessionManager.redisManager();
|
|
113
|
+
expect(manager).to.be.null;
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|