mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-10 03:13:32 -07:00
feat: add auto update support (#65)
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { buildProtectedAppImageUpdateCommand, updateAppImageFromRelease } from './appimage-updater';
|
||||
|
||||
const appImageBytes = Buffer.from('appimage');
|
||||
const appImageHash = createHash('sha256').update(appImageBytes).digest('hex');
|
||||
|
||||
test('updateAppImageFromRelease verifies hash and atomically replaces writable AppImage', async () => {
|
||||
const writes: Array<{ path: string; data: Buffer }> = [];
|
||||
const chmods: Array<{ path: string; mode: number }> = [];
|
||||
const renames: Array<{ from: string; to: string }> = [];
|
||||
|
||||
const result = await updateAppImageFromRelease({
|
||||
release: {
|
||||
tag_name: 'v0.15.0',
|
||||
prerelease: false,
|
||||
draft: false,
|
||||
assets: [{ name: 'SubMiner.AppImage', browser_download_url: 'https://example.test/app' }],
|
||||
},
|
||||
sha256Sums: new Map([['SubMiner.AppImage', appImageHash]]),
|
||||
appImagePath: '/home/kyle/.local/bin/SubMiner.AppImage',
|
||||
downloadAsset: async () => appImageBytes,
|
||||
fs: {
|
||||
stat: async () => ({
|
||||
isFile: () => true,
|
||||
mode: 0o755,
|
||||
}),
|
||||
access: async () => {},
|
||||
writeFile: async (targetPath, data) => {
|
||||
writes.push({ path: targetPath, data });
|
||||
},
|
||||
chmod: async (targetPath, mode) => {
|
||||
chmods.push({ path: targetPath, mode });
|
||||
},
|
||||
rename: async (from, to) => {
|
||||
renames.push({ from, to });
|
||||
},
|
||||
unlink: async () => {},
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(result, {
|
||||
status: 'updated',
|
||||
path: '/home/kyle/.local/bin/SubMiner.AppImage',
|
||||
});
|
||||
assert.deepEqual(writes, [
|
||||
{ path: '/home/kyle/.local/bin/.SubMiner.AppImage.update', data: appImageBytes },
|
||||
]);
|
||||
assert.deepEqual(chmods, [
|
||||
{ path: '/home/kyle/.local/bin/.SubMiner.AppImage.update', mode: 0o755 },
|
||||
]);
|
||||
assert.deepEqual(renames, [
|
||||
{
|
||||
from: '/home/kyle/.local/bin/.SubMiner.AppImage.update',
|
||||
to: '/home/kyle/.local/bin/SubMiner.AppImage',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('updateAppImageFromRelease reports protected command without replacing non-writable AppImage', async () => {
|
||||
const result = await updateAppImageFromRelease({
|
||||
release: {
|
||||
tag_name: 'v0.15.0',
|
||||
prerelease: false,
|
||||
draft: false,
|
||||
assets: [{ name: 'SubMiner.AppImage', browser_download_url: 'https://example.test/app' }],
|
||||
},
|
||||
sha256Sums: new Map([['SubMiner.AppImage', appImageHash]]),
|
||||
appImagePath: '/opt/SubMiner/SubMiner.AppImage',
|
||||
downloadAsset: async () => appImageBytes,
|
||||
fs: {
|
||||
stat: async () => ({
|
||||
isFile: () => true,
|
||||
mode: 0o755,
|
||||
}),
|
||||
access: async () => {
|
||||
throw new Error('EACCES');
|
||||
},
|
||||
writeFile: async () => {
|
||||
throw new Error('unexpected write');
|
||||
},
|
||||
chmod: async () => {},
|
||||
rename: async () => {},
|
||||
unlink: async () => {},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'protected');
|
||||
assert.equal(result.path, '/opt/SubMiner/SubMiner.AppImage');
|
||||
assert.match(result.command ?? '', /curl -fSL 'https:\/\/example\.test\/app' -o "\$tmp"/);
|
||||
assert.match(result.command ?? '', /sha256sum -c -/);
|
||||
assert.match(result.command ?? '', /sudo mv "\$tmp" '\/opt\/SubMiner\/SubMiner\.AppImage'/);
|
||||
});
|
||||
|
||||
test('buildProtectedAppImageUpdateCommand quotes inputs and verifies checksum before sudo move', () => {
|
||||
const command = buildProtectedAppImageUpdateCommand(
|
||||
"https://example.test/Sub Miner.AppImage?sig='abc'",
|
||||
"/opt/Sub Miner/SubMiner's.AppImage",
|
||||
'ABCDEF',
|
||||
);
|
||||
|
||||
assert.match(command, /trap 'rm -f "\$tmp"' EXIT/);
|
||||
assert.match(
|
||||
command,
|
||||
/curl -fSL 'https:\/\/example\.test\/Sub Miner\.AppImage\?sig='\\''abc'\\''' -o "\$tmp"/,
|
||||
);
|
||||
assert.match(command, /printf '%s %s\\n' 'abcdef' "\$tmp" \| sha256sum -c -/);
|
||||
assert.match(command, /sudo mv "\$tmp" '\/opt\/Sub Miner\/SubMiner'\\''s\.AppImage'/);
|
||||
assert.match(command, /sudo chmod \+x '\/opt\/Sub Miner\/SubMiner'\\''s\.AppImage'/);
|
||||
});
|
||||
|
||||
test('updateAppImageFromRelease aborts on hash mismatch', async () => {
|
||||
const result = await updateAppImageFromRelease({
|
||||
release: {
|
||||
tag_name: 'v0.15.0',
|
||||
prerelease: false,
|
||||
draft: false,
|
||||
assets: [{ name: 'SubMiner.AppImage', browser_download_url: 'https://example.test/app' }],
|
||||
},
|
||||
sha256Sums: new Map([['SubMiner.AppImage', '0'.repeat(64)]]),
|
||||
appImagePath: '/home/kyle/.local/bin/SubMiner.AppImage',
|
||||
downloadAsset: async () => appImageBytes,
|
||||
fs: {
|
||||
stat: async () => ({
|
||||
isFile: () => true,
|
||||
mode: 0o755,
|
||||
}),
|
||||
access: async () => {},
|
||||
writeFile: async () => {
|
||||
throw new Error('unexpected write');
|
||||
},
|
||||
chmod: async () => {},
|
||||
rename: async () => {},
|
||||
unlink: async () => {},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'hash-mismatch');
|
||||
});
|
||||
Reference in New Issue
Block a user