@onlineapps/service-common 1.0.0 → 1.0.2
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/onlineapps-service-common-1.0.1.tgz +0 -0
- package/package.json +4 -2
- package/src/index.js +20 -3
- package/src/infrastructure/waitForHealthCheckQueueReady.js +106 -0
- package/src/infrastructure/waitForInfrastructureReady.js +52 -10
- package/src/redisClient.js +134 -0
- package/src/reporting/monitoringFallbackEmail.js +89 -0
- package/tests/unit/monitoringFallbackEmail.test.js +57 -0
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/service-common",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Common utilities for both infrastructure services and business services",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
+
"prepublishOnly": "../../scripts/pre-publish-compatibility-check.sh",
|
|
8
|
+
"postpublish": "../../scripts/update-manifest-from-npm.sh && ../../scripts/update-all-services.sh",
|
|
7
9
|
"test": "jest",
|
|
8
10
|
"test:unit": "jest tests/unit",
|
|
9
11
|
"test:integration": "jest tests/integration"
|
|
@@ -17,6 +19,7 @@
|
|
|
17
19
|
"author": "OA Drive Team",
|
|
18
20
|
"license": "MIT",
|
|
19
21
|
"dependencies": {
|
|
22
|
+
"nodemailer": "^6.9.8",
|
|
20
23
|
"redis": "^4.6.0"
|
|
21
24
|
},
|
|
22
25
|
"devDependencies": {
|
|
@@ -26,4 +29,3 @@
|
|
|
26
29
|
"node": ">=14.0.0"
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
|
-
|
package/src/index.js
CHANGED
|
@@ -2,17 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @onlineapps/service-common
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* Common utilities for both infrastructure services and business services.
|
|
7
7
|
* Provides shared functionality that is used across the entire system.
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
9
|
* This library is for ALL services (infrastructure and business).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const { waitForInfrastructureReady } = require('./infrastructure/waitForInfrastructureReady');
|
|
13
|
+
const { waitForHealthCheckQueueReady } = require('./infrastructure/waitForHealthCheckQueueReady');
|
|
14
|
+
const { sendMonitoringFailFallbackEmail } = require('./reporting/monitoringFallbackEmail');
|
|
15
|
+
const {
|
|
16
|
+
buildRedisUrl,
|
|
17
|
+
createRedisClient,
|
|
18
|
+
connectRedis
|
|
19
|
+
} = require('./redisClient');
|
|
13
20
|
|
|
14
21
|
module.exports = {
|
|
15
22
|
// Infrastructure readiness utilities (used by both infrastructure and business services)
|
|
16
|
-
waitForInfrastructureReady
|
|
23
|
+
waitForInfrastructureReady,
|
|
24
|
+
waitForHealthCheckQueueReady,
|
|
25
|
+
|
|
26
|
+
// Redis utilities (shared between services and tests)
|
|
27
|
+
buildRedisUrl,
|
|
28
|
+
createRedisClient,
|
|
29
|
+
connectRedis,
|
|
30
|
+
|
|
31
|
+
// Reporting utilities
|
|
32
|
+
sendMonitoringFailFallbackEmail
|
|
17
33
|
};
|
|
18
34
|
|
|
35
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* waitForHealthCheckQueueReady.js
|
|
5
|
+
*
|
|
6
|
+
* Waits for Registry to create the infrastructure.health.checks queue.
|
|
7
|
+
* Used by infrastructure services to know when they can start publishing health checks.
|
|
8
|
+
*
|
|
9
|
+
* This is different from waitForInfrastructureReady - this only checks if the queue exists,
|
|
10
|
+
* not if all services are healthy.
|
|
11
|
+
*
|
|
12
|
+
* **How it works:**
|
|
13
|
+
* 1. Connects to Redis
|
|
14
|
+
* 2. Checks `infrastructure:health:queue:ready` key every 2 seconds
|
|
15
|
+
* 3. If key is `"true"` → queue is ready → return success
|
|
16
|
+
* 4. If key is missing → wait and retry
|
|
17
|
+
* 5. If timeout reached → throw error
|
|
18
|
+
*
|
|
19
|
+
* **Usage:**
|
|
20
|
+
* - Infrastructure services: Wait before starting health check publisher
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Wait for health check queue to be ready
|
|
25
|
+
* @param {Object} options - Options
|
|
26
|
+
* @param {string} [options.redisUrl] - Redis URL (default: REDIS_URL env or redis://api_node_cache:6379)
|
|
27
|
+
* @param {number} [options.maxWait] - Maximum wait time in ms (default: 60000 = 1 minute)
|
|
28
|
+
* @param {number} [options.checkInterval] - Check interval in ms (default: 2000 = 2 seconds)
|
|
29
|
+
* @param {Object} [options.logger] - Logger instance (default: console)
|
|
30
|
+
* @returns {Promise<boolean>} - True if queue is ready
|
|
31
|
+
* @throws {Error} - If timeout is reached
|
|
32
|
+
*/
|
|
33
|
+
async function waitForHealthCheckQueueReady(options = {}) {
|
|
34
|
+
const redisUrl = options.redisUrl || process.env.REDIS_URL || 'redis://api_node_cache:6379';
|
|
35
|
+
const maxWait = options.maxWait || parseInt(process.env.INFRASTRUCTURE_HEALTH_QUEUE_WAIT_MAX_TIME) || 60000; // 1 minute
|
|
36
|
+
const checkInterval = options.checkInterval || parseInt(process.env.INFRASTRUCTURE_HEALTH_QUEUE_WAIT_CHECK_INTERVAL) || 2000; // 2 seconds
|
|
37
|
+
const logger = options.logger || console;
|
|
38
|
+
|
|
39
|
+
const log = (msg) => {
|
|
40
|
+
if (logger && typeof logger.info === 'function') {
|
|
41
|
+
logger.info(msg);
|
|
42
|
+
} else if (logger && typeof logger.log === 'function') {
|
|
43
|
+
logger.log(msg);
|
|
44
|
+
} else {
|
|
45
|
+
console.log(msg);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
let redis = null;
|
|
50
|
+
const startTime = Date.now();
|
|
51
|
+
let attemptCount = 0;
|
|
52
|
+
|
|
53
|
+
log(`[HealthCheckQueueReady] Waiting for health check queue to be ready...`);
|
|
54
|
+
log(`[HealthCheckQueueReady] Redis URL: ${redisUrl}`);
|
|
55
|
+
log(`[HealthCheckQueueReady] Max wait: ${maxWait}ms, Check interval: ${checkInterval}ms`);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Connect to Redis
|
|
59
|
+
const { createClient } = require('redis');
|
|
60
|
+
redis = createClient({ url: redisUrl });
|
|
61
|
+
|
|
62
|
+
redis.on('error', (err) => {
|
|
63
|
+
log(`[HealthCheckQueueReady] Redis error: ${err.message}`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await redis.connect();
|
|
67
|
+
log('[HealthCheckQueueReady] Connected to Redis');
|
|
68
|
+
|
|
69
|
+
while (Date.now() - startTime < maxWait) {
|
|
70
|
+
attemptCount++;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Check Redis key: infrastructure:health:queue:ready
|
|
74
|
+
const queueReady = await redis.get('infrastructure:health:queue:ready');
|
|
75
|
+
|
|
76
|
+
if (queueReady === 'true') {
|
|
77
|
+
// Queue is ready, we can proceed
|
|
78
|
+
const elapsed = Date.now() - startTime;
|
|
79
|
+
log(`[HealthCheckQueueReady] ✓ Health check queue is ready (took ${elapsed}ms, ${attemptCount} attempts)`);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
log(`[HealthCheckQueueReady] Attempt ${attemptCount}: Queue not ready yet (current status: ${queueReady || 'unknown'}). Waiting ${checkInterval}ms...`);
|
|
84
|
+
|
|
85
|
+
} catch (error) {
|
|
86
|
+
log(`[HealthCheckQueueReady] Attempt ${attemptCount}: Failed to get queue ready status from Redis (${error.message}). Waiting ${checkInterval}ms...`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const elapsed = Date.now() - startTime;
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Health check queue not ready within ${maxWait}ms (${elapsed}ms elapsed, ${attemptCount} attempts). ` +
|
|
95
|
+
`Check Redis key 'infrastructure:health:queue:ready' and ensure Registry has started.`
|
|
96
|
+
);
|
|
97
|
+
} finally {
|
|
98
|
+
if (redis && redis.isReady) {
|
|
99
|
+
await redis.quit();
|
|
100
|
+
log('[HealthCheckQueueReady] Disconnected from Redis.');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { waitForHealthCheckQueueReady };
|
|
106
|
+
|
|
@@ -72,23 +72,50 @@ async function waitForInfrastructureReady(options = {}) {
|
|
|
72
72
|
log(`[InfrastructureReady] Max wait: ${maxWait}ms, Check interval: ${checkInterval}ms`);
|
|
73
73
|
|
|
74
74
|
try {
|
|
75
|
-
// Connect to Redis
|
|
75
|
+
// Connect to Redis with timeout
|
|
76
76
|
const { createClient } = require('redis');
|
|
77
|
-
redis = createClient({
|
|
77
|
+
redis = createClient({
|
|
78
|
+
url: redisUrl,
|
|
79
|
+
socket: {
|
|
80
|
+
connectTimeout: 5000, // 5 seconds timeout for connection
|
|
81
|
+
reconnectStrategy: false // Don't auto-reconnect - fail fast
|
|
82
|
+
}
|
|
83
|
+
});
|
|
78
84
|
|
|
79
85
|
redis.on('error', (err) => {
|
|
80
86
|
log(`[InfrastructureReady] Redis error: ${err.message}`);
|
|
81
87
|
});
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
// Add timeout for Redis connection
|
|
90
|
+
const CONNECT_TIMEOUT = 10000; // 10 seconds
|
|
91
|
+
const connectPromise = redis.connect();
|
|
92
|
+
const connectTimeoutPromise = new Promise((_, reject) => {
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
reject(new Error(`Redis connection timeout after ${CONNECT_TIMEOUT}ms. Redis may be unavailable at ${redisUrl}`));
|
|
95
|
+
}, CONNECT_TIMEOUT);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await Promise.race([connectPromise, connectTimeoutPromise]);
|
|
100
|
+
log('[InfrastructureReady] Connected to Redis');
|
|
101
|
+
} catch (connectError) {
|
|
102
|
+
throw new Error(`[InfrastructureReady] Failed to connect to Redis: ${connectError.message}`);
|
|
103
|
+
}
|
|
85
104
|
|
|
86
105
|
while (Date.now() - startTime < maxWait) {
|
|
87
106
|
attemptCount++;
|
|
88
107
|
|
|
89
108
|
try {
|
|
90
|
-
// Check Redis key: infrastructure:health:all
|
|
91
|
-
const
|
|
109
|
+
// Check Redis key: infrastructure:health:all with timeout to prevent hanging
|
|
110
|
+
const REDIS_OPERATION_TIMEOUT = 5000; // 5 seconds per operation
|
|
111
|
+
const getPromise = redis.get('infrastructure:health:all');
|
|
112
|
+
const getTimeoutPromise = new Promise((_, reject) => {
|
|
113
|
+
setTimeout(() => {
|
|
114
|
+
reject(new Error(`Redis get() operation timeout after ${REDIS_OPERATION_TIMEOUT}ms`));
|
|
115
|
+
}, REDIS_OPERATION_TIMEOUT);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const allHealthy = await Promise.race([getPromise, getTimeoutPromise]);
|
|
92
119
|
|
|
93
120
|
if (allHealthy === 'true') {
|
|
94
121
|
// All services are UP, we can proceed
|
|
@@ -100,9 +127,18 @@ async function waitForInfrastructureReady(options = {}) {
|
|
|
100
127
|
const serviceKeys = ['gateway', 'registry', 'validator', 'delivery', 'monitoring'];
|
|
101
128
|
const statuses = {};
|
|
102
129
|
for (const serviceName of serviceKeys) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
130
|
+
try {
|
|
131
|
+
const statusPromise = redis.get(`infrastructure:health:${serviceName}`);
|
|
132
|
+
const statusTimeoutPromise = new Promise((_, reject) => {
|
|
133
|
+
setTimeout(() => reject(new Error('timeout')), REDIS_OPERATION_TIMEOUT);
|
|
134
|
+
});
|
|
135
|
+
const status = await Promise.race([statusPromise, statusTimeoutPromise]);
|
|
136
|
+
if (status) {
|
|
137
|
+
statuses[serviceName] = JSON.parse(status);
|
|
138
|
+
}
|
|
139
|
+
} catch (statusError) {
|
|
140
|
+
// Ignore individual service status errors
|
|
141
|
+
log(`[InfrastructureReady] Could not get status for ${serviceName}: ${statusError.message}`);
|
|
106
142
|
}
|
|
107
143
|
}
|
|
108
144
|
if (Object.keys(statuses).length > 0) {
|
|
@@ -115,7 +151,13 @@ async function waitForInfrastructureReady(options = {}) {
|
|
|
115
151
|
log(`[InfrastructureReady] Attempt ${attemptCount}: Not all services ready (current status: ${allHealthy || 'unknown'}). Waiting ${checkInterval}ms...`);
|
|
116
152
|
|
|
117
153
|
} catch (error) {
|
|
118
|
-
|
|
154
|
+
const errorMsg = error.message || String(error);
|
|
155
|
+
log(`[InfrastructureReady] Attempt ${attemptCount}: Failed to get infrastructure health from Redis (${errorMsg}). Waiting ${checkInterval}ms...`);
|
|
156
|
+
|
|
157
|
+
// If Redis connection is lost, throw error immediately instead of retrying
|
|
158
|
+
if (errorMsg.includes('timeout') || errorMsg.includes('ECONNREFUSED') || errorMsg.includes('ENOTFOUND')) {
|
|
159
|
+
throw new Error(`[InfrastructureReady] Redis connection lost: ${errorMsg}. Cannot continue waiting for infrastructure ready.`);
|
|
160
|
+
}
|
|
119
161
|
}
|
|
120
162
|
|
|
121
163
|
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared Redis client utilities for services and tests.
|
|
5
|
+
*
|
|
6
|
+
* Responsibility:
|
|
7
|
+
* - Build Redis connection URL from environment in a consistent way
|
|
8
|
+
* - Create a configured redis client with unified error logging
|
|
9
|
+
* - (Optionally) connect with a hard timeout for tests / tools
|
|
10
|
+
*
|
|
11
|
+
* This module is intentionally small and framework-agnostic so it can be used
|
|
12
|
+
* both in infrastructure services, business services and in test utilities.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { createClient } = require('redis');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build a Redis connection URL from environment.
|
|
19
|
+
*
|
|
20
|
+
* Precedence:
|
|
21
|
+
* 1. REDIS_URL (full URL, e.g. redis://host:port/db)
|
|
22
|
+
* 2. REDIS_HOST + REDIS_PORT
|
|
23
|
+
* 3. Defaults (host/port) or localhost:6379
|
|
24
|
+
*/
|
|
25
|
+
function buildRedisUrl(options = {}) {
|
|
26
|
+
const env = options.env || process.env || {};
|
|
27
|
+
const defaults = options.defaults || {};
|
|
28
|
+
|
|
29
|
+
if (env.REDIS_URL && typeof env.REDIS_URL === 'string' && env.REDIS_URL.length > 0) {
|
|
30
|
+
return env.REDIS_URL;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const host = env.REDIS_HOST || defaults.host || 'localhost';
|
|
34
|
+
const port = env.REDIS_PORT || defaults.port || '6379';
|
|
35
|
+
|
|
36
|
+
return `redis://${host}:${port}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a Redis client with unified error logging and optional test settings.
|
|
41
|
+
*
|
|
42
|
+
* @param {Object} options
|
|
43
|
+
* @param {string} [options.purpose] - Logical purpose (for log context)
|
|
44
|
+
* @param {boolean} [options.forTests=false] - If true, disables automatic reconnect loops
|
|
45
|
+
* @param {Object} [options.logger=console] - Logger with .error/.info methods
|
|
46
|
+
* @param {Object} [options.env=process.env] - Optional env override (for tests)
|
|
47
|
+
* @param {Object} [options.defaults] - Optional default host/port overrides
|
|
48
|
+
* @returns {{ client: import('redis').RedisClientType, url: string }}
|
|
49
|
+
*/
|
|
50
|
+
function createRedisClient(options = {}) {
|
|
51
|
+
const {
|
|
52
|
+
purpose = 'default',
|
|
53
|
+
forTests = false,
|
|
54
|
+
logger = console,
|
|
55
|
+
env,
|
|
56
|
+
defaults
|
|
57
|
+
} = options;
|
|
58
|
+
|
|
59
|
+
const url = buildRedisUrl({ env, defaults });
|
|
60
|
+
|
|
61
|
+
const socketOptions = forTests
|
|
62
|
+
? {
|
|
63
|
+
// In tests we typically want fail-fast behaviour, not infinite reconnect loops
|
|
64
|
+
reconnectStrategy: false
|
|
65
|
+
}
|
|
66
|
+
: {};
|
|
67
|
+
|
|
68
|
+
const client = createClient({
|
|
69
|
+
url,
|
|
70
|
+
socket: socketOptions
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const log = logger && typeof logger.error === 'function' ? logger : console;
|
|
74
|
+
|
|
75
|
+
client.on('error', (err) => {
|
|
76
|
+
try {
|
|
77
|
+
log.error('[Redis] Error', {
|
|
78
|
+
purpose,
|
|
79
|
+
url,
|
|
80
|
+
message: err && err.message ? err.message : String(err)
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
// Last-resort fallback to avoid throwing in error handler
|
|
84
|
+
// eslint-disable-next-line no-console
|
|
85
|
+
console.error('[Redis] Error', err);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return { client, url };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create and connect a Redis client with a hard timeout (useful for tests/tools).
|
|
94
|
+
*
|
|
95
|
+
* @param {Object} options - Same as createRedisClient plus:
|
|
96
|
+
* @param {number} [options.timeoutMs=15000] - Max time to wait for connect()
|
|
97
|
+
* @returns {Promise<import('redis').RedisClientType>}
|
|
98
|
+
*/
|
|
99
|
+
async function connectRedis(options = {}) {
|
|
100
|
+
const { timeoutMs = 15000, logger = console } = options;
|
|
101
|
+
const { client, url } = createRedisClient(options);
|
|
102
|
+
|
|
103
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
104
|
+
await client.connect();
|
|
105
|
+
if (logger && typeof logger.info === 'function') {
|
|
106
|
+
logger.info('[Redis] Connected', { url });
|
|
107
|
+
}
|
|
108
|
+
return client;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const connectPromise = client.connect();
|
|
112
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
113
|
+
setTimeout(
|
|
114
|
+
() => reject(new Error(`Redis connection timeout after ${timeoutMs}ms to ${url}`)),
|
|
115
|
+
timeoutMs
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await Promise.race([connectPromise, timeoutPromise]);
|
|
120
|
+
|
|
121
|
+
if (logger && typeof logger.info === 'function') {
|
|
122
|
+
logger.info('[Redis] Connected', { url });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return client;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
buildRedisUrl,
|
|
130
|
+
createRedisClient,
|
|
131
|
+
connectRedis
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const nodemailer = require('nodemailer');
|
|
4
|
+
|
|
5
|
+
let transporter = null;
|
|
6
|
+
let cachedConfigSignature = null;
|
|
7
|
+
|
|
8
|
+
function loadConfig() {
|
|
9
|
+
return {
|
|
10
|
+
host: process.env.INFRA_REPORT_SMTP_HOST,
|
|
11
|
+
port: process.env.INFRA_REPORT_SMTP_PORT ? parseInt(process.env.INFRA_REPORT_SMTP_PORT, 10) : undefined,
|
|
12
|
+
secure: process.env.INFRA_REPORT_SMTP_SECURE === 'true',
|
|
13
|
+
user: process.env.INFRA_REPORT_SMTP_USER,
|
|
14
|
+
pass: process.env.INFRA_REPORT_SMTP_PASS,
|
|
15
|
+
from: process.env.INFRA_REPORT_FROM,
|
|
16
|
+
to: process.env.INFRA_REPORT_TO
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createTransportIfNeeded(config) {
|
|
21
|
+
const signature = JSON.stringify(config);
|
|
22
|
+
if (transporter && cachedConfigSignature === signature) {
|
|
23
|
+
return transporter;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!config.host || !config.port || !config.user || !config.pass || !config.from || !config.to) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
transporter = nodemailer.createTransport({
|
|
31
|
+
host: config.host,
|
|
32
|
+
port: config.port,
|
|
33
|
+
secure: config.secure,
|
|
34
|
+
auth: {
|
|
35
|
+
user: config.user,
|
|
36
|
+
pass: config.pass
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
cachedConfigSignature = signature;
|
|
40
|
+
return transporter;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function sendMonitoringFailFallbackEmail(subject, text, html) {
|
|
44
|
+
const config = loadConfig();
|
|
45
|
+
const mailer = createTransportIfNeeded(config);
|
|
46
|
+
|
|
47
|
+
if (!mailer) {
|
|
48
|
+
console.warn('[MonitoringFallbackEmail] SMTP configuration missing, skipping email send');
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const recipients = config.to.split(',').map(addr => addr.trim()).filter(Boolean);
|
|
53
|
+
if (recipients.length === 0) {
|
|
54
|
+
console.warn('[MonitoringFallbackEmail] No recipients configured in INFRA_REPORT_TO');
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const result = await mailer.sendMail({
|
|
60
|
+
from: config.from,
|
|
61
|
+
to: recipients,
|
|
62
|
+
subject,
|
|
63
|
+
text,
|
|
64
|
+
html
|
|
65
|
+
});
|
|
66
|
+
console.log('[MonitoringFallbackEmail] Email sent successfully', {
|
|
67
|
+
messageId: result.messageId,
|
|
68
|
+
response: result.response,
|
|
69
|
+
accepted: result.accepted,
|
|
70
|
+
rejected: result.rejected
|
|
71
|
+
});
|
|
72
|
+
return true;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('[MonitoringFallbackEmail] Failed to send email:', {
|
|
75
|
+
message: error.message,
|
|
76
|
+
code: error.code,
|
|
77
|
+
command: error.command,
|
|
78
|
+
response: error.response,
|
|
79
|
+
responseCode: error.responseCode
|
|
80
|
+
});
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
sendMonitoringFailFallbackEmail
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
|