Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/scripts/__tests__/fixtures/scenario-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
"name": "shared view fans wide: RoomView -> nine flows",
"category": "real-domain",
"input": { "diff": ["app/views/RoomView/index.tsx"] },
"expectedShards": [3, 7, 9, 11, 12, 13, 14],
"expectedShards": [3, 5, 7, 9, 12, 13, 14],
"expectedShouldRun": true,
"assertableIn": ["map", "ci"]
},
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/maestro-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: macos-26
permissions:
contents: read
timeout-minutes: 40
timeout-minutes: 65
env:
MAESTRO_VERSION: 2.5.1

Expand Down Expand Up @@ -132,7 +132,7 @@ jobs:
run: chmod +x .github/scripts/run-maestro.sh

- name: Run Maestro Tests
timeout-minutes: 30
timeout-minutes: 45
Comment thread
diegolmello marked this conversation as resolved.
run: ./.github/scripts/run-maestro.sh ios ${{ inputs.shard }}

- name: Upload Maestro Logs
Expand Down
2 changes: 1 addition & 1 deletion .maestro/tests/room/jump-to-message.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ onFlowStart:
onFlowComplete:
- evalScript: ${output.utils.deleteCreatedUsers()}
tags:
- test-11
- test-5

---
- runFlow:
Expand Down
167 changes: 167 additions & 0 deletions app/sagas/__tests__/encryption.settingsRace.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
jest.mock('../../lib/methods/userPreferences', () => ({
__esModule: true,
default: {
getString: jest.fn()
}
}));

jest.mock('../../lib/services/restApi', () => ({
e2eFetchMyKeys: jest.fn()
}));

jest.mock('../../lib/methods/readMessages', () => ({
readMessages: jest.fn()
}));

jest.mock('../../lib/methods/helpers/log', () => ({
__esModule: true,
default: jest.fn()
}));

jest.mock('../../lib/encryption', () => ({
Encryption: {
initialize: jest.fn(),
persistKeys: jest.fn(),
createKeys: jest.fn(),
decodePrivateKey: jest.fn(),
stop: jest.fn()
}
}));

const mockServersFind = jest.fn();

jest.mock('../../lib/database', () => ({
__esModule: true,
default: {
servers: {
get: jest.fn(() => ({
find: () => mockServersFind()
}))
}
}
}));

import encryptionRoot from '../encryption';
import { encryptionInit } from '../../actions/encryption';
import { addSettings } from '../../actions/settings';
import { setUser, logout } from '../../actions/login';
import { selectServerSuccess, selectServerRequest } from '../../actions/server';
import UserPreferences from '../../lib/methods/userPreferences';
import { e2eFetchMyKeys } from '../../lib/services/restApi';
import { Encryption } from '../../lib/encryption';
import { E2E_BANNER_TYPE } from '../../lib/constants/keys';
import { cancelSagaTasks, createRecordingStore, flushSagaMicrotasks } from '../../lib/testUtils/sagaStore';
import type { RecordingStore } from '../../lib/testUtils/sagaStore';

const setupStore = (): RecordingStore => createRecordingStore(encryptionRoot);

const SERVER_URL = 'https://open.rocket.chat';
const USER = { id: 'user-1', token: 'token-1', username: 'diego' };

const selectServerAndLogin = (store: RecordingStore['store']) => {
store.dispatch(selectServerSuccess({ server: SERVER_URL, version: '7.0.0', name: 'Open' }));
store.dispatch(setUser(USER));
};

