Files
cinny/src/app/utils/updateErrors.test.ts
T
Lotus CIandClaude Opus 5.5 674aef94fb
CI / Build & Quality Checks (pull_request) Successful in 1m54s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Failing after 13s
CI / Playwright smoke (e2e) (pull_request) Successful in 12m47s
fix(desktop): the Linux update command verifies the signed SHA256SUMS before installing (cinny-desktop #36)
The pacman / apt update command installed the downloaded package with
no integrity check. Download it with the release's SHA256SUMS.txt and
.minisig into a fresh temp dir, verify the signature with the updater
key (minisign), check the hash, then install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-19 00:37:34 -04:00

101 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
describeUpdateError,
manualDownloadUrl,
MANUAL_DOWNLOAD_URL,
parseUpdateError,
} from './updateErrors';
test('parseUpdateError reads the native phase prefix', () => {
assert.deepEqual(parseUpdateError('download: error sending request for url (x)', 'check'), {
phase: 'download',
message: 'error sending request for url (x)',
});
assert.deepEqual(parseUpdateError('install: access denied', 'download'), {
phase: 'install',
message: 'access denied',
});
});
test('parseUpdateError falls back to the caller phase for unprefixed errors', () => {
assert.deepEqual(parseUpdateError('error sending request', 'check'), {
phase: 'check',
message: 'error sending request',
});
});
test('the reported download failure reads as a busy server, not a failed check', () => {
const { phase, message } = parseUpdateError(
'download: error sending request for url (https://code.lotusguild.org/LotusGuild/cinny-desktop/releases/download/latest/LotusChat-x86_64-setup.nsis.zip)',
'check',
);
const text = describeUpdateError(phase, message);
assert.match(text, /didn’t finish downloading/);
assert.match(text, /download the installer yourself/);
});
test('only signature failures are reported as a failed safety check', () => {
assert.match(
describeUpdateError('download', 'The signature X could not be decoded'),
/safety check/,
);
assert.doesNotMatch(describeUpdateError('download', 'No space left on device'), /safety/);
});
test('check failures never suggest the app is broken', () => {
assert.match(describeUpdateError('check', 'operation timed out'), /Lotus Chat still works/);
assert.match(describeUpdateError('check', 'weird'), /Lotus Chat still works/);
});
test('manual download link: Windows gets the installer, others the release page', () => {
assert.equal(
manualDownloadUrl('Mozilla/5.0 (Windows NT 10.0; Win64; x64) Edg/140'),
MANUAL_DOWNLOAD_URL.windows,
);
assert.equal(manualDownloadUrl('Mozilla/5.0 (X11; Linux x86_64)'), MANUAL_DOWNLOAD_URL.other);
});
test('install kinds: anything unknown keeps the in-app updater', async () => {
const { toInstallKind } = await import('./updateErrors');
assert.equal(toInstallKind('pacman'), 'pacman');
assert.equal(toInstallKind('deb'), 'deb');
assert.equal(toInstallKind('manual'), 'manual');
assert.equal(toInstallKind('in-app'), 'in-app');
assert.equal(toInstallKind(undefined), 'in-app', 'older desktop build without the command');
assert.equal(toInstallKind('rpm'), 'in-app');
});
test('package installs get their package manager’s command, not an Install button', async () => {
const { packageUpdateHelp, isPackageManagedError } = await import('./updateErrors');
const pacman = packageUpdateHelp('pacman')!;
// [cinny-desktop #36] signature, then checksum, then install, in that order;
// a local `pacman -U ./file` (not `<url>`, which also wants `<url>.sig`).
const steps = pacman.command!.split(' && ');
assert.equal(steps[0], 'cd "$(mktemp -d)"');
assert.match(
steps[1],
/-fLO \S+\/LotusChat-x86_64\.pkg\.tar\.zst -fLO \S+\/SHA256SUMS\.txt -fLO \S+\/SHA256SUMS\.txt\.minisig$/,
);
assert.equal(
steps[2],
'minisign -Vm SHA256SUMS.txt -P RWRZZdyii05/NUcz337A5SAbiZK+NPVDWuiL2mM5Jk1p/LfRmNfiZ/6l',
);
assert.equal(steps[3], 'sha256sum -c --ignore-missing SHA256SUMS.txt');
assert.equal(steps[4], 'sudo pacman -U ./LotusChat-x86_64.pkg.tar.zst');
assert.match(pacman.url, /LotusChat-x86_64\.pkg\.tar\.zst$/);
const deb = packageUpdateHelp('deb')!;
assert.match(deb.command!, /minisign -Vm SHA256SUMS\.txt/);
assert.match(deb.command!, /sudo apt install \.\/LotusChat-x86_64\.deb$/);
assert.equal(packageUpdateHelp('manual')?.command, undefined);
assert.equal(packageUpdateHelp('in-app'), undefined);
assert.equal(
isPackageManagedError('package-managed (pacman): update Lotus Chat with your package manager'),
true,
);
assert.equal(
isPackageManagedError('Permission denied (os error 13) at path "/usr/bin/tauri_current_app"'),
false,
);
});