Skip to content

Commit 80b341b

Browse files
committed
Merge branch 'feat/MM-65147-upgrade-react-native-0773' into fix/MM-66825-16kb-pagesize
2 parents b862ecf + 8f6b0f5 commit 80b341b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+4677
-267
lines changed

.github/actions/prepare-ios-build/action.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ runs:
4545
path: |
4646
ios/Pods
4747
libraries/@mattermost/intune/ios/Frameworks
48-
key: ${{ runner.os }}-pods-v3-intune-${{ inputs.intune-enabled }}-${{ steps.intune-hash.outputs.hash }}-${{ hashFiles('ios/Podfile.lock') }}
48+
key: ${{ runner.os }}-pods-v3-intune-${{ inputs.intune-enabled }}-${{ steps.intune-hash.outputs.hash }}-${{ hashFiles('ios/Podfile.lock') }}-${{ github.ref_name }}
4949
restore-keys: |
50+
${{ runner.os }}-pods-v3-intune-${{ inputs.intune-enabled }}-${{ steps.intune-hash.outputs.hash }}-${{ hashFiles('ios/Podfile.lock') }}-
5051
${{ runner.os }}-pods-v3-intune-${{ inputs.intune-enabled }}-${{ steps.intune-hash.outputs.hash }}-
5152
${{ runner.os }}-pods-v3-intune-${{ inputs.intune-enabled }}-
5253

app/actions/local/systems.test.ts

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33

44
import Database from '@nozbe/watermelondb/Database';
55

6+
import {getPosts} from '@actions/local/post';
67
import {ActionType} from '@constants';
78
import {SYSTEM_IDENTIFIERS} from '@constants/database';
9+
import {PostTypes} from '@constants/post';
810
import DatabaseManager from '@database/manager';
911
import TestHelper from '@test/test_helper';
12+
import {logError} from '@utils/log';
1013

1114
import {
1215
storeConfig,
@@ -17,6 +20,7 @@ import {
1720
setLastServerVersionCheck,
1821
setGlobalThreadsTab,
1922
dismissAnnouncement,
23+
expiredBoRPostCleanup,
2024
} from './systems';
2125

2226
import type {DataRetentionPoliciesRequest} from '@actions/remote/systems';
@@ -251,3 +255,269 @@ describe('dismissAnnouncement', () => {
251255
});
252256
});
253257

