From 2455ecf39f62f89ac9a383f70277701023c150a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Thu, 17 Jul 2025 11:54:14 +0200 Subject: [PATCH] Remove playbooks on delete channel (#8994) Co-authored-by: Mattermost Build --- app/actions/local/channel.ts | 2 +- app/actions/local/team.test.ts | 10 +- app/actions/local/team.ts | 2 +- app/actions/remote/entry/common.ts | 2 +- app/actions/remote/team.ts | 2 +- .../models/playbook_checklist.test.ts | 90 ++++++++++++ .../database/models/playbook_checklist.ts | 14 ++ .../database/models/playbook_run.test.ts | 136 ++++++++++++++++++ .../playbooks/database/models/playbook_run.ts | 14 ++ .../database/models/playbook_checklist.ts | 3 + .../types/database/models/playbook_run.ts | 3 + app/queries/servers/channel.test.ts | 25 +++- app/queries/servers/channel.ts | 15 +- app/queries/servers/entry.test.ts | 2 + app/queries/servers/entry.ts | 7 +- app/queries/servers/team.test.ts | 8 +- app/queries/servers/team.ts | 4 +- app/store/ephemeral_store.test.ts | 16 +++ app/store/ephemeral_store.ts | 4 + test/test_helper.ts | 8 +- 20 files changed, 342 insertions(+), 25 deletions(-) create mode 100644 app/products/playbooks/database/models/playbook_checklist.test.ts create mode 100644 app/products/playbooks/database/models/playbook_run.test.ts diff --git a/app/actions/local/channel.ts b/app/actions/local/channel.ts index 4aa096adb..326c95289 100644 --- a/app/actions/local/channel.ts +++ b/app/actions/local/channel.ts @@ -116,7 +116,7 @@ export async function removeCurrentUserFromChannel(serverUrl: string, channelId: if (!channel) { throw new Error('myChannel present but no channel on the database'); } - models.push(...await prepareDeleteChannel(channel)); + models.push(...await prepareDeleteChannel(serverUrl, channel)); let teamId = channel.teamId; if (teamId) { teamId = await getCurrentTeamId(database); diff --git a/app/actions/local/team.test.ts b/app/actions/local/team.test.ts index 1e4543ea4..4b58a894f 100644 --- a/app/actions/local/team.test.ts +++ b/app/actions/local/team.test.ts @@ -129,7 +129,7 @@ describe('removeUserFromTeam', () => { expect(getMyTeamById).toHaveBeenCalledWith(database, team.id); expect(getTeamById).toHaveBeenCalledWith(database, myTeam.id); - expect(prepareDeleteTeam).toHaveBeenCalledWith(team); + expect(prepareDeleteTeam).toHaveBeenCalledWith(serverUrl, team); expect(removeTeamFromTeamHistory).toHaveBeenCalledWith(operator, team.id, true); expect(operator.batchRecords).toHaveBeenCalledWith(preparedModels, 'removeUserFromTeam'); @@ -166,7 +166,7 @@ describe('removeUserFromTeam', () => { expect(getMyTeamById).toHaveBeenCalledWith(database, teamId); expect(getTeamById).toHaveBeenCalledWith(database, myTeam.id); - expect(prepareDeleteTeam).toHaveBeenCalledWith(team); + expect(prepareDeleteTeam).toHaveBeenCalledWith(serverUrl, team); expect(removeTeamFromTeamHistory).toHaveBeenCalledWith(operator, team.id, true); expect(operator.batchRecords).toHaveBeenCalledWith(preparedModels, 'removeUserFromTeam'); expect(logError).toHaveBeenCalledWith('Failed removeUserFromTeam', writeError); @@ -189,7 +189,7 @@ describe('removeUserFromTeam', () => { expect(getMyTeamById).toHaveBeenCalledWith(database, teamId); expect(getTeamById).not.toHaveBeenCalled(); - expect(prepareDeleteTeam).not.toHaveBeenCalled(); + expect(prepareDeleteTeam).not.toHaveBeenCalledWith(serverUrl, null as any); expect(removeTeamFromTeamHistory).not.toHaveBeenCalled(); expect(operator.batchRecords).not.toHaveBeenCalled(); expect(result).toEqual({error: undefined}); @@ -238,7 +238,7 @@ describe('removeUserFromTeam', () => { expect(getMyTeamById).toHaveBeenCalledWith(database, teamId); expect(getTeamById).toHaveBeenCalledWith(database, myTeam.id); - expect(prepareDeleteTeam).toHaveBeenCalledWith(team); + expect(prepareDeleteTeam).toHaveBeenCalledWith(serverUrl, team); expect(removeTeamFromTeamHistory).toHaveBeenCalledWith(operator, team.id, true); expect(operator.batchRecords).toHaveBeenCalledWith(preparedModels, 'removeUserFromTeam'); expect(result).toEqual({error: undefined}); @@ -258,7 +258,7 @@ describe('removeUserFromTeam', () => { expect(getMyTeamById).toHaveBeenCalledWith(database, teamId); expect(getTeamById).toHaveBeenCalledWith(database, myTeam.id); - expect(prepareDeleteTeam).toHaveBeenCalledWith(team); + expect(prepareDeleteTeam).toHaveBeenCalledWith(serverUrl, team); expect(removeTeamFromTeamHistory).toHaveBeenCalledWith(operator, team.id, true); expect(operator.batchRecords).not.toHaveBeenCalled(); expect(result).toEqual({error: undefined}); diff --git a/app/actions/local/team.ts b/app/actions/local/team.ts index c620e140a..f9407be8e 100644 --- a/app/actions/local/team.ts +++ b/app/actions/local/team.ts @@ -18,7 +18,7 @@ export async function removeUserFromTeam(serverUrl: string, teamId: string) { if (!team) { throw new Error('Team not found'); } - const models = await prepareDeleteTeam(team); + const models = await prepareDeleteTeam(serverUrl, team); const system = await removeTeamFromTeamHistory(operator, team.id, true); if (system) { models.push(...system); diff --git a/app/actions/remote/entry/common.ts b/app/actions/remote/entry/common.ts index 04012b212..60874c083 100644 --- a/app/actions/remote/entry/common.ts +++ b/app/actions/remote/entry/common.ts @@ -189,7 +189,7 @@ const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, initialChannelId = await entryInitialChannelId(database, initialChannelId, teamId, initialTeamId, meData?.user?.locale || '', chData?.channels, chData?.memberships); const dt = Date.now(); - const modelsToDeletePromises = await prepareEntryModelsForDeletion({operator, teamData, chData}); + const modelsToDeletePromises = await prepareEntryModelsForDeletion({serverUrl, operator, teamData, chData}); const modelPromises = await prepareEntryModels({operator, teamData, chData, prefData, meData, isCRTEnabled}); const modelsToDelete = await Promise.all(modelsToDeletePromises); const models = await Promise.all(modelPromises); diff --git a/app/actions/remote/team.ts b/app/actions/remote/team.ts index a8f881e56..0747101ff 100644 --- a/app/actions/remote/team.ts +++ b/app/actions/remote/team.ts @@ -183,7 +183,7 @@ export async function fetchMyTeams(serverUrl: string, fetchOnly = false, groupLa // Immediately delete myTeams so that the UI renders only teams the user is a member of. const removeTeams = await queryTeamsById(database, Array.from(removeTeamIds)).fetch(); removeTeams.forEach((team) => { - modelPromises.push(prepareDeleteTeam(team)); + modelPromises.push(prepareDeleteTeam(serverUrl, team)); }); } diff --git a/app/products/playbooks/database/models/playbook_checklist.test.ts b/app/products/playbooks/database/models/playbook_checklist.test.ts new file mode 100644 index 000000000..9fa5edaee --- /dev/null +++ b/app/products/playbooks/database/models/playbook_checklist.test.ts @@ -0,0 +1,90 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import DatabaseManager from '@database/manager'; +import {PLAYBOOK_TABLES} from '@playbooks/constants/database'; +import TestHelper from '@test/test_helper'; + +import type ServerDataOperator from '@database/operator/server_data_operator'; +import type PlaybookChecklistModel from '@playbooks/types/database/models/playbook_checklist'; + +const {PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM} = PLAYBOOK_TABLES; + +const SERVER_URL = 'playbookChecklistModel.test.com'; + +describe('PlaybookChecklistModel', () => { + let operator: ServerDataOperator; + + beforeEach(async () => { + await DatabaseManager.init([SERVER_URL]); + operator = DatabaseManager.serverDatabases[SERVER_URL]!.operator; + }); + + afterEach(async () => { + await DatabaseManager.destroyServerDatabase(SERVER_URL); + }); + + describe('prepareDestroyWithRelations', () => { + it('should prepare checklist and all its items for destruction when checklist has items', async () => { + // Create a playbook run first + const mockRuns = TestHelper.createPlaybookRuns(1, 1, 3, true); + + await operator.handlePlaybookRun({ + runs: mockRuns, + prepareRecordsOnly: false, + processChildren: true, + }); + + const checklistId = mockRuns[0].checklists[0].id; + + // Get the created checklist model + const checklistModel = await operator.database.get(PLAYBOOK_CHECKLIST).find(checklistId); + + // Call the method under test + const preparedModels = await checklistModel.prepareDestroyWithRelations(); + + // Verify the result + expect(preparedModels).toHaveLength(4); // 1 checklist + 3 items + + // Verify the checklist is prepared for destruction + const checklistPrepared = preparedModels.find((model) => model.id === checklistId); + expect(checklistPrepared).toBeDefined(); + expect(checklistPrepared!.collection.table).toBe(PLAYBOOK_CHECKLIST); + + // Verify all items are prepared for destruction + const itemIds = mockRuns[0].checklists[0].items.map((item) => item.id); + itemIds.forEach((itemId) => { + const itemPrepared = preparedModels.find((model) => model.id === itemId); + expect(itemPrepared).toBeDefined(); + expect(itemPrepared!.collection.table).toBe(PLAYBOOK_CHECKLIST_ITEM); + }); + }); + + it('should prepare only checklist for destruction when checklist has no items', async () => { + // Create a playbook run first + const mockRuns = TestHelper.createPlaybookRuns(1, 1, 0, true); + + await operator.handlePlaybookRun({ + runs: mockRuns, + prepareRecordsOnly: false, + processChildren: true, + }); + + const checklistId = mockRuns[0].checklists[0].id; + + // Get the created checklist model + const checklistModel = await operator.database.get(PLAYBOOK_CHECKLIST).find(checklistId); + + // Call the method under test + const preparedModels = await checklistModel.prepareDestroyWithRelations(); + + // Verify the result + expect(preparedModels).toHaveLength(1); // Only the checklist + + // Verify the checklist is prepared for destruction + const checklistPrepared = preparedModels[0]; + expect(checklistPrepared.id).toBe(checklistId); + expect(checklistPrepared.collection.table).toBe(PLAYBOOK_CHECKLIST); + }); + }); +}); diff --git a/app/products/playbooks/database/models/playbook_checklist.ts b/app/products/playbooks/database/models/playbook_checklist.ts index 5ef8c501a..c4f0e760c 100644 --- a/app/products/playbooks/database/models/playbook_checklist.ts +++ b/app/products/playbooks/database/models/playbook_checklist.ts @@ -54,4 +54,18 @@ export default class PlaybookChecklistModel extends Model implements PlaybookChe /** run : The playbook run to which this checklist belongs */ @immutableRelation(PLAYBOOK_RUN, 'run_id') run!: Relation; + + prepareDestroyWithRelations = async (): Promise => { + const preparedModels: Model[] = [this.prepareDestroyPermanently()]; + + const items = await this.items?.fetch(); + if (items?.length) { + for await (const item of items) { + const preparedItem = item.prepareDestroyPermanently(); + preparedModels.push(preparedItem); + } + } + + return preparedModels; + }; } diff --git a/app/products/playbooks/database/models/playbook_run.test.ts b/app/products/playbooks/database/models/playbook_run.test.ts new file mode 100644 index 000000000..ecd049ec2 --- /dev/null +++ b/app/products/playbooks/database/models/playbook_run.test.ts @@ -0,0 +1,136 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import DatabaseManager from '@database/manager'; +import {PLAYBOOK_TABLES} from '@playbooks/constants/database'; +import TestHelper from '@test/test_helper'; + +import type ServerDataOperator from '@database/operator/server_data_operator'; +import type PlaybookRunModel from '@playbooks/types/database/models/playbook_run'; + +const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM} = PLAYBOOK_TABLES; + +const SERVER_URL = 'playbookRunModel.test.com'; + +describe('PlaybookRunModel', () => { + let operator: ServerDataOperator; + + beforeEach(async () => { + await DatabaseManager.init([SERVER_URL]); + operator = DatabaseManager.serverDatabases[SERVER_URL]!.operator; + }); + + afterEach(async () => { + await DatabaseManager.destroyServerDatabase(SERVER_URL); + }); + + describe('prepareDestroyWithRelations', () => { + it('should prepare run and all its checklists and items for destruction when run has checklists with items', async () => { + // Create playbook runs with checklists and items + const mockRuns = TestHelper.createPlaybookRuns(1, 2, 3, true); // 1 run, 2 checklists, 3 items per checklist + + await operator.handlePlaybookRun({ + runs: mockRuns, + prepareRecordsOnly: false, + processChildren: true, + }); + + const runId = mockRuns[0].id; + + // Get the created run model + const runModel = await operator.database.get(PLAYBOOK_RUN).find(runId); + + // Call the method under test + const preparedModels = await runModel.prepareDestroyWithRelations(); + + // Calculate expected count: 1 run + 2 checklists + (2 checklists * 3 items each) = 9 total + const expectedCount = 1 + 2 + (2 * 3); + expect(preparedModels).toHaveLength(expectedCount); + + // Verify the run is prepared for destruction + const runPrepared = preparedModels.find((model) => model.id === runId); + expect(runPrepared).toBeDefined(); + expect(runPrepared!.collection.table).toBe(PLAYBOOK_RUN); + + // Verify all checklists are prepared for destruction + const checklistIds = mockRuns[0].checklists.map((checklist) => checklist.id); + checklistIds.forEach((checklistId) => { + const checklistPrepared = preparedModels.find((model) => model.id === checklistId); + expect(checklistPrepared).toBeDefined(); + expect(checklistPrepared!.collection.table).toBe(PLAYBOOK_CHECKLIST); + }); + + // Verify all items are prepared for destruction + const itemIds = mockRuns[0].checklists.flatMap((checklist) => + checklist.items.map((item) => item.id), + ); + itemIds.forEach((itemId) => { + const itemPrepared = preparedModels.find((model) => model.id === itemId); + expect(itemPrepared).toBeDefined(); + expect(itemPrepared!.collection.table).toBe(PLAYBOOK_CHECKLIST_ITEM); + }); + }); + + it('should prepare only run for destruction when run has no checklists', async () => { + // Create playbook runs with no checklists + const mockRuns = TestHelper.createPlaybookRuns(1, 0, 0, true); // 1 run, 0 checklists, 0 items + + await operator.handlePlaybookRun({ + runs: mockRuns, + prepareRecordsOnly: false, + processChildren: true, + }); + + const runId = mockRuns[0].id; + + // Get the created run model + const runModel = await operator.database.get(PLAYBOOK_RUN).find(runId); + + // Call the method under test + const preparedModels = await runModel.prepareDestroyWithRelations(); + + // Verify the result + expect(preparedModels).toHaveLength(1); // Only the run + + // Verify the run is prepared for destruction + const runPrepared = preparedModels[0]; + expect(runPrepared.id).toBe(runId); + expect(runPrepared.collection.table).toBe(PLAYBOOK_RUN); + }); + + it('should prepare run and checklists for destruction when checklists have no items', async () => { + // Create playbook runs with checklists but no items + const mockRuns = TestHelper.createPlaybookRuns(1, 3, 0, true); // 1 run, 3 checklists, 0 items + + await operator.handlePlaybookRun({ + runs: mockRuns, + prepareRecordsOnly: false, + processChildren: true, + }); + + const runId = mockRuns[0].id; + + // Get the created run model + const runModel = await operator.database.get(PLAYBOOK_RUN).find(runId); + + // Call the method under test + const preparedModels = await runModel.prepareDestroyWithRelations(); + + // Verify the result: 1 run + 3 checklists = 4 total + expect(preparedModels).toHaveLength(4); + + // Verify the run is prepared for destruction + const runPrepared = preparedModels.find((model) => model.id === runId); + expect(runPrepared).toBeDefined(); + expect(runPrepared!.collection.table).toBe(PLAYBOOK_RUN); + + // Verify all checklists are prepared for destruction + const checklistIds = mockRuns[0].checklists.map((checklist) => checklist.id); + checklistIds.forEach((checklistId) => { + const checklistPrepared = preparedModels.find((model) => model.id === checklistId); + expect(checklistPrepared).toBeDefined(); + expect(checklistPrepared!.collection.table).toBe(PLAYBOOK_CHECKLIST); + }); + }); + }); +}); diff --git a/app/products/playbooks/database/models/playbook_run.ts b/app/products/playbooks/database/models/playbook_run.ts index 71990d223..f88858d18 100644 --- a/app/products/playbooks/database/models/playbook_run.ts +++ b/app/products/playbooks/database/models/playbook_run.ts @@ -141,4 +141,18 @@ export default class PlaybookRunModel extends Model implements PlaybookRunModelI const filteredParticipantIds = this.participantIds.filter((id) => id !== this.ownerUserId); return this.database.get(USER).query(Q.where('id', Q.oneOf(filteredParticipantIds))); }; + + prepareDestroyWithRelations = async (): Promise => { + const preparedModels: Model[] = [this.prepareDestroyPermanently()]; + + const checklists = await this.checklists?.fetch(); + if (checklists?.length) { + for await (const checklist of checklists) { + const preparedChecklist = await checklist.prepareDestroyWithRelations(); + preparedModels.push(...preparedChecklist); + } + } + + return preparedModels; + }; } diff --git a/app/products/playbooks/types/database/models/playbook_checklist.ts b/app/products/playbooks/types/database/models/playbook_checklist.ts index 4abd74be8..8ccacbc46 100644 --- a/app/products/playbooks/types/database/models/playbook_checklist.ts +++ b/app/products/playbooks/types/database/models/playbook_checklist.ts @@ -40,6 +40,9 @@ declare class PlaybookChecklistModel extends Model { /** items : All the items associated with this checklist */ items: Query; + + /** prepareDestroyWithRelations : Prepare the model for deletion with its relations */ + prepareDestroyWithRelations: () => Promise; } export default PlaybookChecklistModel; diff --git a/app/products/playbooks/types/database/models/playbook_run.ts b/app/products/playbooks/types/database/models/playbook_run.ts index 9ff755ee1..52c6c6841 100644 --- a/app/products/playbooks/types/database/models/playbook_run.ts +++ b/app/products/playbooks/types/database/models/playbook_run.ts @@ -109,6 +109,9 @@ declare class PlaybookRunModel extends Model { /** participants : The USERS that participate in this Playbook Run */ participants: () => Query; + + /** prepareDestroyWithRelations : Prepare the model for deletion with its relations */ + prepareDestroyWithRelations: () => Promise; } export default PlaybookRunModel; diff --git a/app/queries/servers/channel.test.ts b/app/queries/servers/channel.test.ts index 45ab3ecd0..1f5bce72c 100644 --- a/app/queries/servers/channel.test.ts +++ b/app/queries/servers/channel.test.ts @@ -9,6 +9,7 @@ import {of as of$} from 'rxjs'; import {General, Permissions} from '@constants'; import {MM_TABLES} from '@constants/database'; import ServerDataOperator from '@database/operator/server_data_operator'; +import EphemeralStore from '@store/ephemeral_store'; import TestHelper from '@test/test_helper'; import {hasPermission} from '@utils/role'; @@ -326,6 +327,7 @@ describe('prepareMyChannelsForTeam', () => { describe('prepareDeleteChannel', () => { let channel: ChannelModel; + const serverUrl = 'prepareDeleteChannel.test.com'; beforeEach(() => { channel = TestHelper.fakeChannelModel({ @@ -334,6 +336,8 @@ describe('prepareDeleteChannel', () => { }); it('should prepare models for deletion', async () => { + const unsetSpy = jest.spyOn(EphemeralStore, 'unsetChannelPlaybooksSynced'); + const membershipModel = TestHelper.fakeMyChannelModel({prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'membership'})}); const infoModel = TestHelper.fakeChannelInfoModel({prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'info'})}); const categoryChannelModel = TestHelper.fakeCategoryChannelModel({prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'category'})}); @@ -348,6 +352,16 @@ describe('prepareDeleteChannel', () => { const bookmarkModels = [ TestHelper.fakeChannelBookmarkModel({id: 'bookmark1', prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'bookmark'})}), ]; + const playbookRunModels = [ + TestHelper.fakePlaybookRunModel({ + id: 'playbookRun', + prepareDestroyWithRelations: jest.fn().mockResolvedValue([ + TestHelper.fakePlaybookRunModel({ + id: 'playbookRun', + }), + ]), + }), + ]; jest.mocked(channel.membership.fetch).mockResolvedValue(membershipModel); jest.mocked(channel.info.fetch).mockResolvedValue(infoModel); @@ -357,8 +371,9 @@ describe('prepareDeleteChannel', () => { jest.mocked(channel.postsInChannel.fetch).mockResolvedValue(postsInChannelModels); jest.mocked(channel.posts.fetch).mockResolvedValue(postModels); jest.mocked(channel.bookmarks.fetch).mockResolvedValue(bookmarkModels); + jest.mocked(channel.playbookRuns.fetch).mockResolvedValue(playbookRunModels); - const result = await prepareDeleteChannel(channel); + const result = await prepareDeleteChannel(serverUrl, channel); expect(result).toEqual([ {}, @@ -370,6 +385,7 @@ describe('prepareDeleteChannel', () => { {id: 'postsInChannel'}, {id: 'post'}, {id: 'bookmark'}, + expect.objectContaining({id: 'playbookRun'}), ]); expect(channel.prepareDestroyPermanently).toHaveBeenCalled(); expect(channel.membership.fetch).toHaveBeenCalled(); @@ -380,6 +396,11 @@ describe('prepareDeleteChannel', () => { expect(channel.postsInChannel.fetch).toHaveBeenCalled(); expect(channel.posts.fetch).toHaveBeenCalled(); expect(channel.bookmarks.fetch).toHaveBeenCalled(); + expect(channel.playbookRuns.fetch).toHaveBeenCalled(); + expect(playbookRunModels[0].prepareDestroyWithRelations).toHaveBeenCalled(); + + // Should have cleared the playbooks synced + expect(unsetSpy).toHaveBeenCalledWith(serverUrl, channel.id); }); it('should handle errors gracefully', async () => { @@ -387,7 +408,7 @@ describe('prepareDeleteChannel', () => { jest.mocked(channel.info.fetch).mockRejectedValue(new Error('Test error')); jest.mocked(channel.categoryChannel.fetch).mockRejectedValue(new Error('Test error')); - const result = await prepareDeleteChannel(channel); + const result = await prepareDeleteChannel(serverUrl, channel); expect(result).toEqual([{}]); expect(channel.prepareDestroyPermanently).toHaveBeenCalled(); diff --git a/app/queries/servers/channel.ts b/app/queries/servers/channel.ts index b237caea7..125208945 100644 --- a/app/queries/servers/channel.ts +++ b/app/queries/servers/channel.ts @@ -10,6 +10,7 @@ import {map as map$, switchMap, distinctUntilChanged, combineLatestWith} from 'r import {General, Permissions} from '@constants'; import {MM_TABLES} from '@constants/database'; import {sanitizeLikeString} from '@helpers/database'; +import EphemeralStore from '@store/ephemeral_store'; import {isDefaultChannel} from '@utils/channel'; import {hasPermission} from '@utils/role'; import {isSystemAdmin} from '@utils/user'; @@ -151,7 +152,7 @@ export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, tea return prepareChannels(operator, channels, channelInfos, channelMembers, memberships, isCRTEnabled); }; -export const prepareDeleteChannel = async (channel: ChannelModel): Promise => { +export const prepareDeleteChannel = async (serverUrl: string, channel: ChannelModel): Promise => { const preparedModels: Model[] = [channel.prepareDestroyPermanently()]; const relations: Array | undefined> = [channel.membership, channel.info, channel.categoryChannel]; @@ -198,6 +199,16 @@ export const prepareDeleteChannel = async (channel: ChannelModel): Promise { }; const promises = await prepareEntryModelsForDeletion({ + serverUrl, operator, teamData, }); @@ -190,6 +191,7 @@ describe('Entry Queries', () => { }; const promises = await prepareEntryModelsForDeletion({ + serverUrl, operator, chData, }); diff --git a/app/queries/servers/entry.ts b/app/queries/servers/entry.ts index 4e1c5992a..a57d9184f 100644 --- a/app/queries/servers/entry.ts +++ b/app/queries/servers/entry.ts @@ -30,6 +30,7 @@ type PrepareModelsArgs = { } type PrepareModelsForDeletionArgs = { + serverUrl: string; operator: ServerDataOperator; initialTeamId?: string; teamData?: MyTeamsRequest; @@ -48,7 +49,7 @@ const { MY_CHANNEL, } = MM_TABLES.SERVER; -export async function prepareEntryModelsForDeletion({operator, teamData, chData}: PrepareModelsForDeletionArgs): Promise>> { +export async function prepareEntryModelsForDeletion({serverUrl, operator, teamData, chData}: PrepareModelsForDeletionArgs): Promise>> { const modelPromises: Array> = []; const {database} = operator; let teamIdsToDelete: Set|undefined; @@ -61,7 +62,7 @@ export async function prepareEntryModelsForDeletion({operator, teamData, chData} const removeTeams = await queryTeamsById(database, Array.from(teamIdsToDelete)).fetch(); removeTeams.forEach((team) => { - modelPromises.push(prepareDeleteTeam(team)); + modelPromises.push(prepareDeleteTeam(serverUrl, team)); }); } @@ -79,7 +80,7 @@ export async function prepareEntryModelsForDeletion({operator, teamData, chData} if (removeChannelIds?.length) { removeChannels = await queryChannelsById(database, removeChannelIds).fetch(); removeChannels.forEach((c) => { - modelPromises.push(prepareDeleteChannel(c)); + modelPromises.push(prepareDeleteChannel(serverUrl, c)); }); } } diff --git a/app/queries/servers/team.test.ts b/app/queries/servers/team.test.ts index 381817afb..c4a42e82a 100644 --- a/app/queries/servers/team.test.ts +++ b/app/queries/servers/team.test.ts @@ -921,7 +921,7 @@ describe('Team Queries', () => { }); const team = await getTeamById(database, teamId); - const records = await prepareDeleteTeam(team!); + const records = await prepareDeleteTeam(serverUrl, team!); expect(records.length).toBeGreaterThan(0); }); @@ -932,7 +932,7 @@ describe('Team Queries', () => { }); const team = await getTeamById(database, teamId); - const records = await prepareDeleteTeam(team!); + const records = await prepareDeleteTeam(serverUrl, team!); expect(records.length).toBe(1); // Just the team record }); @@ -957,12 +957,12 @@ describe('Team Queries', () => { }); const team = await getTeamById(database, teamId); - const records = await prepareDeleteTeam(team!); + const records = await prepareDeleteTeam(serverUrl, team!); expect(records.length).toBe(1); // Just the team record }); it('should return empty array on error', async () => { - const records = await prepareDeleteTeam(null as any); + const records = await prepareDeleteTeam(serverUrl, null as any); expect(records).toEqual([]); }); }); diff --git a/app/queries/servers/team.ts b/app/queries/servers/team.ts index b90055d1d..4b078ad30 100644 --- a/app/queries/servers/team.ts +++ b/app/queries/servers/team.ts @@ -234,7 +234,7 @@ export async function deleteMyTeams(operator: ServerDataOperator, myTeams: MyTea } } -export const prepareDeleteTeam = async (team: TeamModel): Promise => { +export const prepareDeleteTeam = async (serverUrl: string, team: TeamModel): Promise => { try { const preparedModels: Model[] = [team.prepareDestroyPermanently()]; @@ -279,7 +279,7 @@ export const prepareDeleteTeam = async (team: TeamModel): Promise => { if (channels.length) { for await (const channel of channels) { try { - const preparedChannel = await prepareDeleteChannel(channel); + const preparedChannel = await prepareDeleteChannel(serverUrl, channel); preparedModels.push(...preparedChannel); } catch { // Record not found, do nothing diff --git a/app/store/ephemeral_store.test.ts b/app/store/ephemeral_store.test.ts index b771ab6ce..e36874ebc 100644 --- a/app/store/ephemeral_store.test.ts +++ b/app/store/ephemeral_store.test.ts @@ -27,5 +27,21 @@ describe('EphemeralStore', () => { EphemeralStore.clearChannelPlaybooksSynced('server-url'); expect(EphemeralStore.getChannelPlaybooksSynced('server-url', 'channel-id')).toBe(false); expect(EphemeralStore.getChannelPlaybooksSynced('server-url-2', 'channel-id')).toBe(true); // The other server is not cleared + + // Expect false if unset + EphemeralStore.setChannelPlaybooksSynced('server-url', 'channel-id'); + EphemeralStore.setChannelPlaybooksSynced('server-url', 'channel-id-2'); + EphemeralStore.setChannelPlaybooksSynced('server-url', 'channel-id-3'); + + EphemeralStore.unsetChannelPlaybooksSynced('server-url', 'channel-id'); + expect(EphemeralStore.getChannelPlaybooksSynced('server-url', 'channel-id')).toBe(false); + expect(EphemeralStore.getChannelPlaybooksSynced('server-url', 'channel-id-2')).toBe(true); + expect(EphemeralStore.getChannelPlaybooksSynced('server-url', 'channel-id-3')).toBe(true); + + // Expect false if cleared + EphemeralStore.clearChannelPlaybooksSynced('server-url'); + expect(EphemeralStore.getChannelPlaybooksSynced('server-url', 'channel-id')).toBe(false); + expect(EphemeralStore.getChannelPlaybooksSynced('server-url', 'channel-id-2')).toBe(false); + expect(EphemeralStore.getChannelPlaybooksSynced('server-url', 'channel-id-3')).toBe(false); }); }); diff --git a/app/store/ephemeral_store.ts b/app/store/ephemeral_store.ts index 5c465cecc..a8f4f0efb 100644 --- a/app/store/ephemeral_store.ts +++ b/app/store/ephemeral_store.ts @@ -296,6 +296,10 @@ class EphemeralStoreSingleton { this.channelPlaybooksSynced[serverUrl]?.add(channelId); }; + unsetChannelPlaybooksSynced = (serverUrl: string, channelId: string) => { + this.channelPlaybooksSynced[serverUrl]?.delete(channelId); + }; + clearChannelPlaybooksSynced = (serverUrl: string) => { delete this.channelPlaybooksSynced[serverUrl]; }; diff --git a/test/test_helper.ts b/test/test_helper.ts index c6a60253f..e4f300798 100644 --- a/test/test_helper.ts +++ b/test/test_helper.ts @@ -1099,13 +1099,13 @@ class TestHelperSingleton { }; }; - createPlaybookRuns = (runsCount = 1, maxChecklistCount = 1, maxItemsPerChecklist = 1): PlaybookRun[] => { + createPlaybookRuns = (runsCount = 1, maxChecklistCount = 1, maxItemsPerChecklist = 1, omitRandom = false): PlaybookRun[] => { const playbookRuns: PlaybookRun[] = []; for (let i = 0; i < runsCount; i++) { const checklists: PlaybookChecklist[] = []; - const checklistCount = Math.floor(Math.random() * maxChecklistCount) + 1; + const checklistCount = omitRandom ? maxChecklistCount : Math.floor(Math.random() * maxChecklistCount) + 1; for (let j = 0; j < checklistCount; j++) { - const itemsCount = Math.floor(Math.random() * maxItemsPerChecklist) + 1; + const itemsCount = omitRandom ? maxItemsPerChecklist : Math.floor(Math.random() * maxItemsPerChecklist) + 1; checklists.push(this.createPlaybookChecklist(`playbook_run_${i}`, itemsCount, j)); } playbookRuns.push({ @@ -1212,6 +1212,7 @@ class TestHelperSingleton { owner: this.fakeRelation(), checklists: this.fakeQuery([]), participants: () => this.fakeQuery([]), + prepareDestroyWithRelations: jest.fn().mockResolvedValue([]), previousReminder: 0, itemsOrder: [], updateAt: 0, @@ -1231,6 +1232,7 @@ class TestHelperSingleton { run: this.fakeRelation(), itemsOrder: [], updateAt: 0, + prepareDestroyWithRelations: jest.fn().mockResolvedValue([]), ...overwrite, }; };