Skip to content

Commit

Permalink
test: allow FakeAgent to respond to remote config requests
Browse files Browse the repository at this point in the history
  • Loading branch information
watson committed Aug 27, 2024
1 parent 2ded936 commit c7147ec
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 36 deletions.
105 changes: 105 additions & 0 deletions integration-tests/helpers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use strict'

const { promisify } = require('util')
const { createHash } = require('crypto')
const uuid = require('crypto-randomuuid')
const express = require('express')
const bodyParser = require('body-parser')
const msgpack = require('msgpack-lite')
Expand All @@ -24,6 +26,7 @@ class FakeAgent extends EventEmitter {
constructor (port = 0) {
super()
this.port = port
this._rc_files = []
}

async start () {
Expand All @@ -38,6 +41,78 @@ class FakeAgent extends EventEmitter {
payload: msgpack.decode(req.body, { codec })
})
})
app.post('/v0.7/config', (req, res) => {
const {
client: { products },
cached_target_files: cachedTargetFiles
} = req.body

const expires = (new Date(Date.now() + 1000 * 60 * 60 * 24)).toISOString() // in 24 hours
const clientID = uuid() // TODO: What is this? It isn't the runtime-id

// Currently, only `opaque_backend_state` and `targets` are used by dd-trace-js in the object below
const targets = {
signatures: [],
signed: {
_type: 'targets',
custom: {
agent_refresh_interval: 5,
opaque_backend_state: ''
},
expires,
spec_version: '1.0.0',
targets: {},
version: 12345
}
}
const opaqueBackendState = {
version: 2,
state: { file_hashes: { key: [] } }
}
const targetFiles = []
const clientConfigs = []

const files = this._rc_files.filter(({ product }) => products.includes(product))

for (const { orgId, product, id, name, config } of files) {
const path = `datadog/${orgId}/${product}/${id}/${name}`
const fileDigest = createHash('sha256').update(config).digest()
const fileDigestHex = fileDigest.toString('hex')

if (cachedTargetFiles.some((cached) =>
path === cached.path &&
fileDigestHex === cached.hashes.find((e) => e.algorithm === 'sha256').hash
)) {
continue // skip files already cached by the client so we don't send them more than once
}

opaqueBackendState.state.file_hashes.key.push(fileDigest.toString('base64'))

targets.signed.targets[path] = {
custom: {
c: [clientID],
'tracer-predicates': { tracer_predicates_v1: [{ clientID }] },
v: 20
},
hashes: { sha256: fileDigestHex },
length: config.length
}

targetFiles.push({ path, raw: base64(config) })
clientConfigs.push(path)
}

targets.signed.custom.opaque_backend_state = base64(opaqueBackendState)

// TODO: What does the real agent do if there's nothing to return? Does it just return empty arrays and objects
// like we do here, or do we need to change the algorithm to align?
res.json({
roots: [], // Not used by dd-trace-js currently, so left empty
targets: base64(targets),
target_files: targetFiles,
client_configs: clientConfigs
})
})
app.post('/profiling/v1/input', upload.any(), (req, res) => {
res.status(200).send()
this.emit('message', {
Expand Down Expand Up @@ -75,6 +150,31 @@ class FakeAgent extends EventEmitter {
})
}

/**
* Remove any existing config added by calls to FakeAgent#addRemoteConfig.
*/
resetRemoteConfig () {
this._rc_files = []
}

/**
* Add a config object to be returned by the fake Remote Config endpoint.
* @param {Object} config - Object containing the Remote Config "file" and metadata
* @param {number} [config.orgId=2] - The Datadog organization ID
* @param {string} config.product - The Remote Config product name
* @param {string} config.id - The Remote Config config ID
* @param {string} [config.name] - The Remote Config "name". Defaults to the sha256 hash of `config.id`
* @param {Object} config.config - The Remote Config "file" object
*/
addRemoteConfig (config) {
config = { ...config }
config.orgId = config.orgId ?? 2
config.name = config.name ?? createHash('sha256').update(config.id).digest('hex')
config.config = JSON.stringify(config.config)

this._rc_files.push(config)
}

// **resolveAtFirstSuccess** - specific use case for Next.js (or any other future libraries)
// where multiple payloads are generated, and only one is expected to have the proper span (ie next.request),
// but it't not guaranteed to be the last one (so, expectedMessageCount would not be helpful).
Expand Down Expand Up @@ -486,6 +586,11 @@ function sandboxCwd () {
return sandbox.folder
}

function base64 (strOrObj) {
const str = typeof strOrObj === 'string' ? strOrObj : JSON.stringify(strOrObj)
return Buffer.from(str).toString('base64')
}

module.exports = {
FakeAgent,
spawnProc,
Expand Down
5 changes: 4 additions & 1 deletion packages/dd-trace/src/appsec/remote_config/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,10 @@ class RemoteConfigManager extends EventEmitter {
const options = {
url: this.url,
method: 'POST',
path: '/v0.7/config'
path: '/v0.7/config',
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
}

request(this.getPayload(), options, (err, data, statusCode) => {
Expand Down
50 changes: 15 additions & 35 deletions packages/dd-trace/test/appsec/remote_config/manager.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,16 @@ describe('RemoteConfigManager', () => {
})

describe('poll', () => {
let expectedPayload

beforeEach(() => {
sinon.stub(rc, 'parseConfig')
expectedPayload = {
url: rc.url,
method: 'POST',
path: '/v0.7/config',
headers: { 'Content-Type': 'application/json; charset=utf-8' }
}
})

it('should request and do nothing when received status 404', (cb) => {
Expand All @@ -188,11 +196,7 @@ describe('RemoteConfigManager', () => {
const payload = JSON.stringify(rc.state)

rc.poll(() => {
expect(request).to.have.been.calledOnceWith(payload, {
url: rc.url,
method: 'POST',
path: '/v0.7/config'
})
expect(request).to.have.been.calledOnceWith(payload, expectedPayload)
expect(log.error).to.not.have.been.called
expect(rc.parseConfig).to.not.have.been.called
cb()
Expand All @@ -206,11 +210,7 @@ describe('RemoteConfigManager', () => {
const payload = JSON.stringify(rc.state)

rc.poll(() => {
expect(request).to.have.been.calledOnceWith(payload, {
url: rc.url,
method: 'POST',
path: '/v0.7/config'
})
expect(request).to.have.been.calledOnceWith(payload, expectedPayload)
expect(log.error).to.have.been.calledOnceWithExactly(err)
expect(rc.parseConfig).to.not.have.been.called
cb()
Expand All @@ -223,11 +223,7 @@ describe('RemoteConfigManager', () => {
const payload = JSON.stringify(rc.state)

rc.poll(() => {
expect(request).to.have.been.calledOnceWith(payload, {
url: rc.url,
method: 'POST',
path: '/v0.7/config'
})
expect(request).to.have.been.calledOnceWith(payload, expectedPayload)
expect(log.error).to.not.have.been.called
expect(rc.parseConfig).to.have.been.calledOnceWithExactly({ a: 'b' })
cb()
Expand All @@ -243,11 +239,7 @@ describe('RemoteConfigManager', () => {
const payload = JSON.stringify(rc.state)

rc.poll(() => {
expect(request).to.have.been.calledOnceWith(payload, {
url: rc.url,
method: 'POST',
path: '/v0.7/config'
})
expect(request).to.have.been.calledOnceWith(payload, expectedPayload)
expect(rc.parseConfig).to.have.been.calledOnceWithExactly({ a: 'b' })
expect(log.error).to.have.been
.calledOnceWithExactly('Could not parse remote config response: Error: Unable to parse config')
Expand All @@ -258,11 +250,7 @@ describe('RemoteConfigManager', () => {

rc.poll(() => {
expect(request).to.have.been.calledTwice
expect(request.secondCall).to.have.been.calledWith(payload2, {
url: rc.url,
method: 'POST',
path: '/v0.7/config'
})
expect(request.secondCall).to.have.been.calledWith(payload2, expectedPayload)
expect(rc.parseConfig).to.have.been.calledOnce
expect(log.error).to.have.been.calledOnce
expect(rc.state.client.state.has_error).to.be.false
Expand All @@ -278,11 +266,7 @@ describe('RemoteConfigManager', () => {
const payload = JSON.stringify(rc.state)

rc.poll(() => {
expect(request).to.have.been.calledOnceWith(payload, {
url: rc.url,
method: 'POST',
path: '/v0.7/config'
})
expect(request).to.have.been.calledOnceWith(payload, expectedPayload)
expect(log.error).to.not.have.been.called
expect(rc.parseConfig).to.not.have.been.called
cb()
Expand All @@ -299,11 +283,7 @@ describe('RemoteConfigManager', () => {
expect(JSON.parse(payload).client.client_tracer.extra_services).to.deep.equal(extraServices)

rc.poll(() => {
expect(request).to.have.been.calledOnceWith(payload, {
url: rc.url,
method: 'POST',
path: '/v0.7/config'
})
expect(request).to.have.been.calledOnceWith(payload, expectedPayload)
cb()
})
})
Expand Down

0 comments on commit c7147ec

Please sign in to comment.