Skip to content
Closed
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
File renamed without changes.
File renamed without changes.
File renamed without changes.
65 changes: 62 additions & 3 deletions scripts/README.md β†’ docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Testing Scripts
# Development & Testing Scripts

This directory contains automation scripts for testing Rocket.Chat Desktop on Linux.
This directory contains automation scripts for testing Rocket.Chat Desktop on Linux and Windows.

## Scripts
## Linux Testing Scripts

### `install-volta.sh`

Expand Down Expand Up @@ -137,3 +137,62 @@ The script will exit with a non-zero status code if any step fails. Common issue
- The app runs in the background; use `kill <PID>` to stop it
- This script is intended for development/testing purposes only

## Windows Testing Scripts

### MSI Test Suite β€” DISABLE_AUTO_UPDATES (PROD-595)

Validates that passing `DISABLE_AUTO_UPDATES=1` to the Rocket.Chat MSI installer correctly
writes `C:\Program Files\Rocket.Chat\resources\update.json` with `{"canUpdate":false,"autoUpdate":false}`.

#### Prerequisites

- macOS host with `sshpass`, `ssh`, `scp` in PATH
- Windows 10 VM with OpenSSH on port 22. Configure `VM_HOST`, `VM_PORT`, `VM_USER`, and `VM_PASS` in your shell before running these scripts. Example:
```bash
export VM_HOST=192.168.13.87 VM_USER=jean VM_PASS='<your password>'
bash scripts/msi-test/run-msi-tests.sh
```
Never commit `VM_PASS` to the repo.
- Built MSI artifact β€” e.g. `dist/rocketchat-4.14.0-win-x64.msi`
- VM must be running before you invoke the orchestrator
- If the VM is reprovisioned behind the same IP, delete `scripts/msi-test/.known_hosts` before running again.

#### Files

| File | Runs on | Purpose |
|------|---------|---------|
| `run-msi-tests.sh` | macOS | Orchestrator: copy files, run PS1, report |
| `test-on-windows.ps1` | Windows VM | Three test scenarios |
| `fetch-logs.sh` | macOS | Pull results + install logs after a run |

#### Scenarios

| Scenario | What it tests |
|----------|---------------|
| A β€” baseline | Plain install leaves `update.json` absent |
| B β€” DISABLE_AUTO_UPDATES=1 | Property written as current user context |
| C β€” SYSTEM context (SCCM sim) | Property written via PsExec -s (SYSTEM account) |

#### Running

```bash
# Full run (copies an existing MSI, executes tests, prints summary)
MSI_PATH=dist/rocketchat-4.14.0-win-x64.msi ./scripts/msi-test/run-msi-tests.sh

# Or pass as argument
./scripts/msi-test/run-msi-tests.sh dist/rocketchat-4.14.0-win-x64.msi

# Fetch logs from a previous run without re-running tests
./scripts/msi-test/fetch-logs.sh
```

#### VM not reachable?

The orchestrator will print a message with the Proxmox web UI URL if port 22 is closed.
Start the VM there, wait ~30 s, then retry.

#### Results