258+
describe('expiredBoRPostCleanup', () => {
259+
it('should delete expired BoR posts', async () => {
260+
const database = operator.database;
261+
jest.spyOn(database.adapter, 'unsafeExecute').mockImplementation(() => Promise.resolve());
262+
263+
const channel: Channel = TestHelper.fakeChannel({
264+
id: 'channelid1',
265+
team_id: 'teamid1',
266+
});
267+
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
268+
269+
const now = Date.now();
270+
271+
const borPostExpiredForAll = TestHelper.fakePost({
272+
id: 'postid1',
273+
channel_id: channel.id,
274+
type: PostTypes.BURN_ON_READ,
275+
props: {expire_at: now - 10000},
276+
});
277+
278+
const borPostExpiredForMe = TestHelper.fakePost({
279+
id: 'postid2',
280+
channel_id: channel.id,
281+
type: PostTypes.BURN_ON_READ,
282+
props: {expire_at: now + 100000},
283+
metadata: {expire_at: now - 10000},
284+
});
285+
286+
await operator.handlePosts({
287+
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
288+
order: [borPostExpiredForAll.id, borPostExpiredForMe.id],
289+
posts: [borPostExpiredForAll, borPostExpiredForMe],
290+
prepareRecordsOnly: false,
291+
});
292+
293+
// verify channel posts
294+
const fetchedPosts = await getPosts(serverUrl, [borPostExpiredForAll.id, borPostExpiredForMe.id]);
295+
expect(fetchedPosts.length).toBe(2);
296+
297+
await expiredBoRPostCleanup(serverUrl);
298+
299+
expect(database.adapter.unsafeExecute).toHaveBeenCalledWith({
300+
sqls: [
301+
[`DELETE FROM Post where id IN ('${borPostExpiredForMe.id}','${borPostExpiredForAll.id}')`, []],
302+
[`DELETE FROM Reaction where post_id IN ('${borPostExpiredForMe.id}','${borPostExpiredForAll.id}')`, []],
303+
[`DELETE FROM File where post_id IN ('${borPostExpiredForMe.id}','${borPostExpiredForAll.id}')`, []],
304+
[`DELETE FROM Draft where root_id IN ('${borPostExpiredForMe.id}','${borPostExpiredForAll.id}')`, []],
305+
[`DELETE FROM PostsInThread where root_id IN ('${borPostExpiredForMe.id}','${borPostExpiredForAll.id}')`, []],
306+
[`DELETE FROM Thread where id IN ('${borPostExpiredForMe.id}','${borPostExpiredForAll.id}')`, []],
307+
[`DELETE FROM ThreadParticipant where thread_id IN ('${borPostExpiredForMe.id}','${borPostExpiredForAll.id}')`, []],
308+
[`DELETE FROM ThreadsInTeam where thread_id IN ('${borPostExpiredForMe.id}','${borPostExpiredForAll.id}')`, []],
309+
],
310+
});
311+
});
312+
313+
it('should not run cleanup when called again within 15 minutes', async () => {
314+
const database = operator.database;
315+
const unsafeExecuteSpy = jest.spyOn(database.adapter, 'unsafeExecute').mockImplementation(() => Promise.resolve());
316+
317+
// Set up a recent last run time (within 15 minutes)
318+
const recentRunTime = Date.now() - (10 * 60 * 1000); // 10 minutes ago
319+
await operator.handleSystem({
320+
systems: [{
321+
id: SYSTEM_IDENTIFIERS.LAST_BOR_POST_CLEANUP_RUN,
322+
value: recentRunTime,
323+
}],
324+
prepareRecordsOnly: false,
325+
});
326+
327+
const channel: Channel = TestHelper.fakeChannel({
328+
id: 'channelid1',
329+
team_id: 'teamid1',
330+
});
331+
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
332+
333+
const now = Date.now();
334+
const borPostExpired = TestHelper.fakePost({
335+
id: 'postid1',
336+
channel_id: channel.id,
337+
type: PostTypes.BURN_ON_READ,
338+
props: {expire_at: now - 10000},
339+
});
340+
341+
await operator.handlePosts({
342+
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
343+
order: [borPostExpired.id],
344+
posts: [borPostExpired],
345+
prepareRecordsOnly: false,
346+
});
347+
348+
// Call cleanup - should not run because last run was within 15 minutes
349+
await expiredBoRPostCleanup(serverUrl);
350+
351+
// Verify that unsafeExecute was not called (no cleanup performed)
352+
expect(unsafeExecuteSpy).not.toHaveBeenCalled();
353+
});
354+
355+
it('should handle no server database gracefully', async () => {
356+
// Try to run cleanup on a non-existent server
357+
await expect(expiredBoRPostCleanup('nonexistent.server.com')).resolves.not.toThrow();
358+
});
359+
360+
it('should handle no BoR posts gracefully', async () => {
361+
const database = operator.database;
362+
const unsafeExecuteSpy = jest.spyOn(database.adapter, 'unsafeExecute').mockImplementation(() => Promise.resolve());
363+
364+
const channel: Channel = TestHelper.fakeChannel({
365+
id: 'channelid1',
366+
team_id: 'teamid1',
367+
});
368+
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
369+
await expiredBoRPostCleanup(serverUrl);
370+
371+
// Verify that unsafeExecute was not called (no BoR posts to clean)
372+
expect(unsafeExecuteSpy).not.toHaveBeenCalled();
373+
});
374+
375+
it('should handle BoR posts that are not expired', async () => {
376+
const database = operator.database;
377+
const unsafeExecuteSpy = jest.spyOn(database.adapter, 'unsafeExecute').mockImplementation(() => Promise.resolve());
378+
379+
const channel: Channel = TestHelper.fakeChannel({
380+
id: 'channelid1',
381+
team_id: 'teamid1',
382+
});
383+
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
384+
385+
const now = Date.now();
386+
387+
// Create BoR posts that are not expired
388+
const borPostNotExpired = TestHelper.fakePost({
389+
id: 'postid1',
390+
channel_id: channel.id,
391+
type: PostTypes.BURN_ON_READ,
392+
props: {expire_at: now + 100000}, // Future expiry
393+
});
394+
395+
const borPostNotExpiredForMe = TestHelper.fakePost({
396+
id: 'postid2',
397+
channel_id: channel.id,
398+
type: PostTypes.BURN_ON_READ,
399+
props: {expire_at: now + 10000},
400+
metadata: {expire_at: now + 100000},
401+
});
402+
403+
await operator.handlePosts({
404+
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
405+
order: [borPostNotExpired.id, borPostNotExpiredForMe.id],
406+
posts: [borPostNotExpired, borPostNotExpiredForMe],
407+
prepareRecordsOnly: false,
408+
});
409+
410+
await expiredBoRPostCleanup(serverUrl);
411+
412+
// Verify that unsafeExecute was not called (no expired BoR posts)
413+
expect(unsafeExecuteSpy).not.toHaveBeenCalled();
414+
});
415+
416+
it('should handle mixed expired and non-expired BoR posts', async () => {
417+
const database = operator.database;
418+
jest.spyOn(database.adapter, 'unsafeExecute').mockImplementation(() => Promise.resolve());
419+
420+
const channel: Channel = TestHelper.fakeChannel({
421+
id: 'channelid1',
422+
team_id: 'teamid1',
423+
});
424+
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
425+
426+
const now = Date.now();
427+
428+
const borPostExpired = TestHelper.fakePost({
429+
id: 'postid1',
430+
channel_id: channel.id,
431+
type: PostTypes.BURN_ON_READ,
432+
props: {expire_at: now - 10000}, // Expired
433+
});
434+
435+
const borPostNotExpired = TestHelper.fakePost({
436+
id: 'postid2',
437+
channel_id: channel.id,
438+
type: PostTypes.BURN_ON_READ,
439+
props: {expire_at: now + 100000}, // Not expired
440+
});
441+
442+
await operator.handlePosts({
443+
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
444+
order: [borPostExpired.id, borPostNotExpired.id],
445+
posts: [borPostExpired, borPostNotExpired],
446+
prepareRecordsOnly: false,
447+
});
448+
449+
await expiredBoRPostCleanup(serverUrl);
450+
451+
// Should only delete the expired post
452+
expect(database.adapter.unsafeExecute).toHaveBeenCalledWith({
453+
sqls: [
454+
[`DELETE FROM Post where id IN ('${borPostExpired.id}')`, []],
455+
[`DELETE FROM Reaction where post_id IN ('${borPostExpired.id}')`, []],
456+
[`DELETE FROM File where post_id IN ('${borPostExpired.id}')`, []],
457+
[`DELETE FROM Draft where root_id IN ('${borPostExpired.id}')`, []],
458+
[`DELETE FROM PostsInThread where root_id IN ('${borPostExpired.id}')`, []],
459+
[`DELETE FROM Thread where id IN ('${borPostExpired.id}')`, []],
460+
[`DELETE FROM ThreadParticipant where thread_id IN ('${borPostExpired.id}')`, []],
461+
[`DELETE FROM ThreadsInTeam where thread_id IN ('${borPostExpired.id}')`, []],
462+
],
463+
});
464+
});
465+
466+
it('should handle database errors gracefully', async () => {
467+
const database = operator.database;
468+
469+
// Mock database query to throw an error
470+
jest.spyOn(database, 'get').mockImplementation(() => {
471+
throw new Error('Database error');
472+
});
473+
474+
// Should not throw an error, just log it
475+
await expect(expiredBoRPostCleanup(serverUrl)).resolves.not.toThrow();
476+
expect(logError).toHaveBeenCalledWith('An error occurred while performing BoR post cleanup', expect.any(Error));
477+
});
478+
479+
it('should handle updateLastBoRCleanupRun error gracefully', async () => {
480+
const database = operator.database;
481+
jest.spyOn(database.adapter, 'unsafeExecute').mockImplementation(() => Promise.resolve());
482+
483+
// Mock handleSystem to throw an error when updating last cleanup run
484+
const handleSystemSpy = jest.spyOn(operator, 'handleSystem').mockImplementation(() => {
485+
throw new Error('System update error');
486+
});
487+
488+
const channel: Channel = TestHelper.fakeChannel({
489+
id: 'channelid1',
490+
team_id: 'teamid1',
491+
});
492+
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
493+
494+
// Restore handleSystem for channel creation, then mock it again for the cleanup run update
495+
handleSystemSpy.mockRestore();
496+
jest.spyOn(operator, 'handleSystem').mockImplementation((args) => {
497+
if (args.systems?.[0]?.id === SYSTEM_IDENTIFIERS.LAST_BOR_POST_CLEANUP_RUN) {
498+
throw new Error('System update error');
499+
}
500+
return Promise.resolve([]);
501+
});
502+
503+
const now = Date.now();
504+
const borPostExpired = TestHelper.fakePost({
505+
id: 'postid1',
506+
channel_id: channel.id,
507+
type: PostTypes.BURN_ON_READ,
508+
props: {expire_at: now - 10000},
509+
});
510+
511+
await operator.handlePosts({
512+
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
513+
order: [borPostExpired.id],
514+
posts: [borPostExpired],
515+
prepareRecordsOnly: false,
516+
});
517+
518+
// Should not throw an error, just log it
519+
await expect(expiredBoRPostCleanup(serverUrl)).resolves.not.toThrow();
520+
expect(logError).toHaveBeenCalledWith('Failed updateLastBoRCleanupRun', expect.any(Error));
521+
});
522+
523+
});

