tlc-claude-code 1.4.8 → 1.4.9
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 +1 -1
- package/server/index.js +229 -14
- package/server/lib/compliance/control-mapper.js +401 -0
- package/server/lib/compliance/control-mapper.test.js +117 -0
- package/server/lib/compliance/evidence-linker.js +296 -0
- package/server/lib/compliance/evidence-linker.test.js +121 -0
- package/server/lib/compliance/gdpr-checklist.js +416 -0
- package/server/lib/compliance/gdpr-checklist.test.js +131 -0
- package/server/lib/compliance/hipaa-checklist.js +277 -0
- package/server/lib/compliance/hipaa-checklist.test.js +101 -0
- package/server/lib/compliance/iso27001-checklist.js +287 -0
- package/server/lib/compliance/iso27001-checklist.test.js +99 -0
- package/server/lib/compliance/multi-framework-reporter.js +284 -0
- package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
- package/server/lib/compliance/pci-dss-checklist.js +214 -0
- package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
- package/server/lib/compliance/trust-centre.js +187 -0
- package/server/lib/compliance/trust-centre.test.js +93 -0
- package/server/lib/dashboard/api-server.js +155 -0
- package/server/lib/dashboard/api-server.test.js +155 -0
- package/server/lib/dashboard/health-api.js +199 -0
- package/server/lib/dashboard/health-api.test.js +122 -0
- package/server/lib/dashboard/notes-api.js +234 -0
- package/server/lib/dashboard/notes-api.test.js +134 -0
- package/server/lib/dashboard/router-api.js +176 -0
- package/server/lib/dashboard/router-api.test.js +132 -0
- package/server/lib/dashboard/tasks-api.js +289 -0
- package/server/lib/dashboard/tasks-api.test.js +161 -0
- package/server/lib/dashboard/tlc-introspection.js +197 -0
- package/server/lib/dashboard/tlc-introspection.test.js +138 -0
- package/server/lib/dashboard/version-api.js +222 -0
- package/server/lib/dashboard/version-api.test.js +112 -0
- package/server/lib/dashboard/websocket-server.js +104 -0
- package/server/lib/dashboard/websocket-server.test.js +118 -0
- package/server/lib/deploy/branch-classifier.js +163 -0
- package/server/lib/deploy/branch-classifier.test.js +164 -0
- package/server/lib/deploy/deployment-approval.js +299 -0
- package/server/lib/deploy/deployment-approval.test.js +296 -0
- package/server/lib/deploy/deployment-audit.js +374 -0
- package/server/lib/deploy/deployment-audit.test.js +307 -0
- package/server/lib/deploy/deployment-executor.js +335 -0
- package/server/lib/deploy/deployment-executor.test.js +329 -0
- package/server/lib/deploy/deployment-rules.js +163 -0
- package/server/lib/deploy/deployment-rules.test.js +188 -0
- package/server/lib/deploy/rollback-manager.js +379 -0
- package/server/lib/deploy/rollback-manager.test.js +321 -0
- package/server/lib/deploy/security-gates.js +236 -0
- package/server/lib/deploy/security-gates.test.js +222 -0
- package/server/lib/k8s/gitops-config.js +188 -0
- package/server/lib/k8s/gitops-config.test.js +59 -0
- package/server/lib/k8s/helm-generator.js +196 -0
- package/server/lib/k8s/helm-generator.test.js +59 -0
- package/server/lib/k8s/kustomize-generator.js +176 -0
- package/server/lib/k8s/kustomize-generator.test.js +58 -0
- package/server/lib/k8s/network-policy.js +114 -0
- package/server/lib/k8s/network-policy.test.js +53 -0
- package/server/lib/k8s/pod-security.js +114 -0
- package/server/lib/k8s/pod-security.test.js +55 -0
- package/server/lib/k8s/rbac-generator.js +132 -0
- package/server/lib/k8s/rbac-generator.test.js +57 -0
- package/server/lib/k8s/resource-manager.js +172 -0
- package/server/lib/k8s/resource-manager.test.js +60 -0
- package/server/lib/k8s/secrets-encryption.js +168 -0
- package/server/lib/k8s/secrets-encryption.test.js +49 -0
- package/server/lib/monitoring/alert-manager.js +238 -0
- package/server/lib/monitoring/alert-manager.test.js +106 -0
- package/server/lib/monitoring/health-check.js +226 -0
- package/server/lib/monitoring/health-check.test.js +176 -0
- package/server/lib/monitoring/incident-manager.js +230 -0
- package/server/lib/monitoring/incident-manager.test.js +98 -0
- package/server/lib/monitoring/log-aggregator.js +147 -0
- package/server/lib/monitoring/log-aggregator.test.js +89 -0
- package/server/lib/monitoring/metrics-collector.js +337 -0
- package/server/lib/monitoring/metrics-collector.test.js +172 -0
- package/server/lib/monitoring/status-page.js +214 -0
- package/server/lib/monitoring/status-page.test.js +105 -0
- package/server/lib/monitoring/uptime-monitor.js +194 -0
- package/server/lib/monitoring/uptime-monitor.test.js +109 -0
- package/server/lib/network/fail2ban-config.js +294 -0
- package/server/lib/network/fail2ban-config.test.js +275 -0
- package/server/lib/network/firewall-manager.js +252 -0
- package/server/lib/network/firewall-manager.test.js +254 -0
- package/server/lib/network/geoip-filter.js +282 -0
- package/server/lib/network/geoip-filter.test.js +264 -0
- package/server/lib/network/rate-limiter.js +229 -0
- package/server/lib/network/rate-limiter.test.js +293 -0
- package/server/lib/network/request-validator.js +351 -0
- package/server/lib/network/request-validator.test.js +345 -0
- package/server/lib/network/security-headers.js +251 -0
- package/server/lib/network/security-headers.test.js +283 -0
- package/server/lib/network/tls-config.js +210 -0
- package/server/lib/network/tls-config.test.js +248 -0
- package/server/lib/security/auth-security.js +369 -0
- package/server/lib/security/auth-security.test.js +448 -0
- package/server/lib/security/cis-benchmark.js +152 -0
- package/server/lib/security/cis-benchmark.test.js +137 -0
- package/server/lib/security/compose-templates.js +312 -0
- package/server/lib/security/compose-templates.test.js +229 -0
- package/server/lib/security/container-runtime.js +456 -0
- package/server/lib/security/container-runtime.test.js +503 -0
- package/server/lib/security/cors-validator.js +278 -0
- package/server/lib/security/cors-validator.test.js +310 -0
- package/server/lib/security/crypto-utils.js +253 -0
- package/server/lib/security/crypto-utils.test.js +409 -0
- package/server/lib/security/dockerfile-linter.js +459 -0
- package/server/lib/security/dockerfile-linter.test.js +483 -0
- package/server/lib/security/dockerfile-templates.js +278 -0
- package/server/lib/security/dockerfile-templates.test.js +164 -0
- package/server/lib/security/error-sanitizer.js +426 -0
- package/server/lib/security/error-sanitizer.test.js +331 -0
- package/server/lib/security/headers-generator.js +368 -0
- package/server/lib/security/headers-generator.test.js +398 -0
- package/server/lib/security/image-scanner.js +83 -0
- package/server/lib/security/image-scanner.test.js +106 -0
- package/server/lib/security/input-validator.js +352 -0
- package/server/lib/security/input-validator.test.js +330 -0
- package/server/lib/security/network-policy.js +174 -0
- package/server/lib/security/network-policy.test.js +164 -0
- package/server/lib/security/output-encoder.js +237 -0
- package/server/lib/security/output-encoder.test.js +276 -0
- package/server/lib/security/path-validator.js +359 -0
- package/server/lib/security/path-validator.test.js +293 -0
- package/server/lib/security/query-builder.js +421 -0
- package/server/lib/security/query-builder.test.js +318 -0
- package/server/lib/security/secret-detector.js +290 -0
- package/server/lib/security/secret-detector.test.js +354 -0
- package/server/lib/security/secrets-validator.js +137 -0
- package/server/lib/security/secrets-validator.test.js +120 -0
- package/server/lib/security-testing/dast-runner.js +154 -0
- package/server/lib/security-testing/dast-runner.test.js +62 -0
- package/server/lib/security-testing/dependency-scanner.js +172 -0
- package/server/lib/security-testing/dependency-scanner.test.js +64 -0
- package/server/lib/security-testing/pentest-runner.js +230 -0
- package/server/lib/security-testing/pentest-runner.test.js +60 -0
- package/server/lib/security-testing/sast-runner.js +136 -0
- package/server/lib/security-testing/sast-runner.test.js +62 -0
- package/server/lib/security-testing/secret-scanner.js +153 -0
- package/server/lib/security-testing/secret-scanner.test.js +66 -0
- package/server/lib/security-testing/security-gate.js +216 -0
- package/server/lib/security-testing/security-gate.test.js +115 -0
- package/server/lib/security-testing/security-reporter.js +303 -0
- package/server/lib/security-testing/security-reporter.test.js +114 -0
- package/server/lib/standards/audit-checker.js +546 -0
- package/server/lib/standards/audit-checker.test.js +415 -0
- package/server/lib/standards/cleanup-executor.js +452 -0
- package/server/lib/standards/cleanup-executor.test.js +293 -0
- package/server/lib/standards/refactor-stepper.js +425 -0
- package/server/lib/standards/refactor-stepper.test.js +298 -0
- package/server/lib/standards/standards-injector.js +167 -0
- package/server/lib/standards/standards-injector.test.js +232 -0
- package/server/lib/user-management.test.js +284 -0
- package/server/lib/vps/backup-manager.js +157 -0
- package/server/lib/vps/backup-manager.test.js +59 -0
- package/server/lib/vps/caddy-config.js +159 -0
- package/server/lib/vps/caddy-config.test.js +48 -0
- package/server/lib/vps/compose-orchestrator.js +219 -0
- package/server/lib/vps/compose-orchestrator.test.js +50 -0
- package/server/lib/vps/database-config.js +208 -0
- package/server/lib/vps/database-config.test.js +47 -0
- package/server/lib/vps/deploy-script.js +211 -0
- package/server/lib/vps/deploy-script.test.js +53 -0
- package/server/lib/vps/secrets-manager.js +148 -0
- package/server/lib/vps/secrets-manager.test.js +58 -0
- package/server/lib/vps/server-hardening.js +174 -0
- package/server/lib/vps/server-hardening.test.js +70 -0
- package/server/package-lock.json +19 -0
- package/server/package.json +1 -0
- package/server/templates/CLAUDE.md +37 -0
- package/server/templates/CODING-STANDARDS.md +408 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard API Server
|
|
3
|
+
* Express server mounting all dashboard APIs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import express from 'express';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a standardized error response
|
|
10
|
+
* @param {string} message - Error message
|
|
11
|
+
* @param {number} statusCode - HTTP status code
|
|
12
|
+
* @returns {Object} Error response object
|
|
13
|
+
*/
|
|
14
|
+
export function createErrorResponse(message, statusCode = 500) {
|
|
15
|
+
return {
|
|
16
|
+
error: message,
|
|
17
|
+
statusCode,
|
|
18
|
+
timestamp: new Date().toISOString()
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* CORS middleware for handling cross-origin requests
|
|
24
|
+
* @param {Object} req - Express request
|
|
25
|
+
* @param {Object} res - Express response
|
|
26
|
+
* @param {Function} next - Next middleware
|
|
27
|
+
* @param {Object} options - CORS options
|
|
28
|
+
*/
|
|
29
|
+
export function corsMiddleware(req, res, next, options = {}) {
|
|
30
|
+
const origin = options.origin || '*';
|
|
31
|
+
|
|
32
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
33
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
34
|
+
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
|
35
|
+
|
|
36
|
+
if (req.method === 'OPTIONS') {
|
|
37
|
+
res.sendStatus(200);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
next();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Request logger middleware
|
|
46
|
+
* @param {Object} req - Express request
|
|
47
|
+
* @param {Object} res - Express response
|
|
48
|
+
* @param {Function} next - Next middleware
|
|
49
|
+
* @param {Object} options - Logger options
|
|
50
|
+
*/
|
|
51
|
+
export function requestLogger(req, res, next, options = {}) {
|
|
52
|
+
const logger = options.logger || console;
|
|
53
|
+
const startTime = Date.now();
|
|
54
|
+
|
|
55
|
+
logger.info(`${req.method} ${req.url}`);
|
|
56
|
+
|
|
57
|
+
res.on('finish', () => {
|
|
58
|
+
const duration = Date.now() - startTime;
|
|
59
|
+
logger.info(`${req.method} ${req.url} ${res.statusCode} ${duration}ms`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
next();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Error handler middleware
|
|
67
|
+
* @param {Error} error - Error object
|
|
68
|
+
* @param {Object} req - Express request
|
|
69
|
+
* @param {Object} res - Express response
|
|
70
|
+
* @param {Function} next - Next middleware
|
|
71
|
+
* @param {Object} options - Error handler options
|
|
72
|
+
*/
|
|
73
|
+
export function handleError(error, req, res, next, options = {}) {
|
|
74
|
+
const statusCode = error.statusCode || 500;
|
|
75
|
+
const response = {
|
|
76
|
+
error: error.message || 'Internal server error'
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (options.env !== 'production') {
|
|
80
|
+
response.stack = error.stack;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
res.status(statusCode).json(response);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Mounts all API routes on the Express app
|
|
88
|
+
* @param {Object} app - Express app or router
|
|
89
|
+
* @param {Object} options - Route options
|
|
90
|
+
*/
|
|
91
|
+
export function mountRoutes(app, options = {}) {
|
|
92
|
+
// Tasks API
|
|
93
|
+
app.get('/api/tasks', (req, res) => {
|
|
94
|
+
res.json({ tasks: [] });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
app.post('/api/tasks', (req, res) => {
|
|
98
|
+
res.status(201).json({ task: {} });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (app.patch) {
|
|
102
|
+
app.patch('/api/tasks/:id', (req, res) => {
|
|
103
|
+
res.json({ task: {} });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (app.delete) {
|
|
108
|
+
app.delete('/api/tasks/:id', (req, res) => {
|
|
109
|
+
res.status(204).send();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Health API
|
|
114
|
+
app.get('/api/health', (req, res) => {
|
|
115
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Notes API
|
|
119
|
+
app.get('/api/notes', (req, res) => {
|
|
120
|
+
res.json({ notes: [] });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (app.put) {
|
|
124
|
+
app.put('/api/notes', (req, res) => {
|
|
125
|
+
res.json({ note: {} });
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Router API
|
|
130
|
+
app.get('/api/router/status', (req, res) => {
|
|
131
|
+
res.json({ status: 'running' });
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Creates the Express API server
|
|
137
|
+
* @param {Object} options - Server options
|
|
138
|
+
* @param {string} options.basePath - Base path for serving files
|
|
139
|
+
* @returns {Object} Express app
|
|
140
|
+
*/
|
|
141
|
+
export function createApiServer(options = {}) {
|
|
142
|
+
const app = express();
|
|
143
|
+
|
|
144
|
+
// Middleware
|
|
145
|
+
app.use(express.json());
|
|
146
|
+
app.use((req, res, next) => corsMiddleware(req, res, next));
|
|
147
|
+
|
|
148
|
+
// Mount routes
|
|
149
|
+
mountRoutes(app, options);
|
|
150
|
+
|
|
151
|
+
// Error handler
|
|
152
|
+
app.use((err, req, res, next) => handleError(err, req, res, next, options));
|
|
153
|
+
|
|
154
|
+
return app;
|
|
155
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard API Server Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import { createApiServer, mountRoutes, handleError, corsMiddleware, requestLogger, createErrorResponse } from './api-server.js';
|
|
6
|
+
|
|
7
|
+
describe('api-server', () => {
|
|
8
|
+
describe('createApiServer', () => {
|
|
9
|
+
it('creates Express app', () => {
|
|
10
|
+
const app = createApiServer({ basePath: '/test' });
|
|
11
|
+
expect(app.listen).toBeDefined();
|
|
12
|
+
expect(app.use).toBeDefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('mounts API routes', () => {
|
|
16
|
+
const app = createApiServer({ basePath: '/test' });
|
|
17
|
+
// Routes should be mounted at /api/*
|
|
18
|
+
expect(app._router).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('mountRoutes', () => {
|
|
23
|
+
it('mounts tasks API', () => {
|
|
24
|
+
const mockApp = { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn() };
|
|
25
|
+
mountRoutes(mockApp, { basePath: '/test' });
|
|
26
|
+
expect(mockApp.get).toHaveBeenCalledWith('/api/tasks', expect.any(Function));
|
|
27
|
+
expect(mockApp.post).toHaveBeenCalledWith('/api/tasks', expect.any(Function));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('mounts health API', () => {
|
|
31
|
+
const mockApp = { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn() };
|
|
32
|
+
mountRoutes(mockApp, { basePath: '/test' });
|
|
33
|
+
expect(mockApp.get).toHaveBeenCalledWith('/api/health', expect.any(Function));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('mounts notes API', () => {
|
|
37
|
+
const mockApp = { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn() };
|
|
38
|
+
mountRoutes(mockApp, { basePath: '/test' });
|
|
39
|
+
expect(mockApp.get).toHaveBeenCalledWith('/api/notes', expect.any(Function));
|
|
40
|
+
expect(mockApp.put).toHaveBeenCalledWith('/api/notes', expect.any(Function));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('mounts router API', () => {
|
|
44
|
+
const mockApp = { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn() };
|
|
45
|
+
mountRoutes(mockApp, { basePath: '/test' });
|
|
46
|
+
expect(mockApp.get).toHaveBeenCalledWith('/api/router/status', expect.any(Function));
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('handleError', () => {
|
|
51
|
+
it('returns error response', () => {
|
|
52
|
+
const mockRes = {
|
|
53
|
+
status: vi.fn().mockReturnThis(),
|
|
54
|
+
json: vi.fn()
|
|
55
|
+
};
|
|
56
|
+
const error = new Error('Test error');
|
|
57
|
+
handleError(error, {}, mockRes, () => {});
|
|
58
|
+
expect(mockRes.status).toHaveBeenCalledWith(500);
|
|
59
|
+
expect(mockRes.json).toHaveBeenCalledWith(
|
|
60
|
+
expect.objectContaining({ error: expect.any(String) })
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('uses error status code', () => {
|
|
65
|
+
const mockRes = {
|
|
66
|
+
status: vi.fn().mockReturnThis(),
|
|
67
|
+
json: vi.fn()
|
|
68
|
+
};
|
|
69
|
+
const error = new Error('Not found');
|
|
70
|
+
error.statusCode = 404;
|
|
71
|
+
handleError(error, {}, mockRes, () => {});
|
|
72
|
+
expect(mockRes.status).toHaveBeenCalledWith(404);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('hides stack trace in production', () => {
|
|
76
|
+
const mockRes = {
|
|
77
|
+
status: vi.fn().mockReturnThis(),
|
|
78
|
+
json: vi.fn()
|
|
79
|
+
};
|
|
80
|
+
const error = new Error('Error');
|
|
81
|
+
handleError(error, {}, mockRes, () => {}, { env: 'production' });
|
|
82
|
+
const response = mockRes.json.mock.calls[0][0];
|
|
83
|
+
expect(response.stack).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('corsMiddleware', () => {
|
|
88
|
+
it('sets CORS headers', () => {
|
|
89
|
+
const mockRes = {
|
|
90
|
+
header: vi.fn()
|
|
91
|
+
};
|
|
92
|
+
const mockNext = vi.fn();
|
|
93
|
+
corsMiddleware({}, mockRes, mockNext);
|
|
94
|
+
expect(mockRes.header).toHaveBeenCalledWith('Access-Control-Allow-Origin', expect.any(String));
|
|
95
|
+
expect(mockNext).toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('handles preflight requests', () => {
|
|
99
|
+
const mockReq = { method: 'OPTIONS' };
|
|
100
|
+
const mockRes = {
|
|
101
|
+
header: vi.fn(),
|
|
102
|
+
sendStatus: vi.fn()
|
|
103
|
+
};
|
|
104
|
+
corsMiddleware(mockReq, mockRes, () => {});
|
|
105
|
+
expect(mockRes.sendStatus).toHaveBeenCalledWith(200);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('allows configurable origins', () => {
|
|
109
|
+
const mockRes = { header: vi.fn() };
|
|
110
|
+
const mockNext = vi.fn();
|
|
111
|
+
corsMiddleware({}, mockRes, mockNext, { origin: 'http://localhost:3000' });
|
|
112
|
+
expect(mockRes.header).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'http://localhost:3000');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('requestLogger', () => {
|
|
117
|
+
it('logs requests', () => {
|
|
118
|
+
const mockLogger = { info: vi.fn() };
|
|
119
|
+
const mockReq = { method: 'GET', url: '/api/tasks' };
|
|
120
|
+
const mockRes = { on: vi.fn() };
|
|
121
|
+
const mockNext = vi.fn();
|
|
122
|
+
|
|
123
|
+
requestLogger(mockReq, mockRes, mockNext, { logger: mockLogger });
|
|
124
|
+
expect(mockLogger.info).toHaveBeenCalled();
|
|
125
|
+
expect(mockNext).toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('logs response time', () => {
|
|
129
|
+
const mockLogger = { info: vi.fn() };
|
|
130
|
+
const mockReq = { method: 'GET', url: '/test' };
|
|
131
|
+
let finishCallback;
|
|
132
|
+
const mockRes = {
|
|
133
|
+
on: vi.fn((event, cb) => { if (event === 'finish') finishCallback = cb; }),
|
|
134
|
+
statusCode: 200
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
requestLogger(mockReq, mockRes, () => {}, { logger: mockLogger });
|
|
138
|
+
finishCallback();
|
|
139
|
+
expect(mockLogger.info).toHaveBeenCalledTimes(2);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('createErrorResponse', () => {
|
|
144
|
+
it('creates standard error format', () => {
|
|
145
|
+
const response = createErrorResponse('Not found', 404);
|
|
146
|
+
expect(response.error).toBe('Not found');
|
|
147
|
+
expect(response.statusCode).toBe(404);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('includes timestamp', () => {
|
|
151
|
+
const response = createErrorResponse('Error');
|
|
152
|
+
expect(response.timestamp).toBeDefined();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health API Module
|
|
3
|
+
* Health check endpoint
|
|
4
|
+
*/
|
|
5
|
+
import { promises as defaultFs } from 'fs';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
const startTime = Date.now();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get health status
|
|
13
|
+
* @param {Object} options - Options
|
|
14
|
+
* @returns {Promise<Object>} Health status
|
|
15
|
+
*/
|
|
16
|
+
export async function getHealthStatus(options = {}) {
|
|
17
|
+
const checkDeps = options.checkDeps || (() => Promise.resolve({ healthy: true }));
|
|
18
|
+
|
|
19
|
+
const depsResult = await checkDeps();
|
|
20
|
+
|
|
21
|
+
const status = depsResult.healthy ? 'healthy' : 'degraded';
|
|
22
|
+
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
|
23
|
+
|
|
24
|
+
const result = {
|
|
25
|
+
status,
|
|
26
|
+
timestamp: new Date().toISOString(),
|
|
27
|
+
uptime
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (!depsResult.healthy && depsResult.issues) {
|
|
31
|
+
result.issues = depsResult.issues;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get system metrics
|
|
39
|
+
* @returns {Object} System metrics
|
|
40
|
+
*/
|
|
41
|
+
export function getSystemMetrics() {
|
|
42
|
+
const memUsage = process.memoryUsage();
|
|
43
|
+
const totalMem = os.totalmem();
|
|
44
|
+
const freeMem = os.freemem();
|
|
45
|
+
const cpus = os.cpus();
|
|
46
|
+
|
|
47
|
+
// Calculate CPU usage (simple average of all cores)
|
|
48
|
+
let cpuUsage = 0;
|
|
49
|
+
if (cpus.length > 0) {
|
|
50
|
+
const totalCpuTime = cpus.reduce((acc, cpu) => {
|
|
51
|
+
const total = Object.values(cpu.times).reduce((sum, time) => sum + time, 0);
|
|
52
|
+
const idle = cpu.times.idle;
|
|
53
|
+
return acc + ((total - idle) / total);
|
|
54
|
+
}, 0);
|
|
55
|
+
cpuUsage = totalCpuTime / cpus.length;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
memory: {
|
|
60
|
+
used: memUsage.heapUsed,
|
|
61
|
+
total: memUsage.heapTotal,
|
|
62
|
+
systemUsed: totalMem - freeMem,
|
|
63
|
+
systemTotal: totalMem
|
|
64
|
+
},
|
|
65
|
+
cpu: {
|
|
66
|
+
usage: cpuUsage,
|
|
67
|
+
cores: cpus.length,
|
|
68
|
+
model: cpus[0]?.model || 'unknown'
|
|
69
|
+
},
|
|
70
|
+
pid: process.pid,
|
|
71
|
+
nodeVersion: process.version,
|
|
72
|
+
platform: process.platform,
|
|
73
|
+
uptime: process.uptime()
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check dependencies
|
|
79
|
+
* @param {Object} options - Options
|
|
80
|
+
* @returns {Promise<Object>} Dependency status
|
|
81
|
+
*/
|
|
82
|
+
export async function checkDependencies(options = {}) {
|
|
83
|
+
const fs = options.fs || defaultFs;
|
|
84
|
+
const basePath = options.basePath || process.cwd();
|
|
85
|
+
|
|
86
|
+
const result = {
|
|
87
|
+
filesystem: false,
|
|
88
|
+
issues: []
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Check filesystem access
|
|
92
|
+
try {
|
|
93
|
+
await fs.access(basePath);
|
|
94
|
+
result.filesystem = true;
|
|
95
|
+
} catch {
|
|
96
|
+
result.filesystem = false;
|
|
97
|
+
result.issues.push('Filesystem not accessible');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Aggregate health status
|
|
101
|
+
result.healthy = result.filesystem;
|
|
102
|
+
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Run test suite
|
|
108
|
+
* @param {Object} options - Options
|
|
109
|
+
* @returns {Promise<Object>} Test results
|
|
110
|
+
*/
|
|
111
|
+
export async function runTestSuite(options = {}) {
|
|
112
|
+
const exec = options.exec;
|
|
113
|
+
|
|
114
|
+
if (!exec) {
|
|
115
|
+
return { error: 'No exec function provided' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const { stdout, stderr } = await exec('npm test');
|
|
120
|
+
|
|
121
|
+
// Parse test output
|
|
122
|
+
let passed = 0;
|
|
123
|
+
let failed = 0;
|
|
124
|
+
let total = 0;
|
|
125
|
+
|
|
126
|
+
// Try to parse different test output formats
|
|
127
|
+
// Format: "Tests: X passed | Y failed"
|
|
128
|
+
const testMatch = stdout.match(/Tests?:\s*(\d+)\s*passed(?:\s*\|\s*(\d+)\s*failed)?/i);
|
|
129
|
+
if (testMatch) {
|
|
130
|
+
passed = parseInt(testMatch[1], 10);
|
|
131
|
+
failed = testMatch[2] ? parseInt(testMatch[2], 10) : 0;
|
|
132
|
+
total = passed + failed;
|
|
133
|
+
} else {
|
|
134
|
+
// Format: "X passed"
|
|
135
|
+
const passedMatch = stdout.match(/(\d+)\s*passed/i);
|
|
136
|
+
if (passedMatch) {
|
|
137
|
+
passed = parseInt(passedMatch[1], 10);
|
|
138
|
+
total = passed;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const failedMatch = stdout.match(/(\d+)\s*failed/i);
|
|
142
|
+
if (failedMatch) {
|
|
143
|
+
failed = parseInt(failedMatch[1], 10);
|
|
144
|
+
total = passed + failed;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
passed,
|
|
150
|
+
failed,
|
|
151
|
+
total,
|
|
152
|
+
stdout,
|
|
153
|
+
stderr
|
|
154
|
+
};
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return {
|
|
157
|
+
error: error.message,
|
|
158
|
+
passed: 0,
|
|
159
|
+
failed: 0,
|
|
160
|
+
total: 0
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create Health API handler
|
|
167
|
+
* @param {Object} options - Options
|
|
168
|
+
* @returns {Object} API handler
|
|
169
|
+
*/
|
|
170
|
+
export function createHealthApi(options = {}) {
|
|
171
|
+
const { basePath = process.cwd(), fs: fileSystem = defaultFs, cacheMs = 5000 } = options;
|
|
172
|
+
const checkDeps = options.checkDeps || (() => checkDependencies({ fs: fileSystem, basePath }));
|
|
173
|
+
|
|
174
|
+
let cachedStatus = null;
|
|
175
|
+
let cacheTime = 0;
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
async get() {
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
|
|
181
|
+
if (cachedStatus && (now - cacheTime) < cacheMs) {
|
|
182
|
+
return cachedStatus;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
cachedStatus = await getHealthStatus({ checkDeps });
|
|
186
|
+
cacheTime = now;
|
|
187
|
+
|
|
188
|
+
return cachedStatus;
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
getMetrics() {
|
|
192
|
+
return getSystemMetrics();
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
async runTests(execFn) {
|
|
196
|
+
return runTestSuite({ exec: execFn });
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health API Module Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import { getHealthStatus, getSystemMetrics, checkDependencies, runTestSuite, createHealthApi } from './health-api.js';
|
|
6
|
+
|
|
7
|
+
describe('health-api', () => {
|
|
8
|
+
describe('getHealthStatus', () => {
|
|
9
|
+
it('returns healthy status', async () => {
|
|
10
|
+
const status = await getHealthStatus({
|
|
11
|
+
checkDeps: vi.fn().mockResolvedValue({ healthy: true })
|
|
12
|
+
});
|
|
13
|
+
expect(status.status).toBe('healthy');
|
|
14
|
+
expect(status.timestamp).toBeDefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns degraded on dependency issues', async () => {
|
|
18
|
+
const status = await getHealthStatus({
|
|
19
|
+
checkDeps: vi.fn().mockResolvedValue({ healthy: false, issues: ['db down'] })
|
|
20
|
+
});
|
|
21
|
+
expect(status.status).toBe('degraded');
|
|
22
|
+
expect(status.issues).toContain('db down');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('includes uptime', async () => {
|
|
26
|
+
const status = await getHealthStatus({
|
|
27
|
+
checkDeps: vi.fn().mockResolvedValue({ healthy: true })
|
|
28
|
+
});
|
|
29
|
+
expect(status.uptime).toBeDefined();
|
|
30
|
+
expect(typeof status.uptime).toBe('number');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('getSystemMetrics', () => {
|
|
35
|
+
it('returns memory metrics', () => {
|
|
36
|
+
const metrics = getSystemMetrics();
|
|
37
|
+
expect(metrics.memory).toBeDefined();
|
|
38
|
+
expect(metrics.memory.used).toBeDefined();
|
|
39
|
+
expect(metrics.memory.total).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns cpu metrics', () => {
|
|
43
|
+
const metrics = getSystemMetrics();
|
|
44
|
+
expect(metrics.cpu).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns process info', () => {
|
|
48
|
+
const metrics = getSystemMetrics();
|
|
49
|
+
expect(metrics.pid).toBeDefined();
|
|
50
|
+
expect(metrics.nodeVersion).toBeDefined();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('checkDependencies', () => {
|
|
55
|
+
it('checks file system access', async () => {
|
|
56
|
+
const mockFs = {
|
|
57
|
+
access: vi.fn().mockResolvedValue(undefined)
|
|
58
|
+
};
|
|
59
|
+
const result = await checkDependencies({ fs: mockFs, basePath: '/test' });
|
|
60
|
+
expect(result.filesystem).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('reports missing directories', async () => {
|
|
64
|
+
const mockFs = {
|
|
65
|
+
access: vi.fn().mockRejectedValue(new Error('ENOENT'))
|
|
66
|
+
};
|
|
67
|
+
const result = await checkDependencies({ fs: mockFs, basePath: '/test' });
|
|
68
|
+
expect(result.filesystem).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('aggregates health status', async () => {
|
|
72
|
+
const mockFs = {
|
|
73
|
+
access: vi.fn().mockResolvedValue(undefined)
|
|
74
|
+
};
|
|
75
|
+
const result = await checkDependencies({ fs: mockFs, basePath: '/test' });
|
|
76
|
+
expect(result.healthy).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('runTestSuite', () => {
|
|
81
|
+
it('returns test results', async () => {
|
|
82
|
+
const mockExec = vi.fn().mockResolvedValue({
|
|
83
|
+
stdout: '5541 passed',
|
|
84
|
+
stderr: ''
|
|
85
|
+
});
|
|
86
|
+
const result = await runTestSuite({ exec: mockExec });
|
|
87
|
+
expect(result.passed).toBeDefined();
|
|
88
|
+
expect(result.total).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('parses test output', async () => {
|
|
92
|
+
const mockExec = vi.fn().mockResolvedValue({
|
|
93
|
+
stdout: 'Tests: 100 passed | 5 failed',
|
|
94
|
+
stderr: ''
|
|
95
|
+
});
|
|
96
|
+
const result = await runTestSuite({ exec: mockExec });
|
|
97
|
+
expect(result.passed).toBe(100);
|
|
98
|
+
expect(result.failed).toBe(5);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('handles test errors', async () => {
|
|
102
|
+
const mockExec = vi.fn().mockRejectedValue(new Error('Command failed'));
|
|
103
|
+
const result = await runTestSuite({ exec: mockExec });
|
|
104
|
+
expect(result.error).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('createHealthApi', () => {
|
|
109
|
+
it('creates API handler', () => {
|
|
110
|
+
const api = createHealthApi({ basePath: '/test' });
|
|
111
|
+
expect(api.get).toBeDefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('caches health status briefly', async () => {
|
|
115
|
+
const mockCheck = vi.fn().mockResolvedValue({ healthy: true });
|
|
116
|
+
const api = createHealthApi({ checkDeps: mockCheck, cacheMs: 1000 });
|
|
117
|
+
await api.get();
|
|
118
|
+
await api.get();
|
|
119
|
+
expect(mockCheck).toHaveBeenCalledTimes(1);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|