- Live output printed to stdout during run
- `scripts/msi-test/logs/<timestamp>/test-results.json` β€” structured pass/fail per scenario
- `scripts/msi-test/logs/<timestamp>/install-*.log` β€” verbose MSI logs from each scenario
File renamed without changes.
31 changes: 31 additions & 0 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,37 @@ export default [
},
],
},
{
external: makeExternal(['@bugsnag/js']),
input: 'src/logViewerWindow/preload.ts',
preserveEntrySignatures: 'strict',
plugins: [
json(),
replace({
'process.env.NODE_ENV': JSON.stringify(NODE_ENV),
'preventAssignment': true,
}),
babel({
babelHelpers: 'bundled',
extensions,
}),
nodeResolve({
browser: true,
extensions,
}),
commonjs(),
run(),
],
output: [
{
dir: 'app/logViewerWindow',
entryFileNames: 'preload.js',
format: 'cjs',
sourcemap: 'inline',
interop: 'auto',
},
],
},
{
external: makeExternal([
'@bugsnag/js',
Expand Down
57 changes: 0 additions & 57 deletions scripts/msi-test/README.md

This file was deleted.

8 changes: 5 additions & 3 deletions src/injected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ const start = async () => {
console.log(
`[Rocket.Chat Desktop] ${moduleName} module loaded successfully`
);
setupReactiveFeatures();
} catch (error) {
console.log(
`[Rocket.Chat Desktop] Failed to load ${moduleName} module:`,
Expand Down Expand Up @@ -332,11 +333,13 @@ const start = async () => {
'[Rocket.Chat Desktop] Presence module resolved without the expected store/listen API; presence will be reported as unsupported'
);
presenceModuleResolved = false;
setupReactiveFeatures();
return;
}
Presence = (module as { Presence: unknown }).Presence;
presenceModuleResolved = true;
console.log('[Rocket.Chat Desktop] Presence module loaded successfully');
setupReactiveFeatures();
})
.catch(() => {
console.warn(
Expand All @@ -345,6 +348,7 @@ const start = async () => {
)}); presence will be reported as unsupported`
);
presenceModuleResolved = false;
setupReactiveFeatures();
});

// Initialize non-module dependent features immediately
Expand Down Expand Up @@ -1050,10 +1054,8 @@ const start = async () => {
}
};

// Call setupReactiveFeatures immediately and then periodically check for new modules
// Call setupReactiveFeatures immediately and then dynamically detect app loads
setupReactiveFeatures();
setInterval(setupReactiveFeatures, 1000); // Check every second for newly loaded modules

console.log('[Rocket.Chat Desktop] Injected');
};

Expand Down
109 changes: 109 additions & 0 deletions src/ipc/sanitize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { ipcRenderer } from 'electron';

const sanitize = (args: any[]) => structuredClone(args);

// Define allowed channels for the webview preload environment.
// This prevents the untrusted server view from exploiting the contextBridge
// to send unauthorized IPC messages to the main process or other windows.
const ALLOWED_CHANNELS = new Set([
'redux/get-initial-state',
'redux/action-dispatched',
'servers/fetch-info',
'notifications/fetch-icon',
'power-monitor/get-system-idle-state',
'certificatesManager/remove',
'server-view/get-url',
'ui-preview/apply',
'ui-preview/restore',
'ui-preview/add',
'ui-preview/refresh',
'server-view/ready',
'server-view/open-url-on-browser',
'server-view/take-pending-conference',
'video-call-window/open-window',
'video-call-window/open-url',
'video-call-window/open-in-main-window',
'video-call-window/open-screen-picker',
'video-call-window/screen-sharing-source-responded',
'video-call-window/screen-recording-is-permission-granted',
'video-call-window/close-requested',
'video-call-window/open-webview-dev-tools',
'video-call-window/handshake',
'video-call-window/renderer-ready',
'video-call-window/request-url',
'video-call-window/url-received',
'video-call-window/webview-created',
'video-call-window/webview-loading',
'video-call-window/webview-ready',
'video-call-window/webview-failed',
'video-call-window/get-language',
'video-call-window/prewarm-capturer-cache',
'video-call-window/media-capture-changed',
'jitsi-desktop-capturer-get-sources',
'desktop-capturer-get-sources',
'outlook-calendar/get-events',
'outlook-calendar/set-exchange-url',
'outlook-calendar/has-credentials',
'outlook-calendar/clear-credentials',
'outlook-calendar/set-user-token',
'browser/open-url',
'document-viewer/open-window',
'document-viewer/fetch-content',
'secondary-window/minimize',
'secondary-window/toggle-maximize',
'secondary-window/close',
'secondary-window/is-maximized',
'screen-picker/open',
'screen-picker/source-responded',
'screen-picker/screen-recording-is-permission-granted',
'screen-picker/open-url',
'telephony/get-diagnostics',
'presence/change-requested',
'navigate-to-route',
'log-viewer-window/get-server-tag'
]);

const originalSend = ipcRenderer.send.bind(ipcRenderer);
const originalInvoke = ipcRenderer.invoke.bind(ipcRenderer);
const originalOn = ipcRenderer.on.bind(ipcRenderer);
const originalSendSync = ipcRenderer.sendSync.bind(ipcRenderer);

const isAllowedChannel = (channel: string) => {
if (ALLOWED_CHANNELS.has(channel)) {
return true;
}
const match = channel.match(/^([^@]+)@[\w-]+$/);
return !!match && ALLOWED_CHANNELS.has(match[1]);
};

ipcRenderer.send = (channel, ...args) => {
if (!isAllowedChannel(channel)) {
console.warn(`Blocked unauthorized IPC send channel: ${channel}`);
return;
}
originalSend(channel, ...sanitize(args));
};

ipcRenderer.invoke = (channel, ...args) => {
if (!ALLOWED_CHANNELS.has(channel)) {
console.warn(`Blocked unauthorized IPC invoke channel: ${channel}`);
return Promise.reject(new Error('Unauthorized IPC'));
}
return originalInvoke(channel, ...sanitize(args));
};

ipcRenderer.sendSync = (channel, ...args) => {
if (!ALLOWED_CHANNELS.has(channel)) {
console.warn(`Blocked unauthorized IPC sendSync channel: ${channel}`);
return;
}
return originalSendSync(channel, ...sanitize(args));
};

ipcRenderer.on = (channel, listener) => {
if (!isAllowedChannel(channel)) {
console.warn(`Blocked unauthorized IPC on channel: ${channel}`);
return ipcRenderer;
}
return originalOn(channel, listener);
};
16 changes: 9 additions & 7 deletions src/logViewerWindow/__tests__/logViewerWindow.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ const invoke = jest.fn();
const on = jest.fn();
const removeListener = jest.fn();

jest.mock('electron', () => ({
ipcRenderer: {
invoke: (...args: any[]) => invoke(...args),
on: (...args: any[]) => on(...args),
removeListener: (...args: any[]) => removeListener(...args),
},
}));
beforeAll(() => {
(window as any).RocketChatDesktop = {
logViewer: {
invoke: (...args: any[]) => invoke(...args),
on: (...args: any[]) => on(...args),
removeListener: (...args: any[]) => removeListener(...args),
},
};
});

jest.mock('react-i18next', () => ({
useTranslation: () => ({
Expand Down
6 changes: 4 additions & 2 deletions src/logViewerWindow/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,10 @@ const buildLogViewerWindow = async (focusOnShow: boolean): Promise<void> => {
}
: {}),
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
preload: path.join(app.getAppPath(), 'app/logViewerWindow/preload.js'),
},
show: false,
});
Expand Down
Loading