describe('encryption saga — settings arriving after ENCRYPTION.INIT', () => {
beforeEach(() => {
jest.mocked(UserPreferences.getString).mockReturnValue(null);
jest.mocked(e2eFetchMyKeys).mockResolvedValue({ privateKey: 'private-key' } as any);
mockServersFind.mockReset().mockRejectedValue(new Error('not found'));
});

afterEach(() => {
cancelSagaTasks();
});

it('sets the banner once E2E_Enable arrives after ENCRYPTION.INIT', async () => {
const { store } = setupStore();
selectServerAndLogin(store);

store.dispatch(encryptionInit());
await flushSagaMicrotasks();

expect(store.getState().encryption.banner).toBe('');

store.dispatch(addSettings({ E2E_Enable: true }));
await flushSagaMicrotasks();

expect(store.getState().encryption.banner).toBe(E2E_BANNER_TYPE.REQUEST_PASSWORD);
expect(Encryption.initialize).not.toHaveBeenCalled();
});

it('sets the banner immediately when settings are already present', async () => {
const { store } = setupStore();
selectServerAndLogin(store);
store.dispatch(addSettings({ E2E_Enable: true }));

store.dispatch(encryptionInit());
await flushSagaMicrotasks();

expect(store.getState().encryption.banner).toBe(E2E_BANNER_TYPE.REQUEST_PASSWORD);
});

it('leaves the banner unset when E2E_Enable arrives as false', async () => {
const { store } = setupStore();
selectServerAndLogin(store);

store.dispatch(encryptionInit());
await flushSagaMicrotasks();

store.dispatch(addSettings({ E2E_Enable: false }));
await flushSagaMicrotasks();

expect(store.getState().encryption.banner).toBe('');
expect(Encryption.initialize).not.toHaveBeenCalled();
});

it('sets the banner when the servers row exists with E2E_Enable undefined and settings arrive later', async () => {
mockServersFind.mockResolvedValue({ E2E_Enable: undefined });

const { store } = setupStore();
selectServerAndLogin(store);

store.dispatch(encryptionInit());
await flushSagaMicrotasks();

expect(store.getState().encryption.banner).toBe('');

store.dispatch(addSettings({ E2E_Enable: true }));
await flushSagaMicrotasks();

expect(store.getState().encryption.banner).toBe(E2E_BANNER_TYPE.REQUEST_PASSWORD);
});

it('stops waiting for settings once the user logs out', async () => {
const { store } = setupStore();
selectServerAndLogin(store);

store.dispatch(encryptionInit());
await flushSagaMicrotasks();

store.dispatch(logout(true));
await flushSagaMicrotasks();

store.dispatch(addSettings({ E2E_Enable: true }));
await flushSagaMicrotasks();

expect(store.getState().encryption.banner).toBe('');
});

it('stops waiting for settings once a server switch is requested', async () => {
const { store } = setupStore();
selectServerAndLogin(store);

store.dispatch(encryptionInit());
await flushSagaMicrotasks();

store.dispatch(selectServerRequest('https://other.rocket.chat', '7.0.0'));
await flushSagaMicrotasks();

store.dispatch(addSettings({ E2E_Enable: true }));
await flushSagaMicrotasks();

expect(store.getState().encryption.banner).toBe('');
expect(Encryption.initialize).not.toHaveBeenCalled();
});
});
28 changes: 25 additions & 3 deletions app/sagas/encryption.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import EJSON from 'ejson';
import { put, select, takeLatest } from 'redux-saga/effects';
import { put, race, select, take, takeLatest } from 'redux-saga/effects';

import { ENCRYPTION } from '../actions/actionsTypes';
import { ENCRYPTION, LOGOUT, SERVER, SETTINGS } from '../actions/actionsTypes';
import { encryptionDecodeKeyFailure, encryptionSet } from '../actions/encryption';
import { Encryption } from '../lib/encryption';
import database from '../lib/database';
Expand All @@ -15,6 +15,23 @@ import { readMessages } from '../lib/methods/readMessages';
const getServer = state => state.server.server;
const getE2eEnable = state => state.settings.E2E_Enable;

const waitForE2eEnable = function* waitForE2eEnable() {
let e2eEnable;
while (e2eEnable === undefined) {
const { logout, selectServer } = yield race({
settingsAdded: take(SETTINGS.ADD),
logout: take(LOGOUT),
selectServer: take(SERVER.SELECT_REQUEST)
});
if (logout || selectServer) {
return;
}

e2eEnable = yield select(getE2eEnable);
}
return e2eEnable;
};

const handleEncryptionInit = function* handleEncryptionInit() {
try {
const server = yield select(getServer);
Expand All @@ -31,8 +48,13 @@ const handleEncryptionInit = function* handleEncryptionInit() {
// Server not found
}

let e2eEnable = E2E_Enable;
if (serverInfo?.E2E_Enable === undefined && E2E_Enable === undefined) {
e2eEnable = yield* waitForE2eEnable();
}

// If E2E is disabled on server, skip
if (!serverInfo?.E2E_Enable && !E2E_Enable) {
if (!serverInfo?.E2E_Enable && !e2eEnable) {
return;
}

Expand Down
Loading