mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-10 03:13:32 -07:00
feat: add auto update support
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import {
|
||||
buildProtectedLauncherUpdateCommand,
|
||||
looksLikeSubminerLauncher,
|
||||
updateLauncherAtPath,
|
||||
} from './launcher-updater';
|
||||
|
||||
const launcherBytes = Buffer.from('#!/usr/bin/env bash\n# SubMiner launcher\nexec SubMiner "$@"\n');
|
||||
const launcherHash = createHash('sha256').update(launcherBytes).digest('hex');
|
||||
|
||||
test('looksLikeSubminerLauncher rejects unrelated executable content', () => {
|
||||
assert.equal(looksLikeSubminerLauncher(Buffer.from('#!/bin/sh\necho nope\n')), false);
|
||||
assert.equal(looksLikeSubminerLauncher(Buffer.from('SubMiner launcher binary payload')), true);
|
||||
});
|
||||
|
||||
test('buildProtectedLauncherUpdateCommand uses sudo curl and chmod for protected paths', () => {
|
||||
assert.equal(
|
||||
buildProtectedLauncherUpdateCommand(
|
||||
'https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer',
|
||||
'/usr/local/bin/subminer',
|
||||
),
|
||||
'sudo curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o /usr/local/bin/subminer && sudo chmod +x /usr/local/bin/subminer',
|
||||
);
|
||||
});
|
||||
|
||||
test('updateLauncherAtPath verifies hash and atomically replaces writable launcher', async () => {
|
||||
const writes: Array<{ path: string; data: Buffer }> = [];
|
||||
const renames: Array<{ from: string; to: string }> = [];
|
||||
const chmods: Array<{ path: string; mode: number }> = [];
|
||||
|
||||
const result = await updateLauncherAtPath({
|
||||
launcherPath: '/home/kyle/.local/bin/subminer',
|
||||
assetUrl: 'https://example.test/subminer',
|
||||
expectedSha256: launcherHash,
|
||||
download: async () => launcherBytes,
|
||||
fs: {
|
||||
readFile: async () => Buffer.from('#!/bin/sh\n# SubMiner launcher\n'),
|
||||
stat: async () => ({ isFile: () => true, mode: 0o755 }),
|
||||
access: async () => undefined,
|
||||
writeFile: async (filePath, data) => {
|
||||
writes.push({ path: filePath, data: Buffer.from(data) });
|
||||
},
|
||||
chmod: async (filePath, mode) => {
|
||||
chmods.push({ path: filePath, mode });
|
||||
},
|
||||
rename: async (from, to) => {
|
||||
renames.push({ from, to });
|
||||
},
|
||||
unlink: async () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'updated');
|
||||
assert.equal(writes.length, 1);
|
||||
assert.equal(writes[0]!.path, '/home/kyle/.local/bin/.subminer.update');
|
||||
assert.equal(writes[0]!.data.equals(launcherBytes), true);
|
||||
assert.deepEqual(chmods, [{ path: '/home/kyle/.local/bin/.subminer.update', mode: 0o755 }]);
|
||||
assert.deepEqual(renames, [
|
||||
{ from: '/home/kyle/.local/bin/.subminer.update', to: '/home/kyle/.local/bin/subminer' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('updateLauncherAtPath reports protected command without replacing non-writable launcher', async () => {
|
||||
const result = await updateLauncherAtPath({
|
||||
launcherPath: '/usr/local/bin/subminer',
|
||||
assetUrl: 'https://example.test/subminer',
|
||||
expectedSha256: launcherHash,
|
||||
download: async () => launcherBytes,
|
||||
fs: {
|
||||
readFile: async () => Buffer.from('#!/bin/sh\n# SubMiner launcher\n'),
|
||||
stat: async () => ({ isFile: () => true, mode: 0o755 }),
|
||||
access: async () => {
|
||||
throw Object.assign(new Error('EACCES'), { code: 'EACCES' });
|
||||
},
|
||||
writeFile: async () => {
|
||||
throw new Error('unexpected write');
|
||||
},
|
||||
chmod: async () => undefined,
|
||||
rename: async () => undefined,
|
||||
unlink: async () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'protected');
|
||||
assert.match(result.command ?? '', /^sudo curl -fSL https:\/\/example\.test\/subminer/);
|
||||
});
|
||||
|
||||
test('updateLauncherAtPath aborts on hash mismatch and suspicious launcher content', async () => {
|
||||
const suspicious = await updateLauncherAtPath({
|
||||
launcherPath: '/home/kyle/bin/subminer',
|
||||
assetUrl: 'https://example.test/subminer',
|
||||
expectedSha256: launcherHash,
|
||||
download: async () => launcherBytes,
|
||||
fs: {
|
||||
readFile: async () => Buffer.from('#!/bin/sh\necho not-subminer\n'),
|
||||
stat: async () => ({ isFile: () => true, mode: 0o755 }),
|
||||
access: async () => undefined,
|
||||
writeFile: async () => undefined,
|
||||
chmod: async () => undefined,
|
||||
rename: async () => undefined,
|
||||
unlink: async () => undefined,
|
||||
},
|
||||
});
|
||||
const mismatch = await updateLauncherAtPath({
|
||||
launcherPath: '/home/kyle/.local/bin/subminer',
|
||||
assetUrl: 'https://example.test/subminer',
|
||||
expectedSha256: '0'.repeat(64),
|
||||
download: async () => launcherBytes,
|
||||
fs: {
|
||||
readFile: async () => Buffer.from('#!/bin/sh\n# SubMiner launcher\n'),
|
||||
stat: async () => ({ isFile: () => true, mode: 0o755 }),
|
||||
access: async () => undefined,
|
||||
writeFile: async () => {
|
||||
throw new Error('unexpected write');
|
||||
},
|
||||
chmod: async () => undefined,
|
||||
rename: async () => undefined,
|
||||
unlink: async () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(suspicious.status, 'skipped');
|
||||
assert.equal(mismatch.status, 'hash-mismatch');
|
||||
});
|
||||
Reference in New Issue
Block a user