Skip to content

Commit 38db411

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 16280a7 commit 38db411

8 files changed

Lines changed: 642 additions & 3 deletions

File tree

Makefile

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ clean: ## Remove build artifacts.
214214
$(MAKE) testclean
215215
$(MAKE) test-addons-clean
216216
$(MAKE) test-agents-prereqs-clean
217+
$(MAKE) test-integrations-prereqs-clean
217218
$(MAKE) bench-addons-clean
218219

219220
.PHONY: testclean
@@ -234,6 +235,8 @@ distclean: ## Remove all build and test artifacts.
234235
$(RM) -r deps/icu
235236
$(RM) -r deps/icu4c*.tgz deps/icu4c*.zip deps/icu-tmp
236237
$(RM) $(BINARYTAR).* $(TARBALL).*
238+
$(MAKE) test-agents-prereqs-clean
239+
$(MAKE) test-integrations-prereqs-clean
237240

238241
.PHONY: check
239242
check: test
@@ -318,7 +321,7 @@ v8: ## Build deps/v8.
318321
tools/make-v8.sh $(V8_ARCH).$(BUILDTYPE_LOWER) $(V8_BUILD_OPTIONS)
319322

320323
.PHONY: jstest
321-
jstest: build-addons build-js-native-api-tests build-node-api-tests build-sqlite-tests test-agents-prereqs ## Runs addon tests and JS tests.
324+
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.
322325
NSOLID_DELAY_INIT="" \
323326
$(PYTHON) tools/test.py $(PARALLEL_ARGS) --mode=$(BUILDTYPE_LOWER) \
324327
$(TEST_CI_ARGS) \
@@ -596,7 +599,7 @@ test-ci-native: | benchmark/napi/.buildstamp test/addons/.buildstamp test/js-nat
596599
.PHONY: test-ci-js
597600
# This target should not use a native compiler at all
598601
# Related CI job: node-test-commit-arm-fanned
599-
test-ci-js: | clear-stalled ## Build and test JavaScript with building anything else.
602+
test-ci-js: | clear-stalled test-agents-prereqs test-integrations-prereqs ## Build and test JavaScript with building anything else.
600603
$(PYTHON) tools/test.py $(PARALLEL_ARGS) -p tap --logfile test.tap \
601604
--mode=$(BUILDTYPE_LOWER) --flaky-tests=$(FLAKY_TESTS) \
602605
--skip-tests=$(CI_SKIP_TESTS) \
@@ -611,7 +614,7 @@ test-ci-js: | clear-stalled ## Build and test JavaScript with building anything
611614
.PHONY: test-ci
612615
# Related CI jobs: most CI tests, excluding node-test-commit-arm-fanned
613616
test-ci: LOGLEVEL := info ## Build and test everything (CI).
614-
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
617+
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
615618
out/Release/cctest --gtest_output=xml:out/junit/cctest.xml
616619
$(PYTHON) tools/test.py $(PARALLEL_ARGS) -p tap --logfile test.tap \
617620
--mode=$(BUILDTYPE_LOWER) --flaky-tests=$(FLAKY_TESTS) \
@@ -1695,6 +1698,18 @@ test-agents-prereqs-clean:
16951698
$(RM) -r test/common/nsolid-zmq-agent/node_modules
16961699
$(RM) -r test/common/nsolid-otlp-agent/node_modules
16971700

1701+
.PHONY: test-integrations-prereqs
1702+
test-integrations-prereqs:
1703+
env npm_config_nodedir=$(PWD) $(NODE) ./deps/npm install express@4 --prefix test/integrations/express/v4 --no-save --no-package-lock
1704+
env npm_config_nodedir=$(PWD) $(NODE) ./deps/npm install express@5 --prefix test/integrations/express/v5 --no-save --no-package-lock
1705+
env npm_config_nodedir=$(PWD) $(NODE) ./deps/npm install fastify@5 --prefix test/integrations/fastify/v5 --no-save --no-package-lock
1706+
1707+
.PHONY: test-integrations-prereqs-clean
1708+
test-integrations-prereqs-clean:
1709+
$(RM) -r test/integrations/express/v4/node_modules
1710+
$(RM) -r test/integrations/express/v5/node_modules
1711+
$(RM) -r test/integrations/fastify/v5/node_modules
1712+
16981713
HAS_DOCKER ?= $(shell command -v docker > /dev/null 2>&1; [ $$? -eq 0 ] && echo 1 || echo 0)
16991714

17001715
.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: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// Flags: --expose-internals
2+
import { mustNotCall, 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+
app.get(/^\/regex\/(\d+)$/, (req, res) => {
114+
res.json({ userId: req.params[0] });
115+
});
116+
117+
// Track connections for forceful close
118+
const connections = new Set();
119+
const server = await new Promise((resolve) => {
120+
const s = app.listen(0, '127.0.0.1', () => {
121+
console.log('Express server started on port', s.address().port);
122+
resolve(s);
123+
});
124+
s.on('connection', (conn) => {
125+
connections.add(conn);
126+
conn.on('close', () => connections.delete(conn));
127+
});
128+
});
129+
130+
const serverPort = server.address().port;
131+
132+
// Track which routes we've seen
133+
// Note: Mounted routes should have full path /api/users/:id, not just /users/:id
134+
const expectedRoutes = new Set(['/api/users/:id', '/api/posts/:postId', '/health']);
135+
const foundRoutes = new Set();
136+
137+
// Listen for metrics
138+
const metricsPromise = new Promise((resolve, reject) => {
139+
grpcServer.on('metrics', (data) => {
140+
const found = checkHttpRouteAttribute(data, expectedRoutes);
141+
for (const route of found) {
142+
if (!foundRoutes.has(route)) {
143+
foundRoutes.add(route);
144+
console.log(`Validated route: ${route} (${foundRoutes.size}/${expectedRoutes.size})`);
145+
}
146+
}
147+
148+
if (foundRoutes.size === expectedRoutes.size) {
149+
console.log('All routes validated!');
150+
resolve();
151+
}
152+
});
153+
});
154+
155+
process.once('uncaughtException', mustNotCall('regex Express routes must not throw in http.server.response.finish'));
156+
157+
// Make HTTP requests to trigger routes
158+
console.log('Making HTTP requests...');
159+
160+
// Request to mounted routes /api/users/:id
161+
let response = await fetch(`http://127.0.0.1:${serverPort}/api/users/123`);
162+
assert.strictEqual(response.status, 200);
163+
console.log('Requested /api/users/123');
164+
165+
// Request to mounted routes /api/posts/:postId
166+
response = await fetch(`http://127.0.0.1:${serverPort}/api/posts/456`);
167+
assert.strictEqual(response.status, 200);
168+
console.log('Requested /api/posts/456');
169+
170+
// Request to direct route /health
171+
response = await fetch(`http://127.0.0.1:${serverPort}/health`);
172+
assert.strictEqual(response.status, 200);
173+
console.log('Requested /health');
174+
175+
// Request to regex route - should not set http.route or throw
176+
response = await fetch(`http://127.0.0.1:${serverPort}/regex/789`);
177+
assert.strictEqual(response.status, 200);
178+
console.log('Requested /regex/789');
179+
180+
// Wait for all metrics to be reported
181+
await metricsPromise;
182+
183+
// Cleanup
184+
console.log('Cleaning up...');
185+
for (const conn of connections) {
186+
conn.destroy();
187+
}
188+
189+
await new Promise((resolve) => {
190+
server.close(() => {
191+
grpcServer.close();
192+
resolve();
193+
});
194+
});
195+
}
196+
197+
await runTest();
198+
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)