From 3607ee7d3dd2cc31723a2ccf76a33480b67d2cb6 Mon Sep 17 00:00:00 2001 From: Antoine Arlaud Date: Wed, 3 Apr 2024 15:33:15 +0200 Subject: [PATCH] feat: add jwt retrieval logic --- config.universaltest.json | 4 ++ lib/client/auth/oauth.ts | 46 ++++++++++++++ lib/client/index.ts | 30 +++++++-- lib/client/socket.ts | 62 ++++++++++++++++--- lib/client/types/client.ts | 4 +- lib/client/types/config.ts | 1 + lib/common/types/options.ts | 8 +++ lib/server/socket.ts | 50 +++++++++++++++ .../client-universal-server.test.ts | 4 ++ test/functional/healthcheck-universal.test.ts | 4 ++ .../server-client-universal.test.ts | 4 ++ test/functional/systemcheck-universal.test.ts | 12 ++++ test/unit/config.test.ts | 14 +++++ ...lay-response-body-form-url-encoded.test.ts | 2 + ...se-body-universal-form-url-encoded.test.ts | 2 + .../relay-response-body-universal.test.ts | 2 + test/unit/relay-response-body.test.ts | 2 + ...-response-headers-form-url-headers.test.ts | 2 + ...headers-universal-form-url-headers.test.ts | 2 + .../relay-response-headers-universal.test.ts | 2 + test/unit/relay-response-headers.test.ts | 2 + 21 files changed, 244 insertions(+), 15 deletions(-) create mode 100644 lib/client/auth/oauth.ts diff --git a/config.universaltest.json b/config.universaltest.json index e6be30c59..89c760c49 100644 --- a/config.universaltest.json +++ b/config.universaltest.json @@ -5,6 +5,10 @@ "BROKER_SERVER_URL": "https://broker2.dev.snyk.io", "BROKER_HA_MODE_ENABLED": "false", "BROKER_DISPATCHER_BASE_URL": "https://api.dev.snyk.io" + }, + "oauth": { + "clientId": "${CLIENT_ID}", + "clientSecret": "${CLIENT_SECRET}" } }, "github": { diff --git a/lib/client/auth/oauth.ts b/lib/client/auth/oauth.ts new file mode 100644 index 000000000..df1379434 --- /dev/null +++ b/lib/client/auth/oauth.ts @@ -0,0 +1,46 @@ +import { makeRequestToDownstream } from '../../common/http/request'; +import { PostFilterPreparedRequest } from '../../common/relay/prepareRequest'; +import { log as logger } from '../../logs/logger'; +interface tokenExchangeResponse { + access_token: string; + expires_in: number; + scope: string; + token_type: string; +} + +export async function fetchJwt( + apiHostname: string, + clientId: string, + clientSecret: string, +) { + try { + const data = { + grant_type: 'client_credentials', + client_id: clientId, + client_secret: clientSecret, + }; + const formData = new URLSearchParams(data); + + const request: PostFilterPreparedRequest = { + url: `${apiHostname}/oauth2/token`, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + method: 'POST', + body: formData.toString(), + }; + const oauthResponse = await makeRequestToDownstream(request); + if (oauthResponse.statusCode != 200) { + const errorBody = JSON.parse(oauthResponse.body); + throw new Error( + `${oauthResponse.statusCode}-${errorBody.error}:${errorBody.error_description}`, + ); + } + const accessToken = JSON.parse(oauthResponse.body) as tokenExchangeResponse; + const jwt = accessToken.access_token; + const type = accessToken.token_type; + const expiresIn = accessToken.expires_in; + + return { expiresIn: expiresIn, authHeader: `${type} ${jwt}` }; + } catch (err) { + logger.error({ err }, 'Unable to retrieve JWT'); + } +} diff --git a/lib/client/index.ts b/lib/client/index.ts index 916f6c201..18ac17d60 100644 --- a/lib/client/index.ts +++ b/lib/client/index.ts @@ -18,6 +18,7 @@ import { loadAllFilters } from '../common/filter/filtersAsync'; import { ClientOpts, LoadedClientOpts } from '../common/types/options'; import { websocketConnectionSelectorMiddleware } from './routesHandler/websocketConnectionMiddlewares'; import { getClientConfigMetadata } from './utils/configHelpers'; +import { fetchJwt } from './auth/oauth'; process.on('uncaughtException', (error) => { if (error.message == 'read ECONNRESET') { @@ -54,6 +55,17 @@ export const main = async (clientOpts: ClientOpts) => { throw new Error('Unable to load filters'); } + if ( + clientOpts.config.brokerClientConfiguration.common.oauth?.clientId && + clientOpts.config.brokerClientConfiguration.common.oauth?.clientSecret + ) { + loadedClientOpts.accessToken = await fetchJwt( + clientOpts.config.API_BASE_URL, + clientOpts.config.brokerClientConfiguration.common.oauth.clientId, + clientOpts.config.brokerClientConfiguration.common.oauth.clientSecret, + ); + } + const globalIdentifyingMetadata: IdentifyingMetadata = { capabilities: ['post-streams'], clientId: brokerClientId, @@ -66,10 +78,20 @@ export const main = async (clientOpts: ClientOpts) => { let websocketConnections: WebSocketConnection[] = []; if (clientOpts.config.universalBrokerEnabled) { - websocketConnections = createWebSockets( - loadedClientOpts, - globalIdentifyingMetadata, - ); + const integrationsKeys = clientOpts.config.connections + ? Object.keys(clientOpts.config.connections) + : []; + if (integrationsKeys.length < 1) { + logger.error( + {}, + `No connection found. Please add connections to config.${process.env.SERVICE_ENV}.json.`, + ); + } else { + websocketConnections = createWebSockets( + loadedClientOpts, + globalIdentifyingMetadata, + ); + } } else { websocketConnections.push( createWebSocket( diff --git a/lib/client/socket.ts b/lib/client/socket.ts index 92a804409..2c96799fe 100644 --- a/lib/client/socket.ts +++ b/lib/client/socket.ts @@ -19,6 +19,7 @@ import { initializeSocketHandlers } from './socketHandlers/init'; import { LoadedClientOpts } from '../common/types/options'; import { translateIntegrationTypeToBrokerIntegrationType } from './utils/integrations'; import { maskToken } from '../common/utils/token'; +import { fetchJwt } from './auth/oauth'; export const createWebSockets = ( clientOpts: LoadedClientOpts, @@ -90,18 +91,27 @@ export const createWebSocket = ( // Will exponentially back-off from 0.5 seconds to a maximum of 20 minutes // Retry for a total period of around 4.5 hours + const socketSettings = { + reconnect: { + factor: 1.5, + retries: 30, + max: 20 * 60 * 1000, + }, + ping: parseInt(localClientOps.config.socketPingInterval) || 25000, + pong: parseInt(localClientOps.config.socketPongTimeout) || 10000, + timeout: parseInt(localClientOps.config.socketConnectTimeout) || 10000, + }; + + if (clientOpts.accessToken) { + socketSettings['transport'] = { + extraHeaders: { + Authorization: clientOpts.accessToken?.authHeader, + }, + }; + } const websocket: WebSocketConnection = new Socket( localClientOps.config.brokerServerUrlForSocket, - { - reconnect: { - factor: 1.5, - retries: 30, - max: 20 * 60 * 1000, - }, - ping: parseInt(localClientOps.config.socketPingInterval) || 25000, - pong: parseInt(localClientOps.config.socketPongTimeout) || 10000, - timeout: parseInt(localClientOps.config.socketConnectTimeout) || 10000, - }, + socketSettings, ); websocket.socketVersion = 1; websocket.socketType = 'client'; @@ -117,6 +127,38 @@ export const createWebSocket = ( websocket.clientConfig = identifyingMetadata.clientConfig; websocket.role = identifyingMetadata.role; + if (clientOpts.accessToken) { + let timeoutHandlerId; + let timeoutHandler = async () => {}; + timeoutHandler = async () => { + logger.debug({}, 'Refreshing oauth access token'); + clearTimeout(timeoutHandlerId); + clientOpts.accessToken = await fetchJwt( + clientOpts.config.API_BASE_URL, + clientOpts.config.brokerClientConfiguration.common.oauth!.clientId, + clientOpts.config.brokerClientConfiguration.common.oauth!.clientSecret, + ); + + websocket.transport.extraHeaders['Authorization'] = + clientOpts.accessToken!.authHeader; + websocket.end(); + websocket.open(); + timeoutHandlerId = setTimeout( + timeoutHandler, + (clientOpts.accessToken!.expiresIn - 60) * 1000, + ); + }; + + timeoutHandlerId = setTimeout( + timeoutHandler, + (clientOpts.accessToken!.expiresIn - 60) * 1000, + ); + } + + websocket.on('incoming::error', (e) => { + websocket.emit('error', { type: e.type, description: e.description }); + }); + logger.info( { url: localClientOps.config.brokerServerUrlForSocket, diff --git a/lib/client/types/client.ts b/lib/client/types/client.ts index 08d7b3b34..4ea96a556 100644 --- a/lib/client/types/client.ts +++ b/lib/client/types/client.ts @@ -69,10 +69,12 @@ export interface WebSocketConnection { socket: any; destroy: any; send: any; + end: any; + open: any; + emit: any; capabilities?: any; on: (string, any) => any; readyState: any; - end: () => any; } // export interface WebSocketConnection { // websocket: Connection; diff --git a/lib/client/types/config.ts b/lib/client/types/config.ts index 9ce11e117..a86132ebb 100644 --- a/lib/client/types/config.ts +++ b/lib/client/types/config.ts @@ -12,6 +12,7 @@ interface BrokerClient { interface BrokerServer { BROKER_SERVER_URL: string; + BROKER_SERVER_MANDATORY_AUTH_ENABLED?: boolean; } /** diff --git a/lib/common/types/options.ts b/lib/common/types/options.ts index 91609b9c7..d09649066 100644 --- a/lib/common/types/options.ts +++ b/lib/common/types/options.ts @@ -6,6 +6,14 @@ export interface ClientOpts { filters: FiltersType | Map; serverId?: string; connections?: Record; + oauth?: { + clientId: string; + clientSecret: string; + }; + accessToken?: { + authHeader: string; + expiresIn: number; + }; } export interface ServerOpts { diff --git a/lib/server/socket.ts b/lib/server/socket.ts index 14b0e4093..99c9dc50e 100644 --- a/lib/server/socket.ts +++ b/lib/server/socket.ts @@ -5,6 +5,8 @@ import { SocketHandler } from './types/socket'; import { handleIoError } from './socketHandlers/errorHandler'; import { handleSocketConnection } from './socketHandlers/connectionHandler'; import { initConnectionHandler } from './socketHandlers/initHandlers'; +import { maskToken } from '../common/utils/token'; +import { log as logger } from '../logs/logger'; const socketConnections = new Map(); @@ -28,6 +30,54 @@ const socket = ({ server, loadedServerOpts }): SocketHandler => { }; const websocket = new Primus(server, ioConfig); + websocket.authorize(async (req, done) => { + const maskedToken = maskToken( + req.uri.pathname.replaceAll(/^\/primus\/([^/]+)\//g, '$1').toLowerCase(), + ); + const authHeader = req.headers['authorization']; + + if ( + (!authHeader || !authHeader.startsWith('Bearer')) && + loadedServerOpts.config.BROKER_SERVER_MANDATORY_AUTH_ENABLED + ) { + logger.error({ maskedToken }, 'request missing Authorization header'); + done({ + statusCode: 401, + authenticate: 'Bearer', + message: 'missing required authorization header', + }); + return; + } + + const jwt = authHeader + ? authHeader.substring(authHeader.indexOf(' ') + 1) + : ''; + if (!jwt) logger.debug({}, `TODO: Validate jwt`); + done(); + // let oauthResponse = await axiosInstance.request({ + // url: 'http://localhost:8080/oauth2/introspect', + // method: 'POST', + // headers: { + // 'Content-Type': 'application/x-www-form-urlencoded', + // }, + // auth: { + // username: 'broker-connection-a', + // password: 'secret', + // }, + // data: `token=${token}`, + // }); + + // if (!oauthResponse.data.active) { + // logger.error({maskedToken}, 'JWT is not active (could be expired, malformed, not issued by us, etc)'); + // done({ + // statusCode: 403, + // message: 'token not active', + // }); + // } else { + // req.oauth_data = oauthResponse.data; + // done(); + // } + }); websocket.socketType = 'server'; websocket.socketVersion = 1; websocket.plugin('emitter', Emitter); diff --git a/test/functional/client-universal-server.test.ts b/test/functional/client-universal-server.test.ts index c8b98ef6a..173b9ffc6 100644 --- a/test/functional/client-universal-server.test.ts +++ b/test/functional/client-universal-server.test.ts @@ -45,6 +45,8 @@ describe('proxy requests originating from behind the broker client', () => { process.env.SNYK_BROKER_CLIENT_CONFIGURATION__common__default__BROKER_SERVER_URL = `http://localhost:${bs.port}`; process.env.SNYK_FILTER_RULES_PATHS__github = clientAccept; process.env.SNYK_FILTER_RULES_PATHS__gitlab = clientAccept; + process.env.CLIENT_ID = 'clienid'; + process.env.CLIENT_SECRET = 'clientsecret'; bc = await createUniversalBrokerClient(); }); @@ -57,6 +59,8 @@ describe('proxy requests originating from behind the broker client', () => { delete process.env.SNYK_BROKER_SERVER_UNIVERSAL_CONFIG_ENABLED; delete process.env .SNYK_BROKER_CLIENT_CONFIGURATION__common__default__BROKER_SERVER_URL; + delete process.env.CLIENT_ID; + delete process.env.CLIENT_SECRET; }); it('server identifies self to client', async () => { diff --git a/test/functional/healthcheck-universal.test.ts b/test/functional/healthcheck-universal.test.ts index ce55ace47..ed072d01d 100644 --- a/test/functional/healthcheck-universal.test.ts +++ b/test/functional/healthcheck-universal.test.ts @@ -23,6 +23,8 @@ describe('proxy requests originating from behind the broker client', () => { tws = await createTestWebServer(); bs = await createBrokerServer({ filters: serverAccept }); process.env.SNYK_BROKER_CLIENT_CONFIGURATION__common__default__BROKER_SERVER_URL = `http://localhost:${bs.port}`; + process.env.CLIENT_ID = 'clienid'; + process.env.CLIENT_SECRET = 'clientsecret'; }); afterAll(async () => { @@ -31,6 +33,8 @@ describe('proxy requests originating from behind the broker client', () => { delete process.env.BROKER_SERVER_URL; delete process.env .SNYK_BROKER_CLIENT_CONFIGURATION__common__default__BROKER_SERVER_URL; + delete process.env.CLIENT_ID; + delete process.env.CLIENT_SECRET; }); afterEach(async () => { diff --git a/test/functional/server-client-universal.test.ts b/test/functional/server-client-universal.test.ts index fd068d2aa..dd535271c 100644 --- a/test/functional/server-client-universal.test.ts +++ b/test/functional/server-client-universal.test.ts @@ -51,6 +51,8 @@ describe('proxy requests originating from behind the broker server', () => { process.env.SNYK_FILTER_RULES_PATHS__gitlab = clientAccept; process.env['SNYK_FILTER_RULES_PATHS__azure-repos'] = clientAccept; process.env['SNYK_FILTER_RULES_PATHS__jira-bearer-auth'] = clientAccept; + process.env.CLIENT_ID = 'clienid'; + process.env.CLIENT_SECRET = 'clientsecret'; bc = await createUniversalBrokerClient(); await waitForUniversalBrokerClientsConnection(bs, 2); @@ -68,6 +70,8 @@ describe('proxy requests originating from behind the broker server', () => { delete process.env.SNYK_BROKER_SERVER_UNIVERSAL_CONFIG_ENABLED; delete process.env .SNYK_BROKER_CLIENT_CONFIGURATION__common__default__BROKER_SERVER_URL; + delete process.env.CLIENT_ID; + delete process.env.CLIENT_SECRET; }); it('successfully broker GET', async () => { diff --git a/test/functional/systemcheck-universal.test.ts b/test/functional/systemcheck-universal.test.ts index 73eb31794..7f39db2cd 100644 --- a/test/functional/systemcheck-universal.test.ts +++ b/test/functional/systemcheck-universal.test.ts @@ -65,6 +65,8 @@ describe('broker client systemcheck endpoint', () => { process.env.AZURE_REPOS_TOKEN = '123'; process.env.AZURE_REPOS_HOST = 'hostname'; process.env.AZURE_REPOS_ORG = 'org'; + process.env.CLIENT_ID = 'clienid'; + process.env.CLIENT_SECRET = 'clientsecret'; process.env.SNYK_BROKER_CLIENT_CONFIGURATION__common__default__BROKER_SERVER_URL = `http://localhost:${bs.port}`; process.env.SNYK_FILTER_RULES_PATHS__github = clientAccept; @@ -88,6 +90,8 @@ describe('broker client systemcheck endpoint', () => { websocketConnectionOpen: true, }), ); + delete process.env.CLIENT_ID; + delete process.env.CLIENT_SECRET; }); // it('good validation url, custom endpoint, no authorization, no json response', async () => { @@ -390,6 +394,8 @@ describe('broker client systemcheck endpoint', () => { process.env.AZURE_REPOS_TOKEN = '123'; process.env.AZURE_REPOS_HOST = 'hostname'; process.env.AZURE_REPOS_ORG = 'org'; + process.env.CLIENT_ID = 'clienid'; + process.env.CLIENT_SECRET = 'clientsecret'; process.env.SNYK_BROKER_CLIENT_CONFIGURATION__common__default__BROKER_SERVER_URL = `http://localhost:${bs.port}`; process.env.SNYK_FILTER_RULES_PATHS__github = clientAccept; @@ -459,6 +465,8 @@ describe('broker client systemcheck endpoint', () => { 'Validation failed, please review connection details for my jira pat', }, ]); + delete process.env.CLIENT_ID; + delete process.env.CLIENT_SECRET; }); it('invalid validation url', async () => { @@ -473,6 +481,8 @@ describe('broker client systemcheck endpoint', () => { process.env.JIRA_HOSTNAME = 'notexists.notexists'; process.env.GITHUB_TOKEN = 'ghtoken'; process.env.GITLAB_TOKEN = 'gltoken'; + process.env.CLIENT_ID = 'clienid'; + process.env.CLIENT_SECRET = 'clientsecret'; process.env.SNYK_BROKER_CLIENT_CONFIGURATION__common__default__BROKER_SERVER_URL = `http://localhost:${bs.port}`; process.env.SNYK_FILTER_RULES_PATHS__github = clientAccept; @@ -524,5 +534,7 @@ describe('broker client systemcheck endpoint', () => { 'Validation failed, please review connection details for my gitlab connection', }, ]); + delete process.env.CLIENT_ID; + delete process.env.CLIENT_SECRET; }); }); diff --git a/test/unit/config.test.ts b/test/unit/config.test.ts index a1257d28b..0082298f8 100644 --- a/test/unit/config.test.ts +++ b/test/unit/config.test.ts @@ -24,6 +24,7 @@ describe('config', () => { const bitbucketTokens = ['1234', '5678']; const githubTokens = ['9012', '3456']; const complexToken = process.env.COMPLEX_TOKEN; + loadBrokerConfig(); const config = getConfig(); @@ -75,6 +76,9 @@ describe('config', () => { process.env.BROKER_TOKEN_4 = 'brokertoken4'; process.env.JIRA_PAT = 'jirapat'; process.env.JIRA_HOSTNAME = 'hostname'; + process.env.CLIENT_ID = 'clienid'; + process.env.CLIENT_SECRET = 'clientsecret'; + loadBrokerConfig(); const configData = getConfigForIdentifier( 'dummyBrokerIdentifier3', @@ -98,6 +102,8 @@ describe('config', () => { identifier: 'dummyBrokerIdentifier3', type: 'azure-repos', }); + delete process.env.CLIENT_ID; + delete process.env.CLIENT_SECRET; }); it('getConfigByidentifier', () => { @@ -111,6 +117,8 @@ describe('config', () => { process.env.BROKER_TOKEN_4 = 'brokertoken4'; process.env.JIRA_PAT = 'jirapat'; process.env.JIRA_HOSTNAME = 'hostname'; + process.env.CLIENT_ID = 'clienid'; + process.env.CLIENT_SECRET = 'clientsecret'; loadBrokerConfig(); const configData = getConfigForIdentifier( 'dummyBrokerIdentifier', @@ -141,6 +149,8 @@ describe('config', () => { delete process.env.BROKER_TOKEN_1; delete process.env.BROKER_TOKEN_2; delete process.env.BROKER_TOKEN_3; + delete process.env.CLIENT_ID; + delete process.env.CLIENT_SECRET; }); it('getConfigByidentifier with global BROKER_CLIENT_URL', () => { @@ -152,6 +162,8 @@ describe('config', () => { process.env.BROKER_TOKEN_1 = 'dummyBrokerIdentifier'; process.env.BROKER_TOKEN_2 = 'dummyBrokerIdentifier2'; process.env.BROKER_TOKEN_3 = 'dummyBrokerIdentifier3'; + process.env.CLIENT_ID = 'clienid'; + process.env.CLIENT_SECRET = 'clientsecret'; loadBrokerConfig(); const configData = getConfigForIdentifier( 'dummyBrokerIdentifier', @@ -184,6 +196,8 @@ describe('config', () => { delete process.env.BROKER_TOKEN_1; delete process.env.BROKER_TOKEN_2; delete process.env.BROKER_TOKEN_3; + delete process.env.CLIENT_ID; + delete process.env.CLIENT_SECRET; }); it('fails to load if missing env var', () => { diff --git a/test/unit/relay-response-body-form-url-encoded.test.ts b/test/unit/relay-response-body-form-url-encoded.test.ts index 2ab2dbf01..8fbc5a0c1 100644 --- a/test/unit/relay-response-body-form-url-encoded.test.ts +++ b/test/unit/relay-response-body-form-url-encoded.test.ts @@ -43,6 +43,8 @@ const dummyWebsocketHandler: WebSocketConnection = { on: () => {}, end: () => {}, role: Role.primary, + open: () => {}, + emit: () => {}, readyState: 3, }; diff --git a/test/unit/relay-response-body-universal-form-url-encoded.test.ts b/test/unit/relay-response-body-universal-form-url-encoded.test.ts index ddbe23f11..157de8fe5 100644 --- a/test/unit/relay-response-body-universal-form-url-encoded.test.ts +++ b/test/unit/relay-response-body-universal-form-url-encoded.test.ts @@ -43,6 +43,8 @@ const dummyWebsocketHandler: WebSocketConnection = { on: () => {}, end: () => {}, role: Role.primary, + open: () => {}, + emit: () => {}, readyState: 3, }; diff --git a/test/unit/relay-response-body-universal.test.ts b/test/unit/relay-response-body-universal.test.ts index d3ff45565..744ed5a15 100644 --- a/test/unit/relay-response-body-universal.test.ts +++ b/test/unit/relay-response-body-universal.test.ts @@ -45,6 +45,8 @@ const dummyWebsocketHandler: WebSocketConnection = { on: () => {}, end: () => {}, role: Role.primary, + open: () => {}, + emit: () => {}, readyState: 3, supportedIntegrationType: 'github', }; diff --git a/test/unit/relay-response-body.test.ts b/test/unit/relay-response-body.test.ts index 47b80e377..a25fea447 100644 --- a/test/unit/relay-response-body.test.ts +++ b/test/unit/relay-response-body.test.ts @@ -43,6 +43,8 @@ const dummyWebsocketHandler: WebSocketConnection = { on: () => {}, end: () => {}, role: Role.primary, + open: () => {}, + emit: () => {}, readyState: 3, }; diff --git a/test/unit/relay-response-headers-form-url-headers.test.ts b/test/unit/relay-response-headers-form-url-headers.test.ts index 4317ec7d6..775f56705 100644 --- a/test/unit/relay-response-headers-form-url-headers.test.ts +++ b/test/unit/relay-response-headers-form-url-headers.test.ts @@ -42,6 +42,8 @@ const dummyWebsocketHandler: WebSocketConnection = { on: () => {}, end: () => {}, role: Role.primary, + open: () => {}, + emit: () => {}, readyState: 3, }; diff --git a/test/unit/relay-response-headers-universal-form-url-headers.test.ts b/test/unit/relay-response-headers-universal-form-url-headers.test.ts index 4ab7b9d59..e976da634 100644 --- a/test/unit/relay-response-headers-universal-form-url-headers.test.ts +++ b/test/unit/relay-response-headers-universal-form-url-headers.test.ts @@ -42,6 +42,8 @@ const dummyWebsocketHandler: WebSocketConnection = { on: () => {}, end: () => {}, role: Role.primary, + open: () => {}, + emit: () => {}, readyState: 3, }; diff --git a/test/unit/relay-response-headers-universal.test.ts b/test/unit/relay-response-headers-universal.test.ts index 12a46905a..755c1607e 100644 --- a/test/unit/relay-response-headers-universal.test.ts +++ b/test/unit/relay-response-headers-universal.test.ts @@ -42,6 +42,8 @@ const dummyWebsocketHandler: WebSocketConnection = { on: () => {}, end: () => {}, role: Role.primary, + open: () => {}, + emit: () => {}, readyState: 3, }; diff --git a/test/unit/relay-response-headers.test.ts b/test/unit/relay-response-headers.test.ts index 0d57d9717..7c6a2296b 100644 --- a/test/unit/relay-response-headers.test.ts +++ b/test/unit/relay-response-headers.test.ts @@ -42,6 +42,8 @@ const dummyWebsocketHandler: WebSocketConnection = { on: () => {}, end: () => {}, role: Role.primary, + open: () => {}, + emit: () => {}, readyState: 3, };