app/actions/local/systems.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,22 @@ import {DeviceEventEmitter} from 'react-native';
77

88
import {Events} from '@constants';
99
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
10+
import {PostTypes, BOR_POST_CLEANUP_MIN_RUN_INTERVAL} from '@constants/post';
1011
import DatabaseManager from '@database/manager';
1112
import {getServerCredentials} from '@init/credentials';
1213
import {queryAllChannelsForTeam} from '@queries/servers/channel';
13-
import {getConfig, getLicense, getGlobalDataRetentionPolicy, getGranularDataRetentionPolicies, getLastGlobalDataRetentionRun, getIsDataRetentionEnabled} from '@queries/servers/system';
14+
import {queryPostsByType} from '@queries/servers/post';
15+
import {
16+
getConfig,
17+
getLicense,
18+
getGlobalDataRetentionPolicy,
19+
getGranularDataRetentionPolicies,
20+
getLastGlobalDataRetentionRun,
21+
getIsDataRetentionEnabled,
22+
getLastBoRPostCleanupRun,
23+
} from '@queries/servers/system';
1424
import PostModel from '@typings/database/models/servers/post';
25+
import {isExpiredBoRPost} from '@utils/bor';
1526
import {logError} from '@utils/log';
1627

1728
import {deletePosts} from './post';
@@ -239,7 +250,7 @@ async function dataRetentionWithoutPolicyCleanup(serverUrl: string) {
239250
}
240251
}
241252

