Skip to content

Commit 00e8e75

Browse files
committed
test: add http.route integration coverage
Extend the integrations test harness to discover nested test-*.mjs files and add Express v4/v5 and Fastify v5 scenarios. Update the Makefile to wire these integrations into the existing test targets. For each framework, exercise multiple routes and verify that HTTP server latency histograms include the expected http.route patterns.
1 parent 38ec304 commit 00e8e75

8 files changed

Lines changed: 599 additions & 3 deletions

File tree

Makefile

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ clean: ## Remove build artifacts.
206206
$(MAKE) testclean
207207
$(MAKE) test-addons-clean
208208
$(MAKE) test-agents-prereqs-clean
209+
$(MAKE) test-integrations-prereqs-clean
209210
$(MAKE) bench-addons-clean
210211

211212
.PHONY: testclean
@@ -311,7 +312,7 @@ v8: ## Build deps/v8.
311312
tools/make-v8.sh $(V8_ARCH).$(BUILDTYPE_LOWER) $(V8_BUILD_OPTIONS)
312313

313314
.PHONY: jstest
314-
jstest: build-addons build-js-native-api-tests build-node-api-tests build-sqlite-tests test-agents-prereqs ## Runs addon tests and JS tests.
315+
jstest: build-addons build-js-native-api-tests build-node-api-tests build-sqlite-tests test-agents-prereqs test-integrations-prereqs ## Runs addon tests and JS tests.
315316
NSOLID_DELAY_INIT="" \
316317
$(PYTHON) tools/test.py $(PARALLEL_ARGS) --mode=$(BUILDTYPE_LOWER) \
317318
$(TEST_CI_ARGS) \
@@ -589,7 +590,7 @@ test-ci-native: | benchmark/napi/.buildstamp test/addons/.buildstamp test/js-nat
589590
.PHONY: test-ci-js
590591
# This target should not use a native compiler at all
591592
# Related CI job: node-test-commit-arm-fanned
592-
test-ci-js: | clear-stalled ## Build and test JavaScript with building anything else.
593+
test-ci-js: | clear-stalled test-agents-prereqs test-integrations-prereqs ## Build and test JavaScript with building anything else.
593594
$(PYTHON) tools/test.py $(PARALLEL_ARGS) -p tap --logfile test.tap \
594595
--mode=$(BUILDTYPE_LOWER) --flaky-tests=$(FLAKY_TESTS) \
595596
--skip-tests=$(CI_SKIP_TESTS) \
@@ -604,7 +605,7 @@ test-ci-js: | clear-stalled ## Build and test JavaScript with building anything
604605
.PHONY: test-ci
605606
# Related CI jobs: most CI tests, excluding node-test-commit-arm-fanned
606607
test-ci: LOGLEVEL := info ## Build and test everything (CI).
607-
test-ci: | clear-stalled bench-addons-build build-addons build-js-native-api-tests build-node-api-tests build-sqlite-tests doc-only test-agents-prereqs
608+
test-ci: | clear-stalled bench-addons-build build-addons build-js-native-api-tests build-node-api-tests build-sqlite-tests doc-only test-agents-prereqs test-integrations-prereqs
608609
out/Release/cctest --gtest_output=xml:out/junit/cctest.xml
609610
$(PYTHON) tools/test.py $(PARALLEL_ARGS) -p tap --logfile test.tap \
610611
--mode=$(BUILDTYPE_LOWER) --flaky-tests=$(FLAKY_TESTS) \
@@ -1685,6 +1686,18 @@ test-agents-prereqs-clean:
16851686
$(RM) -r test/common/nsolid-zmq-agent/node_modules
16861687
$(RM) -r test/common/nsolid-otlp-agent/node_modules
16871688

