mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
feat(desktop): bundled renderer, modules and instance accounts (#3254)
This commit is contained in:
1 parent
c748c8af4e
commit
7c5fa2180a
1275 files changed
+215074
-81246
No files matched your search
@@ -0,0 +1,509 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import {readFileSync} from 'node:fs';
|
||||
import {describe, test} from 'node:test';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {installElectronStub} from './LocalAppTestSupport.test.mjs';
|
||||
|
||||
const events = [];
|
||||
const ipcRegistrations = [];
|
||||
let openExternalFailure = null;
|
||||
|
||||
class FakeWebContents {
|
||||
constructor() {
|
||||
this.sent = [];
|
||||
this.destroyed = false;
|
||||
}
|
||||
|
||||
on() {}
|
||||
|
||||
setWindowOpenHandler() {}
|
||||
|
||||
isDestroyed() {
|
||||
return this.destroyed;
|
||||
}
|
||||
|
||||
send(channel, payload) {
|
||||
this.sent.push({channel, payload});
|
||||
}
|
||||
}
|
||||
|
||||
class FakeBrowserWindow {
|
||||
constructor() {
|
||||
this.webContents = new FakeWebContents();
|
||||
this.destroyed = false;
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
on() {}
|
||||
|
||||
once() {}
|
||||
|
||||
isDestroyed() {
|
||||
return this.destroyed;
|
||||
}
|
||||
|
||||
isVisible() {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
show() {
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
showInactive() {
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
setSkipTaskbar() {}
|
||||
|
||||
setAlwaysOnTop() {}
|
||||
|
||||
close() {
|
||||
this.destroyed = true;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.destroyed = true;
|
||||
}
|
||||
|
||||
loadURL() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
installElectronStub({
|
||||
app: {
|
||||
isPackaged: true,
|
||||
isReady: () => true,
|
||||
getAppPath: () => process.cwd(),
|
||||
getPath: () => process.cwd(),
|
||||
quit: () => {
|
||||
events.push('quit');
|
||||
},
|
||||
},
|
||||
BrowserWindow: FakeBrowserWindow,
|
||||
ipcMain: {
|
||||
on: (channel, handler) => {
|
||||
ipcRegistrations.push({channel, handler});
|
||||
},
|
||||
},
|
||||
shell: {
|
||||
openExternal: async (url) => {
|
||||
if (openExternalFailure != null) {
|
||||
const failure = openExternalFailure;
|
||||
openExternalFailure = null;
|
||||
throw failure;
|
||||
}
|
||||
events.push(`open:${url}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const {openExternalDeduped, shouldOpenExternalUrl} = await import('@electron/main/OpenExternal');
|
||||
const {openSplashWindow} = await import('@electron/main/SplashWindow');
|
||||
const {buildManualLatestDownloadUrl, DESKTOP_DOWNLOAD_ARCH, getDesktopDownloadArch, getUpdateBaseUrl} = await import(
|
||||
'@electron/main/ShellDownloadFormats'
|
||||
);
|
||||
const {BUILD_CHANNEL} = await import('@electron/common/BuildChannel');
|
||||
const {decideShellUpdatePlan, resolveShellUpdatePlan, ShellUpdateCapability} = await import(
|
||||
'@electron/main/ShellUpdateCapability'
|
||||
);
|
||||
const {DOWNLOAD_PAGE_URL} = await import('@electron/main/UpdaterDownloads');
|
||||
const {armBlockedShellUpdate, getBlockedShellUpdate, SPLASH_MANUAL_UPDATE_MESSAGE} = await import(
|
||||
'@electron/main/ShellUpdateSplash'
|
||||
);
|
||||
|
||||
const UPDATER_SOURCE = readFileSync(fileURLToPath(new URL('./Updater.ts', import.meta.url)), 'utf8');
|
||||
|
||||
const SELF_UPDATE_PLAN = {capability: ShellUpdateCapability.SELF_UPDATE, updater: 'velopack'};
|
||||
const MANUAL_PLATFORM_PLAN = {capability: ShellUpdateCapability.MANUAL_DOWNLOAD, reason: 'platform'};
|
||||
const MANUAL_UNPACKAGED_PLAN = {capability: ShellUpdateCapability.MANUAL_DOWNLOAD, reason: 'unpackaged'};
|
||||
const MANAGED_PLAN = {capability: ShellUpdateCapability.MANAGED_PACKAGE};
|
||||
const APPIMAGE_TARGET = {
|
||||
installedPath: '/home/user/Applications/Fluxer.AppImage',
|
||||
directory: '/home/user/Applications',
|
||||
};
|
||||
|
||||
const EXPECTED_PICKER_OPTIONS = [
|
||||
{value: 'deb', label: 'Debian (deb)', buttonLabel: 'Download', kind: 'download'},
|
||||
{value: 'rpm', label: 'Fedora (rpm)', buttonLabel: 'Download', kind: 'download'},
|
||||
{value: 'appimage', label: 'Linux (AppImage)', buttonLabel: 'Download', kind: 'download'},
|
||||
{value: 'tar_gz', label: 'Linux (tar.gz)', buttonLabel: 'Download', kind: 'download'},
|
||||
{value: 'nope', label: "I'll figure it out", buttonLabel: 'Okay', kind: 'quit'},
|
||||
];
|
||||
|
||||
function emitIpc(channel, event, payload) {
|
||||
for (const registration of ipcRegistrations) {
|
||||
if (registration.channel === channel) {
|
||||
registration.handler(event, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function settle() {
|
||||
return new Promise((resolve) => {
|
||||
setImmediate(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function withProperty(target, name, value, run) {
|
||||
const original = Object.getOwnPropertyDescriptor(target, name);
|
||||
Object.defineProperty(target, name, {value, configurable: true, writable: true});
|
||||
try {
|
||||
return run();
|
||||
} finally {
|
||||
if (original == null) {
|
||||
delete target[name];
|
||||
} else {
|
||||
Object.defineProperty(target, name, original);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function withPlatform(platform, run) {
|
||||
return withProperty(process, 'platform', platform, run);
|
||||
}
|
||||
|
||||
function planFor(runtime) {
|
||||
return decideShellUpdatePlan({
|
||||
channel: 'canary',
|
||||
packaged: true,
|
||||
portable: false,
|
||||
flatpak: false,
|
||||
appImage: null,
|
||||
platform: 'linux',
|
||||
...runtime,
|
||||
});
|
||||
}
|
||||
|
||||
function hasAffordance(blocked) {
|
||||
return blocked.action != null || blocked.options != null;
|
||||
}
|
||||
|
||||
describe('shell update capability', () => {
|
||||
test('every runtime signal picks one updater, first match wins', () => {
|
||||
assert.deepEqual(planFor({packaged: false, platform: 'win32'}), {
|
||||
capability: 'manual-download',
|
||||
reason: 'unpackaged',
|
||||
});
|
||||
assert.deepEqual(planFor({portable: true, platform: 'win32'}), {capability: 'manual-download', reason: 'platform'});
|
||||
assert.deepEqual(planFor({flatpak: true, platform: 'linux'}), {capability: 'managed-package'});
|
||||
assert.deepEqual(planFor({platform: 'win32'}), {capability: 'self-update', updater: 'velopack'});
|
||||
assert.deepEqual(planFor({platform: 'darwin'}), {capability: 'self-update', updater: 'electron'});
|
||||
assert.deepEqual(planFor({platform: 'linux', appImage: APPIMAGE_TARGET}), {
|
||||
capability: 'self-update',
|
||||
updater: 'appimage',
|
||||
target: APPIMAGE_TARGET,
|
||||
});
|
||||
assert.deepEqual(planFor({platform: 'linux'}), {capability: 'manual-download', reason: 'platform'});
|
||||
assert.deepEqual(planFor({platform: 'freebsd'}), {capability: 'manual-download', reason: 'platform'});
|
||||
});
|
||||
|
||||
test('a development shell never self-updates, because no feed publishes development builds', () => {
|
||||
for (const platform of ['darwin', 'win32', 'linux']) {
|
||||
assert.deepEqual(planFor({channel: 'development', platform, appImage: APPIMAGE_TARGET}), {
|
||||
capability: 'manual-download',
|
||||
reason: 'unpackaged',
|
||||
});
|
||||
}
|
||||
assert.deepEqual(planFor({channel: 'stable', platform: 'darwin'}), {
|
||||
capability: 'self-update',
|
||||
updater: 'electron',
|
||||
});
|
||||
});
|
||||
|
||||
test('a store runtime outranks a portable marker, so a sandboxed install is never offered a deb or rpm it cannot apply', () => {
|
||||
assert.deepEqual(planFor({portable: true, flatpak: true, platform: 'linux'}), {capability: 'managed-package'});
|
||||
assert.deepEqual(planFor({portable: true, appImage: APPIMAGE_TARGET, platform: 'linux'}), {
|
||||
capability: 'manual-download',
|
||||
reason: 'platform',
|
||||
});
|
||||
assert.deepEqual(planFor({packaged: false, flatpak: true, platform: 'linux'}), {
|
||||
capability: 'manual-download',
|
||||
reason: 'unpackaged',
|
||||
});
|
||||
});
|
||||
|
||||
test('resolves the live runtime through the same decision', () => {
|
||||
assert.deepEqual(
|
||||
resolveShellUpdatePlan(),
|
||||
decideShellUpdatePlan({
|
||||
channel: BUILD_CHANNEL,
|
||||
packaged: true,
|
||||
portable: false,
|
||||
flatpak: false,
|
||||
appImage: null,
|
||||
platform: process.platform,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('the updater keeps no platform checks and no download page of its own', () => {
|
||||
assert.doesNotMatch(UPDATER_SOURCE, /isPortableMode|isFlatpakRuntime|isRunningFromAppImage/);
|
||||
assert.doesNotMatch(UPDATER_SOURCE, /const DOWNLOAD_PAGE_URL =/);
|
||||
assert.match(UPDATER_SOURCE, /const plan = resolveShellUpdatePlan\(\);/);
|
||||
});
|
||||
|
||||
test('the updater reads the arch and the format urls from the shared leaf instead of keeping its own', () => {
|
||||
assert.doesNotMatch(UPDATER_SOURCE, /const UPDATE_BASE_URL =|function getUpdateBaseUrl\(/);
|
||||
assert.doesNotMatch(UPDATER_SOURCE, /function buildManualLatestDownloadUrl/);
|
||||
assert.match(UPDATER_SOURCE, /from '@electron\/main\/ShellDownloadFormats'/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('blocked shell update states', () => {
|
||||
test('every plan lands on an affordance that can resolve the block', () => {
|
||||
for (const plan of [SELF_UPDATE_PLAN, MANUAL_PLATFORM_PLAN, MANUAL_UNPACKAGED_PLAN, MANAGED_PLAN]) {
|
||||
for (const platform of ['linux', 'darwin', 'win32']) {
|
||||
const blocked = withPlatform(platform, () => getBlockedShellUpdate(plan, '2026.823.1'));
|
||||
assert.ok(hasAffordance(blocked), `${plan.capability} on ${platform} has no way out`);
|
||||
assert.notEqual(
|
||||
blocked.action?.kind,
|
||||
'retry',
|
||||
`${plan.capability} cannot escape a relaunch: the same binary refetches the same manifest and blocks again`,
|
||||
);
|
||||
for (const label of [blocked.action?.label, ...(blocked.options ?? []).map((option) => option.label)]) {
|
||||
assert.ok(label == null || label.length > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('a shell that could have updated itself and did not falls back to the download page', () => {
|
||||
const blocked = withPlatform('win32', () => getBlockedShellUpdate(SELF_UPDATE_PLAN, '2026.823.1'));
|
||||
|
||||
assert.deepEqual(blocked, {
|
||||
layout: 'splash',
|
||||
status: 'blocked-shell-update',
|
||||
action: {kind: 'download', label: 'Download update'},
|
||||
message: null,
|
||||
versionLabel: null,
|
||||
options: null,
|
||||
downloadUrl: DOWNLOAD_PAGE_URL,
|
||||
downloadUrls: new Map(),
|
||||
});
|
||||
});
|
||||
|
||||
test('a packaged linux install gets the picker, with the version main already knows', () => {
|
||||
const blocked = withPlatform('linux', () => getBlockedShellUpdate(MANUAL_PLATFORM_PLAN, '2026.823.1'));
|
||||
|
||||
assert.equal(blocked.layout, 'manual-update');
|
||||
assert.equal(blocked.status, 'blocked-shell-update');
|
||||
assert.equal(blocked.action, null);
|
||||
assert.equal(blocked.message, SPLASH_MANUAL_UPDATE_MESSAGE);
|
||||
assert.equal(blocked.versionLabel, 'Version 2026.823.1 available');
|
||||
assert.deepEqual(blocked.options, EXPECTED_PICKER_OPTIONS);
|
||||
assert.equal(blocked.downloadUrl, null);
|
||||
});
|
||||
|
||||
test('an unknown latest version leaves the bottom line off rather than inventing one', () => {
|
||||
const blocked = withPlatform('linux', () => getBlockedShellUpdate(MANUAL_PLATFORM_PLAN, null));
|
||||
|
||||
assert.equal(blocked.versionLabel, null);
|
||||
assert.deepEqual(blocked.options, EXPECTED_PICKER_OPTIONS);
|
||||
});
|
||||
|
||||
test('a dev run from source is never handed a distro artifact', () => {
|
||||
const blocked = withPlatform('linux', () => getBlockedShellUpdate(MANUAL_UNPACKAGED_PLAN, '2026.823.1'));
|
||||
|
||||
assert.equal(blocked.layout, 'splash');
|
||||
assert.equal(blocked.options, null);
|
||||
assert.deepEqual(blocked.action, {kind: 'download', label: 'Download update'});
|
||||
assert.equal(blocked.downloadUrl, DOWNLOAD_PAGE_URL);
|
||||
});
|
||||
|
||||
test('a portable win32 or darwin build gets the page, never the linux picker', () => {
|
||||
for (const platform of ['win32', 'darwin']) {
|
||||
const blocked = withPlatform(platform, () => getBlockedShellUpdate(MANUAL_PLATFORM_PLAN, '2026.823.1'));
|
||||
assert.equal(blocked.layout, 'splash', platform);
|
||||
assert.equal(blocked.options, null, platform);
|
||||
assert.equal(blocked.downloadUrl, DOWNLOAD_PAGE_URL, platform);
|
||||
}
|
||||
});
|
||||
|
||||
test('a package manager owned install is offered no download url at all', () => {
|
||||
const blocked = withPlatform('linux', () => getBlockedShellUpdate(MANAGED_PLAN, '2026.823.1'));
|
||||
|
||||
assert.equal(blocked.downloadUrl, null);
|
||||
assert.equal(blocked.downloadUrls.size, 0);
|
||||
assert.equal(blocked.options, null);
|
||||
assert.equal(blocked.status, 'blocked-shell-update-managed');
|
||||
assert.deepEqual(blocked.action, {kind: 'quit', label: 'Quit'});
|
||||
});
|
||||
|
||||
test('the download page is a channel aware page, never an artifact', () => {
|
||||
assert.equal(shouldOpenExternalUrl(DOWNLOAD_PAGE_URL), true);
|
||||
assert.equal(new URL(DOWNLOAD_PAGE_URL).pathname, '/download');
|
||||
});
|
||||
|
||||
test('every picker entry resolves to the latest linux artifact route for the running arch', () => {
|
||||
const blocked = withPlatform('linux', () => getBlockedShellUpdate(MANUAL_PLATFORM_PLAN, '2026.823.1'));
|
||||
|
||||
assert.deepEqual(
|
||||
[...blocked.downloadUrls.keys()],
|
||||
EXPECTED_PICKER_OPTIONS.filter((option) => option.kind === 'download').map((option) => option.value),
|
||||
);
|
||||
for (const [format, url] of blocked.downloadUrls) {
|
||||
assert.equal(url, `${getUpdateBaseUrl('linux')}/latest/${format}`);
|
||||
assert.equal(new URL(url).pathname.endsWith(`/linux/${DESKTOP_DOWNLOAD_ARCH}/latest/${format}`), true, url);
|
||||
assert.equal(shouldOpenExternalUrl(url), true);
|
||||
}
|
||||
assert.equal(blocked.downloadUrls.has('nope'), false);
|
||||
});
|
||||
|
||||
test('the artifact route reads the platform at call time, so a darwin runner still proves the linux shape', () => {
|
||||
const linux = withPlatform('linux', () => buildManualLatestDownloadUrl('deb'));
|
||||
const win32 = withPlatform('win32', () => buildManualLatestDownloadUrl('setup'));
|
||||
|
||||
assert.equal(linux, `${getUpdateBaseUrl('linux')}/latest/deb`);
|
||||
assert.ok(linux.includes('/linux/'), linux);
|
||||
assert.ok(win32.includes('/win32/'), win32);
|
||||
});
|
||||
|
||||
test('the download arch collapses to the two we publish', () => {
|
||||
assert.equal(getDesktopDownloadArch('arm64'), 'arm64');
|
||||
assert.equal(getDesktopDownloadArch('x64'), 'x64');
|
||||
assert.equal(getDesktopDownloadArch('ia32'), 'x64');
|
||||
assert.equal(getDesktopDownloadArch('ppc64'), 'x64');
|
||||
});
|
||||
});
|
||||
|
||||
describe('arming the blocked shell update', () => {
|
||||
test('a package manager owned install arms nothing that could open a url', async () => {
|
||||
const window = openSplashWindow();
|
||||
events.length = 0;
|
||||
|
||||
armBlockedShellUpdate(MANAGED_PLAN, '2026.823.1');
|
||||
|
||||
assert.deepEqual(window.webContents.sent.at(-1), {
|
||||
channel: 'desktop-splash:state',
|
||||
payload: {
|
||||
layout: 'splash',
|
||||
status: 'blocked-shell-update-managed',
|
||||
requiredSecurityUpdate: false,
|
||||
current: null,
|
||||
total: null,
|
||||
progress: null,
|
||||
seconds: null,
|
||||
action: {kind: 'quit', label: 'Quit'},
|
||||
message: null,
|
||||
versionLabel: null,
|
||||
options: null,
|
||||
},
|
||||
});
|
||||
|
||||
emitIpc('desktop-splash:open-download', {sender: window.webContents});
|
||||
await settle();
|
||||
|
||||
assert.deepEqual(events, []);
|
||||
});
|
||||
|
||||
test('the download action opens the allowlisted page and then quits', async () => {
|
||||
const window = openSplashWindow();
|
||||
events.length = 0;
|
||||
|
||||
withPlatform('win32', () => armBlockedShellUpdate(SELF_UPDATE_PLAN, '2026.823.1'));
|
||||
|
||||
assert.deepEqual(window.webContents.sent.at(-1), {
|
||||
channel: 'desktop-splash:state',
|
||||
payload: {
|
||||
layout: 'splash',
|
||||
status: 'blocked-shell-update',
|
||||
requiredSecurityUpdate: false,
|
||||
current: null,
|
||||
total: null,
|
||||
progress: null,
|
||||
seconds: null,
|
||||
action: {kind: 'download', label: 'Download update'},
|
||||
message: null,
|
||||
versionLabel: null,
|
||||
options: null,
|
||||
},
|
||||
});
|
||||
|
||||
emitIpc('desktop-splash:open-download', {sender: window.webContents});
|
||||
await settle();
|
||||
|
||||
assert.deepEqual(events, [`open:${DOWNLOAD_PAGE_URL}`, 'quit']);
|
||||
});
|
||||
|
||||
test('the picker sends the whole manual layout and opens the artifact the user chose', async () => {
|
||||
const window = openSplashWindow();
|
||||
events.length = 0;
|
||||
|
||||
const blocked = withPlatform('linux', () => armBlockedShellUpdate(MANUAL_PLATFORM_PLAN, '2026.823.1'));
|
||||
|
||||
assert.deepEqual(window.webContents.sent.at(-1), {
|
||||
channel: 'desktop-splash:state',
|
||||
payload: {
|
||||
layout: 'manual-update',
|
||||
status: 'blocked-shell-update',
|
||||
requiredSecurityUpdate: false,
|
||||
current: null,
|
||||
total: null,
|
||||
progress: null,
|
||||
seconds: null,
|
||||
action: null,
|
||||
message: SPLASH_MANUAL_UPDATE_MESSAGE,
|
||||
versionLabel: 'Version 2026.823.1 available',
|
||||
options: EXPECTED_PICKER_OPTIONS,
|
||||
},
|
||||
});
|
||||
|
||||
emitIpc('desktop-splash:open-download', {sender: window.webContents}, 'rpm');
|
||||
await settle();
|
||||
|
||||
assert.deepEqual(events, [`open:${blocked.downloadUrls.get('rpm')}`, 'quit']);
|
||||
});
|
||||
|
||||
test('a token that was never armed is dropped instead of being turned into a url', async () => {
|
||||
const window = openSplashWindow();
|
||||
withPlatform('linux', () => armBlockedShellUpdate(MANUAL_PLATFORM_PLAN, '2026.823.1'));
|
||||
events.length = 0;
|
||||
|
||||
for (const value of ['setup', 'dmg', 'nope', 'unknown_format']) {
|
||||
emitIpc('desktop-splash:open-download', {sender: window.webContents}, value);
|
||||
}
|
||||
await settle();
|
||||
|
||||
assert.deepEqual(events, []);
|
||||
});
|
||||
|
||||
test('a download that cannot be opened still resolves the screen instead of stranding the user', async () => {
|
||||
const window = openSplashWindow();
|
||||
withPlatform('linux', () => armBlockedShellUpdate(MANUAL_PLATFORM_PLAN, '2026.823.1'));
|
||||
events.length = 0;
|
||||
openExternalFailure = new Error('no handler for https');
|
||||
|
||||
emitIpc('desktop-splash:open-download', {sender: window.webContents}, 'appimage');
|
||||
await settle();
|
||||
|
||||
assert.deepEqual(events, ['quit']);
|
||||
});
|
||||
|
||||
test('a protocol outside the allowlist never reaches the shell', async () => {
|
||||
events.length = 0;
|
||||
|
||||
for (const url of [
|
||||
'file:///etc/passwd',
|
||||
'javascript:alert(1)',
|
||||
'vbscript:msgbox(1)',
|
||||
'data:text/html,hello',
|
||||
'about:blank',
|
||||
'chrome://settings',
|
||||
'shell:Startup',
|
||||
'ms-appinstaller:?source=https://example.com',
|
||||
'a:payload',
|
||||
]) {
|
||||
await assert.rejects(openExternalDeduped(url), /External URL open request blocked/, url);
|
||||
}
|
||||
|
||||
assert.deepEqual(events, []);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user