diff --git a/api/server/routes/__tests__/rum.spec.js b/api/server/routes/__tests__/rum.spec.js new file mode 100644 index 0000000000..ad161a75eb --- /dev/null +++ b/api/server/routes/__tests__/rum.spec.js @@ -0,0 +1,60 @@ +const express = require('express'); +const request = require('supertest'); + +const mockRequireJwtAuth = jest.fn((_req, _res, next) => next()); +const mockIsRumProxyEnabled = jest.fn(); +const mockProxyRumRequest = jest.fn((_req, res) => res.status(202).send()); + +jest.mock('~/server/middleware', () => ({ + requireJwtAuth: (...args) => mockRequireJwtAuth(...args), +})); + +jest.mock('@librechat/api', () => ({ + getRumProxyBodyLimit: jest.fn(() => '3mb'), + isRumProxyEnabled: (...args) => mockIsRumProxyEnabled(...args), + proxyRumRequest: (...args) => mockProxyRumRequest(...args), +})); + +describe('RUM proxy routes', () => { + let app; + + beforeAll(() => { + const rumRouter = require('../rum'); + + app = express(); + app.use('/api/rum', rumRouter); + }); + + beforeEach(() => { + mockRequireJwtAuth.mockClear(); + mockIsRumProxyEnabled.mockReset(); + mockProxyRumRequest.mockClear(); + }); + + it('returns 404 before auth and proxying when RUM proxy mode is disabled', async () => { + mockIsRumProxyEnabled.mockReturnValue(false); + + const response = await request(app) + .post('/api/rum/v1/traces') + .set('Content-Type', 'application/x-protobuf') + .send(Buffer.from('payload')); + + expect(response.status).toBe(404); + expect(response.body).toEqual({ message: 'RUM proxy is not configured' }); + expect(mockRequireJwtAuth).not.toHaveBeenCalled(); + expect(mockProxyRumRequest).not.toHaveBeenCalled(); + }); + + it('authenticates and proxies when RUM proxy mode is enabled', async () => { + mockIsRumProxyEnabled.mockReturnValue(true); + + const response = await request(app) + .post('/api/rum/v1/traces') + .set('Content-Type', 'application/x-protobuf') + .send(Buffer.from('payload')); + + expect(response.status).toBe(202); + expect(mockRequireJwtAuth).toHaveBeenCalledTimes(1); + expect(mockProxyRumRequest).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/api/src/rum/proxy.spec.ts b/packages/api/src/rum/proxy.spec.ts index 74225eb417..989dea46a5 100644 --- a/packages/api/src/rum/proxy.spec.ts +++ b/packages/api/src/rum/proxy.spec.ts @@ -48,6 +48,7 @@ describe('RUM proxy configuration', () => { }); it('resolves OTLP paths against the configured collector base URL', () => { + process.env.RUM_ENABLED = 'true'; process.env.RUM_AUTH_MODE = 'proxy'; process.env.RUM_PROXY_TARGET_URL = 'http://otel-collector:4318'; @@ -57,6 +58,17 @@ describe('RUM proxy configuration', () => { expect(resolveRumProxyTarget('/v1/metrics')).toBeUndefined(); }); + it('does not enable proxy mode when RUM is disabled', () => { + process.env.RUM_ENABLED = 'false'; + process.env.RUM_AUTH_MODE = 'proxy'; + process.env.RUM_PROXY_TARGET_URL = 'http://otel-collector:4318'; + + expect(isRumProxyEnabled()).toBe(false); + + delete process.env.RUM_ENABLED; + expect(isRumProxyEnabled()).toBe(false); + }); + it('rejects unsafe collector target URLs', () => { process.env.RUM_PROXY_TARGET_URL = 'https://user:pass@collector.example.com'; expect(getRumProxyTargetBaseUrl()).toBeUndefined(); @@ -66,6 +78,7 @@ describe('RUM proxy configuration', () => { }); it('forwards OTLP requests without forwarding app authorization', async () => { + process.env.RUM_ENABLED = 'true'; process.env.RUM_AUTH_MODE = 'proxy'; process.env.RUM_PROXY_TARGET_URL = 'http://otel-collector:4318'; const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValue( @@ -106,6 +119,7 @@ describe('RUM proxy configuration', () => { }); it('returns 400 for missing payloads and 404 for unsupported OTLP paths', async () => { + process.env.RUM_ENABLED = 'true'; process.env.RUM_AUTH_MODE = 'proxy'; process.env.RUM_PROXY_TARGET_URL = 'http://otel-collector:4318'; const missingBodyRes = makeResponse(); @@ -122,6 +136,7 @@ describe('RUM proxy configuration', () => { }); it('returns 502 when the collector request fails', async () => { + process.env.RUM_ENABLED = 'true'; process.env.RUM_AUTH_MODE = 'proxy'; process.env.RUM_PROXY_TARGET_URL = 'http://otel-collector:4318'; const fetchMock = jest.spyOn(global, 'fetch').mockRejectedValue(new Error('collector down')); diff --git a/packages/api/src/rum/proxy.ts b/packages/api/src/rum/proxy.ts index 051f74f5af..804186f238 100644 --- a/packages/api/src/rum/proxy.ts +++ b/packages/api/src/rum/proxy.ts @@ -1,5 +1,6 @@ import type { Request, Response } from 'express'; import { logger } from '@librechat/data-schemas'; +import { isEnabled } from '~/utils'; const DEFAULT_PROXY_PATH = '/api/rum'; const DEFAULT_BODY_LIMIT = '3mb'; @@ -52,7 +53,11 @@ export function getRumProxyTargetBaseUrl(): URL | undefined { } export function isRumProxyEnabled(): boolean { - return process.env.RUM_AUTH_MODE === 'proxy' && getRumProxyTargetBaseUrl() != null; + return ( + isEnabled(process.env.RUM_ENABLED) && + process.env.RUM_AUTH_MODE === 'proxy' && + getRumProxyTargetBaseUrl() != null + ); } export function resolveRumProxyTarget(path: string): string | undefined {