From f36cd25b9b65f80a0905e65d183528e4cf9753c0 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 15 May 2026 14:43:11 -0400 Subject: [PATCH] fix: enforce expired conversation shares in all retention --- api/server/routes/__tests__/share.spec.js | 35 +++++++++++++++++++++++ api/server/routes/share.js | 34 +++++++++++----------- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/api/server/routes/__tests__/share.spec.js b/api/server/routes/__tests__/share.spec.js index 235a55b7b6..9752786de3 100644 --- a/api/server/routes/__tests__/share.spec.js +++ b/api/server/routes/__tests__/share.spec.js @@ -118,6 +118,23 @@ describe('share routes retention', () => { expect(createSharedLink).not.toHaveBeenCalled(); }); + it('rejects new shares for expired conversations in all retention mode', async () => { + mongoose.models.Conversation.findOne.mockReturnValue(lean(expiredRetainedConvo)); + createSharedLink.mockResolvedValue({ 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(mongoose.models.Conversation.findOne).toHaveBeenCalledWith( + { conversationId: 'convo-123', user: 'user-123' }, + 'isTemporary expiredAt', + ); + expect(createTempChatExpirationDate).not.toHaveBeenCalled(); + expect(createSharedLink).not.toHaveBeenCalled(); + }); + it('expires updated shares for retained non-temporary conversations', async () => { mongoose.models.SharedLink.findOne.mockReturnValue(lean({ conversationId: 'convo-123' })); mongoose.models.Conversation.findOne.mockReturnValue(lean(retainedConvo)); @@ -154,6 +171,24 @@ describe('share routes retention', () => { expect(updateSharedLink).not.toHaveBeenCalled(); }); + it('rejects updated shares for expired conversations in all retention mode', async () => { + mongoose.models.SharedLink.findOne.mockReturnValue(lean({ conversationId: 'convo-123' })); + mongoose.models.Conversation.findOne.mockReturnValue(lean(expiredRetainedConvo)); + 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(createTempChatExpirationDate).not.toHaveBeenCalled(); + expect(updateSharedLink).not.toHaveBeenCalled(); + }); + it('clears updated share expiration when the conversation is no longer retained', async () => { mongoose.models.SharedLink.findOne.mockReturnValue(lean({ conversationId: 'convo-123' })); mongoose.models.Conversation.findOne.mockReturnValue( diff --git a/api/server/routes/share.js b/api/server/routes/share.js index 5ce0ff3709..ccfbfafe63 100644 --- a/api/server/routes/share.js +++ b/api/server/routes/share.js @@ -21,29 +21,27 @@ const router = express.Router(); async function getSharedLinkExpiration(req, conversationId) { const isRetentionAll = req?.config?.interfaceConfig?.retentionMode === RetentionMode.ALL; - if (!isRetentionAll) { - if (!conversationId) { - return; - } + if (!conversationId) { + return; + } - const Conversation = mongoose.models.Conversation; - const convo = await Conversation.findOne( - { conversationId, user: req.user.id }, - 'isTemporary expiredAt', - ).lean(); + const Conversation = mongoose.models.Conversation; + const convo = await Conversation.findOne( + { conversationId, user: req.user.id }, + 'isTemporary expiredAt', + ).lean(); - if (!convo) { - return; - } + if (!convo) { + return; + } - const conversationExpiredAt = getConversationExpirationDate(convo); - if (conversationExpiredAt == null) { + const conversationExpiredAt = getConversationExpirationDate(convo); + if (conversationExpiredAt == null) { + if (!isRetentionAll) { return null; } - - if (!isActiveExpirationDate(conversationExpiredAt)) { - return conversationExpiredAt; - } + } else if (!isActiveExpirationDate(conversationExpiredAt)) { + return conversationExpiredAt; } try {