1689+
.PHONY: test-integrations-prereqs
1690+
test-integrations-prereqs:
1691+
env npm_config_nodedir=$(PWD) $(NODE) ./deps/npm install express@4 --prefix test/integrations/express/v4 --no-save --no-package-lock
1692+
env npm_config_nodedir=$(PWD) $(NODE) ./deps/npm install express@5 --prefix test/integrations/express/v5 --no-save --no-package-lock
1693+
env npm_config_nodedir=$(PWD) $(NODE) ./deps/npm install fastify@5 --prefix test/integrations/fastify/v5 --no-save --no-package-lock
1694+
1695+
.PHONY: test-integrations-prereqs-clean
1696+
test-integrations-prereqs-clean:
1697+
$(RM) -r test/integrations/express/v4/node_modules
1698+
$(RM) -r test/integrations/express/v5/node_modules
1699+
$(RM) -r test/integrations/fastify/v5/node_modules
1700+
16881701
HAS_DOCKER ?= $(shell command -v docker > /dev/null 2>&1; [ $$? -eq 0 ] && echo 1 || echo 0)
16891702

16901703
.PHONY: gen-openssl
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "nsolid-integration-express-v4",
3+
"version": "1.0.0",
4+
"private": true,
5+
"dependencies": {
6+
"express": "^4.21.0"
7+
}
8+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Flags: --expose-internals
2+
import { mustSucceed } from '../../../common/index.mjs';
3+
import assert from 'node:assert';
4+
import { existsSync } from 'node:fs';
5+
import { fileURLToPath } from 'node:url';
6+
import { dirname, join } from 'node:path';
7+
import {
8+
GRPCServer,
9+
} from '../../../common/nsolid-grpc-agent/index.js';
10+
import validators from 'internal/validators';
11+
import nsolid from 'nsolid';
12+
13+
const {
14+
validateArray,
15+
} = validators;
16+
17+
const __dirname = dirname(fileURLToPath(import.meta.url));
18+
19+
// Skip test if dependencies not installed
20+
if (!existsSync(join(__dirname, 'node_modules'))) {
21+
console.log('SKIP: node_modules not found. Run "make test-integrations-prereqs" first.');
22+
process.exit(0);
23+
}
24+
25+
const { default: express } = await import('express');
26+
27+
function getAttr(attributes, key) {
28+
return attributes.find((a) => a.key === key);
29+
}
30+
31+
function checkHttpRouteAttribute(metricsData, expectedRoutes) {
32+
const resourceMetrics = metricsData.resourceMetrics;
33+
if (!resourceMetrics || resourceMetrics.length === 0) return [];
34+
35+
const foundRoutes = [];
36+
37+
// Iterate over all resourceMetrics and their scopeMetrics
38+
for (const resource of resourceMetrics) {
39+
const scopeMetrics = resource.scopeMetrics;
40+
if (!scopeMetrics || scopeMetrics.length === 0) continue;
41+
42+
for (const scope of scopeMetrics) {
43+
const metrics = scope.metrics;
44+
if (!metrics) continue;
45+
46+
for (const metric of metrics) {
47+
if (metric.name !== 'http.server.request.duration') continue;
48+
if (metric.data !== 'exponentialHistogram') continue;
49+
50+
const dataPoints = metric.exponentialHistogram.dataPoints;
51+
validateArray(dataPoints, 'dataPoints');
52+
53+
for (const dp of dataPoints) {
54+
const count = parseInt(dp.count, 10);
55+
if (count === 0) continue;
56+
57+
// Check for http.route attribute
58+
const routeAttr = getAttr(dp.attributes, 'http.route');
59+
if (routeAttr) {
60+
const routeValue = routeAttr.value.stringValue;
61+
console.log(`Found route: ${routeValue} (count: ${count})`);
62+
if (expectedRoutes.has(routeValue) && !foundRoutes.includes(routeValue)) {
63+
foundRoutes.push(routeValue);
64+
}
65+
}
66+
}
67+
}
68+
}
69+
}
70+
71+
return foundRoutes;
72+
}
73+
74+
async function runTest() {
75+
// Start gRPC server as child process
76+
const grpcServer = new GRPCServer();
77+
78+
const grpcPort = await new Promise((resolve, reject) => {
79+
grpcServer.start(mustSucceed((port) => {
80+
console.log('gRPC server started on port', port);
81+
resolve(port);
82+
}));
83+
});
84+
85+
// Configure NSolid to connect to our gRPC server
86+
process.env.NSOLID_GRPC_INSECURE = '1';
87+
process.env.NODE_DEBUG_NATIVE = 'nsolid_grpc_agent';
88+
89+
// Initialize NSolid
90+
nsolid.start({ grpc: `localhost:${grpcPort}`, interval: 500 });
91+
92+
// Create Express server with multiple routes including mounted router
93+
const app = express();
94+
const apiRouter = express.Router();
95+
96+
// Mounted router routes - these test the baseUrl + route.path composition
97+
apiRouter.get('/users/:id', (req, res) => {
98+
res.json({ userId: req.params.id });
99+
});
100+
101+
apiRouter.get('/posts/:postId', (req, res) => {
102+
res.json({ postId: req.params.postId });
103+
});
104+
105+
// Mount the router at /api
106+
app.use('/api', apiRouter);
107+
108+
// Direct route (not mounted)
109+
app.get('/health', (req, res) => {
110+
res.json({ status: 'ok' });
111+
});
112+
113+
// Track connections for forceful close
114+
const connections = new Set();
115+
const server = await new Promise((resolve) => {
116+
const s = app.listen(0, '127.0.0.1', () => {
117+
console.log('Express server started on port', s.address().port);
118+
resolve(s);
119+
});
120+
s.on('connection', (conn) => {
121+
connections.add(conn);
122+
conn.on('close', () => connections.delete(conn));
123+
});
124+
});
125+
126+
const serverPort = server.address().port;
127+
128+
// Track which routes we've seen
129+
// Note: Mounted routes should have full path /api/users/:id, not just /users/:id
130+
const expectedRoutes = new Set(['/api/users/:id', '/api/posts/:postId', '/health']);
131+
const foundRoutes = new Set();
132+
133+
// Listen for metrics
134+
const metricsPromise = new Promise((resolve, reject) => {
135+
grpcServer.on('metrics', (data) => {
136+
const found = checkHttpRouteAttribute(data, expectedRoutes);
137+
for (const route of found) {
138+
if (!foundRoutes.has(route)) {
139+
foundRoutes.add(route);
140+
console.log(`Validated route: ${route} (${foundRoutes.size}/${expectedRoutes.size})`);
141+
}
142+
}
143+
144+
if (foundRoutes.size === expectedRoutes.size) {
145+
console.log('All routes validated!');
146+
resolve();
147+
}
148+
});
149+
});
150+
151+
// Make HTTP requests to trigger routes
152+
console.log('Making HTTP requests...');
153+
154+
// Request to mounted routes /api/users/:id
155+
let response = await fetch(`http://127.0.0.1:${serverPort}/api/users/123`);
156+
assert.strictEqual(response.status, 200);
157+
console.log('Requested /api/users/123');
158+
159+
// Request to mounted routes /api/posts/:postId
160+
response = await fetch(`http://127.0.0.1:${serverPort}/api/posts/456`);
161+
assert.strictEqual(response.status, 200);
162+
console.log('Requested /api/posts/456');
163+
164+
// Request to direct route /health
165+
response = await fetch(`http://127.0.0.1:${serverPort}/health`);
166+
assert.strictEqual(response.status, 200);
167+
console.log('Requested /health');
168+
169+
// Wait for all metrics to be reported
170+
await metricsPromise;
171+
172+
// Cleanup
173+
console.log('Cleaning up...');
174+
for (const conn of connections) {
175+
conn.destroy();
176+
}
177+
178+
await new Promise((resolve) => {
179+
server.close(() => {
180+
grpcServer.close();
181+
resolve();
182+
});
183+
});
184+
}
185+
186+
await runTest();
187+
console.log('Express v4 route test passed!');
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "nsolid-integration-express-v5",
3+
"version": "1.0.0",
4+
"private": true,
5+
"dependencies": {
6+
"express": "^5.0.0"
7+
}
8+
}

0 commit comments

Comments
 (0)