Skip to content

Commit 760c373

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 f28380f commit 760c373

8 files changed

Lines changed: 569 additions & 2 deletions

File tree

Makefile

Lines changed: 15 additions & 2 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) \
@@ -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: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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 scopeMetrics = resourceMetrics[0].scopeMetrics;
36+
if (!scopeMetrics || scopeMetrics.length === 0) return [];
37+
38+
const metrics = scopeMetrics[0].metrics;
39+
if (!metrics) return [];
40+
41+
const foundRoutes = [];
42+
43+
for (const metric of metrics) {
44+
if (metric.name !== 'http.server.request.duration') continue;
45+
if (metric.data !== 'exponentialHistogram') continue;
46+
47+
const dataPoints = metric.exponentialHistogram.dataPoints;
48+
validateArray(dataPoints, 'dataPoints');
49+
50+
for (const dp of dataPoints) {
51+
const count = parseInt(dp.count, 10);
52+
if (count === 0) continue;
53+
54+
// Check for http.route attribute
55+
const routeAttr = getAttr(dp.attributes, 'http.route');
56+
if (routeAttr) {
57+
const routeValue = routeAttr.value.stringValue;
58+
console.log(`Found route: ${routeValue} (count: ${count})`);
59+
if (expectedRoutes.has(routeValue) && !foundRoutes.includes(routeValue)) {
60+
foundRoutes.push(routeValue);
61+
}
62+
}
63+
}
64+
}
65+
66+
return foundRoutes;
67+
}
68+
69+
async function runTest() {
70+
// Start gRPC server as child process
71+
const grpcServer = new GRPCServer();
72+
73+
const grpcPort = await new Promise((resolve, reject) => {
74+
grpcServer.start(mustSucceed((port) => {
75+
console.log('gRPC server started on port', port);
76+
resolve(port);
77+
}));
78+
});
79+
80+
// Configure NSolid to connect to our gRPC server
81+
process.env.NSOLID_GRPC_INSECURE = '1';
82+
process.env.NODE_DEBUG_NATIVE = 'nsolid_grpc_agent';
83+
84+
// Initialize NSolid
85+
nsolid.start({ grpc: `localhost:${grpcPort}`, interval: 1000 });
86+
87+
// Create Express server with multiple routes
88+
const app = express();
89+
90+
app.get('/users/:id', (req, res) => {
91+
res.json({ userId: req.params.id });
92+
});
93+
94+
app.get('/posts/:postId', (req, res) => {
95+
res.json({ postId: req.params.postId });
96+
});
97+
98+
app.get('/health', (req, res) => {
99+
res.json({ status: 'ok' });
100+
});
101+
102+
// Track connections for forceful close
103+
const connections = new Set();
104+
const server = await new Promise((resolve) => {
105+
const s = app.listen(0, '127.0.0.1', () => {
106+
console.log('Express server started on port', s.address().port);
107+
resolve(s);
108+
});
109+
s.on('connection', (conn) => {
110+
connections.add(conn);
111+
conn.on('close', () => connections.delete(conn));
112+
});
113+
});
114+
115+
const serverPort = server.address().port;
116+
117+
// Track which routes we've seen
118+
const expectedRoutes = new Set(['/users/:id', '/posts/:postId', '/health']);
119+
const foundRoutes = new Set();
120+
121+
// Listen for metrics
122+
const metricsPromise = new Promise((resolve, reject) => {
123+
grpcServer.on('metrics', (data) => {
124+
const found = checkHttpRouteAttribute(data, expectedRoutes);
125+
for (const route of found) {
126+
if (!foundRoutes.has(route)) {
127+
foundRoutes.add(route);
128+
console.log(`Validated route: ${route} (${foundRoutes.size}/${expectedRoutes.size})`);
129+
}
130+
}
131+
132+
if (foundRoutes.size === expectedRoutes.size) {
133+
console.log('All routes validated!');
134+
resolve();
135+
}
136+
});
137+
});
138+
139+
// Make HTTP requests to trigger routes
140+
console.log('Making HTTP requests...');
141+
142+
// Request to /users/:id
143+
let response = await fetch(`http://127.0.0.1:${serverPort}/users/123`);
144+
assert.strictEqual(response.status, 200);
145+
console.log('Requested /users/123');
146+
147+
// Request to /posts/:postId
148+
response = await fetch(`http://127.0.0.1:${serverPort}/posts/456`);
149+
assert.strictEqual(response.status, 200);
150+
console.log('Requested /posts/456');
151+
152+
// Request to /health
153+
response = await fetch(`http://127.0.0.1:${serverPort}/health`);
154+
assert.strictEqual(response.status, 200);
155+
console.log('Requested /health');
156+
157+
// Wait for all metrics to be reported
158+
await metricsPromise;
159+
160+
// Cleanup
161+
console.log('Cleaning up...');
162+
for (const conn of connections) {
163+
conn.destroy();
164+
}
165+
166+
await new Promise((resolve) => {
167+
server.close(() => {
168+
grpcServer.close();
169+
resolve();
170+
});
171+
});
172+
}
173+
174+
await runTest();
175+
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)