LibreChat/api/server/routes/__tests__/share.spec.js
Ravi Kumar L a70bcf66a4
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
🪢 feat: show Langfuse session link in shared chats (#15273)
* feat: show Langfuse sessions in shared chats

* fix: wait for optional auth before loading shares

* fix: require Langfuse access for shared session links

* refactor: move shared Langfuse policy into API package

* fix: wrap shared chat actions on mobile

* fix: coordinate shared Langfuse link loading

* perf: batch shared Langfuse capability checks
2026-08-27 14:29:57 -04:00

1990 lines
68 KiB
JavaScript

const express = require('express');
const request = require('supertest');
const mongoose = require('mongoose');
const mockGetSharedLinkExpiration = jest.fn();
const mockGrantCreationPermissions = jest.fn();
const mockUpdateSharedLinkPermissionsExpiration = jest.fn();
const mockSharedLinksAccess = jest.fn((_req, _res, next) => next());
const mockSharedLinkConfigMiddleware = jest.fn((_req, _res, next) => next());
let mockShareTenantId;
const mockHasCapability = jest.fn();
const mockHasConfigCapability = jest.fn();
const mockGetSharedLangfuseSessionUrl = jest.fn();
const mockBuildSharedLinkStartupPayload = jest.fn();
const mockCanAccessSharedLink = jest.fn((req, _res, next) => {
req.shareResourceId = 'resource-123';
req.shareTenantId = mockShareTenantId;
req.shareConversationId = 'conversation-owner-123';
req.shareOwnerId = 'owner-123';
next();
});
const mockGetAppConfig = jest.fn();
const mockParseSharedLinksPageSize = jest.fn(() => 10);
const mockIsValidSharedLinksCursor = jest.fn(() => true);
const mockAssertConversationContentAllowed = jest.fn();
const mockAssertModelBoundContent = jest.fn();
const mockAssertSharedFileMetadataAllowed = jest.fn();
const mockCreateShareContentPreflight = jest.fn((filters, options = {}) => {
const legacyPii = options.legacyPii;
if (filters == null && legacyPii == null) {
return undefined;
}
const omitFiles = (messages) =>
messages.map(({ files: _files, attachments: _attachments, ...message }) => ({
...message,
...(Array.isArray(message.content)
? {
content: message.content.map((part) => {
if (part?.type !== 'steer' || part.files === undefined) {
return part;
}
const { files: _partFiles, ...rest } = part;
return rest;
}),
}
: {}),
}));
return async ({ title, messages, shareId }) => {
const inspectSharedFileMetadata = options.sharedFileMetadata === true;
const inspectSharedFiles =
inspectSharedFileMetadata && options.sharedFileMetadataFiles !== false;
const snapshot = {
conversations: [{ title }],
messages:
options.snapshotFiles === false || inspectSharedFiles ? omitFiles(messages) : messages,
};
const context = {
...(options.user == null ? {} : { user: options.user, getFiles: options.getFiles }),
...(legacyPii == null ? {} : { legacyPii }),
};
await mockAssertConversationContentAllowed(
filters,
snapshot,
...(Object.keys(context).length === 0 ? [] : [context]),
);
if (inspectSharedFileMetadata) {
mockAssertSharedFileMetadataAllowed({
filters,
messages: options.snapshotFiles === false ? omitFiles(messages) : messages,
shareId,
...(options.sharedFileMetadataFiles === false && { includeFiles: false }),
});
}
};
});
jest.mock('@librechat/api', () => ({
assertModelBoundContent: (...args) => mockAssertModelBoundContent(...args),
assertSharedFileMetadataAllowed: (...args) => mockAssertSharedFileMetadataAllowed(...args),
createShareContentPreflight: (...args) => mockCreateShareContentPreflight(...args),
isEnabled: jest.fn(() => true),
generateCheckAccess: jest.fn(() => mockSharedLinksAccess),
grantCreationPermissions: (...args) => mockGrantCreationPermissions(...args),
updateSharedLinkPermissionsExpiration: (...args) =>
mockUpdateSharedLinkPermissionsExpiration(...args),
ensureLinkPermissions: jest.fn(),
isFileSnapshotEnabled: jest.fn(() => true),
isFileSnapshotKillSwitchActive: jest.fn(() => false),
buildSharedLinkStartupPayload: (...args) => mockBuildSharedLinkStartupPayload(...args),
createSharedLinkConfigMiddleware: jest.fn(() => mockSharedLinkConfigMiddleware),
deleteSharedLinkWithCleanup: jest.fn(),
getSharedLinkExpiration: (...args) => mockGetSharedLinkExpiration(...args),
isActiveExpirationDate: jest.fn((expiredAt) => expiredAt > new Date()),
/* The list/query helpers are behaviour-tested in `@librechat/api`; the route only has
to be wired to them, so they stand in as doubles here. */
parseSharedLinksPageSize: (...args) => mockParseSharedLinksPageSize(...args),
isValidSharedLinksCursor: (...args) => mockIsValidSharedLinksCursor(...args),
buildShareFileEtag: (file) =>
`"share-${file.file_id}-${file.previewRevision ?? 0}-${file.bytes ?? 0}-${file.filepath ?? ''}"`,
MAX_SHARED_LINK_SEARCH_LENGTH: 256,
createSharedLangfuseSessionResolver: jest.fn(
() =>
(...args) =>
mockGetSharedLangfuseSessionUrl(...args),
),
isContentFilterError: jest.fn(
(error) =>
error?.code === 'content_filter_block' || error?.code === 'content_filter_uninspectable',
),
}));
jest.mock('@librechat/data-schemas', () => ({
logger: { error: jest.fn(), warn: jest.fn() },
createTempChatExpirationDate: jest.fn(() => new Date('2030-01-01T00:00:00.000Z')),
runAsSystem: jest.fn((fn) => fn()),
tenantStorage: { run: jest.fn((_ctx, fn) => fn()) },
SYSTEM_TENANT_ID: '__SYSTEM__',
SystemCapabilities: { ACCESS_ADMIN: 'access:admin' },
}));
jest.mock('librechat-data-provider', () => ({
PermissionTypes: {
SHARED_LINKS: 'SHARED_LINKS',
},
Permissions: {
CREATE: 'CREATE',
SHARE_PUBLIC: 'SHARE_PUBLIC',
},
RetentionMode: {
ALL: 'all',
TEMPORARY: 'temporary',
},
FileSources: {
local: 'local',
s3: 's3',
cloudfront: 'cloudfront',
azure_blob: 'azure_blob',
firebase: 'firebase',
text: 'text',
},
}));
jest.mock('mongoose', () => ({
models: {
Conversation: {
findOne: jest.fn(),
},
SharedLink: {
findOne: jest.fn(),
},
},
}));
jest.mock('~/models', () => ({
getFiles: jest.fn(),
updateFile: jest.fn(),
getSharedMessages: jest.fn(),
createSharedLink: jest.fn(),
updateSharedLink: jest.fn(),
deleteSharedLink: jest.fn(),
getSharedLinks: jest.fn(),
getSharedLink: jest.fn(),
getSharedLinkFile: jest.fn(),
backfillSharedLinkFiles: jest.fn(),
getMessages: jest.fn(),
getRoleByName: jest.fn(),
}));
jest.mock('~/server/middleware/roles/capabilities', () => ({
hasCapability: (...args) => mockHasCapability(...args),
hasConfigCapability: (...args) => mockHasConfigCapability(...args),
}));
const mockGetStrategyFunctions = jest.fn();
jest.mock('~/server/services/Files/strategies', () => ({
getStrategyFunctions: (...args) => mockGetStrategyFunctions(...args),
}));
jest.mock('~/server/utils/files', () => ({
cleanFileName: jest.fn((name) => name),
getContentDisposition: jest.fn((name, disposition = 'attachment') => `${disposition}; ${name}`),
}));
jest.mock(
'~/server/middleware/canAccessSharedLink',
() =>
(...args) =>
mockCanAccessSharedLink(...args),
);
jest.mock('~/server/middleware/optionalShareFileAuth', () => (_req, _res, next) => next());
jest.mock('~/server/middleware/optionalJwtAuth', () => (req, _res, next) => next());
jest.mock('~/server/middleware/requireJwtAuth', () => (req, res, next) => next());
jest.mock('~/server/middleware/config/app', () => (_req, _res, next) => next());
jest.mock('~/server/services/Config/app', () => ({
getAppConfig: (...args) => mockGetAppConfig(...args),
}));
jest.mock('~/server/middleware/limiters', () => ({
createForkLimiters: () => ({
forkIpLimiter: (_req, _res, next) => next(),
forkUserLimiter: (_req, _res, next) => next(),
}),
}));
jest.mock('~/server/utils/import/fork', () => ({
forkSharedConversation: jest.fn(),
}));
jest.mock('~/server/utils/import/importBatchBuilder', () => ({
assertConversationContentAllowed: (...args) => mockAssertConversationContentAllowed(...args),
}));
const { Readable } = require('stream');
const { RetentionMode } = require('librechat-data-provider');
const { createTempChatExpirationDate, logger } = require('@librechat/data-schemas');
const {
deleteSharedLinkWithCleanup,
isFileSnapshotEnabled,
isFileSnapshotKillSwitchActive,
} = require('@librechat/api');
const {
getFiles,
updateFile,
getSharedMessages,
createSharedLink,
updateSharedLink,
getSharedLinks,
getSharedLinkFile,
backfillSharedLinkFiles,
getRoleByName,
} = require('~/models');
const { forkSharedConversation } = require('~/server/utils/import/fork');
const shareRouter = require('../share');
const activeExpiration = new Date('2030-01-01T00:00:00.000Z');
const expiredExpiration = new Date('2020-01-01T00:00:00.000Z');
const contentFilters = {
messages: {
pii: {
starterPatterns: [],
customPatterns: [{ id: 'private', label: 'private value', regex: 'PRIVATE-[A-Z]+' }],
},
},
};
const lean = (value) => ({
lean: jest.fn().mockResolvedValue(value),
});
const buildApp = ({
retentionMode = RetentionMode.TEMPORARY,
user = { id: 'user-123' },
filters,
messageFilter,
langfuse,
} = {}) => {
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
req.user = user;
req.config = {
interfaceConfig: { retentionMode },
...(filters == null ? {} : { filters }),
...(messageFilter == null ? {} : { messageFilter }),
...(langfuse == null ? {} : { langfuse }),
};
next();
});
app.use('/api/share', shareRouter);
return app;
};
const mockSharedMessagesResult = (result) => {
getSharedMessages.mockImplementation(async (_shareId, _resourceId, options) => {
await options?.preflight?.(result);
return result;
});
};
const mockUpdatedShare = (result) => {
updateSharedLink.mockImplementationOnce(async (...args) => {
await args[5]?.({ title: 'Safe share', messages: [] });
await args[6]?.();
return result;
});
};
const useResolvedSharedLinkConfig = () => {
mockSharedLinkConfigMiddleware.mockImplementationOnce(async (req, _res, next) => {
const tenantId = req.shareTenantId;
req.config = await mockGetAppConfig(
tenantId && tenantId !== '__SYSTEM__' ? { tenantId } : { baseOnly: true },
);
next();
});
};
describe('share routes', () => {
beforeEach(() => {
jest.clearAllMocks();
mockShareTenantId = undefined;
mockHasCapability.mockResolvedValue(false);
mockHasConfigCapability.mockResolvedValue(false);
mockGetSharedLangfuseSessionUrl.mockResolvedValue(null);
mockGetAppConfig.mockResolvedValue({
interfaceConfig: {
privacyPolicy: { externalUrl: 'https://example.com/privacy' },
},
});
mockBuildSharedLinkStartupPayload.mockReturnValue({
appTitle: 'Shared Chat',
bundlerURL: 'https://bundler.example.com',
interface: {
privacyPolicy: { externalUrl: 'https://example.com/privacy' },
},
});
getRoleByName.mockResolvedValue({
permissions: {
SHARED_LINKS: {
SHARE_PUBLIC: true,
},
},
});
mockGrantCreationPermissions.mockResolvedValue(undefined);
});
it('serves shared startup config after shared-link access is granted', async () => {
useResolvedSharedLinkConfig();
const response = await request(buildApp()).get('/api/share/share-123/config');
expect(response.status).toBe(200);
expect(response.headers['cache-control']).toBe('private, no-store');
expect(mockCanAccessSharedLink).toHaveBeenCalled();
expect(mockGetAppConfig).toHaveBeenCalledWith({ baseOnly: true });
expect(mockBuildSharedLinkStartupPayload).toHaveBeenCalledWith({
interfaceConfig: {
privacyPolicy: { externalUrl: 'https://example.com/privacy' },
},
});
expect(response.body).toEqual({
appTitle: 'Shared Chat',
bundlerURL: 'https://bundler.example.com',
interface: {
privacyPolicy: { externalUrl: 'https://example.com/privacy' },
},
});
});
it('uses tenant-scoped app config for shared startup config when tenant context is present', async () => {
mockShareTenantId = 'tenant-abc';
useResolvedSharedLinkConfig();
const response = await request(buildApp()).get('/api/share/share-123/config');
expect(response.status).toBe(200);
expect(mockGetAppConfig).toHaveBeenCalledWith({ tenantId: 'tenant-abc' });
});
it('uses base app config for shared startup config in system context', async () => {
mockShareTenantId = '__SYSTEM__';
useResolvedSharedLinkConfig();
const response = await request(buildApp()).get('/api/share/share-123/config');
expect(response.status).toBe(200);
expect(mockGetAppConfig).toHaveBeenCalledWith({ baseOnly: true });
});
it('prevents successful shared message responses from being cached', async () => {
mockSharedMessagesResult({ shareId: 'share-123', messages: [] });
const response = await request(buildApp()).get('/api/share/share-123');
expect(response.status).toBe(200);
expect(response.headers['cache-control']).toBe('private, no-store');
expect(mockAssertConversationContentAllowed).not.toHaveBeenCalled();
expect(mockAssertSharedFileMetadataAllowed).not.toHaveBeenCalled();
});
it('includes the source conversation session for an admin in the share tenant', async () => {
mockShareTenantId = 'tenant-abc';
mockGetSharedLangfuseSessionUrl.mockResolvedValue(
'https://cloud.langfuse.com/project/project-1/sessions/conversation-owner-123',
);
mockSharedMessagesResult({ shareId: 'share-123', messages: [] });
const langfuse = { enabled: true, destination: 'eu', projectId: 'project-1' };
const response = await request(
buildApp({
user: { id: 'admin-123', role: 'ADMIN', tenantId: 'tenant-abc' },
langfuse,
}),
).get('/api/share/share-123');
expect(response.status).toBe(200);
expect(response.body.langfuseSessionUrl).toBe(
'https://cloud.langfuse.com/project/project-1/sessions/conversation-owner-123',
);
expect(mockGetSharedLangfuseSessionUrl).toHaveBeenCalledWith({
viewer: expect.objectContaining({ id: 'admin-123', tenantId: 'tenant-abc' }),
shareTenantId: 'tenant-abc',
shareConversationId: 'conversation-owner-123',
shareOwnerId: 'owner-123',
config: langfuse,
});
});
it('omits the session link when the shared-session resolver denies access', async () => {
mockShareTenantId = 'tenant-abc';
mockSharedMessagesResult({ shareId: 'share-123', messages: [] });
const response = await request(
buildApp({ user: { id: 'user-123', role: 'USER', tenantId: 'tenant-abc' } }),
).get('/api/share/share-123');
expect(response.status).toBe(200);
expect(response.body).not.toHaveProperty('langfuseSessionUrl');
});
it('serves the shared chat when session-link resolution fails', async () => {
mockShareTenantId = 'tenant-abc';
mockGetSharedLangfuseSessionUrl.mockRejectedValue(new Error('Langfuse lookup failed'));
mockSharedMessagesResult({ shareId: 'share-123', messages: [] });
const response = await request(
buildApp({ user: { id: 'admin-123', role: 'ADMIN', tenantId: 'tenant-abc' } }),
).get('/api/share/share-123');
expect(response.status).toBe(200);
expect(response.body).not.toHaveProperty('langfuseSessionUrl');
expect(logger.warn).toHaveBeenCalledWith(
'[share] Failed to resolve Langfuse session link:',
expect.any(Error),
);
});
it('resolves the session link while loading the shared snapshot', async () => {
let resolveShare;
let markShareStarted;
const shareStarted = new Promise((resolve) => {
markShareStarted = resolve;
});
const pendingShare = new Promise((resolve) => {
resolveShare = resolve;
});
getSharedMessages.mockImplementationOnce(() => {
markShareStarted();
return pendingShare;
});
mockGetSharedLangfuseSessionUrl.mockResolvedValue(null);
const responsePromise = request(buildApp())
.get('/api/share/share-123')
.then((response) => response);
await shareStarted;
expect(mockGetSharedLangfuseSessionUrl).toHaveBeenCalledTimes(1);
resolveShare({ shareId: 'share-123', messages: [] });
await expect(responsePromise).resolves.toMatchObject({ status: 200 });
});
it('normalizes shared-link list parameters without double-decoding search text', async () => {
getSharedLinks.mockResolvedValue({ links: [], hasNextPage: false });
mockParseSharedLinksPageSize.mockReturnValueOnce(100);
const cursor = '2030-01-01T00:00:00.000Z';
const response = await request(buildApp()).get(
`/api/share?pageSize=1000&sortBy=createdAt&sortDirection=asc&search=100%25%20ready&cursor=${encodeURIComponent(cursor)}`,
);
expect(response.status).toBe(200);
expect(getSharedLinks).toHaveBeenCalledWith(
'user-123',
cursor,
100,
'createdAt',
'asc',
'100% ready',
);
});
it('rejects a cursor the validator refuses before querying', async () => {
mockIsValidSharedLinksCursor.mockReturnValueOnce(false);
const response = await request(buildApp()).get('/api/share?cursor=not-a-date');
expect(response.status).toBe(400);
expect(mockIsValidSharedLinksCursor).toHaveBeenCalledWith('not-a-date', 'createdAt');
expect(getSharedLinks).not.toHaveBeenCalled();
});
it('accepts the composite cursor issued for a titleless page', async () => {
getSharedLinks.mockResolvedValue({ links: [], hasNextPage: false });
const cursor = Buffer.from(
JSON.stringify({ primary: null, id: '0123456789abcdef01234567' }),
).toString('base64');
const response = await request(buildApp()).get(
`/api/share?sortBy=title&cursor=${encodeURIComponent(cursor)}`,
);
expect(response.status).toBe(200);
expect(getSharedLinks).toHaveBeenCalledWith('user-123', cursor, 10, 'title', 'desc', undefined);
});
it('does not expose internal list errors in the response', async () => {
getSharedLinks.mockRejectedValue(new Error('mongodb.internal:27017'));
const response = await request(buildApp()).get('/api/share');
expect(response.status).toBe(500);
expect(response.body).toEqual({ message: 'Error getting shared links' });
});
it('reapplies the current content policy before serving an existing share', async () => {
const share = {
shareId: 'share-123',
title: 'Protected Conversation',
messages: [
{
text: 'safe message',
files: [{ file_id: 'file-1', filename: 'safe-report.pdf' }],
attachments: [{ file_id: 'file-2', name: 'safe-image.png' }],
content: [
{
type: 'steer',
steer: 'safe user steer',
files: [{ file_id: 'file-3', filename: 'safe-context.txt' }],
},
],
},
],
};
mockSharedMessagesResult(share);
const response = await request(buildApp({ filters: contentFilters })).get(
'/api/share/share-123',
);
expect(response.status).toBe(200);
expect(mockAssertConversationContentAllowed).toHaveBeenCalledWith(contentFilters, {
conversations: [{ title: share.title }],
messages: [
{
text: 'safe message',
content: [{ type: 'steer', steer: 'safe user steer' }],
},
],
});
expect(mockAssertSharedFileMetadataAllowed).toHaveBeenCalledWith({
filters: contentFilters,
messages: share.messages,
shareId: 'share-123',
});
});
it('uses the share tenant policy when an authenticated viewer belongs to another tenant', async () => {
const ownerFilters = {
messages: {
pii: {
starterPatterns: ['sk_prefix'],
},
},
};
const share = {
shareId: 'share-123',
title: 'Protected Conversation',
messages: [{ isCreatedByUser: true, text: 'safe message' }],
};
mockShareTenantId = 'tenant-owner';
mockSharedLinkConfigMiddleware.mockImplementationOnce((req, _res, next) => {
expect(req.user.tenantId).toBe('tenant-viewer');
expect(req.shareTenantId).toBe('tenant-owner');
req.config = { filters: ownerFilters };
next();
});
mockSharedMessagesResult(share);
const response = await request(
buildApp({ user: { id: 'viewer', role: 'USER', tenantId: 'tenant-viewer' } }),
).get('/api/share/share-123');
expect(response.status).toBe(200);
expect(mockAssertConversationContentAllowed).toHaveBeenCalledWith(ownerFilters, {
conversations: [{ title: share.title }],
messages: share.messages,
});
expect(mockAssertSharedFileMetadataAllowed).toHaveBeenCalledWith({
filters: ownerFilters,
messages: share.messages,
shareId: share.shareId,
});
});
it('threads a legacy-only detector into strict shared-content preflight', async () => {
const strictFilters = {
messages: { unattributedAssistantContent: 'inspect' },
};
const legacyPii = {
starterPatterns: [],
customPatterns: [{ id: 'private', label: 'private value', regex: 'PRIVATE-[A-Z]+' }],
};
const share = {
shareId: 'share-123',
title: 'Protected Conversation',
messages: [{ isCreatedByUser: false, role: 'assistant', text: 'safe model output' }],
};
mockSharedMessagesResult(share);
const response = await request(
buildApp({ filters: strictFilters, messageFilter: { pii: legacyPii } }),
).get('/api/share/share-123');
expect(response.status).toBe(200);
expect(mockAssertConversationContentAllowed).toHaveBeenCalledWith(
strictFilters,
{
conversations: [{ title: share.title }],
messages: share.messages,
},
{ legacyPii },
);
});
it('keeps shared-message preflight active with only legacy message filtering', async () => {
const legacyPii = { starterPatterns: ['sk_prefix'] };
const share = {
shareId: 'share-123',
title: 'Protected Conversation',
messages: [{ isCreatedByUser: true, text: 'safe user input' }],
};
mockSharedMessagesResult(share);
const response = await request(buildApp({ messageFilter: { pii: legacyPii } })).get(
'/api/share/share-123',
);
expect(response.status).toBe(200);
expect(mockAssertConversationContentAllowed).toHaveBeenCalledWith(
undefined,
{
conversations: [{ title: share.title }],
messages: share.messages,
},
{ legacyPii },
);
});
it('returns a raw-free 400 when existing shared metadata fails current policy', async () => {
const error = Object.assign(new Error('PRIVATE-SENTINEL'), {
code: 'content_filter_block',
statusCode: 400,
body: {
error: 'content_filter_block',
message: 'Submitted content contains a private value. Remove it and try again.',
source: 'message',
field: 'attachment_reference',
},
});
mockSharedMessagesResult({
shareId: 'share-123',
title: 'Protected Conversation',
messages: [
{
text: 'safe message',
files: [{ file_id: 'file-1', filename: 'PRIVATE-SENTINEL.pdf' }],
},
],
});
mockAssertSharedFileMetadataAllowed.mockImplementationOnce(() => {
throw error;
});
const response = await request(buildApp({ filters: contentFilters })).get(
'/api/share/share-123',
);
expect(response.status).toBe(400);
expect(response.body).toEqual(error.body);
expect(JSON.stringify(response.body)).not.toContain('PRIVATE-SENTINEL');
expect(logger.error).not.toHaveBeenCalled();
});
it('rechecks legacy shared response fields before returning public JSON', async () => {
const error = Object.assign(new Error('PRIVATE-SENTINEL'), {
code: 'content_filter_block',
statusCode: 400,
body: {
error: 'content_filter_block',
message: 'Submitted content contains a private value. Remove it and try again.',
source: 'message',
field: 'attachment_reference',
},
});
const share = {
shareId: 'share-123',
title: 'Legacy Shared Conversation',
messages: [
{
isCreatedByUser: true,
text: 'safe message',
iconURL: 'https://example.test/PRIVATE-SENTINEL',
},
],
};
mockSharedMessagesResult(share);
mockAssertSharedFileMetadataAllowed.mockImplementationOnce(() => {
throw error;
});
const response = await request(buildApp({ filters: contentFilters })).get(
'/api/share/share-123',
);
expect(response.status).toBe(400);
expect(response.body).toEqual(error.body);
expect(mockAssertSharedFileMetadataAllowed).toHaveBeenCalledWith({
filters: contentFilters,
messages: share.messages,
shareId: share.shareId,
});
expect(JSON.stringify(response.body)).not.toContain('PRIVATE-SENTINEL');
expect(logger.error).not.toHaveBeenCalled();
});
it('returns a raw-free 400 when an existing share fails the current policy', async () => {
const error = Object.assign(new Error('PRIVATE-SENTINEL'), {
code: 'content_filter_block',
statusCode: 400,
body: {
error: 'content_filter_block',
message: 'Submitted content contains a private value. Remove it and try again.',
source: 'message',
field: 'text',
},
});
mockSharedMessagesResult({
shareId: 'share-123',
title: 'Protected Conversation',
messages: [{ text: 'PRIVATE-SENTINEL' }],
});
mockAssertConversationContentAllowed.mockImplementationOnce(() => {
throw error;
});
const response = await request(buildApp({ filters: contentFilters })).get(
'/api/share/share-123',
);
expect(response.status).toBe(400);
expect(response.body).toEqual(error.body);
expect(JSON.stringify(response.body)).not.toContain('PRIVATE-SENTINEL');
expect(logger.error).not.toHaveBeenCalled();
});
it('expires new shares for retained non-temporary conversations', async () => {
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
createSharedLink.mockResolvedValue({ _id: 'link-123', shareId: 'share-123' });
const response = await request(buildApp())
.post('/api/share/convo-123')
.send({ targetMessageId: 'msg-123' });
expect(response.status).toBe(200);
expect(mockGetSharedLinkExpiration).toHaveBeenCalledWith(
expect.objectContaining({
conversationId: 'convo-123',
req: expect.objectContaining({ user: { id: 'user-123' } }),
}),
expect.objectContaining({
getConvo: expect.any(Function),
createExpirationDate: createTempChatExpirationDate,
logger,
}),
);
const [, dependencies] = mockGetSharedLinkExpiration.mock.calls[0];
mongoose.models.Conversation.findOne.mockReturnValue(lean({ expiredAt: activeExpiration }));
await dependencies.getConvo('user-123', 'convo-123');
expect(mongoose.models.Conversation.findOne).toHaveBeenCalledWith(
{ conversationId: 'convo-123', user: 'user-123' },
'isTemporary expiredAt',
);
expect(createSharedLink).toHaveBeenCalledWith(
'user-123',
'convo-123',
'msg-123',
new Date('2030-01-01T00:00:00.000Z'),
true,
);
expect(mockGrantCreationPermissions).toHaveBeenCalledWith(
'link-123',
'user-123',
true,
new Date('2030-01-01T00:00:00.000Z'),
);
expect(mockSharedLinksAccess).toHaveBeenCalled();
});
it('snapshots files by default when the user does not opt out', async () => {
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
createSharedLink.mockResolvedValue({ _id: 'link-123', shareId: 'share-123' });
await request(buildApp()).post('/api/share/convo-123').send({ targetMessageId: 'msg-123' });
expect(createSharedLink).toHaveBeenCalledWith(
'user-123',
'convo-123',
'msg-123',
expect.anything(),
true,
);
});
it('rejects invalid create options before resolving retention', async () => {
const invalidTarget = await request(buildApp())
.post('/api/share/convo-123')
.send({ targetMessageId: '' });
const invalidSnapshot = await request(buildApp())
.post('/api/share/convo-123')
.send({ snapshotFiles: 'false' });
expect(invalidTarget.status).toBe(400);
expect(invalidSnapshot.status).toBe(400);
expect(mockGetSharedLinkExpiration).not.toHaveBeenCalled();
expect(createSharedLink).not.toHaveBeenCalled();
});
it('maps an existing-share domain error to conflict', async () => {
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
createSharedLink.mockRejectedValue(
Object.assign(new Error('Share already exists'), { code: 'SHARE_EXISTS' }),
);
const response = await request(buildApp()).post('/api/share/convo-123').send({});
expect(response.status).toBe(409);
expect(response.body).toEqual({ message: 'Share already exists' });
});
it('returns a raw-free 400 when the exact create snapshot fails policy preflight', async () => {
const error = Object.assign(new Error('PRIVATE-SENTINEL'), {
code: 'content_filter_block',
statusCode: 400,
body: {
error: 'content_filter_block',
message: 'Submitted content contains a private value. Remove it and try again.',
source: 'message',
field: 'text',
},
});
const messages = [{ text: 'PRIVATE-SENTINEL' }];
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
mockAssertConversationContentAllowed.mockImplementationOnce(() => {
throw error;
});
createSharedLink.mockImplementationOnce(async (...args) => {
await args[5]({ title: 'Protected Conversation', messages });
return { _id: 'link-123', shareId: 'share-123' };
});
const response = await request(buildApp({ filters: contentFilters }))
.post('/api/share/convo-123')
.send({});
expect(response.status).toBe(400);
expect(response.body).toEqual(error.body);
expect(JSON.stringify(response.body)).not.toContain('PRIVATE-SENTINEL');
expect(mockAssertConversationContentAllowed).toHaveBeenCalledWith(
contentFilters,
{
conversations: [{ title: 'Protected Conversation' }],
messages,
},
{
user: { id: 'user-123' },
getFiles,
},
);
expect(mockGrantCreationPermissions).not.toHaveBeenCalled();
expect(logger.error).not.toHaveBeenCalled();
});
it('checks public message metadata before creating the shared link', async () => {
const error = Object.assign(new Error('PRIVATE-SENTINEL'), {
code: 'content_filter_block',
statusCode: 400,
body: {
error: 'content_filter_block',
message: 'Submitted content contains a private value. Remove it and try again.',
source: 'message',
field: 'attachment_reference',
},
});
const messages = [
{
isCreatedByUser: true,
text: 'safe message',
iconURL: 'https://example.test/PRIVATE-SENTINEL',
},
];
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
mockAssertSharedFileMetadataAllowed.mockImplementationOnce(() => {
throw error;
});
createSharedLink.mockImplementationOnce(async (...args) => {
await args[5]({ title: 'Protected Conversation', messages });
return { _id: 'link-123', shareId: 'share-123' };
});
const response = await request(buildApp({ filters: contentFilters }))
.post('/api/share/convo-123')
.send({});
expect(response.status).toBe(400);
expect(response.body).toEqual(error.body);
expect(mockAssertConversationContentAllowed).toHaveBeenCalledWith(
contentFilters,
{
conversations: [{ title: 'Protected Conversation' }],
messages,
},
{
user: { id: 'user-123' },
getFiles,
},
);
expect(mockAssertSharedFileMetadataAllowed).toHaveBeenCalledWith({
filters: contentFilters,
messages,
shareId: undefined,
includeFiles: false,
});
expect(mockGrantCreationPermissions).not.toHaveBeenCalled();
expect(JSON.stringify(response.body)).not.toContain('PRIVATE-SENTINEL');
expect(logger.error).not.toHaveBeenCalled();
});
it('preflights only the exact text snapshot when shared files are opted out', async () => {
const messages = [
{
text: 'safe message',
files: [{ file_id: 'file-top-level' }],
attachments: [{ file_id: 'attachment-top-level' }],
content: [
{ type: 'text', text: 'safe model content' },
{
type: 'steer',
steer: 'safe user steer',
files: [{ file_id: 'steer-file' }],
},
],
},
];
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
createSharedLink.mockImplementationOnce(async (...args) => {
await args[5]({ title: 'Protected Conversation', messages });
return { _id: 'link-123', shareId: 'share-123' };
});
const response = await request(buildApp({ filters: contentFilters }))
.post('/api/share/convo-123')
.send({ snapshotFiles: false });
expect(response.status).toBe(200);
expect(mockAssertConversationContentAllowed).toHaveBeenCalledWith(
contentFilters,
{
conversations: [{ title: 'Protected Conversation' }],
messages: [
{
text: 'safe message',
content: [
{ type: 'text', text: 'safe model content' },
{ type: 'steer', steer: 'safe user steer' },
],
},
],
},
{
user: { id: 'user-123' },
getFiles,
},
);
});
it('does not snapshot files when the user opts out (snapshotFiles=false)', async () => {
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
createSharedLink.mockResolvedValue({ _id: 'link-123', shareId: 'share-123' });
await request(buildApp())
.post('/api/share/convo-123')
.send({ targetMessageId: 'msg-123', snapshotFiles: false });
expect(createSharedLink).toHaveBeenCalledWith(
'user-123',
'convo-123',
'msg-123',
expect.anything(),
false,
);
});
it('forces snapshotFiles=false when the feature is disabled, ignoring the body flag', async () => {
isFileSnapshotEnabled.mockReturnValueOnce(false);
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
createSharedLink.mockResolvedValue({ _id: 'link-123', shareId: 'share-123' });
await request(buildApp())
.post('/api/share/convo-123')
.send({ targetMessageId: 'msg-123', snapshotFiles: true });
expect(createSharedLink).toHaveBeenCalledWith(
'user-123',
'convo-123',
'msg-123',
expect.anything(),
false,
);
});
it('passes the snapshotFiles opt-out through on update', async () => {
mongoose.models.SharedLink.findOne.mockReturnValue(
lean({ _id: 'link-456', conversationId: 'convo-123' }),
);
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
mockUpdatedShare({ _id: 'link-456', shareId: 'share-456' });
await request(buildApp()).patch('/api/share/share-123').send({ snapshotFiles: false });
expect(updateSharedLink).toHaveBeenCalledWith(
'user-123',
'share-123',
undefined,
expect.anything(),
false,
undefined,
expect.any(Function),
);
});
it('returns a raw-free 400 when the exact update snapshot fails policy preflight', async () => {
const error = Object.assign(new Error('PRIVATE-SENTINEL'), {
code: 'content_filter_uninspectable',
statusCode: 400,
body: {
error: 'content_filter_uninspectable',
message: 'Submitted file content could not be inspected before processing.',
source: 'file',
field: 'content',
},
});
const messages = [{ files: [{ file_id: 'file_PRIVATE-SENTINEL' }] }];
mongoose.models.SharedLink.findOne.mockReturnValue(
lean({ _id: 'link-456', conversationId: 'convo-123' }),
);
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
mockAssertConversationContentAllowed.mockImplementationOnce(() => {
throw error;
});
updateSharedLink.mockImplementationOnce(async (...args) => {
await args[5]({ title: 'Protected Share', messages });
await args[6]?.();
return { _id: 'link-456', shareId: 'share-456' };
});
const response = await request(buildApp({ filters: contentFilters })).patch(
'/api/share/share-123',
);
expect(response.status).toBe(400);
expect(response.body).toEqual(error.body);
expect(JSON.stringify(response.body)).not.toContain('PRIVATE-SENTINEL');
expect(mockAssertConversationContentAllowed).toHaveBeenCalledWith(
contentFilters,
{
conversations: [{ title: 'Protected Share' }],
messages,
},
{
user: { id: 'user-123' },
getFiles,
},
);
expect(mockUpdateSharedLinkPermissionsExpiration).not.toHaveBeenCalled();
expect(logger.error).not.toHaveBeenCalled();
});
it('rejects new shares when the retained conversation expired', async () => {
mockGetSharedLinkExpiration.mockResolvedValue(expiredExpiration);
createSharedLink.mockResolvedValue({ _id: 'link-123', shareId: 'share-123' });
const response = await request(buildApp())
.post('/api/share/convo-123')
.send({ targetMessageId: 'msg-123' });
expect(response.status).toBe(404);
expect(createSharedLink).not.toHaveBeenCalled();
});
it('rejects new shares for expired conversations in all retention mode', async () => {
mockGetSharedLinkExpiration.mockResolvedValue(expiredExpiration);
createSharedLink.mockResolvedValue({ _id: 'link-123', shareId: 'share-123' });
const response = await request(buildApp({ retentionMode: RetentionMode.ALL }))
.post('/api/share/convo-123')
.send({ targetMessageId: 'msg-123' });
expect(response.status).toBe(404);
expect(createSharedLink).not.toHaveBeenCalled();
});
it('expires updated shares for retained non-temporary conversations', async () => {
mongoose.models.SharedLink.findOne.mockReturnValue(
lean({ _id: 'link-456', conversationId: 'convo-123' }),
);
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
mockUpdatedShare({ _id: 'link-456', shareId: 'share-456' });
const response = await request(buildApp()).patch('/api/share/share-123');
expect(response.status).toBe(200);
expect(mongoose.models.SharedLink.findOne).toHaveBeenCalledWith(
{ shareId: 'share-123', user: 'user-123' },
'conversationId',
);
expect(mockGetSharedLinkExpiration).toHaveBeenCalledTimes(1);
expect(mockGetSharedLinkExpiration).toHaveBeenCalledWith(
expect.objectContaining({
conversationId: 'convo-123',
req: expect.objectContaining({ user: { id: 'user-123' } }),
}),
expect.objectContaining({
getConvo: expect.any(Function),
createExpirationDate: createTempChatExpirationDate,
logger,
}),
);
expect(updateSharedLink).toHaveBeenCalledWith(
'user-123',
'share-123',
undefined,
new Date('2030-01-01T00:00:00.000Z'),
true,
undefined,
expect.any(Function),
);
expect(mockUpdateSharedLinkPermissionsExpiration).toHaveBeenCalledWith(
'link-456',
new Date('2030-01-01T00:00:00.000Z'),
);
});
it('does not re-publish content when re-scoping the grants fails', async () => {
mongoose.models.SharedLink.findOne.mockReturnValue(
lean({ _id: 'link-456', conversationId: 'convo-123' }),
);
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
mockUpdateSharedLinkPermissionsExpiration.mockRejectedValueOnce(new Error('acl down'));
mockUpdatedShare({ _id: 'link-456', shareId: 'share-456' });
const response = await request(buildApp()).patch('/api/share/share-123');
expect(response.status).toBe(500);
// The shareId is stable, so publishing first would expose the update behind
// the existing grants while the owner is told the update failed.
expect(updateSharedLink).toHaveBeenCalledTimes(1);
});
it('rejects updated shares when the retained conversation expired', async () => {
mongoose.models.SharedLink.findOne.mockReturnValue(
lean({ _id: 'link-456', conversationId: 'convo-123' }),
);
mockGetSharedLinkExpiration.mockResolvedValue(expiredExpiration);
updateSharedLink.mockResolvedValue({ shareId: 'share-456' });
const response = await request(buildApp()).patch('/api/share/share-123');
expect(response.status).toBe(404);
expect(updateSharedLink).not.toHaveBeenCalled();
});
it('rejects updated shares for expired conversations in all retention mode', async () => {
mongoose.models.SharedLink.findOne.mockReturnValue(
lean({ _id: 'link-456', conversationId: 'convo-123' }),
);
mockGetSharedLinkExpiration.mockResolvedValue(expiredExpiration);
updateSharedLink.mockResolvedValue({ shareId: 'share-456' });
const response = await request(buildApp({ retentionMode: RetentionMode.ALL })).patch(
'/api/share/share-123',
);
expect(response.status).toBe(404);
expect(mongoose.models.SharedLink.findOne).toHaveBeenCalledWith(
{ shareId: 'share-123', user: 'user-123' },
'conversationId',
);
expect(updateSharedLink).not.toHaveBeenCalled();
});
it('clears updated share expiration when the conversation is no longer retained', async () => {
mongoose.models.SharedLink.findOne.mockReturnValue(
lean({ _id: 'link-456', conversationId: 'convo-123' }),
);
mockGetSharedLinkExpiration.mockResolvedValue(null);
mockUpdatedShare({ _id: 'link-456', shareId: 'share-456' });
const response = await request(buildApp()).patch('/api/share/share-123');
expect(response.status).toBe(200);
expect(updateSharedLink).toHaveBeenCalledWith(
'user-123',
'share-123',
undefined,
null,
true,
undefined,
expect.any(Function),
);
expect(mockUpdateSharedLinkPermissionsExpiration).toHaveBeenCalledWith('link-456', null);
expect(mockSharedLinksAccess).toHaveBeenCalled();
});
it('gates updates on the CREATE permission so revoking it stops link updates', async () => {
mockSharedLinksAccess.mockImplementationOnce((_req, res) =>
res.status(403).json({ message: 'Forbidden' }),
);
const response = await request(buildApp()).patch('/api/share/share-123');
expect(response.status).toBe(403);
expect(updateSharedLink).not.toHaveBeenCalled();
});
it('preserves updated share expiration when the conversation cannot be found', async () => {
mongoose.models.SharedLink.findOne.mockReturnValue(
lean({ _id: 'link-456', conversationId: 'convo-123' }),
);
mockGetSharedLinkExpiration.mockResolvedValue(undefined);
mockUpdatedShare({ shareId: 'share-456' });
const response = await request(buildApp()).patch('/api/share/share-123');
expect(response.status).toBe(200);
expect(updateSharedLink).toHaveBeenCalledWith(
'user-123',
'share-123',
undefined,
undefined,
true,
undefined,
undefined,
);
expect(mockUpdateSharedLinkPermissionsExpiration).not.toHaveBeenCalled();
});
it('clears updated share expiration when creating a new expiration throws', async () => {
const error = new Error('bad config');
mongoose.models.SharedLink.findOne.mockReturnValue(
lean({ _id: 'link-456', conversationId: 'convo-123' }),
);
mockGetSharedLinkExpiration.mockImplementationOnce(async (_input, dependencies) => {
dependencies.logger.error('[getSharedLinkExpiration] Error creating expiration date:', error);
return null;
});
mockUpdatedShare({ _id: 'link-456', shareId: 'share-456' });
const response = await request(buildApp()).patch('/api/share/share-123');
expect(response.status).toBe(200);
expect(logger.error).toHaveBeenCalledWith(
'[getSharedLinkExpiration] Error creating expiration date:',
error,
);
expect(updateSharedLink).toHaveBeenCalledWith(
'user-123',
'share-123',
undefined,
null,
true,
undefined,
expect.any(Function),
);
expect(mockUpdateSharedLinkPermissionsExpiration).toHaveBeenCalledWith('link-456', null);
});
it('updates share target message while applying retention expiration', async () => {
mongoose.models.SharedLink.findOne.mockReturnValue(
lean({ _id: 'link-456', conversationId: 'convo-123' }),
);
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
mockUpdatedShare({ shareId: 'share-456', targetMessageId: 'msg-456' });
const response = await request(buildApp())
.patch('/api/share/share-123')
.send({ targetMessageId: 'msg-456' });
expect(response.status).toBe(200);
expect(updateSharedLink).toHaveBeenCalledWith(
'user-123',
'share-123',
'msg-456',
new Date('2030-01-01T00:00:00.000Z'),
true,
undefined,
expect.any(Function),
);
});
it('rejects non-string target message updates', async () => {
const response = await request(buildApp())
.patch('/api/share/share-123')
.send({ targetMessageId: 123 });
expect(response.status).toBe(400);
expect(updateSharedLink).not.toHaveBeenCalled();
});
it('rejects invalid snapshot options on update', async () => {
const response = await request(buildApp())
.patch('/api/share/share-123')
.send({ snapshotFiles: 'false' });
expect(response.status).toBe(400);
expect(updateSharedLink).not.toHaveBeenCalled();
});
it('maps a missing-share update domain error to not found', async () => {
mongoose.models.SharedLink.findOne.mockReturnValue(lean(null));
updateSharedLink.mockRejectedValue(
Object.assign(new Error('Share not found'), { code: 'SHARE_NOT_FOUND' }),
);
const response = await request(buildApp()).patch('/api/share/share-123').send({});
expect(response.status).toBe(404);
expect(response.body).toEqual({ message: 'Share not found' });
});
it('allows deleting existing shares without CREATE permission gate', async () => {
deleteSharedLinkWithCleanup.mockResolvedValue({ shareId: 'share-123' });
const response = await request(buildApp()).delete('/api/share/share-123');
expect(response.status).toBe(200);
expect(mockSharedLinksAccess).not.toHaveBeenCalled();
expect(deleteSharedLinkWithCleanup).toHaveBeenCalledWith('user-123', 'share-123');
});
it('returns an internal error status when deletion fails', async () => {
deleteSharedLinkWithCleanup.mockRejectedValue(new Error('database unavailable'));
const response = await request(buildApp()).delete('/api/share/share-123');
expect(response.status).toBe(500);
expect(response.body).toEqual({ message: 'Error deleting shared link' });
});
});
describe('share fork route', () => {
beforeEach(() => {
jest.clearAllMocks();
mockShareTenantId = undefined;
});
it('forks a shared conversation for the requesting user', async () => {
const forkResult = {
conversation: { conversationId: 'convo-456', title: 'Shared Title' },
messages: [{ messageId: 'msg-456' }],
};
forkSharedConversation.mockResolvedValue(forkResult);
const response = await request(
buildApp({ user: { id: 'user-123', role: 'USER', tenantId: 'tenant-viewer' } }),
)
.post('/api/share/share-123/fork')
.send({ targetMessageIndex: 3, shareRevision: '2026-01-01T00:00:00.000Z' });
expect(response.status).toBe(201);
expect(response.body).toEqual(forkResult);
expect(forkSharedConversation).toHaveBeenCalledWith({
shareId: 'share-123',
shareResourceId: 'resource-123',
requestUserId: 'user-123',
userRole: 'USER',
userTenantId: 'tenant-viewer',
targetMessageIndex: 3,
shareRevision: '2026-01-01T00:00:00.000Z',
snapshotFiles: true,
sharedContentPreflight: undefined,
});
});
it('passes the share tenant policy into a cross-tenant fork read preflight', async () => {
const share = {
shareId: 'share-123',
title: 'Protected Conversation',
messages: [{ text: 'safe', files: [{ filename: 'safe-report.pdf' }] }],
};
forkSharedConversation.mockImplementationOnce(async ({ sharedContentPreflight }) => {
await sharedContentPreflight(share);
return {
conversation: { conversationId: 'convo-456' },
messages: [],
};
});
mockShareTenantId = 'tenant-owner';
mockSharedLinkConfigMiddleware.mockImplementationOnce((req, _res, next) => {
expect(req.user.tenantId).toBe('tenant-viewer');
expect(req.shareTenantId).toBe('tenant-owner');
req.config = { filters: contentFilters };
next();
});
const response = await request(
buildApp({ user: { id: 'viewer', role: 'USER', tenantId: 'tenant-viewer' } }),
).post('/api/share/share-123/fork');
expect(response.status).toBe(201);
expect(mockAssertConversationContentAllowed).toHaveBeenCalledWith(contentFilters, {
conversations: [{ title: share.title }],
messages: [{ text: 'safe' }],
});
expect(mockAssertSharedFileMetadataAllowed).toHaveBeenCalledWith({
filters: contentFilters,
messages: share.messages,
shareId: share.shareId,
});
});
it('forces snapshotFiles=false into the fork when the file snapshot kill switch is active', async () => {
isFileSnapshotKillSwitchActive.mockReturnValueOnce(true);
forkSharedConversation.mockResolvedValue({
conversation: { conversationId: 'convo-456' },
messages: [],
});
await request(buildApp()).post('/api/share/share-123/fork');
expect(forkSharedConversation).toHaveBeenCalledWith(
expect.objectContaining({ snapshotFiles: false }),
);
});
it('returns 404 when the shared conversation is missing or empty', async () => {
forkSharedConversation.mockResolvedValue(null);
const response = await request(buildApp()).post('/api/share/share-123/fork');
expect(response.status).toBe(404);
});
it('returns 500 when forking fails', async () => {
forkSharedConversation.mockRejectedValue(new Error('db down'));
const response = await request(buildApp()).post('/api/share/share-123/fork');
expect(response.status).toBe(500);
});
it('answers 409 when the viewer forks a payload the owner has republished', async () => {
const conflict = new Error('Shared link was updated');
conflict.code = 'SHARE_REVISION_MISMATCH';
forkSharedConversation.mockRejectedValue(conflict);
const response = await request(buildApp())
.post('/api/share/share-123/fork')
.send({ targetMessageIndex: 3, shareRevision: '2026-01-01T00:00:00.000Z' });
expect(response.status).toBe(409);
expect(response.body).toEqual({ message: 'Shared link was updated' });
});
});
describe('share-scoped file routes', () => {
beforeEach(() => {
jest.clearAllMocks();
mockShareTenantId = undefined;
mockGetStrategyFunctions.mockReturnValue({
getDownloadStream: jest.fn(async () => Readable.from(['file-bytes'])),
});
// Live file record present by default (resolveShareFile requires it).
getFiles.mockResolvedValue([{ status: 'ready' }]);
});
it('serves a snapshotted image inline from its original stored object', async () => {
const getDownloadStream = jest.fn(async () => Readable.from(['file-bytes']));
mockGetStrategyFunctions.mockReturnValue({ getDownloadStream });
getSharedLinkFile.mockResolvedValue({
file: {
file_id: 'file-1',
source: 'local',
filepath: '/images/owner/pic.png',
type: 'image/png',
filename: 'pic.png',
},
hasSnapshots: true,
});
const response = await request(buildApp()).get('/api/share/share-123/files/file-1');
expect(response.status).toBe(200);
expect(response.headers['content-type']).toContain('image/png');
expect(response.headers['x-content-type-options']).toBe('nosniff');
expect(response.headers['content-disposition']).toContain('inline');
expect(mockGetStrategyFunctions).toHaveBeenCalledWith('local');
expect(getDownloadStream).toHaveBeenCalledWith(expect.anything(), '/images/owner/pic.png');
expect(backfillSharedLinkFiles).not.toHaveBeenCalled();
});
it('requires revalidation so a revoked link cannot be served from cache', async () => {
const getDownloadStream = jest.fn(async () => Readable.from(['file-bytes']));
mockGetStrategyFunctions.mockReturnValue({ getDownloadStream });
getSharedLinkFile.mockResolvedValue({
file: {
file_id: 'file-1',
source: 'local',
filepath: '/images/owner/pic.png',
type: 'image/png',
filename: 'pic.png',
bytes: 1234,
},
hasSnapshots: true,
});
const response = await request(buildApp()).get('/api/share/share-123/files/file-1');
expect(response.status).toBe(200);
// The shareId is stable across updates now, so the URL alone can no longer bust caches.
expect(response.headers['cache-control']).toBe('private, no-cache');
expect(response.headers['etag']).toBeDefined();
});
it('answers an unchanged snapshot with 304 instead of re-sending bytes', async () => {
const getDownloadStream = jest.fn(async () => Readable.from(['file-bytes']));
mockGetStrategyFunctions.mockReturnValue({ getDownloadStream });
getSharedLinkFile.mockResolvedValue({
file: {
file_id: 'file-1',
source: 'local',
filepath: '/images/owner/pic.png',
type: 'image/png',
filename: 'pic.png',
bytes: 1234,
},
hasSnapshots: true,
});
const app = buildApp();
const first = await request(app).get('/api/share/share-123/files/file-1');
expect(first.status).toBe(200);
getDownloadStream.mockClear();
const response = await request(app)
.get('/api/share/share-123/files/file-1')
.set('If-None-Match', first.headers['etag']);
expect(response.status).toBe(304);
expect(getDownloadStream).not.toHaveBeenCalled();
});
it('changes the validator when the snapshot revision moves', async () => {
const getDownloadStream = jest.fn(async () => Readable.from(['file-bytes']));
mockGetStrategyFunctions.mockReturnValue({ getDownloadStream });
const snapshot = {
file_id: 'file-1',
source: 'local',
filepath: '/images/owner/pic.png',
type: 'image/png',
filename: 'pic.png',
bytes: 1234,
};
getSharedLinkFile.mockResolvedValue({ file: snapshot, hasSnapshots: true });
const app = buildApp();
const first = await request(app).get('/api/share/share-123/files/file-1');
// Must match the snapshot, or resolveShareFile 404s on the version mismatch first.
getFiles.mockResolvedValue([{ status: 'ready', previewRevision: 7, bytes: 1234 }]);
getSharedLinkFile.mockResolvedValue({
file: { ...snapshot, previewRevision: 7 },
hasSnapshots: true,
});
const response = await request(app)
.get('/api/share/share-123/files/file-1')
.set('If-None-Match', first.headers['etag']);
expect(response.status).toBe(200);
expect(response.headers['etag']).not.toBe(first.headers['etag']);
});
it('changes the validator when a same-size replacement moves the stored object', async () => {
const getDownloadStream = jest.fn(async () => Readable.from(['file-bytes']));
mockGetStrategyFunctions.mockReturnValue({ getDownloadStream });
const snapshot = {
file_id: 'file-1',
source: 'local',
filepath: '/images/owner/plot.png?v=1',
type: 'image/png',
filename: 'plot.png',
bytes: 1234,
};
getSharedLinkFile.mockResolvedValue({ file: snapshot, hasSnapshots: true });
const app = buildApp();
const first = await request(app).get('/api/share/share-123/files/file-1');
// Re-published output: same file_id, same size, no revision, new stored path.
getSharedLinkFile.mockResolvedValue({
file: { ...snapshot, filepath: '/images/owner/plot.png?v=2' },
hasSnapshots: true,
});
const response = await request(app)
.get('/api/share/share-123/files/file-1')
.set('If-None-Match', first.headers['etag']);
expect(response.status).toBe(200);
expect(response.headers['etag']).not.toBe(first.headers['etag']);
});
it('reapplies the share tenant file policy for a cross-tenant viewer', async () => {
const liveFile = {
file_id: 'file-1',
status: 'ready',
filename: 'report.pdf',
text: 'safe extracted text',
};
getSharedLinkFile.mockResolvedValue({
file: {
file_id: 'file-1',
source: 'local',
filepath: '/uploads/owner/file-1',
type: 'application/pdf',
filename: 'report.pdf',
},
hasSnapshots: true,
});
getFiles.mockResolvedValue([liveFile]);
mockShareTenantId = 'tenant-owner';
mockSharedLinkConfigMiddleware.mockImplementationOnce((req, _res, next) => {
expect(req.user.tenantId).toBe('tenant-viewer');
expect(req.shareTenantId).toBe('tenant-owner');
req.config = { filters: contentFilters };
next();
});
const response = await request(
buildApp({ user: { id: 'viewer', role: 'USER', tenantId: 'tenant-viewer' } }),
).get('/api/share/share-123/files/file-1/download');
expect(response.status).toBe(200);
expect(mockAssertModelBoundContent).toHaveBeenCalledWith({
filters: contentFilters,
files: [liveFile],
});
});
it('returns a raw-free 400 before serving a file that fails the current policy', async () => {
const error = Object.assign(new Error('PRIVATE-SENTINEL'), {
code: 'content_filter_uninspectable',
statusCode: 400,
body: {
error: 'content_filter_uninspectable',
message: 'Submitted file content could not be inspected before processing.',
source: 'file',
field: 'content',
},
});
getSharedLinkFile.mockResolvedValue({
file: {
file_id: 'file-1',
source: 'local',
filepath: '/uploads/owner/file-1',
type: 'application/pdf',
filename: 'PRIVATE-SENTINEL.pdf',
},
hasSnapshots: true,
});
getFiles.mockResolvedValue([
{ file_id: 'file-1', status: 'ready', filename: 'PRIVATE-SENTINEL.pdf' },
]);
mockAssertModelBoundContent.mockImplementationOnce(() => {
throw error;
});
const response = await request(buildApp({ filters: contentFilters })).get(
'/api/share/share-123/files/file-1/download',
);
expect(response.status).toBe(400);
expect(response.body).toEqual(error.body);
expect(JSON.stringify(response.body)).not.toContain('PRIVATE-SENTINEL');
expect(mockGetStrategyFunctions).not.toHaveBeenCalled();
expect(logger.error).not.toHaveBeenCalled();
});
it('forces attachment for unsafe inline types (no stored XSS)', async () => {
const getDownloadStream = jest.fn(async () => Readable.from(['<svg/>']));
mockGetStrategyFunctions.mockReturnValue({ getDownloadStream });
getSharedLinkFile.mockResolvedValue({
file: {
file_id: 'file-1',
source: 'local',
filepath: '/uploads/owner/evil.svg',
type: 'image/svg+xml',
filename: 'evil.svg',
},
hasSnapshots: true,
});
const response = await request(buildApp()).get('/api/share/share-123/files/file-1');
expect(response.status).toBe(200);
expect(response.headers['content-type']).toContain('application/octet-stream');
expect(response.headers['content-disposition']).toContain('attachment');
expect(response.headers['x-content-type-options']).toBe('nosniff');
});
it('downloads a snapshotted file as an attachment', async () => {
getSharedLinkFile.mockResolvedValue({
file: {
file_id: 'file-1',
source: 'local',
filepath: '/uploads/owner/file-1',
type: 'application/pdf',
filename: 'report.pdf',
},
hasSnapshots: true,
});
const response = await request(buildApp()).get('/api/share/share-123/files/file-1/download');
expect(response.status).toBe(200);
expect(response.headers['content-disposition']).toContain('attachment');
});
it('downloads stored text for a snapshotted text-source file', async () => {
getFiles.mockResolvedValue([{ status: 'ready', text: 'Shared extracted text' }]);
getSharedLinkFile.mockResolvedValue({
file: {
file_id: 'file-1',
source: 'text',
filepath: 'mistral_ocr',
type: 'application/pdf',
filename: 'report.pdf',
},
hasSnapshots: true,
});
const response = await request(buildApp()).get('/api/share/share-123/files/file-1/download');
expect(response.status).toBe(200);
expect(response.headers['content-type']).toContain('text/plain');
expect(response.headers['content-disposition']).toContain('attachment; report.pdf.txt');
expect(response.text).toBe('Shared extracted text');
expect(mockGetStrategyFunctions).not.toHaveBeenCalled();
});
it('returns 500 when the backing stream fails before sending bytes', async () => {
const failingStream = new Readable({
read() {
this.destroy(new Error('storage unavailable'));
},
});
mockGetStrategyFunctions.mockReturnValue({
getDownloadStream: jest.fn(async () => failingStream),
});
getSharedLinkFile.mockResolvedValue({
file: {
file_id: 'file-1',
source: 'local',
filepath: '/uploads/owner/file-1',
type: 'application/pdf',
filename: 'report.pdf',
},
hasSnapshots: true,
});
const response = await request(buildApp()).get('/api/share/share-123/files/file-1');
expect(response.status).toBe(500);
});
it('returns preview status read live from the file record', async () => {
getSharedLinkFile.mockResolvedValue({
file: { file_id: 'file-1', source: 'local' },
hasSnapshots: true,
});
getFiles.mockResolvedValue([{ status: 'ready', text: 'extracted text', textFormat: 'text' }]);
const response = await request(buildApp()).get('/api/share/share-123/files/file-1/preview');
expect(response.status).toBe(200);
expect(response.body).toEqual({
file_id: 'file-1',
status: 'ready',
text: 'extracted text',
textFormat: 'text',
});
expect(getFiles).toHaveBeenCalledWith({ file_id: 'file-1' }, null, {});
});
it('404s for a file not in the snapshot without rebuilding it', async () => {
getSharedLinkFile.mockResolvedValue({ file: null, hasSnapshots: true });
const response = await request(buildApp()).get('/api/share/share-123/files/not-shared');
expect(response.status).toBe(404);
expect(backfillSharedLinkFiles).not.toHaveBeenCalled();
expect(mockGetStrategyFunctions).not.toHaveBeenCalled();
});
it('lazily backfills only a legacy share that has no snapshot field', async () => {
getSharedLinkFile.mockResolvedValue({ file: null, hasSnapshots: false });
backfillSharedLinkFiles.mockResolvedValue({
file_id: 'file-1',
source: 'local',
filepath: '/images/owner/pic.png',
type: 'image/png',
filename: 'pic.png',
});
const response = await request(buildApp()).get('/api/share/share-123/files/file-1');
expect(response.status).toBe(200);
expect(backfillSharedLinkFiles).toHaveBeenCalledWith('share-123', 'file-1');
});
it('404s cleanly when the snapshotted file is no longer available', async () => {
getSharedLinkFile.mockResolvedValue({
file: { file_id: 'file-1', source: 'local', filepath: '/uploads/owner/gone.pdf' },
hasSnapshots: true,
});
getFiles.mockResolvedValue([]); // original record deleted/expired
const response = await request(buildApp()).get('/api/share/share-123/files/file-1');
expect(response.status).toBe(404);
expect(mockGetStrategyFunctions).not.toHaveBeenCalled();
});
it('404s (no serving) when the global kill switch is active', async () => {
isFileSnapshotKillSwitchActive.mockReturnValueOnce(true);
const response = await request(buildApp()).get('/api/share/share-123/files/file-1');
expect(response.status).toBe(404);
expect(getSharedLinkFile).not.toHaveBeenCalled();
expect(mockGetStrategyFunctions).not.toHaveBeenCalled();
});
it('404s (no serving, no backfill) for a link that opted out of file sharing', async () => {
getSharedLinkFile.mockResolvedValue({ file: null, hasSnapshots: false, optedOut: true });
const response = await request(buildApp()).get('/api/share/share-123/files/file-1');
expect(response.status).toBe(404);
expect(backfillSharedLinkFiles).not.toHaveBeenCalled();
expect(mockGetStrategyFunctions).not.toHaveBeenCalled();
});
it('404s when the snapshotted file version was overwritten (revision mismatch)', async () => {
getSharedLinkFile.mockResolvedValue({
file: {
file_id: 'file-1',
source: 'local',
filepath: '/uploads/owner/x',
previewRevision: 'r1',
},
hasSnapshots: true,
});
getFiles.mockResolvedValue([{ status: 'ready', previewRevision: 'r2' }]);
const response = await request(buildApp()).get('/api/share/share-123/files/file-1');
expect(response.status).toBe(404);
expect(mockGetStrategyFunctions).not.toHaveBeenCalled();
});
it('404s when the snapshotted file was overwritten (size/bytes mismatch)', async () => {
getSharedLinkFile.mockResolvedValue({
file: { file_id: 'file-1', source: 'local', filepath: '/uploads/owner/x', bytes: 100 },
hasSnapshots: true,
});
getFiles.mockResolvedValue([{ status: 'ready', bytes: 200 }]);
const response = await request(buildApp()).get('/api/share/share-123/files/file-1');
expect(response.status).toBe(404);
expect(mockGetStrategyFunctions).not.toHaveBeenCalled();
});
it('404s when a same-size file reuse has a different source generation', async () => {
getSharedLinkFile.mockResolvedValue({
file: {
file_id: 'file-1',
source: 'local',
filepath: '/uploads/owner/x',
bytes: 100,
previewRevision: null,
sourceDispatchedAt: 1000,
},
hasSnapshots: true,
});
getFiles.mockResolvedValue([
{
status: 'ready',
bytes: 100,
previewRevision: null,
metadata: { sourceDispatchedAt: 2000 },
},
]);
const response = await request(buildApp()).get('/api/share/share-123/files/file-1');
expect(response.status).toBe(404);
expect(mockGetStrategyFunctions).not.toHaveBeenCalled();
});
it('uses legacy snapshot markers when only the live file has a source generation', async () => {
const getDownloadStream = jest.fn(async () => Readable.from(['bytes']));
mockGetStrategyFunctions.mockReturnValue({ getDownloadStream });
getSharedLinkFile.mockResolvedValue({
file: {
file_id: 'file-1',
source: 'local',
filepath: '/uploads/owner/x',
bytes: 100,
},
hasSnapshots: true,
});
getFiles.mockResolvedValue([
{
status: 'ready',
bytes: 100,
metadata: { sourceDispatchedAt: 2000 },
},
]);
const response = await request(buildApp()).get('/api/share/share-123/files/file-1');
expect(response.status).toBe(200);
expect(getDownloadStream).toHaveBeenCalled();
});
it('strips a cache-busting query string before local streaming', async () => {
const getDownloadStream = jest.fn(async () => Readable.from(['bytes']));
mockGetStrategyFunctions.mockReturnValue({ getDownloadStream });
getSharedLinkFile.mockResolvedValue({
file: {
file_id: 'file-1',
source: 'local',
filepath: '/images/owner/pic.png?v=2',
type: 'image/png',
filename: 'pic.png',
bytes: 100,
},
hasSnapshots: true,
});
getFiles.mockResolvedValue([{ status: 'ready', bytes: 100 }]);
const response = await request(buildApp()).get('/api/share/share-123/files/file-1');
expect(response.status).toBe(200);
expect(getDownloadStream).toHaveBeenCalledWith(expect.anything(), '/images/owner/pic.png');
});
it('sweeps an orphaned pending preview to failed', async () => {
getSharedLinkFile.mockResolvedValue({
file: { file_id: 'file-1', source: 'local' },
hasSnapshots: true,
});
const stale = new Date(Date.now() - 5 * 60 * 1000);
getFiles.mockResolvedValue([{ status: 'pending', updatedAt: stale }]);
updateFile.mockResolvedValue({ status: 'failed', previewError: 'orphaned' });
const response = await request(buildApp()).get('/api/share/share-123/files/file-1/preview');
expect(response.status).toBe(200);
expect(response.body).toEqual({
file_id: 'file-1',
status: 'failed',
previewError: 'orphaned',
});
expect(updateFile).toHaveBeenCalledWith(
{ file_id: 'file-1', status: 'failed', previewError: 'orphaned' },
{ status: 'pending', updatedAt: stale },
);
});
});