@friggframework/core 2.0.0-next.25 → 2.0.0-next.27
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/encrypt/encrypt.js +5 -33
- package/handlers/routers/HEALTHCHECK.md +240 -0
- package/handlers/routers/health.js +451 -0
- package/handlers/routers/health.test.js +203 -0
- package/package.json +5 -5
package/encrypt/encrypt.js
CHANGED
|
@@ -16,28 +16,18 @@ const findOneEvents = [
|
|
|
16
16
|
|
|
17
17
|
const shouldBypassEncryption = (STAGE) => {
|
|
18
18
|
const defaultBypassStages = ['dev', 'test', 'local'];
|
|
19
|
-
|
|
20
|
-
// If the env is set to anything or an empty string, use the env. Otherwise, use the default array
|
|
21
|
-
const useEnv = !String(bypassStageEnv) || !!bypassStageEnv;
|
|
22
|
-
const bypassStages = useEnv
|
|
23
|
-
? bypassStageEnv.split(',').map((stage) => stage.trim())
|
|
24
|
-
: defaultBypassStages;
|
|
25
|
-
return bypassStages.includes(STAGE);
|
|
19
|
+
return defaultBypassStages.includes(STAGE);
|
|
26
20
|
};
|
|
27
21
|
|
|
28
|
-
|
|
29
|
-
function Encrypt(schema, options) {
|
|
22
|
+
function Encrypt(schema) {
|
|
30
23
|
const { STAGE, KMS_KEY_ARN, AES_KEY_ID } = process.env;
|
|
31
24
|
|
|
32
25
|
if (shouldBypassEncryption(STAGE)) {
|
|
33
26
|
return;
|
|
34
27
|
}
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
'Local and AWS encryption keys are both set in the environment.'
|
|
39
|
-
);
|
|
40
|
-
}
|
|
29
|
+
const hasAES = AES_KEY_ID && AES_KEY_ID.trim() !== '';
|
|
30
|
+
const hasKMS = KMS_KEY_ARN && KMS_KEY_ARN.trim() !== '' && !hasAES;
|
|
41
31
|
|
|
42
32
|
const fields = Object.values(schema.paths)
|
|
43
33
|
.map(({ path, options }) => (options.lhEncrypt === true ? path : ''))
|
|
@@ -48,25 +38,17 @@ function Encrypt(schema, options) {
|
|
|
48
38
|
}
|
|
49
39
|
|
|
50
40
|
const cryptor = new Cryptor({
|
|
51
|
-
|
|
52
|
-
shouldUseAws: !!KMS_KEY_ARN,
|
|
53
|
-
// Find all the fields in the schema with lhEncrypt === true
|
|
41
|
+
shouldUseAws: hasKMS,
|
|
54
42
|
fields: fields,
|
|
55
43
|
});
|
|
56
44
|
|
|
57
|
-
// ---------------------------------------------
|
|
58
|
-
// ### Encrypt fields before save/update/insert.
|
|
59
|
-
// ---------------------------------------------
|
|
60
|
-
|
|
61
45
|
schema.pre('save', async function encryptionPreSave() {
|
|
62
|
-
// `this` will be a doc
|
|
63
46
|
await cryptor.encryptFieldsInDocuments([this]);
|
|
64
47
|
});
|
|
65
48
|
|
|
66
49
|
schema.pre(
|
|
67
50
|
'insertMany',
|
|
68
51
|
async function encryptionPreInsertMany(_, docs, options) {
|
|
69
|
-
// `this` will be the model
|
|
70
52
|
if (options?.rawResult) {
|
|
71
53
|
throw new Error(
|
|
72
54
|
'Raw result not supported for insertMany with Encrypt plugin'
|
|
@@ -78,17 +60,14 @@ function Encrypt(schema, options) {
|
|
|
78
60
|
);
|
|
79
61
|
|
|
80
62
|
schema.pre(updateOneEvents, async function encryptionPreUpdateOne() {
|
|
81
|
-
// `this` will be a query
|
|
82
63
|
await cryptor.encryptFieldsInQuery(this);
|
|
83
64
|
});
|
|
84
65
|
|
|
85
66
|
schema.pre('updateMany', async function encryptionPreUpdateMany() {
|
|
86
|
-
// `this` will be a query
|
|
87
67
|
cryptor.expectNotToUpdateManyEncrypted(this.getUpdate());
|
|
88
68
|
});
|
|
89
69
|
|
|
90
70
|
schema.pre('update', async function encryptionPreUpdate() {
|
|
91
|
-
// `this` will be a query
|
|
92
71
|
const { multiple } = this.getOptions();
|
|
93
72
|
|
|
94
73
|
if (multiple) {
|
|
@@ -99,16 +78,11 @@ function Encrypt(schema, options) {
|
|
|
99
78
|
await cryptor.encryptFieldsInQuery(this);
|
|
100
79
|
});
|
|
101
80
|
|
|
102
|
-
// --------------------------------------------
|
|
103
|
-
// ### Decrypt documents after they are loaded.
|
|
104
|
-
// --------------------------------------------
|
|
105
81
|
schema.post('save', async function encryptionPreSave() {
|
|
106
|
-
// `this` will be a doc
|
|
107
82
|
await cryptor.decryptFieldsInDocuments([this]);
|
|
108
83
|
});
|
|
109
84
|
|
|
110
85
|
schema.post(findOneEvents, async function encryptionPostFindOne(doc) {
|
|
111
|
-
// `this` will be a query
|
|
112
86
|
const { rawResult } = this.getOptions();
|
|
113
87
|
|
|
114
88
|
if (rawResult) {
|
|
@@ -119,12 +93,10 @@ function Encrypt(schema, options) {
|
|
|
119
93
|
});
|
|
120
94
|
|
|
121
95
|
schema.post('find', async function encryptionPostFind(docs) {
|
|
122
|
-
// `this` will be a query
|
|
123
96
|
await cryptor.decryptFieldsInDocuments(docs);
|
|
124
97
|
});
|
|
125
98
|
|
|
126
99
|
schema.post('insertMany', async function encryptionPostInsertMany(docs) {
|
|
127
|
-
// `this` will be the model
|
|
128
100
|
await cryptor.decryptFieldsInDocuments(docs);
|
|
129
101
|
});
|
|
130
102
|
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Frigg Healthcheck Endpoint Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Frigg service includes comprehensive healthcheck endpoints to monitor service health, connectivity, and readiness. These endpoints follow industry best practices and are designed for use with monitoring systems, load balancers, and container orchestration platforms.
|
|
6
|
+
|
|
7
|
+
## Endpoints
|
|
8
|
+
|
|
9
|
+
### 1. Basic Health Check
|
|
10
|
+
**GET** `/health`
|
|
11
|
+
|
|
12
|
+
Simple health check endpoint that returns basic service information. No authentication required. This endpoint is rate-limited at the API Gateway level.
|
|
13
|
+
|
|
14
|
+
**Response:**
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"status": "ok",
|
|
18
|
+
"timestamp": "2024-01-10T12:00:00.000Z",
|
|
19
|
+
"service": "frigg-core-api"
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Status Codes:**
|
|
24
|
+
- `200 OK` - Service is running
|
|
25
|
+
|
|
26
|
+
### 2. Detailed Health Check
|
|
27
|
+
**GET** `/health/detailed`
|
|
28
|
+
|
|
29
|
+
Comprehensive health check that tests all service components and dependencies.
|
|
30
|
+
|
|
31
|
+
**Authentication Required:**
|
|
32
|
+
- Header: `x-api-key: YOUR_API_KEY`
|
|
33
|
+
- The API key must match the `HEALTH_API_KEY` environment variable
|
|
34
|
+
|
|
35
|
+
**Response:**
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"service": "frigg-core-api",
|
|
39
|
+
"status": "healthy", // "healthy" or "unhealthy"
|
|
40
|
+
"timestamp": "2024-01-10T12:00:00.000Z",
|
|
41
|
+
"checks": {
|
|
42
|
+
"database": {
|
|
43
|
+
"status": "healthy",
|
|
44
|
+
"state": "connected",
|
|
45
|
+
"responseTime": 5 // milliseconds
|
|
46
|
+
},
|
|
47
|
+
"externalApis": {
|
|
48
|
+
"github": {
|
|
49
|
+
"status": "healthy",
|
|
50
|
+
"statusCode": 200,
|
|
51
|
+
"responseTime": 150,
|
|
52
|
+
"reachable": true
|
|
53
|
+
},
|
|
54
|
+
"npm": {
|
|
55
|
+
"status": "healthy",
|
|
56
|
+
"statusCode": 200,
|
|
57
|
+
"responseTime": 200,
|
|
58
|
+
"reachable": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"integrations": {
|
|
62
|
+
"status": "healthy",
|
|
63
|
+
"modules": {
|
|
64
|
+
"count": 10,
|
|
65
|
+
"available": ["module1", "module2", "..."]
|
|
66
|
+
},
|
|
67
|
+
"integrations": {
|
|
68
|
+
"count": 5,
|
|
69
|
+
"available": ["integration1", "integration2", "..."]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"responseTime": 250 // total endpoint response time in milliseconds
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Status Codes:**
|
|
78
|
+
- `200 OK` - Service is healthy (all components operational)
|
|
79
|
+
- `503 Service Unavailable` - Service is unhealthy (any component failure)
|
|
80
|
+
- `401 Unauthorized` - Missing or invalid x-api-key header
|
|
81
|
+
|
|
82
|
+
### 3. Liveness Probe
|
|
83
|
+
**GET** `/health/live`
|
|
84
|
+
|
|
85
|
+
Kubernetes-style liveness probe. Returns whether the service process is alive.
|
|
86
|
+
|
|
87
|
+
**Authentication Required:**
|
|
88
|
+
- Header: `x-api-key: YOUR_API_KEY`
|
|
89
|
+
|
|
90
|
+
**Response:**
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"status": "alive",
|
|
94
|
+
"timestamp": "2024-01-10T12:00:00.000Z"
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Status Codes:**
|
|
99
|
+
- `200 OK` - Service process is alive
|
|
100
|
+
|
|
101
|
+
### 4. Readiness Probe
|
|
102
|
+
**GET** `/health/ready`
|
|
103
|
+
|
|
104
|
+
Kubernetes-style readiness probe. Returns whether the service is ready to receive traffic.
|
|
105
|
+
|
|
106
|
+
**Authentication Required:**
|
|
107
|
+
- Header: `x-api-key: YOUR_API_KEY`
|
|
108
|
+
|
|
109
|
+
**Response:**
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"ready": true,
|
|
113
|
+
"timestamp": "2024-01-10T12:00:00.000Z",
|
|
114
|
+
"checks": {
|
|
115
|
+
"database": true,
|
|
116
|
+
"modules": true
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Status Codes:**
|
|
122
|
+
- `200 OK` - Service is ready
|
|
123
|
+
- `503 Service Unavailable` - Service is not ready
|
|
124
|
+
|
|
125
|
+
## Health Status Definitions
|
|
126
|
+
|
|
127
|
+
- **healthy**: All components are functioning normally
|
|
128
|
+
- **unhealthy**: Any component is failing, service may not function properly
|
|
129
|
+
|
|
130
|
+
## Component Checks
|
|
131
|
+
|
|
132
|
+
### Database Connectivity
|
|
133
|
+
- Checks database connection state
|
|
134
|
+
- Performs ping test with 2-second timeout if connected
|
|
135
|
+
- Reports connection state and response time
|
|
136
|
+
- Database type is not exposed for security reasons
|
|
137
|
+
|
|
138
|
+
### External API Connectivity
|
|
139
|
+
- Tests connectivity to external services (GitHub, npm registry)
|
|
140
|
+
- Configurable timeout (default: 5 seconds)
|
|
141
|
+
- Reports reachability and response times
|
|
142
|
+
- Uses Promise.all for parallel checking
|
|
143
|
+
|
|
144
|
+
### Integration Status
|
|
145
|
+
- Verifies available modules and integrations are loaded
|
|
146
|
+
- Reports counts and lists of available components
|
|
147
|
+
|
|
148
|
+
## Usage Examples
|
|
149
|
+
|
|
150
|
+
### Monitoring Systems
|
|
151
|
+
Configure your monitoring system to poll `/health/detailed` every 30-60 seconds:
|
|
152
|
+
```bash
|
|
153
|
+
curl -H "x-api-key: YOUR_API_KEY" https://your-frigg-instance.com/health/detailed
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Load Balancer Health Checks
|
|
157
|
+
Configure load balancers to use the simple `/health` endpoint:
|
|
158
|
+
```bash
|
|
159
|
+
curl https://your-frigg-instance.com/health
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Kubernetes Configuration
|
|
163
|
+
```yaml
|
|
164
|
+
livenessProbe:
|
|
165
|
+
httpGet:
|
|
166
|
+
path: /health/live
|
|
167
|
+
port: 8080
|
|
168
|
+
httpHeaders:
|
|
169
|
+
- name: x-api-key
|
|
170
|
+
value: YOUR_API_KEY
|
|
171
|
+
periodSeconds: 10
|
|
172
|
+
timeoutSeconds: 5
|
|
173
|
+
|
|
174
|
+
readinessProbe:
|
|
175
|
+
httpGet:
|
|
176
|
+
path: /health/ready
|
|
177
|
+
port: 8080
|
|
178
|
+
httpHeaders:
|
|
179
|
+
- name: x-api-key
|
|
180
|
+
value: YOUR_API_KEY
|
|
181
|
+
initialDelaySeconds: 30
|
|
182
|
+
periodSeconds: 10
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Customization
|
|
186
|
+
|
|
187
|
+
### Adding External API Checks
|
|
188
|
+
To add more external API checks, modify the `externalAPIs` array in the health router:
|
|
189
|
+
```javascript
|
|
190
|
+
const externalAPIs = [
|
|
191
|
+
{ name: 'github', url: 'https://api.github.com/status' },
|
|
192
|
+
{ name: 'npm', url: 'https://registry.npmjs.org' },
|
|
193
|
+
{ name: 'your-api', url: 'https://your-api.com/health' }
|
|
194
|
+
];
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Adjusting Timeouts
|
|
198
|
+
The default timeout for external API checks is 5 seconds. Database ping timeout is set to 2 seconds:
|
|
199
|
+
```javascript
|
|
200
|
+
const checkExternalAPI = (url, timeout = 5000) => {
|
|
201
|
+
// ...
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
await mongoose.connection.db.admin().ping({ maxTimeMS: 2000 });
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Best Practices
|
|
208
|
+
|
|
209
|
+
1. **Authentication**: Basic `/health` endpoint requires no authentication, but detailed endpoints require `x-api-key` header
|
|
210
|
+
2. **Rate Limiting**: Configure rate limiting at the API Gateway level to prevent abuse
|
|
211
|
+
3. **Fast Response**: Health checks should respond quickly (< 1 second)
|
|
212
|
+
4. **Strict Status Codes**: Return 503 for any non-healthy state to ensure proper alerting
|
|
213
|
+
5. **Detailed Logging**: Failed health checks are logged for debugging
|
|
214
|
+
6. **Security**: No sensitive information (DB types, versions) exposed in responses
|
|
215
|
+
7. **Lambda Considerations**: Uptime and memory metrics not included as they're not relevant in serverless
|
|
216
|
+
|
|
217
|
+
## Troubleshooting
|
|
218
|
+
|
|
219
|
+
### Database Connection Issues
|
|
220
|
+
- Check `MONGO_URI` environment variable
|
|
221
|
+
- Verify network connectivity to MongoDB
|
|
222
|
+
- Check MongoDB server status
|
|
223
|
+
|
|
224
|
+
### External API Failures
|
|
225
|
+
- May indicate network issues or external service downtime
|
|
226
|
+
- Service reports "unhealthy" status if any external API is unreachable
|
|
227
|
+
|
|
228
|
+
## Security Considerations
|
|
229
|
+
|
|
230
|
+
- Basic health endpoint requires no authentication for monitoring compatibility
|
|
231
|
+
- Detailed endpoints require `x-api-key` header authentication
|
|
232
|
+
- Health endpoints do not expose sensitive information
|
|
233
|
+
- Database connection strings and credentials are never included in responses
|
|
234
|
+
- External API checks use read-only endpoints
|
|
235
|
+
- Rate limiting should be configured at the API Gateway level
|
|
236
|
+
- Consider IP whitelisting for health endpoints in production
|
|
237
|
+
|
|
238
|
+
## Environment Variables
|
|
239
|
+
|
|
240
|
+
- `HEALTH_API_KEY`: Required API key for accessing detailed health endpoints
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
const { Router } = require('express');
|
|
2
|
+
const mongoose = require('mongoose');
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const { moduleFactory, integrationFactory } = require('./../backend-utils');
|
|
6
|
+
const { createAppHandler } = require('./../app-handler-helpers');
|
|
7
|
+
|
|
8
|
+
const router = Router();
|
|
9
|
+
|
|
10
|
+
const validateApiKey = (req, res, next) => {
|
|
11
|
+
const apiKey = req.headers['x-api-key'];
|
|
12
|
+
|
|
13
|
+
if (req.path === '/health') {
|
|
14
|
+
return next();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!apiKey || apiKey !== process.env.HEALTH_API_KEY) {
|
|
18
|
+
return res.status(401).json({
|
|
19
|
+
status: 'error',
|
|
20
|
+
message: 'Unauthorized'
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
next();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
router.use(validateApiKey);
|
|
28
|
+
|
|
29
|
+
const checkExternalAPI = (url, timeout = 5000) => {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const protocol = url.startsWith('https:') ? https : http;
|
|
32
|
+
const startTime = Date.now();
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const request = protocol.get(url, { timeout }, (res) => {
|
|
36
|
+
const responseTime = Date.now() - startTime;
|
|
37
|
+
resolve({
|
|
38
|
+
status: 'healthy',
|
|
39
|
+
statusCode: res.statusCode,
|
|
40
|
+
responseTime,
|
|
41
|
+
reachable: res.statusCode < 500
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
request.on('error', (error) => {
|
|
46
|
+
resolve({
|
|
47
|
+
status: 'unhealthy',
|
|
48
|
+
error: error.message,
|
|
49
|
+
responseTime: Date.now() - startTime,
|
|
50
|
+
reachable: false
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
request.on('timeout', () => {
|
|
55
|
+
request.destroy();
|
|
56
|
+
resolve({
|
|
57
|
+
status: 'timeout',
|
|
58
|
+
error: 'Request timeout',
|
|
59
|
+
responseTime: timeout,
|
|
60
|
+
reachable: false
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
resolve({
|
|
65
|
+
status: 'error',
|
|
66
|
+
error: error.message,
|
|
67
|
+
responseTime: Date.now() - startTime,
|
|
68
|
+
reachable: false
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const getDatabaseState = () => {
|
|
75
|
+
const stateMap = {
|
|
76
|
+
0: 'disconnected',
|
|
77
|
+
1: 'connected',
|
|
78
|
+
2: 'connecting',
|
|
79
|
+
3: 'disconnecting'
|
|
80
|
+
};
|
|
81
|
+
const readyState = mongoose.connection.readyState;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
readyState,
|
|
85
|
+
stateName: stateMap[readyState],
|
|
86
|
+
isConnected: readyState === 1
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const checkDatabaseHealth = async () => {
|
|
91
|
+
const { stateName, isConnected } = getDatabaseState();
|
|
92
|
+
const result = {
|
|
93
|
+
status: isConnected ? 'healthy' : 'unhealthy',
|
|
94
|
+
state: stateName
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (isConnected) {
|
|
98
|
+
const pingStart = Date.now();
|
|
99
|
+
await mongoose.connection.db.admin().ping({ maxTimeMS: 2000 });
|
|
100
|
+
result.responseTime = Date.now() - pingStart;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const getEncryptionConfiguration = () => {
|
|
107
|
+
const { STAGE, BYPASS_ENCRYPTION_STAGE, KMS_KEY_ARN, AES_KEY_ID } = process.env;
|
|
108
|
+
|
|
109
|
+
const defaultBypassStages = ['dev', 'test', 'local'];
|
|
110
|
+
const useEnv = BYPASS_ENCRYPTION_STAGE !== undefined;
|
|
111
|
+
const bypassStages = useEnv
|
|
112
|
+
? BYPASS_ENCRYPTION_STAGE.split(',').map((s) => s.trim())
|
|
113
|
+
: defaultBypassStages;
|
|
114
|
+
|
|
115
|
+
const isBypassed = bypassStages.includes(STAGE);
|
|
116
|
+
const hasAES = AES_KEY_ID && AES_KEY_ID.trim() !== '';
|
|
117
|
+
const hasKMS = KMS_KEY_ARN && KMS_KEY_ARN.trim() !== '' && !hasAES;
|
|
118
|
+
const mode = hasAES ? 'aes' : hasKMS ? 'kms' : 'none';
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
stage: STAGE || null,
|
|
122
|
+
isBypassed,
|
|
123
|
+
hasAES,
|
|
124
|
+
hasKMS,
|
|
125
|
+
mode,
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const createTestEncryptionModel = () => {
|
|
130
|
+
const { Encrypt } = require('./../../encrypt');
|
|
131
|
+
|
|
132
|
+
const testSchema = new mongoose.Schema({
|
|
133
|
+
testSecret: { type: String, lhEncrypt: true },
|
|
134
|
+
normalField: { type: String },
|
|
135
|
+
nestedSecret: {
|
|
136
|
+
value: { type: String, lhEncrypt: true }
|
|
137
|
+
}
|
|
138
|
+
}, { timestamps: false });
|
|
139
|
+
|
|
140
|
+
testSchema.plugin(Encrypt);
|
|
141
|
+
|
|
142
|
+
return mongoose.models.TestEncryption ||
|
|
143
|
+
mongoose.model('TestEncryption', testSchema);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const createTestDocument = async (TestModel) => {
|
|
147
|
+
const testData = {
|
|
148
|
+
testSecret: 'This is a secret value that should be encrypted',
|
|
149
|
+
normalField: 'This is a normal field that should not be encrypted',
|
|
150
|
+
nestedSecret: {
|
|
151
|
+
value: 'This is a nested secret that should be encrypted'
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const testDoc = new TestModel(testData);
|
|
156
|
+
await testDoc.save();
|
|
157
|
+
|
|
158
|
+
return { testDoc, testData };
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const verifyDecryption = (retrievedDoc, originalData) => {
|
|
162
|
+
return retrievedDoc &&
|
|
163
|
+
retrievedDoc.testSecret === originalData.testSecret &&
|
|
164
|
+
retrievedDoc.normalField === originalData.normalField &&
|
|
165
|
+
retrievedDoc.nestedSecret?.value === originalData.nestedSecret.value;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const verifyEncryptionInDatabase = async (testDoc, originalData, TestModel) => {
|
|
169
|
+
const collectionName = TestModel.collection.name;
|
|
170
|
+
const rawDoc = await mongoose.connection.db
|
|
171
|
+
.collection(collectionName)
|
|
172
|
+
.findOne({ _id: testDoc._id });
|
|
173
|
+
|
|
174
|
+
const secretIsEncrypted = rawDoc &&
|
|
175
|
+
typeof rawDoc.testSecret === 'string' &&
|
|
176
|
+
rawDoc.testSecret.includes(':') &&
|
|
177
|
+
rawDoc.testSecret !== originalData.testSecret;
|
|
178
|
+
|
|
179
|
+
const nestedIsEncrypted = rawDoc?.nestedSecret?.value &&
|
|
180
|
+
typeof rawDoc.nestedSecret.value === 'string' &&
|
|
181
|
+
rawDoc.nestedSecret.value.includes(':') &&
|
|
182
|
+
rawDoc.nestedSecret.value !== originalData.nestedSecret.value;
|
|
183
|
+
|
|
184
|
+
const normalNotEncrypted = rawDoc &&
|
|
185
|
+
rawDoc.normalField === originalData.normalField;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
secretIsEncrypted,
|
|
189
|
+
nestedIsEncrypted,
|
|
190
|
+
normalNotEncrypted
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const evaluateEncryptionTestResults = (decryptionWorks, encryptionResults) => {
|
|
195
|
+
const { secretIsEncrypted, nestedIsEncrypted, normalNotEncrypted } = encryptionResults;
|
|
196
|
+
|
|
197
|
+
if (decryptionWorks && secretIsEncrypted && nestedIsEncrypted && normalNotEncrypted) {
|
|
198
|
+
return {
|
|
199
|
+
status: 'enabled',
|
|
200
|
+
testResult: 'Encryption and decryption verified successfully'
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (decryptionWorks && (!secretIsEncrypted || !nestedIsEncrypted)) {
|
|
205
|
+
return {
|
|
206
|
+
status: 'unhealthy',
|
|
207
|
+
testResult: 'Fields are not being encrypted in database'
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (decryptionWorks && !normalNotEncrypted) {
|
|
212
|
+
return {
|
|
213
|
+
status: 'unhealthy',
|
|
214
|
+
testResult: 'Normal fields are being incorrectly encrypted'
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
status: 'unhealthy',
|
|
220
|
+
testResult: 'Decryption failed or data mismatch'
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const testEncryption = async () => {
|
|
225
|
+
const TestModel = createTestEncryptionModel();
|
|
226
|
+
const { testDoc, testData } = await createTestDocument(TestModel);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const retrievedDoc = await TestModel.findById(testDoc._id);
|
|
230
|
+
const decryptionWorks = verifyDecryption(retrievedDoc, testData);
|
|
231
|
+
const encryptionResults = await verifyEncryptionInDatabase(testDoc, testData, TestModel);
|
|
232
|
+
|
|
233
|
+
const evaluation = evaluateEncryptionTestResults(decryptionWorks, encryptionResults);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
...evaluation,
|
|
237
|
+
encryptionWorks: decryptionWorks
|
|
238
|
+
};
|
|
239
|
+
} finally {
|
|
240
|
+
await TestModel.deleteOne({ _id: testDoc._id });
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const checkEncryptionHealth = async () => {
|
|
245
|
+
const config = getEncryptionConfiguration();
|
|
246
|
+
|
|
247
|
+
if (config.isBypassed || config.mode === 'none') {
|
|
248
|
+
const testResult = config.isBypassed
|
|
249
|
+
? 'Encryption bypassed for this stage'
|
|
250
|
+
: 'No encryption keys configured';
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
status: 'disabled',
|
|
254
|
+
mode: config.mode,
|
|
255
|
+
bypassed: config.isBypassed,
|
|
256
|
+
stage: config.stage,
|
|
257
|
+
testResult,
|
|
258
|
+
encryptionWorks: false,
|
|
259
|
+
debug: {
|
|
260
|
+
hasKMS: config.hasKMS,
|
|
261
|
+
hasAES: config.hasAES
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const testResults = await testEncryption();
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
...testResults,
|
|
271
|
+
mode: config.mode,
|
|
272
|
+
bypassed: config.isBypassed,
|
|
273
|
+
stage: config.stage,
|
|
274
|
+
debug: {
|
|
275
|
+
hasKMS: config.hasKMS,
|
|
276
|
+
hasAES: config.hasAES
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
} catch (error) {
|
|
280
|
+
return {
|
|
281
|
+
status: 'unhealthy',
|
|
282
|
+
mode: config.mode,
|
|
283
|
+
bypassed: config.isBypassed,
|
|
284
|
+
stage: config.stage,
|
|
285
|
+
testResult: `Encryption test failed: ${error.message}`,
|
|
286
|
+
encryptionWorks: false,
|
|
287
|
+
debug: {
|
|
288
|
+
hasKMS: config.hasKMS,
|
|
289
|
+
hasAES: config.hasAES
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const checkExternalAPIs = async () => {
|
|
296
|
+
const apis = [
|
|
297
|
+
{ name: 'github', url: 'https://api.github.com/status' },
|
|
298
|
+
{ name: 'npm', url: 'https://registry.npmjs.org' }
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
const results = await Promise.all(
|
|
302
|
+
apis.map(api =>
|
|
303
|
+
checkExternalAPI(api.url).then(result => ({ name: api.name, ...result }))
|
|
304
|
+
)
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const apiStatuses = {};
|
|
308
|
+
let allReachable = true;
|
|
309
|
+
|
|
310
|
+
results.forEach(({ name, ...checkResult }) => {
|
|
311
|
+
apiStatuses[name] = checkResult;
|
|
312
|
+
if (!checkResult.reachable) {
|
|
313
|
+
allReachable = false;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return { apiStatuses, allReachable };
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const checkIntegrations = () => {
|
|
321
|
+
const moduleTypes = Array.isArray(moduleFactory.moduleTypes)
|
|
322
|
+
? moduleFactory.moduleTypes
|
|
323
|
+
: [];
|
|
324
|
+
|
|
325
|
+
const integrationTypes = Array.isArray(integrationFactory.integrationTypes)
|
|
326
|
+
? integrationFactory.integrationTypes
|
|
327
|
+
: [];
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
status: 'healthy',
|
|
331
|
+
modules: {
|
|
332
|
+
count: moduleTypes.length,
|
|
333
|
+
available: moduleTypes,
|
|
334
|
+
},
|
|
335
|
+
integrations: {
|
|
336
|
+
count: integrationTypes.length,
|
|
337
|
+
available: integrationTypes,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const buildHealthCheckResponse = (startTime) => {
|
|
343
|
+
return {
|
|
344
|
+
service: 'frigg-core-api',
|
|
345
|
+
status: 'healthy',
|
|
346
|
+
timestamp: new Date().toISOString(),
|
|
347
|
+
checks: {},
|
|
348
|
+
calculateResponseTime: () => Date.now() - startTime
|
|
349
|
+
};
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
router.get('/health', async (_req, res) => {
|
|
353
|
+
const status = {
|
|
354
|
+
status: 'ok',
|
|
355
|
+
timestamp: new Date().toISOString(),
|
|
356
|
+
service: 'frigg-core-api'
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
res.status(200).json(status);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
router.get('/health/detailed', async (_req, res) => {
|
|
363
|
+
const startTime = Date.now();
|
|
364
|
+
const response = buildHealthCheckResponse(startTime);
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
response.checks.database = await checkDatabaseHealth();
|
|
368
|
+
const dbState = getDatabaseState();
|
|
369
|
+
if (!dbState.isConnected) {
|
|
370
|
+
response.status = 'unhealthy';
|
|
371
|
+
}
|
|
372
|
+
} catch (error) {
|
|
373
|
+
response.checks.database = {
|
|
374
|
+
status: 'unhealthy',
|
|
375
|
+
error: error.message
|
|
376
|
+
};
|
|
377
|
+
response.status = 'unhealthy';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
response.checks.encryption = await checkEncryptionHealth();
|
|
382
|
+
if (response.checks.encryption.status === 'unhealthy') {
|
|
383
|
+
response.status = 'unhealthy';
|
|
384
|
+
}
|
|
385
|
+
} catch (error) {
|
|
386
|
+
response.checks.encryption = {
|
|
387
|
+
status: 'unhealthy',
|
|
388
|
+
error: error.message
|
|
389
|
+
};
|
|
390
|
+
response.status = 'unhealthy';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const { apiStatuses, allReachable } = await checkExternalAPIs();
|
|
394
|
+
response.checks.externalApis = apiStatuses;
|
|
395
|
+
if (!allReachable) {
|
|
396
|
+
response.status = 'unhealthy';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
response.checks.integrations = checkIntegrations();
|
|
401
|
+
} catch (error) {
|
|
402
|
+
response.checks.integrations = {
|
|
403
|
+
status: 'unhealthy',
|
|
404
|
+
error: error.message
|
|
405
|
+
};
|
|
406
|
+
response.status = 'unhealthy';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
response.responseTime = response.calculateResponseTime();
|
|
410
|
+
delete response.calculateResponseTime;
|
|
411
|
+
|
|
412
|
+
const statusCode = response.status === 'healthy' ? 200 : 503;
|
|
413
|
+
res.status(statusCode).json(response);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
router.get('/health/live', (_req, res) => {
|
|
417
|
+
res.status(200).json({
|
|
418
|
+
status: 'alive',
|
|
419
|
+
timestamp: new Date().toISOString()
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
router.get('/health/ready', async (_req, res) => {
|
|
424
|
+
const dbState = getDatabaseState();
|
|
425
|
+
const isDbReady = dbState.isConnected;
|
|
426
|
+
|
|
427
|
+
let areModulesReady = false;
|
|
428
|
+
try {
|
|
429
|
+
const moduleTypes = Array.isArray(moduleFactory.moduleTypes)
|
|
430
|
+
? moduleFactory.moduleTypes
|
|
431
|
+
: [];
|
|
432
|
+
areModulesReady = moduleTypes.length > 0;
|
|
433
|
+
} catch (error) {
|
|
434
|
+
areModulesReady = false;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const isReady = isDbReady && areModulesReady;
|
|
438
|
+
|
|
439
|
+
res.status(isReady ? 200 : 503).json({
|
|
440
|
+
ready: isReady,
|
|
441
|
+
timestamp: new Date().toISOString(),
|
|
442
|
+
checks: {
|
|
443
|
+
database: isDbReady,
|
|
444
|
+
modules: areModulesReady
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const handler = createAppHandler('HTTP Event: Health', router);
|
|
450
|
+
|
|
451
|
+
module.exports = { handler, router };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
process.env.HEALTH_API_KEY = 'test-api-key';
|
|
2
|
+
|
|
3
|
+
jest.mock('mongoose', () => ({
|
|
4
|
+
set: jest.fn(),
|
|
5
|
+
connection: {
|
|
6
|
+
readyState: 1,
|
|
7
|
+
db: {
|
|
8
|
+
admin: () => ({
|
|
9
|
+
ping: jest.fn().mockResolvedValue(true)
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
jest.mock('./../backend-utils', () => ({
|
|
16
|
+
moduleFactory: {
|
|
17
|
+
moduleTypes: ['test-module', 'another-module']
|
|
18
|
+
},
|
|
19
|
+
integrationFactory: {
|
|
20
|
+
integrationTypes: ['test-integration', 'another-integration']
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
jest.mock('./../app-handler-helpers', () => ({
|
|
25
|
+
createAppHandler: jest.fn((name, router) => ({ name, router }))
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const { router } = require('./health');
|
|
29
|
+
const mongoose = require('mongoose');
|
|
30
|
+
|
|
31
|
+
const mockRequest = (path, headers = {}) => ({
|
|
32
|
+
path,
|
|
33
|
+
headers
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const mockResponse = () => {
|
|
37
|
+
const res = {};
|
|
38
|
+
res.status = jest.fn().mockReturnValue(res);
|
|
39
|
+
res.json = jest.fn().mockReturnValue(res);
|
|
40
|
+
return res;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe('Health Check Endpoints', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
mongoose.connection.readyState = 1;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('Middleware - validateApiKey', () => {
|
|
49
|
+
it('should allow access to /health without authentication', async () => {
|
|
50
|
+
expect(true).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('GET /health', () => {
|
|
55
|
+
it('should return basic health status', async () => {
|
|
56
|
+
const req = mockRequest('/health');
|
|
57
|
+
const res = mockResponse();
|
|
58
|
+
|
|
59
|
+
const routeHandler = router.stack.find(layer =>
|
|
60
|
+
layer.route && layer.route.path === '/health'
|
|
61
|
+
).route.stack[0].handle;
|
|
62
|
+
|
|
63
|
+
await routeHandler(req, res);
|
|
64
|
+
|
|
65
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
66
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
67
|
+
status: 'ok',
|
|
68
|
+
timestamp: expect.any(String),
|
|
69
|
+
service: 'frigg-core-api'
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('GET /health/detailed', () => {
|
|
75
|
+
it('should return detailed health status when healthy', async () => {
|
|
76
|
+
const req = mockRequest('/health/detailed', { 'x-api-key': 'test-api-key' });
|
|
77
|
+
const res = mockResponse();
|
|
78
|
+
|
|
79
|
+
const originalPromiseAll = Promise.all;
|
|
80
|
+
Promise.all = jest.fn().mockResolvedValue([
|
|
81
|
+
{ name: 'github', status: 'healthy', reachable: true, statusCode: 200, responseTime: 100 },
|
|
82
|
+
{ name: 'npm', status: 'healthy', reachable: true, statusCode: 200, responseTime: 150 }
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const routeHandler = router.stack.find(layer =>
|
|
86
|
+
layer.route && layer.route.path === '/health/detailed'
|
|
87
|
+
).route.stack[0].handle;
|
|
88
|
+
|
|
89
|
+
await routeHandler(req, res);
|
|
90
|
+
|
|
91
|
+
Promise.all = originalPromiseAll;
|
|
92
|
+
|
|
93
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
94
|
+
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
95
|
+
status: 'healthy',
|
|
96
|
+
service: 'frigg-core-api',
|
|
97
|
+
timestamp: expect.any(String),
|
|
98
|
+
checks: expect.objectContaining({
|
|
99
|
+
database: expect.objectContaining({
|
|
100
|
+
status: 'healthy',
|
|
101
|
+
state: 'connected'
|
|
102
|
+
}),
|
|
103
|
+
integrations: expect.objectContaining({
|
|
104
|
+
status: 'healthy'
|
|
105
|
+
})
|
|
106
|
+
}),
|
|
107
|
+
responseTime: expect.any(Number)
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
const response = res.json.mock.calls[0][0];
|
|
111
|
+
expect(response).not.toHaveProperty('version');
|
|
112
|
+
expect(response).not.toHaveProperty('uptime');
|
|
113
|
+
expect(response.checks).not.toHaveProperty('memory');
|
|
114
|
+
expect(response.checks.database).not.toHaveProperty('type');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should return 503 when database is disconnected', async () => {
|
|
118
|
+
mongoose.connection.readyState = 0;
|
|
119
|
+
|
|
120
|
+
const req = mockRequest('/health/detailed', { 'x-api-key': 'test-api-key' });
|
|
121
|
+
const res = mockResponse();
|
|
122
|
+
|
|
123
|
+
const originalPromiseAll = Promise.all;
|
|
124
|
+
Promise.all = jest.fn().mockResolvedValue([
|
|
125
|
+
{ name: 'github', status: 'healthy', reachable: true, statusCode: 200, responseTime: 100 },
|
|
126
|
+
{ name: 'npm', status: 'healthy', reachable: true, statusCode: 200, responseTime: 150 }
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
const routeHandler = router.stack.find(layer =>
|
|
130
|
+
layer.route && layer.route.path === '/health/detailed'
|
|
131
|
+
).route.stack[0].handle;
|
|
132
|
+
|
|
133
|
+
await routeHandler(req, res);
|
|
134
|
+
|
|
135
|
+
Promise.all = originalPromiseAll;
|
|
136
|
+
|
|
137
|
+
expect(res.status).toHaveBeenCalledWith(503);
|
|
138
|
+
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
139
|
+
status: 'unhealthy'
|
|
140
|
+
}));
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('GET /health/live', () => {
|
|
145
|
+
it('should return alive status', async () => {
|
|
146
|
+
const req = mockRequest('/health/live', { 'x-api-key': 'test-api-key' });
|
|
147
|
+
const res = mockResponse();
|
|
148
|
+
|
|
149
|
+
const routeHandler = router.stack.find(layer =>
|
|
150
|
+
layer.route && layer.route.path === '/health/live'
|
|
151
|
+
).route.stack[0].handle;
|
|
152
|
+
|
|
153
|
+
routeHandler(req, res);
|
|
154
|
+
|
|
155
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
156
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
157
|
+
status: 'alive',
|
|
158
|
+
timestamp: expect.any(String)
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('GET /health/ready', () => {
|
|
164
|
+
it('should return ready when all checks pass', async () => {
|
|
165
|
+
const req = mockRequest('/health/ready', { 'x-api-key': 'test-api-key' });
|
|
166
|
+
const res = mockResponse();
|
|
167
|
+
|
|
168
|
+
const routeHandler = router.stack.find(layer =>
|
|
169
|
+
layer.route && layer.route.path === '/health/ready'
|
|
170
|
+
).route.stack[0].handle;
|
|
171
|
+
|
|
172
|
+
await routeHandler(req, res);
|
|
173
|
+
|
|
174
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
175
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
176
|
+
ready: true,
|
|
177
|
+
timestamp: expect.any(String),
|
|
178
|
+
checks: {
|
|
179
|
+
database: true,
|
|
180
|
+
modules: true
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should return 503 when database is not connected', async () => {
|
|
186
|
+
mongoose.connection.readyState = 0;
|
|
187
|
+
|
|
188
|
+
const req = mockRequest('/health/ready', { 'x-api-key': 'test-api-key' });
|
|
189
|
+
const res = mockResponse();
|
|
190
|
+
|
|
191
|
+
const routeHandler = router.stack.find(layer =>
|
|
192
|
+
layer.route && layer.route.path === '/health/ready'
|
|
193
|
+
).route.stack[0].handle;
|
|
194
|
+
|
|
195
|
+
await routeHandler(req, res);
|
|
196
|
+
|
|
197
|
+
expect(res.status).toHaveBeenCalledWith(503);
|
|
198
|
+
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
199
|
+
ready: false
|
|
200
|
+
}));
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0-next.
|
|
4
|
+
"version": "2.0.0-next.27",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@hapi/boom": "^10.0.1",
|
|
7
7
|
"aws-sdk": "^2.1200.0",
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"uuid": "^9.0.1"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@friggframework/eslint-config": "2.0.0-next.
|
|
26
|
-
"@friggframework/prettier-config": "2.0.0-next.
|
|
27
|
-
"@friggframework/test": "2.0.0-next.
|
|
25
|
+
"@friggframework/eslint-config": "2.0.0-next.27",
|
|
26
|
+
"@friggframework/prettier-config": "2.0.0-next.27",
|
|
27
|
+
"@friggframework/test": "2.0.0-next.27",
|
|
28
28
|
"@types/lodash": "4.17.15",
|
|
29
29
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
30
30
|
"chai": "^4.3.6",
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
},
|
|
54
54
|
"homepage": "https://github.com/friggframework/frigg#readme",
|
|
55
55
|
"description": "",
|
|
56
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "82dec739e8d482b55f995eecf088ef05f7931188"
|
|
57
57
|
}
|