-
Notifications
You must be signed in to change notification settings - Fork 407
/
Copy pathtest-collector.js
233 lines (208 loc) · 5.91 KB
/
test-collector.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
'use strict'
// This provides an in-process http server to use in place of
// collector.newrelic.com. It allows for custom handlers so that test specific
// assertions can be made.
const https = require('node:https')
const querystring = require('node:querystring')
const helper = require('./agent_helper')
const fakeCert = require('./fake-cert')
class Collector {
#handlers = new Map()
#server
#address
#runId
constructor({ runId = 42 } = {}) {
this.#runId = runId
this.#server = https.createServer({
key: fakeCert.privateKey,
cert: fakeCert.certificate
})
this.#server.on('request', (req, res) => {
const qs = querystring.decode(req.url.slice(req.url.indexOf('?') + 1))
const handler = this.#handlers.get(qs.method)
if (typeof handler !== 'function') {
res.writeHead(500)
return res.end('handler not found: ' + req.url)
}
res.json = function ({ payload, code = 200 }) {
this.writeHead(code, { 'content-type': 'application/json' })
this.end(JSON.stringify(payload))
}
req.body = function () {
let resolve
let reject
const promise = new Promise((res, rej) => {
resolve = res
reject = rej
})
let data = ''
this.on('data', (d) => {
data += d
})
this.on('end', () => {
resolve(data)
})
this.on('error', (error) => {
reject(error)
})
return promise
}
handler.isDone = true
handler(req, res)
})
// We don't need this server keeping the process alive.
this.#server.unref()
}
/**
* A configuration object that can be passed to an "agent" instance so that
* the agent will communicate with this test server instead of the real
* server.
*
* Important: the `.listen` method must be invoked first in order to have
* the `host` and `port` defined.
*
* @returns {object}
*/
get agentConfig() {
return {
host: this.host,
port: this.port,
license_key: 'testing',
certificates: [this.cert]
}
}
/**
* The host the server is listening on.
*
* @returns {string}
*/
get host() {
return this.#address?.address
}
/**
* The port number the server is listening on.
*
* @returns {number}
*/
get port() {
return this.#address?.port
}
/**
* A copy of the public certificate used to secure the server. Use this
* like `new Agent({ certificates: [collector.cert] })`.
*
* @returns {string}
*/
get cert() {
return fakeCert.certificate
}
/**
* The most basic `agent_settings` handler. Useful when you do not need to
* customize the handler.
*
* @returns {function}
*/
get agentSettingsHandler() {
return function (req, res) {
res.json({ payload: { return_value: [] } })
}
}
/**
* the most basic `connect` handler. Useful when you do not need to
* customize the handler.
*
* @returns {function}
*/
get connectHandler() {
const runId = this.#runId
return function (req, res) {
res.json({ payload: { return_value: { agent_run_id: runId } } })
}
}
/**
* The most basic `preconnect` handler. Useful when you do not need to
* customize the handler.
*
* @returns {function}
*/
get preconnectHandler() {
const host = this.host
const port = this.port
return function (req, res) {
res.json({
payload: {
return_value: {
redirect_host: `${host}:${port}`,
security_policies: {}
}
}
})
}
}
/**
* Adds a new handler for the provided endpoint.
*
* @param {string} endpoint A string like
* `/agent_listener/invoke_raw_method?method=preconnect`. Notice that a query
* string with the `method` parameter is present. This is required, as the
* value of `method` will be used to look up the handler when receiving
* requests.
* @param {function} handler A typical `(req, res) => {}` handler. For
* convenience, `res` is extended with a `json({ payload, code = 200 })`
* method for easily sending JSON responses. Also, `req` is extended with
* a `body()` method that returns a promise which resolves to the string
* data supplied via POST-like requests.
*/
addHandler(endpoint, handler) {
const qs = querystring.decode(endpoint.slice(endpoint.indexOf('?') + 1))
this.#handlers.set(qs.method, handler)
}
/**
* Shutdown the server and forcefully close all current connections.
*/
close() {
this.#server.closeAllConnections()
}
/**
* Determine if a handler has been invoked.
*
* @param {string} method Name of the method to check, e.g. "preconnect".
* @returns {boolean}
*/
isDone(method) {
return this.#handlers.get(method)?.isDone === true
}
/**
* Start the server listening for requests.
*
* @returns {Promise<object>} Returns a standard server address object.
*/
async listen() {
let address
await new Promise((resolve, reject) => {
this.#server.listen(0, '127.0.0.1', (err) => {
if (err) {
return reject(err)
}
address = this.#server.address()
resolve()
})
})
this.#address = address
// Add handlers for the required agent startup connections. These should
// be overwritten by tests that exercise the startup phase, but adding these
// stubs makes it easier to test other connection events.
this.addHandler(helper.generateCollectorPath('preconnect', this.#runId), this.preconnectHandler)
this.addHandler(helper.generateCollectorPath('connect', this.#runId), this.connectHandler)
this.addHandler(
helper.generateCollectorPath('agent_settings', this.#runId),
this.agentSettingsHandler
)
return address
}
}
module.exports = Collector