@onlineapps/service-common 1.1.0 → 1.1.1

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/service-common",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Common utilities for both infrastructure services and business services (JWT auth, Redis/Postgres clients, business errors, runtime config)",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -1,171 +0,0 @@
1
- 'use strict';
2
-
3
- const {
4
- BusinessError,
5
- NotFoundError,
6
- ValidationError,
7
- ConflictError,
8
- BusinessRuleError,
9
- AuthorizationError,
10
- ServiceUnavailableError,
11
- isBusinessError,
12
- businessErrorHandler,
13
- notFoundHandler
14
- } = require('../../src/errors');
15
-
16
- describe('BusinessError hierarchy', () => {
17
- test('BusinessError has correct defaults', () => {
18
- const err = new BusinessError('Something went wrong');
19
- expect(err).toBeInstanceOf(Error);
20
- expect(err).toBeInstanceOf(BusinessError);
21
- expect(err.name).toBe('BusinessError');
22
- expect(err.message).toBe('Something went wrong');
23
- expect(err.statusCode).toBe(500);
24
- expect(err.errorCode).toBe('INTERNAL_ERROR');
25
- expect(err.type).toBe('BUSINESS');
26
- expect(err.details).toEqual([]);
27
- });
28
-
29
- test('NotFoundError sets 404', () => {
30
- const err = new NotFoundError('Document not found', {
31
- details: ['UUID abc123 does not exist'],
32
- operation: 'getDocument'
33
- });
34
- expect(err).toBeInstanceOf(BusinessError);
35
- expect(err.statusCode).toBe(404);
36
- expect(err.errorCode).toBe('RESOURCE_NOT_FOUND');
37
- expect(err.type).toBe('BUSINESS');
38
- expect(err.details).toEqual(['UUID abc123 does not exist']);
39
- expect(err.operation).toBe('getDocument');
40
- });
41
-
42
- test('ValidationError sets 400', () => {
43
- const err = new ValidationError('Invalid input', { details: ['field X is required'] });
44
- expect(err.statusCode).toBe(400);
45
- expect(err.errorCode).toBe('VALIDATION_FAILED');
46
- expect(err.type).toBe('VALIDATION');
47
- });
48
-
49
- test('ConflictError sets 409', () => {
50
- const err = new ConflictError('Duplicate');
51
- expect(err.statusCode).toBe(409);
52
- expect(err.errorCode).toBe('DUPLICATE_RESOURCE');
53
- });
54
-
55
- test('BusinessRuleError sets 422', () => {
56
- const err = new BusinessRuleError('Cannot invoice draft');
57
- expect(err.statusCode).toBe(422);
58
- expect(err.errorCode).toBe('BUSINESS_RULE_VIOLATED');
59
- expect(err.type).toBe('VALIDATION');
60
- });
61
-
62
- test('AuthorizationError sets 403', () => {
63
- const err = new AuthorizationError('No access');
64
- expect(err.statusCode).toBe(403);
65
- expect(err.errorCode).toBe('FORBIDDEN');
66
- });
67
-
68
- test('ServiceUnavailableError sets 503', () => {
69
- const err = new ServiceUnavailableError('DB down');
70
- expect(err.statusCode).toBe(503);
71
- expect(err.errorCode).toBe('SERVICE_UNAVAILABLE');
72
- expect(err.type).toBe('TRANSIENT');
73
- });
74
-
75
- test('isBusinessError returns true for BusinessError subtypes', () => {
76
- expect(isBusinessError(new NotFoundError('x'))).toBe(true);
77
- expect(isBusinessError(new Error('x'))).toBe(false);
78
- expect(isBusinessError(null)).toBe(false);
79
- });
80
-
81
- test('toJSON returns structured error', () => {
82
- const err = new NotFoundError('Not found', {
83
- details: ['detail1'],
84
- operation: 'findDoc',
85
- service: 'invoicing'
86
- });
87
- const json = err.toJSON();
88
- expect(json).toEqual({
89
- code: 'RESOURCE_NOT_FOUND',
90
- message: 'Not found',
91
- statusCode: 404,
92
- details: ['detail1'],
93
- service: 'invoicing',
94
- operation: 'findDoc'
95
- });
96
- });
97
-
98
- test('custom errorCode can be overridden', () => {
99
- const err = new NotFoundError('Tenant missing', { errorCode: 'TENANT_NOT_FOUND' });
100
- expect(err.errorCode).toBe('TENANT_NOT_FOUND');
101
- });
102
- });
103
-
104
- describe('businessErrorHandler middleware', () => {
105
- let req, res, next;
106
-
107
- beforeEach(() => {
108
- req = { method: 'GET', originalUrl: '/api/test', app: { locals: { logger: { warn: jest.fn(), error: jest.fn() } } } };
109
- res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), headersSent: false };
110
- next = jest.fn();
111
- });
112
-
113
- test('formats BusinessError as structured JSON', () => {
114
- const err = new NotFoundError('Doc not found', { details: ['UUID 123'] });
115
- businessErrorHandler(err, req, res, next);
116
-
117
- expect(res.status).toHaveBeenCalledWith(404);
118
- expect(res.json).toHaveBeenCalledWith({
119
- error: {
120
- code: 'RESOURCE_NOT_FOUND',
121
- message: 'Doc not found',
122
- statusCode: 404,
123
- details: ['UUID 123'],
124
- service: undefined,
125
- operation: undefined
126
- }
127
- });
128
- });
129
-
130
- test('wraps generic Error as INTERNAL_ERROR 500', () => {
131
- const err = new Error('kaboom');
132
- businessErrorHandler(err, req, res, next);
133
-
134
- expect(res.status).toHaveBeenCalledWith(500);
135
- const body = res.json.mock.calls[0][0];
136
- expect(body.error.code).toBe('INTERNAL_ERROR');
137
- expect(body.error.statusCode).toBe(500);
138
- });
139
-
140
- test('does not leak stack in production', () => {
141
- const origEnv = process.env.NODE_ENV;
142
- process.env.NODE_ENV = 'production';
143
-
144
- const err = new Error('secret details');
145
- businessErrorHandler(err, req, res, next);
146
-
147
- const body = res.json.mock.calls[0][0];
148
- expect(body.error.message).toBe('Internal server error');
149
-
150
- process.env.NODE_ENV = origEnv;
151
- });
152
-
153
- test('calls next() if headers already sent', () => {
154
- res.headersSent = true;
155
- const err = new Error('late');
156
- businessErrorHandler(err, req, res, next);
157
- expect(next).toHaveBeenCalledWith(err);
158
- expect(res.status).not.toHaveBeenCalled();
159
- });
160
- });
161
-
162
- describe('notFoundHandler middleware', () => {
163
- test('returns ROUTE_NOT_FOUND 404', () => {
164
- const req = { method: 'GET', originalUrl: '/unknown', app: { locals: { logger: { warn: jest.fn() } } } };
165
- const res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis() };
166
- notFoundHandler(req, res);
167
-
168
- expect(res.status).toHaveBeenCalledWith(404);
169
- expect(res.json.mock.calls[0][0].error.code).toBe('ROUTE_NOT_FOUND');
170
- });
171
- });
@@ -1,48 +0,0 @@
1
- 'use strict';
2
-
3
- const defaults = require('../../src/defaults');
4
- const { getCriticalConfigWithFallbacks, getInfrastructureHealthConfig } = require('../../src/runtime-config');
5
- const { buildRedisUrl } = require('../../src/redisClient');
6
-
7
- describe('@onlineapps/service-common defaults @unit', () => {
8
- test('should export stable module-owned defaults', () => {
9
- expect(defaults.infrastructureHealthQueueName).toBe('infrastructure.health.checks');
10
- expect(defaults.infrastructureHealthPublishIntervalMs).toBe(5000);
11
- expect(defaults.infrastructureHealthWaitMaxTimeMs).toBe(60000);
12
- expect(defaults.infrastructureHealthWaitCheckIntervalMs).toBe(2000);
13
- expect(defaults.infrastructureHealthTimeoutMs).toBe(15000);
14
- expect(defaults.infrastructureHealthRedisTtlSeconds).toBe(30);
15
- expect(defaults.infrastructureHealthCleanupIntervalMs).toBe(10000);
16
- expect(defaults.infrastructureHealthQueueWaitMaxTimeMs).toBe(60000);
17
- expect(defaults.infrastructureHealthQueueWaitCheckIntervalMs).toBe(2000);
18
- });
19
-
20
- test('getCriticalConfigWithFallbacks should fail-fast when env is missing (no topology defaults)', () => {
21
- const oldRedis = process.env.REDIS_URL;
22
- const oldRabbit = process.env.RABBITMQ_URL;
23
- delete process.env.REDIS_URL;
24
- delete process.env.RABBITMQ_URL;
25
-
26
- expect(() => getCriticalConfigWithFallbacks()).toThrow(/Missing required config/i);
27
-
28
- if (oldRedis !== undefined) process.env.REDIS_URL = oldRedis;
29
- if (oldRabbit !== undefined) process.env.RABBITMQ_URL = oldRabbit;
30
- });
31
-
32
- test('getInfrastructureHealthConfig should default queue name from module defaults', () => {
33
- const old = process.env.INFRASTRUCTURE_HEALTH_QUEUE;
34
- delete process.env.INFRASTRUCTURE_HEALTH_QUEUE;
35
-
36
- const cfg = getInfrastructureHealthConfig();
37
- expect(cfg.queueName).toBe(defaults.infrastructureHealthQueueName);
38
-
39
- if (old !== undefined) process.env.INFRASTRUCTURE_HEALTH_QUEUE = old;
40
- });
41
-
42
- test('buildRedisUrl should fall back to module defaults host/port', () => {
43
- expect(() => buildRedisUrl({ env: {}, defaults: {} })).toThrow(/Missing required config/i);
44
- });
45
- });
46
-
47
-
48
-
@@ -1,170 +0,0 @@
1
- 'use strict';
2
-
3
- const jwt = require('jsonwebtoken');
4
-
5
- const TEST_SECRET = 'test-jwt-secret-at-least-16-chars';
6
-
7
- function createValidAccessToken(overrides = {}) {
8
- const payload = {
9
- sub: 'u_person-uuid',
10
- person_id: 42,
11
- email: 'test@example.com',
12
- tenants: [{ tenant_uuid: 'a_t1', tenant_id: 1, role: 'OWNER' }],
13
- type: 'access',
14
- ...overrides
15
- };
16
- return jwt.sign(payload, TEST_SECRET, { issuer: 'oa-auth', expiresIn: '15m' });
17
- }
18
-
19
- function mockRes() {
20
- const res = {
21
- statusCode: null,
22
- body: null,
23
- status(code) { res.statusCode = code; return res; },
24
- json(data) { res.body = data; return res; }
25
- };
26
- return res;
27
- }
28
-
29
- describe('createJwtValidator @unit', () => {
30
- const originalEnv = process.env.JWT_SECRET;
31
-
32
- beforeEach(() => {
33
- process.env.JWT_SECRET = TEST_SECRET;
34
- jest.resetModules();
35
- });
36
-
37
- afterEach(() => {
38
- if (originalEnv !== undefined) {
39
- process.env.JWT_SECRET = originalEnv;
40
- } else {
41
- delete process.env.JWT_SECRET;
42
- }
43
- });
44
-
45
- function loadModule() {
46
- return require('../../../src/jwt/createJwtValidator');
47
- }
48
-
49
- const logger = { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() };
50
-
51
- it('throws if logger is missing', () => {
52
- const { createJwtValidator } = loadModule();
53
- expect(() => createJwtValidator({ logger: null })).toThrow('Logger is required');
54
- });
55
-
56
- it('returns 401 when Authorization header is missing', () => {
57
- const { createJwtValidator } = loadModule();
58
- const middleware = createJwtValidator({ logger });
59
- const req = { path: '/test', headers: {} };
60
- const res = mockRes();
61
- const next = jest.fn();
62
-
63
- middleware(req, res, next);
64
-
65
- expect(res.statusCode).toBe(401);
66
- expect(res.body.error).toContain('Missing or invalid Authorization header');
67
- expect(next).not.toHaveBeenCalled();
68
- });
69
-
70
- it('returns 401 when Authorization header has no Bearer prefix', () => {
71
- const { createJwtValidator } = loadModule();
72
- const middleware = createJwtValidator({ logger });
73
- const req = { path: '/test', headers: { authorization: 'Basic abc123' } };
74
- const res = mockRes();
75
- const next = jest.fn();
76
-
77
- middleware(req, res, next);
78
-
79
- expect(res.statusCode).toBe(401);
80
- expect(next).not.toHaveBeenCalled();
81
- });
82
-
83
- it('sets req.auth and calls next() for valid token', () => {
84
- const { createJwtValidator } = loadModule();
85
- const middleware = createJwtValidator({ logger });
86
- const token = createValidAccessToken();
87
- const req = { path: '/test', headers: { authorization: `Bearer ${token}` } };
88
- const res = mockRes();
89
- const next = jest.fn();
90
-
91
- middleware(req, res, next);
92
-
93
- expect(next).toHaveBeenCalledTimes(1);
94
- expect(req.auth).toBeDefined();
95
- expect(req.auth.person_uuid).toBe('u_person-uuid');
96
- expect(req.auth.person_id).toBe(42);
97
- expect(req.auth.email).toBe('test@example.com');
98
- expect(req.auth.tenants).toHaveLength(1);
99
- });
100
-
101
- it('returns 401 with refresh instruction for expired token', () => {
102
- const { createJwtValidator } = loadModule();
103
- const middleware = createJwtValidator({ logger });
104
- const token = jwt.sign(
105
- { sub: 'u_test', person_id: 1, type: 'access' },
106
- TEST_SECRET,
107
- { issuer: 'oa-auth', expiresIn: '0s' }
108
- );
109
- const req = { path: '/test', headers: { authorization: `Bearer ${token}` } };
110
- const res = mockRes();
111
- const next = jest.fn();
112
-
113
- middleware(req, res, next);
114
-
115
- expect(res.statusCode).toBe(401);
116
- expect(res.body.error).toContain('expired');
117
- expect(res.body.error).toContain('refresh');
118
- expect(next).not.toHaveBeenCalled();
119
- });
120
-
121
- it('returns 401 for refresh token (wrong type)', () => {
122
- const { createJwtValidator } = loadModule();
123
- const middleware = createJwtValidator({ logger });
124
- const token = jwt.sign(
125
- { sub: 'u_test', person_id: 1, type: 'refresh' },
126
- TEST_SECRET,
127
- { issuer: 'oa-auth', expiresIn: '7d' }
128
- );
129
- const req = { path: '/test', headers: { authorization: `Bearer ${token}` } };
130
- const res = mockRes();
131
- const next = jest.fn();
132
-
133
- middleware(req, res, next);
134
-
135
- expect(res.statusCode).toBe(401);
136
- expect(res.body.error).toContain('Invalid token type');
137
- expect(next).not.toHaveBeenCalled();
138
- });
139
-
140
- it('skips validation for excluded paths', () => {
141
- const { createJwtValidator } = loadModule();
142
- const middleware = createJwtValidator({ logger, excludePaths: ['/health', '/public'] });
143
- const req = { path: '/health', headers: {} };
144
- const res = mockRes();
145
- const next = jest.fn();
146
-
147
- middleware(req, res, next);
148
-
149
- expect(next).toHaveBeenCalledTimes(1);
150
- expect(res.statusCode).toBeNull();
151
- });
152
-
153
- it('logs warning on invalid token', () => {
154
- const { createJwtValidator } = loadModule();
155
- const warnFn = jest.fn();
156
- const testLogger = { ...logger, warn: warnFn };
157
- const middleware = createJwtValidator({ logger: testLogger });
158
- const req = { path: '/test', headers: { authorization: 'Bearer invalid.token.here' } };
159
- const res = mockRes();
160
- const next = jest.fn();
161
-
162
- middleware(req, res, next);
163
-
164
- expect(warnFn).toHaveBeenCalledWith(
165
- '[JWT] Token verification failed',
166
- expect.objectContaining({ error: expect.any(String) })
167
- );
168
- expect(res.statusCode).toBe(401);
169
- });
170
- });
@@ -1,148 +0,0 @@
1
- 'use strict';
2
-
3
- describe('extractTenantContext @unit', () => {
4
- function loadModule() {
5
- return require('../../../src/jwt/extractTenantContext');
6
- }
7
-
8
- const singleTenantAuth = {
9
- person_uuid: 'u_person1',
10
- person_id: 42,
11
- email: 'test@example.com',
12
- tenants: [
13
- { tenant_uuid: 'a_tenant1', tenant_id: 10, role: 'OWNER' }
14
- ]
15
- };
16
-
17
- const multiTenantAuth = {
18
- person_uuid: 'u_person1',
19
- person_id: 42,
20
- email: 'test@example.com',
21
- tenants: [
22
- { tenant_uuid: 'a_tenant1', tenant_id: 10, role: 'OWNER' },
23
- { tenant_uuid: 'a_tenant2', tenant_id: 20, role: 'EDITOR' }
24
- ]
25
- };
26
-
27
- it('throws 400 for single tenant without workspace header (requireWorkspace: true)', () => {
28
- const { extractTenantContext } = loadModule();
29
- expect(() => extractTenantContext(singleTenantAuth, {})).toThrow('Missing x-workspace-id');
30
- });
31
-
32
- it('auto-picks single tenant without workspace when requireWorkspace: false', () => {
33
- const { extractTenantContext } = loadModule();
34
- const ctx = extractTenantContext(singleTenantAuth, {}, { requireWorkspace: false });
35
-
36
- expect(ctx).toEqual({
37
- tenant_id: 10,
38
- tenant_uuid: 'a_tenant1',
39
- workspace_id: null,
40
- person_id: 42,
41
- person_uuid: 'u_person1',
42
- role: 'OWNER'
43
- });
44
- });
45
-
46
- it('uses workspace from header when provided', () => {
47
- const { extractTenantContext } = loadModule();
48
- const ctx = extractTenantContext(singleTenantAuth, { 'x-workspace-id': '5' });
49
-
50
- expect(ctx.workspace_id).toBe(5);
51
- });
52
-
53
- it('picks correct tenant when x-tenant-uuid is provided', () => {
54
- const { extractTenantContext } = loadModule();
55
- const ctx = extractTenantContext(multiTenantAuth, { 'x-tenant-uuid': 'a_tenant2', 'x-workspace-id': '1' });
56
-
57
- expect(ctx.tenant_id).toBe(20);
58
- expect(ctx.tenant_uuid).toBe('a_tenant2');
59
- expect(ctx.role).toBe('EDITOR');
60
- expect(ctx.workspace_id).toBe(1);
61
- });
62
-
63
- it('picks correct tenant with custom workspace', () => {
64
- const { extractTenantContext } = loadModule();
65
- const ctx = extractTenantContext(multiTenantAuth, {
66
- 'x-tenant-uuid': 'a_tenant1',
67
- 'x-workspace-id': '3'
68
- });
69
-
70
- expect(ctx.tenant_id).toBe(10);
71
- expect(ctx.workspace_id).toBe(3);
72
- });
73
-
74
- it('throws 400 when multiple tenants and no x-tenant-uuid (requireWorkspace: false)', () => {
75
- const { extractTenantContext } = loadModule();
76
-
77
- expect(() => extractTenantContext(multiTenantAuth, {}, { requireWorkspace: false })).toThrow('Missing tenant context');
78
- try {
79
- extractTenantContext(multiTenantAuth, {}, { requireWorkspace: false });
80
- } catch (err) {
81
- expect(err.statusCode).toBe(400);
82
- }
83
- });
84
-
85
- it('throws 403 when x-tenant-uuid does not match any membership', () => {
86
- const { extractTenantContext } = loadModule();
87
-
88
- expect(() => extractTenantContext(singleTenantAuth, { 'x-tenant-uuid': 'a_nonexistent', 'x-workspace-id': '1' }))
89
- .toThrow('No membership for specified tenant');
90
- try {
91
- extractTenantContext(singleTenantAuth, { 'x-tenant-uuid': 'a_nonexistent', 'x-workspace-id': '1' });
92
- } catch (err) {
93
- expect(err.statusCode).toBe(403);
94
- }
95
- });
96
-
97
- it('parses workspace header as integer', () => {
98
- const { extractTenantContext } = loadModule();
99
- const ctx = extractTenantContext(singleTenantAuth, { 'x-workspace-id': '42' });
100
-
101
- expect(ctx.workspace_id).toBe(42);
102
- expect(typeof ctx.workspace_id).toBe('number');
103
- });
104
-
105
- it('throws 401 when auth is null', () => {
106
- const { extractTenantContext } = loadModule();
107
-
108
- expect(() => extractTenantContext(null, {})).toThrow('Missing auth data');
109
- try {
110
- extractTenantContext(null, {});
111
- } catch (err) {
112
- expect(err.statusCode).toBe(401);
113
- }
114
- });
115
-
116
- it('throws 401 when auth.tenants is not an array', () => {
117
- const { extractTenantContext } = loadModule();
118
-
119
- expect(() => extractTenantContext({ person_id: 1 }, {})).toThrow('Missing auth data');
120
- });
121
-
122
- it('single tenant with only x-workspace-id header (no x-tenant-uuid)', () => {
123
- const { extractTenantContext } = loadModule();
124
- const ctx = extractTenantContext(singleTenantAuth, { 'x-workspace-id': '7' });
125
-
126
- expect(ctx.tenant_id).toBe(10);
127
- expect(ctx.workspace_id).toBe(7);
128
- });
129
-
130
- it('multiple tenants with only x-workspace-id header throws 400', () => {
131
- const { extractTenantContext } = loadModule();
132
-
133
- expect(() => extractTenantContext(multiTenantAuth, { 'x-workspace-id': '2' }))
134
- .toThrow('Missing tenant context');
135
- });
136
-
137
- it('workspace_id null when requireWorkspace false and no header', () => {
138
- const { extractTenantContext } = loadModule();
139
- const ctx = extractTenantContext(singleTenantAuth, {}, { requireWorkspace: false });
140
- expect(ctx.workspace_id).toBeNull();
141
- });
142
-
143
- it('workspace_id from header even when requireWorkspace false', () => {
144
- const { extractTenantContext } = loadModule();
145
- const ctx = extractTenantContext(singleTenantAuth, { 'x-workspace-id': '9' }, { requireWorkspace: false });
146
- expect(ctx.workspace_id).toBe(9);
147
- });
148
- });
@@ -1,137 +0,0 @@
1
- 'use strict';
2
-
3
- const jwt = require('jsonwebtoken');
4
-
5
- const TEST_SECRET = 'test-jwt-secret-at-least-16-chars';
6
- const WRONG_SECRET = 'wrong-secret-also-16-chars-long';
7
-
8
- function createValidAccessToken(overrides = {}) {
9
- const payload = {
10
- sub: 'u_test-person-uuid',
11
- person_id: 42,
12
- email: 'test@example.com',
13
- tenants: [{ tenant_uuid: 'a_tenant1', tenant_id: 1, role: 'OWNER' }],
14
- type: 'access',
15
- ...overrides
16
- };
17
- return jwt.sign(payload, TEST_SECRET, { issuer: 'oa-auth', expiresIn: '15m' });
18
- }
19
-
20
- describe('verifyAccessToken @unit', () => {
21
- const originalEnv = process.env.JWT_SECRET;
22
-
23
- beforeEach(() => {
24
- process.env.JWT_SECRET = TEST_SECRET;
25
- jest.resetModules();
26
- });
27
-
28
- afterEach(() => {
29
- if (originalEnv !== undefined) {
30
- process.env.JWT_SECRET = originalEnv;
31
- } else {
32
- delete process.env.JWT_SECRET;
33
- }
34
- });
35
-
36
- function loadModule() {
37
- return require('../../../src/jwt/verifyAccessToken');
38
- }
39
-
40
- it('returns decoded payload for valid access token', () => {
41
- const { verifyAccessToken } = loadModule();
42
- const token = createValidAccessToken();
43
- const decoded = verifyAccessToken(token);
44
-
45
- expect(decoded.sub).toBe('u_test-person-uuid');
46
- expect(decoded.person_id).toBe(42);
47
- expect(decoded.email).toBe('test@example.com');
48
- expect(decoded.tenants).toHaveLength(1);
49
- expect(decoded.type).toBe('access');
50
- expect(decoded.iss).toBe('oa-auth');
51
- });
52
-
53
- it('throws on expired token', () => {
54
- const { verifyAccessToken } = loadModule();
55
- const token = jwt.sign(
56
- { sub: 'u_test', person_id: 1, type: 'access' },
57
- TEST_SECRET,
58
- { issuer: 'oa-auth', expiresIn: '0s' }
59
- );
60
-
61
- expect(() => verifyAccessToken(token)).toThrow();
62
- try {
63
- verifyAccessToken(token);
64
- } catch (err) {
65
- expect(err.name).toBe('TokenExpiredError');
66
- }
67
- });
68
-
69
- it('throws on invalid signature (wrong secret)', () => {
70
- const { verifyAccessToken } = loadModule();
71
- const token = jwt.sign(
72
- { sub: 'u_test', person_id: 1, type: 'access' },
73
- WRONG_SECRET,
74
- { issuer: 'oa-auth' }
75
- );
76
-
77
- expect(() => verifyAccessToken(token)).toThrow('invalid signature');
78
- });
79
-
80
- it('throws when JWT_SECRET is missing', () => {
81
- delete process.env.JWT_SECRET;
82
- const { verifyAccessToken } = loadModule();
83
- const token = createValidAccessToken();
84
-
85
- expect(() => verifyAccessToken(token)).toThrow('JWT_SECRET');
86
- });
87
-
88
- it('throws when JWT_SECRET is too short', () => {
89
- process.env.JWT_SECRET = 'short';
90
- const { verifyAccessToken } = loadModule();
91
- const token = createValidAccessToken();
92
-
93
- expect(() => verifyAccessToken(token)).toThrow('at least 16 characters');
94
- });
95
-
96
- it('throws on refresh token (type != access)', () => {
97
- const { verifyAccessToken } = loadModule();
98
- const token = jwt.sign(
99
- { sub: 'u_test', person_id: 1, type: 'refresh' },
100
- TEST_SECRET,
101
- { issuer: 'oa-auth', expiresIn: '7d' }
102
- );
103
-
104
- expect(() => verifyAccessToken(token)).toThrow('Invalid token type');
105
- try {
106
- verifyAccessToken(token);
107
- } catch (err) {
108
- expect(err.code).toBe('INVALID_TOKEN_TYPE');
109
- }
110
- });
111
-
112
- it('throws on wrong issuer', () => {
113
- const { verifyAccessToken } = loadModule();
114
- const token = jwt.sign(
115
- { sub: 'u_test', person_id: 1, type: 'access' },
116
- TEST_SECRET,
117
- { issuer: 'wrong-issuer' }
118
- );
119
-
120
- expect(() => verifyAccessToken(token)).toThrow();
121
- });
122
-
123
- it('throws on empty token', () => {
124
- const { verifyAccessToken } = loadModule();
125
-
126
- expect(() => verifyAccessToken('')).toThrow('Token is required');
127
- expect(() => verifyAccessToken(null)).toThrow('Token is required');
128
- expect(() => verifyAccessToken(undefined)).toThrow('Token is required');
129
- });
130
-
131
- it('throws on non-string token', () => {
132
- const { verifyAccessToken } = loadModule();
133
-
134
- expect(() => verifyAccessToken(12345)).toThrow('Token is required');
135
- expect(() => verifyAccessToken({})).toThrow('Token is required');
136
- });
137
- });
@@ -1,57 +0,0 @@
1
- 'use strict';
2
-
3
- jest.mock('nodemailer', () => {
4
- return {
5
- createTransport: jest.fn(() => ({
6
- sendMail: jest.fn().mockResolvedValue(true)
7
- }))
8
- };
9
- });
10
-
11
- describe('monitoringFallbackEmail', () => {
12
- const ORIGINAL_ENV = process.env;
13
-
14
- beforeEach(() => {
15
- jest.resetModules();
16
- process.env = { ...ORIGINAL_ENV };
17
- });
18
-
19
- afterEach(() => {
20
- process.env = ORIGINAL_ENV;
21
- });
22
-
23
- function loadReporter() {
24
- return require('../../src/reporting/monitoringFallbackEmail');
25
- }
26
-
27
- test('returns false when SMTP config missing', async () => {
28
- delete process.env.INFRA_REPORT_SMTP_HOST;
29
- const { sendMonitoringFailFallbackEmail } = loadReporter();
30
- const result = await sendMonitoringFailFallbackEmail('t', 't', '<p>t</p>');
31
- expect(result).toBe(false);
32
- });
33
-
34
- test('sends email when SMTP config provided', async () => {
35
- process.env.INFRA_REPORT_SMTP_HOST = 'smtp.example.com';
36
- process.env.INFRA_REPORT_SMTP_PORT = '587';
37
- process.env.INFRA_REPORT_SMTP_SECURE = 'false';
38
- process.env.INFRA_REPORT_SMTP_USER = 'user@example.com';
39
- process.env.INFRA_REPORT_SMTP_PASS = 'secret';
40
- process.env.INFRA_REPORT_FROM = 'infra@example.com';
41
- process.env.INFRA_REPORT_TO = 'ops@example.com';
42
-
43
- const nodemailer = require('nodemailer');
44
- const transportMock = {
45
- sendMail: jest.fn().mockResolvedValue(true)
46
- };
47
- nodemailer.createTransport.mockReturnValue(transportMock);
48
-
49
- const { sendMonitoringFailFallbackEmail } = loadReporter();
50
- const result = await sendMonitoringFailFallbackEmail('Subject', 'Body', '<p>Body</p>');
51
-
52
- expect(result).toBe(true);
53
- expect(transportMock.sendMail).toHaveBeenCalledTimes(1);
54
- });
55
- });
56
-
57
-