LibreChat/api/server/routes/__tests__/messages-get-real-validation.spec.js
Danny Avila 67b7b441b2
🛂 feat: Filter Model-Bound Content by Source (#14425)
* feat: introduce optional content protection seam

* feat: enforce source-aware content filters

* feat: complete source-aware content enforcement

* test: activate skill file-text fail-close fixtures

* fix: harden source-aware content filters

* fix: harden model-bound content filtering

* fix: preserve legacy filters and generated files

* fix: inspect shared scalar metadata

* test: align mocks with current dev dependencies

* feat: add persisted content filter safeguards

* feat: complete source-aware content filter enforcement

* fix: move resume content preflight into TypeScript

* fix: close content inspection edge cases

* fix: harden content protection boundaries

* fix: complete content protection safeguards

* test: align persisted memory filter coverage

* fix: reconcile content protection with current dev

* fix: reconcile content protection with latest dev

* fix: close content protection review gaps

* fix: enforce source-aware provider boundaries

* fix: preserve legacy PII preflight semantics

* test: stabilize stored branch preflight fixture

* fix: defer agent writes until protected model admission

* perf: harden source-aware model-bound filtering

* fix: canonicalize provider lineage before validation

* fix: satisfy model-bound callback type checks

* perf: Bound content protection filtering work

* fix: Bound submission array traversal

* fix: Stabilize bounded content snapshots

* fix: Scope model-bound traversal overflows

* fix: Preserve scoped content inspection

* fix: Accumulate aggregate traversal scopes

* fix: centralize content policy boundaries

* test: align deferred tool policy context

* test: align controller policy mocks

* style: normalize content protection imports

* fix: close content policy review gaps

* fix: narrow active skill policy config

* fix: address content protection review boundaries

* fix: retain exact provenance overflow sentinel

* fix: preserve literal and scoped provenance updates

* fix: narrow persisted edit provenance

* fix: isolate exact overflow attribution

* fix: centralize stored prompt protection

* fix: fail closed on incomplete transcript evidence

* fix: align canonical transcript routing

* refactor: centralize content policy preflights

* fix: isolate upload policy error typing

* style: sort policy preflight imports

* refactor: centralize content policy boundaries
2026-08-21 22:43:32 -04:00

204 lines
6.4 KiB
JavaScript

const { CLIENT_MESSAGE_SELECT } = require('@librechat/data-schemas');
const express = require('express');
const request = require('supertest');
jest.mock('@librechat/agents', () => ({
...jest.requireActual('@librechat/agents'),
CODE_EXECUTION_TOOLS: new Set(['execute_code', 'bash_tool']),
BashExecutionToolDefinition: {
name: 'bash_tool',
description: 'bash',
schema: { type: 'object', properties: {} },
},
ReadFileToolDefinition: {
name: 'read_file',
description: 'Read a file',
parameters: { type: 'object', properties: {} },
responseFormat: 'content',
},
buildBashExecutionToolDescription: () => 'bash',
sleep: jest.fn(),
}));
jest.mock('@librechat/api', () => ({
createContentFilter: jest.fn(() => (req, res, next) => next()),
inspectContent: jest.fn(() => null),
extractFeedbackContent: jest.fn(() => []),
extractStoredMessageContent: jest.fn(() => []),
contentFilterBlockResponse: jest.fn(),
createMessageRequestMiddleware:
jest.requireActual('@librechat/api').createMessageRequestMiddleware,
unescapeLaTeX: jest.fn((x) => x),
countTokens: jest.fn().mockResolvedValue(10),
sendFeedbackScore: jest.fn().mockResolvedValue(undefined),
traceIdForMessage: jest.fn((messageId) => `trace-${messageId}`),
mergeQuotedTextForCount: jest.fn((text) => text),
GenerationJobManager: {
getJob: jest.fn(),
},
isPendingActionStale: jest.fn(() => false),
CHILD_THREAD_READ_ONLY_ERROR: 'Child thread is view-only.',
isSubagentThreadWriteBlocked: jest.fn().mockResolvedValue(false),
requireFeedbackEnabled: (req, res, next) => next(),
}));
jest.mock('~/server/services/Endpoints/agents/subagentThreadStore', () => ({}));
jest.mock('@librechat/data-schemas', () => ({
...jest.requireActual('@librechat/data-schemas'),
logger: {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
},
}));
jest.mock('librechat-data-provider', () => ({
...jest.requireActual('librechat-data-provider'),
}));
jest.mock('~/models', () => ({
saveConvo: jest.fn(),
getConvoOwnership: jest.fn(),
getMessage: jest.fn(),
saveMessage: jest.fn(),
getMessages: jest.fn(),
updateMessage: jest.fn(),
deleteMessages: jest.fn(),
getConvosQueried: jest.fn(),
searchMessages: jest.fn(),
getMessagesByCursor: jest.fn(),
}));
jest.mock('~/server/services/Artifacts/update', () => ({
findAllArtifacts: jest.fn(),
replaceArtifactContent: jest.fn(),
}));
jest.mock('~/server/middleware/requireJwtAuth', () => (req, res, next) => next());
jest.mock('~/server/middleware', () => {
const { sendValidationResponse, validateMessageReq, prepareMessageRequestValidation } =
jest.requireActual('~/server/middleware/messageValidation');
return {
requireJwtAuth: (req, res, next) => next(),
validateMessageReq,
sendValidationResponse,
prepareMessageRequestValidation,
configMiddleware: (req, res, next) => next(),
};
});
jest.mock('~/db/models', () => ({
Message: {
findOne: jest.fn(),
find: jest.fn(),
meiliSearch: jest.fn(),
},
}));
describe('GET /api/messages/:conversationId with real validation middleware', () => {
let app;
const { getConvoOwnership, getMessages } = require('~/models');
const authenticatedUserId = 'user-owner-123';
beforeAll(() => {
const messagesRouter = require('../messages');
app = express();
app.use(express.json());
app.use((req, res, next) => {
req.user = { id: authenticatedUserId };
next();
});
app.use('/api/messages', messagesRouter);
});
beforeEach(() => {
jest.clearAllMocks();
});
it('returns the existing empty response for new conversations without fetching messages', async () => {
const response = await request(app).get('/api/messages/new');
expect(response.status).toBe(200);
expect(response.body).toEqual([]);
expect(getConvoOwnership).not.toHaveBeenCalled();
expect(getMessages).not.toHaveBeenCalled();
});
it('starts user-scoped message reads before real conversation validation resolves', async () => {
const events = [];
let resolveConvo;
const convoPromise = new Promise((resolve) => {
resolveConvo = resolve;
});
getConvoOwnership.mockImplementation(() => {
events.push('convo-started');
return convoPromise;
});
let resolveMessagesStarted;
const messagesStartedPromise = new Promise((resolve) => {
resolveMessagesStarted = resolve;
});
getMessages.mockImplementation(() => {
events.push('messages-started');
resolveMessagesStarted();
return Promise.resolve([{ messageId: 'message-1', conversationId: 'convo-1' }]);
});
const responsePromise = new Promise((resolve, reject) => {
request(app)
.get('/api/messages/convo-1')
.end((error, response) => (error ? reject(error) : resolve(response)));
});
await Promise.race([
messagesStartedPromise,
new Promise((resolve) => setTimeout(resolve, 100)),
]);
const eventsBeforeValidation = [...events];
resolveConvo({ conversationId: 'convo-1', user: authenticatedUserId });
const response = await responsePromise;
expect(eventsBeforeValidation).toEqual(['convo-started', 'messages-started']);
expect(getConvoOwnership).toHaveBeenCalledWith(authenticatedUserId, 'convo-1');
expect(getMessages).toHaveBeenCalledWith(
{ conversationId: 'convo-1', user: authenticatedUserId },
CLIENT_MESSAGE_SELECT,
);
expect(response.status).toBe(200);
expect(response.body).toEqual([{ messageId: 'message-1', conversationId: 'convo-1' }]);
});
it('does not return messages for a directly addressed child thread', async () => {
getConvoOwnership.mockResolvedValue({
conversationId: 'child-convo',
user: authenticatedUserId,
subagentThread: { parentConversationId: 'parent-convo' },
});
getMessages.mockResolvedValue([{ messageId: 'child-message', conversationId: 'child-convo' }]);
const response = await request(app).get('/api/messages/child-convo');
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'Conversation not found' });
});
it('does not expose a directly addressed child thread through HEAD', async () => {
getConvoOwnership.mockResolvedValue({
conversationId: 'child-convo',
user: authenticatedUserId,
subagentThread: { parentConversationId: 'parent-convo' },
});
const response = await request(app).head('/api/messages/child-convo');
expect(response.status).toBe(404);
});
});