242-
async function dataRetentionCleanPosts(serverUrl: string, postIds: string[]) {
253+
export async function dataRetentionCleanPosts(serverUrl: string, postIds: string[]) {
243254
if (postIds.length) {
244255
const batchSize = 1000;
245256
const deletePromises = [];
@@ -314,3 +325,55 @@ export async function dismissAnnouncement(serverUrl: string, announcementText: s
314325
return {error};
315326
}
316327
}
328+
329+
export async function expiredBoRPostCleanup(serverUrl: string) {
330+
try {
331+
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
332+
const lastRunAt = await getLastBoRPostCleanupRun(database);
333+
334+
const shouldRunNow = (Date.now() - lastRunAt) > BOR_POST_CLEANUP_MIN_RUN_INTERVAL;
335+
336+
if (!shouldRunNow) {
337+
return;
338+
}
339+
340+
const {error} = await removeExpiredBoRPosts(serverUrl);
341+
if (!error) {
342+
await updateLastBoRCleanupRun(serverUrl);
343+
}
344+
} catch (error) {
345+
logError('An error occurred running the Burn on Read cleanup task', error);
346+
}
347+
}
348+
349+
async function removeExpiredBoRPosts(serverUrl: string) {
350+
try {
351+
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
352+
const allBoRPosts = await queryPostsByType(database, PostTypes.BURN_ON_READ).fetch();
353+
const expiredBoRPostIDs = allBoRPosts.
354+
filter((post) => isExpiredBoRPost(post)).
355+
map((post) => post.id);
356+
357+
await dataRetentionCleanPosts(serverUrl, expiredBoRPostIDs);
358+
return {error: undefined};
359+
} catch (error) {
360+
logError('An error occurred while performing BoR post cleanup', error);
361+
return {error};
362+
}
363+
}
364+
365+
async function updateLastBoRCleanupRun(serverUrl: string) {
366+
try {
367+
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
368+
369+
const systems: IdValue[] = [{
370+
id: SYSTEM_IDENTIFIERS.LAST_BOR_POST_CLEANUP_RUN,
371+
value: Date.now(),
372+
}];
373+
374+
return operator.handleSystem({systems, prepareRecordsOnly: false});
375+
} catch (error) {
376+
logError('Failed updateLastBoRCleanupRun', error);
377+
return {error};
378+
}
379+
}

0 commit comments

Comments
 (0)