Remove playbooks on delete channel (#8994)

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Daniel Espino García 2025-07-17 11:54:14 +02:00 committed by GitHub
parent b925baa21b
commit 2455ecf39f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 342 additions and 25 deletions

View file

@ -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);

View file

@ -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});

View file

@ -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);

View file

@ -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);

View file

@ -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));
});
}

View file

@ -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<PlaybookChecklistModel>(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<PlaybookChecklistModel>(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);
});
});
});

View file

@ -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<PlaybookRunModel>;
prepareDestroyWithRelations = async (): Promise<Model[]> => {
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;
};
}

View file

@ -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<PlaybookRunModel>(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<PlaybookRunModel>(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<PlaybookRunModel>(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);
});
});
});
});

View file

@ -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<UserModel>(USER).query(Q.where('id', Q.oneOf(filteredParticipantIds)));
};
prepareDestroyWithRelations = async (): Promise<Model[]> => {
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;
};
}

View file

@ -40,6 +40,9 @@ declare class PlaybookChecklistModel extends Model {
/** items : All the items associated with this checklist */
items: Query<PlaybookChecklistItemModel>;
/** prepareDestroyWithRelations : Prepare the model for deletion with its relations */
prepareDestroyWithRelations: () => Promise<Model[]>;
}
export default PlaybookChecklistModel;

View file

@ -109,6 +109,9 @@ declare class PlaybookRunModel extends Model {
/** participants : The USERS that participate in this Playbook Run */
participants: () => Query<UserModel>;
/** prepareDestroyWithRelations : Prepare the model for deletion with its relations */
prepareDestroyWithRelations: () => Promise<Model[]>;
}
export default PlaybookRunModel;

View file

@ -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();

View file

@ -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<Model[]> => {
export const prepareDeleteChannel = async (serverUrl: string, channel: ChannelModel): Promise<Model[]> => {
const preparedModels: Model[] = [channel.prepareDestroyPermanently()];
const relations: Array<Relation<Model|MyChannelModel> | undefined> = [channel.membership, channel.info, channel.categoryChannel];
@ -198,6 +199,16 @@ export const prepareDeleteChannel = async (channel: ChannelModel): Promise<Model
}
}
const playbookRuns = await channel.playbookRuns?.fetch();
if (playbookRuns?.length) {
for await (const run of playbookRuns) {
const preparedRun = await run.prepareDestroyWithRelations();
preparedModels.push(...preparedRun);
}
}
EphemeralStore.unsetChannelPlaybooksSynced(serverUrl, channel.id);
return preparedModels;
};
@ -706,7 +717,7 @@ export const queryChannelsForAutocomplete = (database: Database, matchTerm: stri
Q.where('type', Q.oneOf([General.DM_CHANNEL, General.GM_CHANNEL])),
Q.on(CHANNEL_MEMBERSHIP, Q.on(USER,
Q.or(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: condition type error
Q.unsafeSqlExpr(`first_name || ' ' || last_name LIKE '${likeTerm}'`),
Q.where('nickname', Q.like(likeTerm)),

View file

@ -157,6 +157,7 @@ describe('Entry Queries', () => {
};
const promises = await prepareEntryModelsForDeletion({
serverUrl,
operator,
teamData,
});
@ -190,6 +191,7 @@ describe('Entry Queries', () => {
};
const promises = await prepareEntryModelsForDeletion({
serverUrl,
operator,
chData,
});

View file

@ -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<Array<Promise<Model[]>>> {
export async function prepareEntryModelsForDeletion({serverUrl, operator, teamData, chData}: PrepareModelsForDeletionArgs): Promise<Array<Promise<Model[]>>> {
const modelPromises: Array<Promise<Model[]>> = [];
const {database} = operator;
let teamIdsToDelete: Set<string>|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));
});
}
}

View file

@ -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([]);
});
});

View file

@ -234,7 +234,7 @@ export async function deleteMyTeams(operator: ServerDataOperator, myTeams: MyTea
}
}
export const prepareDeleteTeam = async (team: TeamModel): Promise<Model[]> => {
export const prepareDeleteTeam = async (serverUrl: string, team: TeamModel): Promise<Model[]> => {
try {
const preparedModels: Model[] = [team.prepareDestroyPermanently()];
@ -279,7 +279,7 @@ export const prepareDeleteTeam = async (team: TeamModel): Promise<Model[]> => {
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

View file

@ -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);
});
});

View file

@ -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];
};

View file

@ -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,
};
};