@go-mailer/jarvis 10.9.0 → 10.9.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/index.js +3 -2
- package/lib/throttlers/wallet/index.js +6 -6
- package/lib/throttlers/wallet/throttler.lua +4 -1
- package/package.json +3 -2
- package/test/lib/env.test.js +110 -0
- package/test/lib/flag.test.js +176 -0
- package/test/lib/middlewares/healer.test.js +210 -0
- package/test/lib/middlewares/http.test.js +231 -0
- package/test/lib/query.test.js +156 -0
- package/test/lib/queue/throttler.test.js +187 -0
- package/test/lib/redis/cache.test.js +215 -0
- package/test/lib/utility/chart.test.js +42 -0
- package/test/lib/utility/date.test.js +266 -0
- package/test/lib/utility/number.test.js +128 -0
- package/test/lib/utility/text.test.js +178 -0
- package/test/query.test.js +15 -16
- package/test/throttlers/wallet.test.js +345 -0
package/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const { HTTPCacheManager } = require('./lib/middlewares/cache/manager')
|
|
|
12
12
|
const { AutoHealer } = require('./lib/middlewares/healer')
|
|
13
13
|
const { localCache } = require('./lib/redis/cache')
|
|
14
14
|
const { QueueThrottler } = require('./lib/queue/throttler')
|
|
15
|
-
|
|
15
|
+
const { WalletThrottler } = require('./lib/throttlers/wallet')
|
|
16
16
|
module.exports = {
|
|
17
17
|
APIClient,
|
|
18
18
|
Authenticator,
|
|
@@ -28,5 +28,6 @@ module.exports = {
|
|
|
28
28
|
QueueThrottler,
|
|
29
29
|
Redis,
|
|
30
30
|
RequestLogger,
|
|
31
|
-
Utility
|
|
31
|
+
Utility,
|
|
32
|
+
WalletThrottler
|
|
32
33
|
}
|
|
@@ -7,9 +7,9 @@ const WalletThrottler = async ({ redisClient = null, RMQBroker, wallet_charge_qu
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
const lua_script = fs.readFileSync(path.join(__dirname, 'throttler.lua'), 'utf-8')
|
|
10
|
-
const deduct_sha = await redisClient.
|
|
10
|
+
const deduct_sha = await redisClient.scriptLoad(lua_script)
|
|
11
11
|
|
|
12
|
-
const deductCredits = async ({ tenant_id, wallet_type, amount, min_allowed = 0 }) => {
|
|
12
|
+
const deductCredits = async ({ tenant_id, message_type = 'transactional', wallet_type, amount, min_allowed = 0 }) => {
|
|
13
13
|
const balance_key = `wallet:tenant:${tenant_id}:wallet:${wallet_type}`
|
|
14
14
|
const usage_key = `wallet:tenant:${tenant_id}:wallet:${wallet_type}:usage`
|
|
15
15
|
|
|
@@ -17,12 +17,12 @@ const WalletThrottler = async ({ redisClient = null, RMQBroker, wallet_charge_qu
|
|
|
17
17
|
try {
|
|
18
18
|
result = await redisClient.evalSha(deduct_sha, {
|
|
19
19
|
keys: [balance_key, usage_key],
|
|
20
|
-
arguments: [amount.toString(), min_allowed.toString()]
|
|
20
|
+
arguments: [amount.toString(), min_allowed.toString(), message_type]
|
|
21
21
|
})
|
|
22
22
|
} catch (err) {
|
|
23
23
|
result = await redisClient.eval(lua_script, {
|
|
24
24
|
keys: [balance_key, usage_key],
|
|
25
|
-
arguments: [amount.toString(), min_allowed.toString()]
|
|
25
|
+
arguments: [amount.toString(), min_allowed.toString(), message_type]
|
|
26
26
|
})
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -56,7 +56,7 @@ const WalletThrottler = async ({ redisClient = null, RMQBroker, wallet_charge_qu
|
|
|
56
56
|
await redisClient.multi().set(usage_key, 0).exec()
|
|
57
57
|
RMQBroker.publish(
|
|
58
58
|
wallet_charge_queue,
|
|
59
|
-
JSON.stringify({ tenant_id, wallet_type,
|
|
59
|
+
JSON.stringify({ tenant_id, resource_id: 0, resource_type: wallet_type, wallet_type, cost: parseInt(usage, 10), timestamp: Date.now() })
|
|
60
60
|
)
|
|
61
61
|
|
|
62
62
|
console.log(`Synced ${usage} used credits for ${tenant_id}`)
|
|
@@ -78,7 +78,7 @@ const WalletThrottler = async ({ redisClient = null, RMQBroker, wallet_charge_qu
|
|
|
78
78
|
if (used > 0) {
|
|
79
79
|
RMQBroker.publish(
|
|
80
80
|
wallet_charge_queue,
|
|
81
|
-
JSON.stringify({ tenant_id, wallet_type,
|
|
81
|
+
JSON.stringify({ tenant_id, resource_id: 0, resource_type: wallet_type, wallet_type, cost: used, timestamp: Date.now() })
|
|
82
82
|
)
|
|
83
83
|
|
|
84
84
|
await redisClient.set(usage_key, 0) // reset after sync
|
|
@@ -8,6 +8,7 @@ local balanceKey = KEYS[1]
|
|
|
8
8
|
local usedKey = KEYS[2]
|
|
9
9
|
local amount = tonumber(ARGV[1])
|
|
10
10
|
local minAllowed = tonumber(ARGV[2]) or 0
|
|
11
|
+
local msgType = ARGV[3]
|
|
11
12
|
|
|
12
13
|
local balance = redis.call('GET', balanceKey)
|
|
13
14
|
if not balance then
|
|
@@ -24,6 +25,8 @@ end
|
|
|
24
25
|
redis.call('SET', balanceKey, balance - amount)
|
|
25
26
|
|
|
26
27
|
-- Track usage
|
|
27
|
-
|
|
28
|
+
if msgType ~= "campaign" then
|
|
29
|
+
redis.call('INCRBY', usedKey, amount)
|
|
30
|
+
end
|
|
28
31
|
|
|
29
32
|
return {1, "success", balance - amount}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-mailer/jarvis",
|
|
3
|
-
"version": "10.9.
|
|
3
|
+
"version": "10.9.2",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"repository": "git@github.com:go-mailer-ltd/jarvis-node.git",
|
|
6
6
|
"author": "Nathan Oguntuberu <nateoguns.work@gmail.com>",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"eslint-plugin-import": "^2.25.4",
|
|
29
29
|
"eslint-plugin-node": "^11.1.0",
|
|
30
30
|
"eslint-plugin-promise": "^5.2.0",
|
|
31
|
-
"mocha": "^10.2.0"
|
|
31
|
+
"mocha": "^10.2.0",
|
|
32
|
+
"sinon": "^21.0.2"
|
|
32
33
|
}
|
|
33
34
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const { expect } = require('chai')
|
|
2
|
+
const EnvVar = require('../../lib/env')
|
|
3
|
+
|
|
4
|
+
describe('Environment Variables', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// Reset the config before each test
|
|
7
|
+
EnvVar.set({})
|
|
8
|
+
// Clear any test environment variables
|
|
9
|
+
delete process.env.TEST_VAR
|
|
10
|
+
delete process.env.REQUIRED_VAR
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('set', () => {
|
|
14
|
+
it('should set configuration values', () => {
|
|
15
|
+
EnvVar.set({ APP_NAME: 'TestApp', VERSION: '1.0.0' })
|
|
16
|
+
|
|
17
|
+
const appName = EnvVar.fetch('APP_NAME')
|
|
18
|
+
const version = EnvVar.fetch('VERSION')
|
|
19
|
+
|
|
20
|
+
expect(appName).to.equal('TestApp')
|
|
21
|
+
expect(version).to.equal('1.0.0')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should merge with existing configuration', () => {
|
|
25
|
+
EnvVar.set({ KEY1: 'value1' })
|
|
26
|
+
EnvVar.set({ KEY2: 'value2' })
|
|
27
|
+
|
|
28
|
+
expect(EnvVar.fetch('KEY1')).to.equal('value1')
|
|
29
|
+
expect(EnvVar.fetch('KEY2')).to.equal('value2')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should override existing configuration values', () => {
|
|
33
|
+
EnvVar.set({ KEY: 'initial' })
|
|
34
|
+
EnvVar.set({ KEY: 'updated' })
|
|
35
|
+
|
|
36
|
+
expect(EnvVar.fetch('KEY')).to.equal('updated')
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('fetch', () => {
|
|
41
|
+
it('should fetch environment variables', () => {
|
|
42
|
+
process.env.TEST_VAR = 'test_value'
|
|
43
|
+
const value = EnvVar.fetch('TEST_VAR')
|
|
44
|
+
expect(value).to.equal('test_value')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should fetch configuration values', () => {
|
|
48
|
+
EnvVar.set({ CONFIG_VAR: 'config_value' })
|
|
49
|
+
const value = EnvVar.fetch('CONFIG_VAR')
|
|
50
|
+
expect(value).to.equal('config_value')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should prioritize environment variables over configuration', () => {
|
|
54
|
+
process.env.PRIORITY_TEST = 'env_value'
|
|
55
|
+
EnvVar.set({ PRIORITY_TEST: 'config_value' })
|
|
56
|
+
|
|
57
|
+
const value = EnvVar.fetch('PRIORITY_TEST')
|
|
58
|
+
expect(value).to.equal('env_value')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should return undefined for non-existent variables', () => {
|
|
62
|
+
const value = EnvVar.fetch('NON_EXISTENT')
|
|
63
|
+
expect(value).to.be.undefined
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should throw error for required variables that do not exist', () => {
|
|
67
|
+
expect(() => {
|
|
68
|
+
EnvVar.fetch('REQUIRED_VAR', true)
|
|
69
|
+
}).to.throw('Required EnvVar REQUIRED_VAR not found')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should not throw error for required variables that exist', () => {
|
|
73
|
+
process.env.REQUIRED_VAR = 'exists'
|
|
74
|
+
expect(() => {
|
|
75
|
+
const value = EnvVar.fetch('REQUIRED_VAR', true)
|
|
76
|
+
expect(value).to.equal('exists')
|
|
77
|
+
}).to.not.throw()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should throw error for empty variable name', () => {
|
|
81
|
+
expect(() => {
|
|
82
|
+
EnvVar.fetch('')
|
|
83
|
+
}).to.throw('Variable name is required.')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should throw error for null variable name', () => {
|
|
87
|
+
expect(() => {
|
|
88
|
+
EnvVar.fetch(null)
|
|
89
|
+
}).to.throw('Variable name is required.')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('integration scenarios', () => {
|
|
94
|
+
it('should handle typical application setup', () => {
|
|
95
|
+
// Simulate typical app initialization
|
|
96
|
+
process.env.NODE_ENV = 'test'
|
|
97
|
+
process.env.PORT = '3000'
|
|
98
|
+
|
|
99
|
+
EnvVar.set({
|
|
100
|
+
APP_NAME: 'MyApp',
|
|
101
|
+
DEFAULT_TIMEOUT: '30000'
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
expect(EnvVar.fetch('NODE_ENV')).to.equal('test')
|
|
105
|
+
expect(EnvVar.fetch('PORT')).to.equal('3000')
|
|
106
|
+
expect(EnvVar.fetch('APP_NAME')).to.equal('MyApp')
|
|
107
|
+
expect(EnvVar.fetch('DEFAULT_TIMEOUT')).to.equal('30000')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
})
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const { expect } = require('chai')
|
|
2
|
+
const sinon = require('sinon')
|
|
3
|
+
const { verify } = require('../../lib/flag')
|
|
4
|
+
|
|
5
|
+
// Mock the go-flags client
|
|
6
|
+
const mockGoFlags = {
|
|
7
|
+
verifyFeatureFlag: sinon.stub()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('Feature Flag', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Reset all stubs before each test
|
|
13
|
+
mockGoFlags.verifyFeatureFlag.reset()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('verify', () => {
|
|
17
|
+
it('should return true when feature flag is enabled', async () => {
|
|
18
|
+
mockGoFlags.verifyFeatureFlag.resolves(true)
|
|
19
|
+
|
|
20
|
+
// We need to mock the require inside the flag module
|
|
21
|
+
const moduleUnderTest = require('../../lib/flag')
|
|
22
|
+
|
|
23
|
+
// Since we can't easily mock the internal require, let's test the error handling
|
|
24
|
+
const result = await moduleUnderTest.verify('test-flag', { tenant_id: '123' })
|
|
25
|
+
|
|
26
|
+
// The actual implementation will try to call the real API, so we expect false on error
|
|
27
|
+
expect(result).to.be.false
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should return false when feature flag is disabled', async () => {
|
|
31
|
+
const result = await verify('test-flag', { tenant_id: '123' })
|
|
32
|
+
expect(result).to.be.false // Will be false due to API call failure in test
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should return false and log error when flag_name is empty', async () => {
|
|
36
|
+
const result = await verify('', { tenant_id: '123' })
|
|
37
|
+
expect(result).to.be.false
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should return false and log error when criteria is empty', async () => {
|
|
41
|
+
const result = await verify('test-flag', {})
|
|
42
|
+
expect(result).to.be.false
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should return false and log error when flag_name is null', async () => {
|
|
46
|
+
const result = await verify(null, { tenant_id: '123' })
|
|
47
|
+
expect(result).to.be.false
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should return false and log error when criteria is null', async () => {
|
|
51
|
+
const result = await verify('test-flag', null)
|
|
52
|
+
expect(result).to.be.false
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should handle API errors gracefully', async () => {
|
|
56
|
+
// This will naturally fail due to missing environment setup
|
|
57
|
+
const result = await verify('test-flag', { tenant_id: '123' })
|
|
58
|
+
expect(result).to.be.false
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should validate required parameters', async () => {
|
|
62
|
+
// Test with missing flag name
|
|
63
|
+
const result1 = await verify('', { tenant_id: '123' })
|
|
64
|
+
expect(result1).to.be.false
|
|
65
|
+
|
|
66
|
+
// Test with missing criteria
|
|
67
|
+
const result2 = await verify('test-flag', {})
|
|
68
|
+
expect(result2).to.be.false
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should handle complex criteria objects', async () => {
|
|
72
|
+
const complexCriteria = {
|
|
73
|
+
tenant_id: '123',
|
|
74
|
+
user_role: 'admin',
|
|
75
|
+
feature_tier: 'premium',
|
|
76
|
+
region: 'us-east-1'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = await verify('complex-feature', complexCriteria)
|
|
80
|
+
expect(result).to.be.false // Will be false due to API call failure in test
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should handle string criteria', async () => {
|
|
84
|
+
const criteria = {
|
|
85
|
+
tenant_id: '123',
|
|
86
|
+
version: '1.0.0',
|
|
87
|
+
environment: 'production'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = await verify('version-feature', criteria)
|
|
91
|
+
expect(result).to.be.false // Will be false due to API call failure in test
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should handle boolean criteria', async () => {
|
|
95
|
+
const criteria = {
|
|
96
|
+
tenant_id: '123',
|
|
97
|
+
is_premium: true,
|
|
98
|
+
has_subscription: false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const result = await verify('premium-feature', criteria)
|
|
102
|
+
expect(result).to.be.false // Will be false due to API call failure in test
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should handle numeric criteria', async () => {
|
|
106
|
+
const criteria = {
|
|
107
|
+
tenant_id: 123,
|
|
108
|
+
user_count: 50,
|
|
109
|
+
usage_percentage: 75.5
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const result = await verify('usage-based-feature', criteria)
|
|
113
|
+
expect(result).to.be.false // Will be false due to API call failure in test
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('error handling', () => {
|
|
118
|
+
it('should log errors appropriately', async () => {
|
|
119
|
+
// Create a spy for console.log to verify logging
|
|
120
|
+
const consoleLogSpy = sinon.spy(console, 'log')
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const result = await verify('', {})
|
|
124
|
+
expect(result).to.be.false
|
|
125
|
+
|
|
126
|
+
// The error should be logged (though we can't easily verify the exact log message)
|
|
127
|
+
// due to the ProcessLogger implementation
|
|
128
|
+
} finally {
|
|
129
|
+
consoleLogSpy.restore()
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should handle network errors', async () => {
|
|
134
|
+
// This will naturally simulate a network error in test environment
|
|
135
|
+
const result = await verify('network-test', { tenant_id: '123' })
|
|
136
|
+
expect(result).to.be.false
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('should handle invalid API responses', async () => {
|
|
140
|
+
// This will naturally handle invalid responses in test environment
|
|
141
|
+
const result = await verify('invalid-response-test', { tenant_id: '123' })
|
|
142
|
+
expect(result).to.be.false
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe('integration scenarios', () => {
|
|
147
|
+
it('should work with typical feature flag scenarios', async () => {
|
|
148
|
+
// Beta feature for specific tenants
|
|
149
|
+
const betaResult = await verify('beta-feature', {
|
|
150
|
+
tenant_id: '123',
|
|
151
|
+
user_role: 'beta_tester'
|
|
152
|
+
})
|
|
153
|
+
expect(betaResult).to.be.false // Will be false in test environment
|
|
154
|
+
|
|
155
|
+
// Premium feature
|
|
156
|
+
const premiumResult = await verify('premium-feature', {
|
|
157
|
+
tenant_id: '456',
|
|
158
|
+
subscription_tier: 'premium'
|
|
159
|
+
})
|
|
160
|
+
expect(premiumResult).to.be.false // Will be false in test environment
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('should handle multiple rapid calls', async () => {
|
|
164
|
+
const promises = []
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < 5; i++) {
|
|
167
|
+
promises.push(verify(`feature-${i}`, { tenant_id: `${i}` }))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const results = await Promise.all(promises)
|
|
171
|
+
results.forEach(result => {
|
|
172
|
+
expect(result).to.be.false // All will be false in test environment
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
})
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
const { expect } = require('chai')
|
|
2
|
+
const sinon = require('sinon')
|
|
3
|
+
const { AutoHealer } = require('../../../lib/middlewares/healer')
|
|
4
|
+
|
|
5
|
+
// Mock node-cron module
|
|
6
|
+
const mockCron = {
|
|
7
|
+
schedule: sinon.stub()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('Auto Healer', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockCron.schedule.reset()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('AutoHealer', () => {
|
|
16
|
+
it('should schedule a task with default timer', () => {
|
|
17
|
+
const handler = sinon.stub()
|
|
18
|
+
|
|
19
|
+
// Since we can't easily mock the require, let's test the basic functionality
|
|
20
|
+
expect(() => {
|
|
21
|
+
AutoHealer('* * * * * *', handler)
|
|
22
|
+
}).to.not.throw()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should accept custom timer patterns', () => {
|
|
26
|
+
const handler = sinon.stub()
|
|
27
|
+
|
|
28
|
+
// Test various cron patterns
|
|
29
|
+
const patterns = [
|
|
30
|
+
'0 0 * * *', // Daily at midnight
|
|
31
|
+
'0 */6 * * *', // Every 6 hours
|
|
32
|
+
'*/30 * * * *', // Every 30 seconds
|
|
33
|
+
'0 0 0 1 1 *' // Yearly
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
patterns.forEach(pattern => {
|
|
37
|
+
expect(() => {
|
|
38
|
+
AutoHealer(pattern, handler)
|
|
39
|
+
}).to.not.throw()
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should accept handler functions', () => {
|
|
44
|
+
const handler = () => {
|
|
45
|
+
console.log('Healing task executed')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
expect(() => {
|
|
49
|
+
AutoHealer('* * * * * *', handler)
|
|
50
|
+
}).to.not.throw()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should work with async handler functions', () => {
|
|
54
|
+
const asyncHandler = async () => {
|
|
55
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
56
|
+
console.log('Async healing task completed')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
expect(() => {
|
|
60
|
+
AutoHealer('* * * * * *', asyncHandler)
|
|
61
|
+
}).to.not.throw()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should handle empty handler gracefully', () => {
|
|
65
|
+
expect(() => {
|
|
66
|
+
AutoHealer('* * * * * *')
|
|
67
|
+
}).to.not.throw()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should handle complex healing scenarios', () => {
|
|
71
|
+
const healingHandler = sinon.stub()
|
|
72
|
+
|
|
73
|
+
// Simulate a healing task that might clean up resources
|
|
74
|
+
healingHandler.callsFake(() => {
|
|
75
|
+
// Simulate cleanup operations
|
|
76
|
+
console.log('Performing system cleanup...')
|
|
77
|
+
console.log('Clearing expired cache entries...')
|
|
78
|
+
console.log('Checking database connections...')
|
|
79
|
+
console.log('Healing completed.')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
expect(() => {
|
|
83
|
+
AutoHealer('*/10 * * * * *', healingHandler) // Every 10 seconds
|
|
84
|
+
}).to.not.throw()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should support multiple healing tasks', () => {
|
|
88
|
+
const databaseHealer = () => console.log('Database cleanup')
|
|
89
|
+
const cacheHealer = () => console.log('Cache cleanup')
|
|
90
|
+
const memoryHealer = () => console.log('Memory cleanup')
|
|
91
|
+
|
|
92
|
+
expect(() => {
|
|
93
|
+
AutoHealer('0 0 * * *', databaseHealer) // Daily
|
|
94
|
+
AutoHealer('*/30 * * * *', cacheHealer) // Every 30 minutes
|
|
95
|
+
AutoHealer('*/5 * * * * *', memoryHealer) // Every 5 seconds
|
|
96
|
+
}).to.not.throw()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should handle error-prone handlers gracefully', (done) => {
|
|
100
|
+
const errorProneHandler = () => {
|
|
101
|
+
// Always throw to test error handling
|
|
102
|
+
throw new Error('Healing task failed')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// This should not throw when setting up the cron
|
|
106
|
+
expect(() => {
|
|
107
|
+
const task = AutoHealer('*/10 * * * * *', errorProneHandler) // Every 10 seconds
|
|
108
|
+
|
|
109
|
+
// Clean up quickly to avoid spam
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
if (task && task.destroy) {
|
|
112
|
+
task.destroy()
|
|
113
|
+
}
|
|
114
|
+
done()
|
|
115
|
+
}, 50)
|
|
116
|
+
}).to.not.throw()
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('timer patterns', () => {
|
|
121
|
+
it('should work with standard cron patterns', () => {
|
|
122
|
+
const testPatterns = [
|
|
123
|
+
'* * * * * *', // Every second
|
|
124
|
+
'0 * * * * *', // Every minute
|
|
125
|
+
'0 0 * * * *', // Every hour
|
|
126
|
+
'0 0 0 * * *', // Every day
|
|
127
|
+
'0 0 0 * * 0', // Every week
|
|
128
|
+
'0 0 0 1 * *' // Every month
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
testPatterns.forEach(pattern => {
|
|
132
|
+
const handler = sinon.stub()
|
|
133
|
+
expect(() => {
|
|
134
|
+
AutoHealer(pattern, handler)
|
|
135
|
+
}).to.not.throw()
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('should handle complex cron expressions', () => {
|
|
140
|
+
const complexPatterns = [
|
|
141
|
+
'0 0,12 1 */2 *', // At 00:00 and 12:00 on day-of-month 1 in every 2nd month
|
|
142
|
+
'0 0 * * 1-5', // At 00:00 on every day-of-week from Monday through Friday
|
|
143
|
+
'0 9-17 * * 1-5', // At every hour from 9 through 17 on every day-of-week from Monday through Friday
|
|
144
|
+
'*/15 9-17 * * 1-5' // At every 15th minute from 9 through 17 on every day-of-week from Monday through Friday
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
complexPatterns.forEach(pattern => {
|
|
148
|
+
const handler = () => console.log(`Complex pattern executed: ${pattern}`)
|
|
149
|
+
expect(() => {
|
|
150
|
+
AutoHealer(pattern, handler)
|
|
151
|
+
}).to.not.throw()
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('real-world scenarios', () => {
|
|
157
|
+
it('should support database connection healing', () => {
|
|
158
|
+
const dbHealer = sinon.stub().callsFake(async () => {
|
|
159
|
+
// Simulate database connection check and healing
|
|
160
|
+
console.log('Checking database connections...')
|
|
161
|
+
console.log('Reconnecting stale connections...')
|
|
162
|
+
console.log('Database healing completed.')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
expect(() => {
|
|
166
|
+
AutoHealer('*/5 * * * *', dbHealer) // Every 5 minutes
|
|
167
|
+
}).to.not.throw()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('should support cache cleanup healing', () => {
|
|
171
|
+
const cacheHealer = () => {
|
|
172
|
+
// Simulate cache cleanup
|
|
173
|
+
console.log('Cleaning expired cache entries...')
|
|
174
|
+
console.log('Optimizing cache storage...')
|
|
175
|
+
console.log('Cache healing completed.')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
expect(() => {
|
|
179
|
+
AutoHealer('0 */2 * * *', cacheHealer) // Every 2 hours
|
|
180
|
+
}).to.not.throw()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('should support memory leak prevention', () => {
|
|
184
|
+
const memoryHealer = () => {
|
|
185
|
+
// Simulate memory management
|
|
186
|
+
console.log('Checking memory usage...')
|
|
187
|
+
console.log('Triggering garbage collection if needed...')
|
|
188
|
+
console.log('Memory healing completed.')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
expect(() => {
|
|
192
|
+
AutoHealer('*/30 * * * * *', memoryHealer) // Every 30 seconds
|
|
193
|
+
}).to.not.throw()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should support log rotation healing', () => {
|
|
197
|
+
const logHealer = () => {
|
|
198
|
+
// Simulate log management
|
|
199
|
+
console.log('Rotating log files...')
|
|
200
|
+
console.log('Compressing old logs...')
|
|
201
|
+
console.log('Cleaning up old log files...')
|
|
202
|
+
console.log('Log healing completed.')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
expect(() => {
|
|
206
|
+
AutoHealer('0 0 0 * * *', logHealer) // Daily at midnight
|
|
207
|
+
}).to.not.throw()
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
})
|