From 528777926c4907f323c53d7706d687032a7cfe62 Mon Sep 17 00:00:00 2001 From: Hampus Date: Thu, 20 Aug 2026 15:50:51 +0200 Subject: [PATCH] fix(api): stop one unreachable community from breaking bookmarks (#1779) --- .../user/services/UserContentService.test.ts | 30 +++++++++++++++++++ .../api/user/services/UserContentService.ts | 16 ++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 fluxer_api/src/api/user/services/UserContentService.test.ts diff --git a/fluxer_api/src/api/user/services/UserContentService.test.ts b/fluxer_api/src/api/user/services/UserContentService.test.ts new file mode 100644 index 000000000..2a69e58c8 --- /dev/null +++ b/fluxer_api/src/api/user/services/UserContentService.test.ts @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {UnknownChannelError} from '@fluxer/errors/src/domains/channel/UnknownChannelError'; +import {UnknownMessageError} from '@fluxer/errors/src/domains/channel/UnknownMessageError'; +import {MissingPermissionsError} from '@fluxer/errors/src/domains/core/MissingPermissionsError'; +import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildError'; +import {describe, expect, it} from 'vitest'; +import {UserContentServiceTestHooks} from './UserContentService'; + +const {isUnreachableEntityError} = UserContentServiceTestHooks; + +describe('isUnreachableEntityError', () => { + it('treats a deleted or left community as unreachable rather than fatal', () => { + expect(isUnreachableEntityError(new UnknownGuildError())).toBe(true); + }); + + it('treats a gone channel and a lost permission as unreachable', () => { + expect(isUnreachableEntityError(new UnknownChannelError())).toBe(true); + expect(isUnreachableEntityError(new MissingPermissionsError())).toBe(true); + }); + + it('leaves a deleted message to the delete path instead of marking it unavailable', () => { + expect(isUnreachableEntityError(new UnknownMessageError())).toBe(false); + }); + + it('still lets unexpected failures surface', () => { + expect(isUnreachableEntityError(new Error('database is on fire'))).toBe(false); + expect(isUnreachableEntityError(null)).toBe(false); + }); +}); diff --git a/fluxer_api/src/api/user/services/UserContentService.ts b/fluxer_api/src/api/user/services/UserContentService.ts index afba73b30..1555e705a 100644 --- a/fluxer_api/src/api/user/services/UserContentService.ts +++ b/fluxer_api/src/api/user/services/UserContentService.ts @@ -6,6 +6,7 @@ import {UnknownChannelError} from '@fluxer/errors/src/domains/channel/UnknownCha import {UnknownMessageError} from '@fluxer/errors/src/domains/channel/UnknownMessageError'; import {MaxBookmarksError} from '@fluxer/errors/src/domains/core/MaxBookmarksError'; import {MissingPermissionsError} from '@fluxer/errors/src/domains/core/MissingPermissionsError'; +import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildError'; import {HarvestExpiredError} from '@fluxer/errors/src/domains/moderation/HarvestExpiredError'; import {HarvestFailedError} from '@fluxer/errors/src/domains/moderation/HarvestFailedError'; import {HarvestNotReadyError} from '@fluxer/errors/src/domains/moderation/HarvestNotReadyError'; @@ -94,6 +95,13 @@ function normalizeProviderEnvironment( return platform === 'ios_apns' ? DEFAULT_APNS_PROVIDER_ENVIRONMENT : null; } +const isUnreachableEntityError = (error: unknown): boolean => + error instanceof MissingPermissionsError || + error instanceof UnknownChannelError || + error instanceof UnknownGuildError; + +export const UserContentServiceTestHooks = {isUnreachableEntityError}; + export class UserContentService { private readonly updatePropagator: BaseUserUpdatePropagator; private readonly userRepository: UserContentRepository; @@ -138,11 +146,7 @@ export class UserContentService { messageId: mention.messageId, }); } catch (error) { - if ( - error instanceof UnknownMessageError || - error instanceof MissingPermissionsError || - error instanceof UnknownChannelError - ) { + if (error instanceof UnknownMessageError || isUnreachableEntityError(error)) { return null; } throw error; @@ -188,7 +192,7 @@ export class UserContentService { await this.userRepository.deleteSavedMessage(userId, savedMessage.messageId); return null; } - if (error instanceof MissingPermissionsError || error instanceof UnknownChannelError) { + if (isUnreachableEntityError(error)) { status = 'missing_permissions'; } else { throw error;