mattermost-mobile-upstream/app/actions/remote/entry/notification.test.ts
Carlos Garcia a5ff8a0d95
Mm 68767 zero persistence (#9803)
* implements auto cache cleanup when zero persistence mode is enabled (0 value)

* uses memory flag with sqlite instead of loki when running in zero persistence mode

* renames ephemeral mode manager and improves readability

* zero persistence refactors and restore values during transitions

* fixes ephemeral mode tests

* use initializeClient instead of just initialize to wait for doReconnect to finish

that way teams loading is awaited while WS reconnects during ZPM transitions

* adds inital sync as a mount hook to prevent undesired load error states

during app cold start in ZPM mode, we need a way to load team and channels
without showing loading error during the transition. The nre is initial sync
hook allows for it

* reset started sync subject on wipe

* removes extra log lines

* resets tracked server during ephemeral mode transitions

* improvements to apply persistence mode changes when something fails

* address coderabbit PR suggestions

* add batching time logging

* throws ephemeral mode change error as is

* make deleteServerDatabaseFiles private

* inline derive persistence flag function

* return current user id when refetching current user

* support zero persistence mode server for push notifications

* adds support for zero persistence mode when sharing to mattermost

* adds clarifying comment

* catch and log error deleting files

* reorder translations to correct position

* adds some new tests to improve coverage

* ios push notification native fix

* logout when fetchMe fails with 401 status code during reconnection

* properly surface errors

* return proper false value when checking for zero persistence flag

* change comments and properly await on some operations

* prevent not set persistence mode to report as an error

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* include servers with no persistence flag set in share list

* disable button for accessibility

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* improves when to store and remove signing key from global area

* review push notification reception running in zero persistence mode

* add server to ephemeral mode manager right after login

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-16 13:55:45 +02:00

217 lines
8.7 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ActionType} from '@constants';
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import PerformanceMetricsManager from '@managers/performance_metrics_manager';
import {prepareThreadsFromReceivedPosts} from '@queries/servers/thread';
import {NavigationStore} from '@store/navigation_store';
import {mockApiClient} from '@test/mock_api_client';
import TestHelper from '@test/test_helper';
import {pushNotificationEntry} from './notification';
import type ServerDataOperator from '@database/operator/server_data_operator';
jest.mock('@managers/performance_metrics_manager');
jest.mock('@store/navigation_store');
const mockDismissKeyboard = jest.fn();
jest.mock('@utils/keyboard', () => ({
dismissKeyboard: (...args: unknown[]) => mockDismissKeyboard(...args),
}));
let mockEmitNotificationError: jest.Mock;
jest.mock('@utils/notification', () => {
const original = jest.requireActual('@utils/notification');
mockEmitNotificationError = jest.fn();
return {
...original,
emitNotificationError: mockEmitNotificationError,
};
});
const mockedNavigationStore = jest.mocked(NavigationStore);
describe('Performance metrics are set correctly', () => {
const serverUrl = 'http://www.someserverurl.com';
let operator: ServerDataOperator;
let post: Post;
beforeAll(() => {
mockApiClient.get.mockImplementation((url: string) => {
if (url.match(/\/api\/v4\/channels\/[a-z1-90-]*\/posts/)) {
return {status: 200, ok: true, data: {order: [], posts: {}}};
}
if (url.match(/\/api\/v4\/channels\/[a-z1-90-]*\/stats/)) {
return {status: 200, ok: true, data: {}};
}
if (url.match(/\/api\/v4\/posts\/[a-z1-90-]*\/thread/)) {
return {status: 200, ok: true, data: {order: [], posts: {}}};
}
console.log(`GET ${url} not registered in the mock`);
return {status: 404, ok: false};
});
mockApiClient.post.mockImplementation((url: string) => {
if (url.match(/\/api\/v4\/channels\/members\/me\/view/)) {
return {status: 200, ok: true, data: {}};
}
console.log(`POST ${url} not registered in the mock`);
return {status: 404, ok: false};
});
mockedNavigationStore.waitUntilScreenIsTop.mockImplementation(() => Promise.resolve());
// There are no problems when running the tests for this file alone without this line
// but for some reason, when running several tests together, it fails if we don't add this.
mockedNavigationStore.getScreensInStack.mockImplementation(() => []);
});
afterAll(() => {
mockApiClient.get.mockReset();
mockApiClient.post.mockReset();
});
beforeEach(async () => {
const client = await NetworkManager.createClient(serverUrl);
expect(client).toBeTruthy();
operator = (await TestHelper.setupServerDatabase(serverUrl)).operator;
await DatabaseManager.setActiveServerDatabase(serverUrl);
post = TestHelper.fakePost({channel_id: TestHelper.basicChannel!.id});
await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_NEW,
order: [post.id],
posts: [post],
prepareRecordsOnly: false,
});
const threadModels = await prepareThreadsFromReceivedPosts(operator, [post], true);
await operator.batchRecords(threadModels, 'test');
});
afterEach(async () => {
await TestHelper.tearDown();
NetworkManager.invalidateClient(serverUrl);
mockEmitNotificationError.mockClear();
});
it('channel notification', async () => {
await operator.handleConfigs({
configs: [
{id: 'CollapsedThreads', value: 'default_on'},
{id: 'FeatureFlagCollapsedThreads', value: 'true'},
],
configsToDelete: [],
prepareRecordsOnly: false,
});
await pushNotificationEntry(serverUrl, {
channel_id: TestHelper.basicChannel!.id,
team_id: TestHelper.basicTeam!.id,
isCRTEnabled: false, // isCRTEnabled is not checked at this level
post_id: '', // Post ID is not checked at this level
type: '', // Type is not checked at this level
version: '', // Version is not checked at this level
});
expect(mockDismissKeyboard).toHaveBeenCalled();
expect(PerformanceMetricsManager.setLoadTarget).toHaveBeenCalledWith('CHANNEL');
});
it('thread notification', async () => {
const commentPost = TestHelper.fakePost({channel_id: TestHelper.basicChannel!.id, root_id: post.id});
await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_NEW,
order: [commentPost.id],
posts: [commentPost],
prepareRecordsOnly: false,
});
const threadModels = await prepareThreadsFromReceivedPosts(operator, [commentPost], true);
await operator.batchRecords(threadModels, 'test');
await operator.handleConfigs({
configs: [
{id: 'CollapsedThreads', value: 'default_on'},
{id: 'FeatureFlagCollapsedThreads', value: 'true'},
],
configsToDelete: [],
prepareRecordsOnly: false,
});
await pushNotificationEntry(serverUrl, {
root_id: post.id,
channel_id: TestHelper.basicChannel!.id,
team_id: TestHelper.basicTeam!.id,
isCRTEnabled: false, // isCRTEnabled is not checked at this level
post_id: '', // Post ID is not checked at this level
type: '', // Type is not checked at this level
version: '', // Version is not checked at this level
});
expect(PerformanceMetricsManager.setLoadTarget).toHaveBeenCalledWith('THREAD');
});
it('thread notification with wrong root id', async () => {
const commentPost = TestHelper.fakePost({channel_id: TestHelper.basicChannel!.id, root_id: post.id});
await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_NEW,
order: [commentPost.id],
posts: [commentPost],
prepareRecordsOnly: false,
});
const threadModels = await prepareThreadsFromReceivedPosts(operator, [commentPost], true);
await operator.batchRecords(threadModels, 'test');
await operator.handleConfigs({
configs: [
{id: 'CollapsedThreads', value: 'default_on'},
{id: 'FeatureFlagCollapsedThreads', value: 'true'},
],
configsToDelete: [],
prepareRecordsOnly: false,
});
await pushNotificationEntry(serverUrl, {
root_id: commentPost.id,
channel_id: TestHelper.basicChannel!.id,
team_id: TestHelper.basicTeam!.id,
isCRTEnabled: false, // isCRTEnabled is not checked at this level
post_id: '', // Post ID is not checked at this level
type: '', // Type is not checked at this level
version: '', // Version is not checked at this level
});
expect(PerformanceMetricsManager.setLoadTarget).toHaveBeenCalledWith('THREAD');
});
it('thread notification with non crt', async () => {
const commentPost = TestHelper.fakePost({channel_id: TestHelper.basicChannel!.id, root_id: post.id});
await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_NEW,
order: [commentPost.id],
posts: [commentPost],
prepareRecordsOnly: false,
});
const threadModels = await prepareThreadsFromReceivedPosts(operator, [commentPost], true);
await operator.batchRecords(threadModels, 'test');
await operator.handleConfigs({
configs: [
{id: 'CollapsedThreads', value: 'disabled'},
{id: 'FeatureFlagCollapsedThreads', value: 'false'},
],
configsToDelete: [],
prepareRecordsOnly: false,
});
await pushNotificationEntry(serverUrl, {
root_id: post.id,
channel_id: TestHelper.basicChannel!.id,
team_id: TestHelper.basicTeam!.id,
isCRTEnabled: false, // isCRTEnabled is not checked at this level
post_id: '', // Post ID is not checked at this level
type: '', // Type is not checked at this level
version: '', // Version is not checked at this level
});
expect(PerformanceMetricsManager.setLoadTarget).toHaveBeenCalledWith('CHANNEL');
});
});