mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 13:35:42 +09:00
217 lines
6.7 KiB
JavaScript
217 lines
6.7 KiB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import assert from 'node:assert/strict';
|
|
import {Buffer} from 'node:buffer';
|
|
import {createHash} from 'node:crypto';
|
|
import {mkdtempSync, writeFileSync} from 'node:fs';
|
|
import {rm} from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import {after, describe, test} from 'node:test';
|
|
import './LocalAppTestSupport.test.mjs';
|
|
|
|
const {ModulePackageIOError, verifyAndExtract} = await import('@electron/main/ModulePackage');
|
|
const {isDeterministicPackageFailure, ModulePackageInstaller} = await import('@electron/main/ModulePackageInstaller');
|
|
|
|
const temporaryRoots = [];
|
|
|
|
after(async () => {
|
|
for (const root of temporaryRoots) {
|
|
await rm(root, {recursive: true, force: true});
|
|
}
|
|
});
|
|
|
|
function createRoot() {
|
|
const root = mkdtempSync(path.join(os.tmpdir(), 'module-package-installer-'));
|
|
temporaryRoots.push(root);
|
|
return root;
|
|
}
|
|
|
|
function sha256(data) {
|
|
return createHash('sha256').update(data).digest('hex');
|
|
}
|
|
|
|
describe('module package failure classification', () => {
|
|
test('an EEXIST failure from the extractor is deterministic', async () => {
|
|
const root = createRoot();
|
|
const packed = Buffer.from('module package bytes', 'utf8');
|
|
const packedPath = path.join(root, 'package.br');
|
|
writeFileSync(packedPath, packed);
|
|
const destinationDir = path.join(root, 'incoming');
|
|
writeFileSync(destinationDir, 'occupied');
|
|
const failure = await verifyAndExtract({packedPath, expectedSha256: sha256(packed), destinationDir}).then(
|
|
() => null,
|
|
(error) => error,
|
|
);
|
|
assert.equal(failure?.name, 'ModulePackageIOError');
|
|
assert.equal(failure?.cause?.code, 'EEXIST');
|
|
assert.equal(isDeterministicPackageFailure(failure), true);
|
|
});
|
|
|
|
test('a storage-full failure from the extractor stays non-deterministic', () => {
|
|
const cause = Object.assign(new Error('no space left on device'), {code: 'ENOSPC'});
|
|
const failure = new ModulePackageIOError('failed to write index.html', {cause});
|
|
assert.equal(isDeterministicPackageFailure(failure), false);
|
|
});
|
|
});
|
|
|
|
const PACKAGE_BYTES = Buffer.from('0123456789abcdefghijklmnopqrstuvwxyz', 'utf8');
|
|
const PACKAGE_ORIGIN = 'https://pkgs.example.test';
|
|
|
|
function planItem({
|
|
url = `${PACKAGE_ORIGIN}/desktop/canary/modules/fluxer_renderer/${sha256(PACKAGE_BYTES)}/package.br`,
|
|
} = {}) {
|
|
return {
|
|
module: 'fluxer_renderer',
|
|
entry: {sha256: sha256(PACKAGE_BYTES), bytes: PACKAGE_BYTES.length, url},
|
|
requirement: 'required',
|
|
};
|
|
}
|
|
|
|
async function collect(download) {
|
|
const parts = [];
|
|
for await (const chunk of download.chunks) {
|
|
parts.push(Buffer.from(chunk));
|
|
}
|
|
return {offset: download.offset, bytes: Buffer.concat(parts)};
|
|
}
|
|
|
|
function createInstaller({respond, resumeFrom}) {
|
|
const requests = [];
|
|
const reports = [];
|
|
const downloads = [];
|
|
const installer = new ModulePackageInstaller({
|
|
store: {
|
|
installModule: async ({download}) => {
|
|
downloads.push(await collect(await download(resumeFrom)));
|
|
},
|
|
},
|
|
packageOrigin: PACKAGE_ORIGIN,
|
|
fetch: async (url, init) => {
|
|
requests.push({url, range: init.headers.range ?? null});
|
|
return respond(requests.length, init);
|
|
},
|
|
onProgress: () => {},
|
|
report: (report) => reports.push(report),
|
|
});
|
|
return {installer, requests, reports, downloads};
|
|
}
|
|
|
|
describe('resuming a module package download', () => {
|
|
test('asks for the remaining bytes and hands back the range the server confirmed', async () => {
|
|
const {installer, requests, downloads} = createInstaller({
|
|
resumeFrom: 10,
|
|
respond: () =>
|
|
new Response(PACKAGE_BYTES.subarray(10), {
|
|
status: 206,
|
|
headers: {'content-range': `bytes 10-${PACKAGE_BYTES.length - 1}/${PACKAGE_BYTES.length}`},
|
|
}),
|
|
});
|
|
|
|
await installer.install(planItem(), 1, 1);
|
|
|
|
assert.deepEqual(
|
|
requests.map((request) => request.range),
|
|
['bytes=10-'],
|
|
);
|
|
assert.equal(downloads[0].offset, 10);
|
|
assert.deepEqual(downloads[0].bytes, PACKAGE_BYTES.subarray(10));
|
|
});
|
|
|
|
test('a full response to a range request restarts from the first byte', async () => {
|
|
const {installer, downloads} = createInstaller({
|
|
resumeFrom: 10,
|
|
respond: () => new Response(PACKAGE_BYTES, {status: 200}),
|
|
});
|
|
|
|
await installer.install(planItem(), 1, 1);
|
|
|
|
assert.equal(downloads[0].offset, 0);
|
|
assert.deepEqual(downloads[0].bytes, PACKAGE_BYTES);
|
|
});
|
|
|
|
test('an unsatisfiable range falls back to a plain request', async () => {
|
|
const {installer, requests, downloads} = createInstaller({
|
|
resumeFrom: 10,
|
|
respond: (attempt) =>
|
|
attempt === 1 ? new Response('', {status: 416}) : new Response(PACKAGE_BYTES, {status: 200}),
|
|
});
|
|
|
|
await installer.install(planItem(), 1, 1);
|
|
|
|
assert.deepEqual(
|
|
requests.map((request) => request.range),
|
|
['bytes=10-', null],
|
|
);
|
|
assert.equal(downloads[0].offset, 0);
|
|
});
|
|
|
|
test('a partial response that starts somewhere else is discarded for a plain request', async () => {
|
|
const {installer, requests, downloads} = createInstaller({
|
|
resumeFrom: 10,
|
|
respond: (attempt) =>
|
|
attempt === 1
|
|
? new Response(PACKAGE_BYTES.subarray(5), {
|
|
status: 206,
|
|
headers: {'content-range': `bytes 5-${PACKAGE_BYTES.length - 1}/${PACKAGE_BYTES.length}`},
|
|
})
|
|
: new Response(PACKAGE_BYTES, {status: 200}),
|
|
});
|
|
|
|
await installer.install(planItem(), 1, 1);
|
|
|
|
assert.deepEqual(
|
|
requests.map((request) => request.range),
|
|
['bytes=10-', null],
|
|
);
|
|
assert.deepEqual(downloads[0].bytes, PACKAGE_BYTES);
|
|
});
|
|
|
|
test('a partial file that already holds every byte needs no request', async () => {
|
|
const {installer, requests, downloads} = createInstaller({
|
|
resumeFrom: PACKAGE_BYTES.length,
|
|
respond: () => {
|
|
throw new Error('unexpected request');
|
|
},
|
|
});
|
|
|
|
await installer.install(planItem(), 1, 1);
|
|
|
|
assert.equal(requests.length, 0);
|
|
assert.equal(downloads[0].offset, PACKAGE_BYTES.length);
|
|
assert.equal(downloads[0].bytes.length, 0);
|
|
});
|
|
|
|
test('a partial file longer than the package is downloaded again from the start', async () => {
|
|
const {installer, requests} = createInstaller({
|
|
resumeFrom: PACKAGE_BYTES.length + 1,
|
|
respond: () => new Response(PACKAGE_BYTES, {status: 200}),
|
|
});
|
|
|
|
await installer.install(planItem(), 1, 1);
|
|
|
|
assert.deepEqual(
|
|
requests.map((request) => request.range),
|
|
[null],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('reporting deterministic package failures', () => {
|
|
test('a package url the policy refuses is reported with the url', async () => {
|
|
const url = 'http://192.168.139.107:48780/package.br';
|
|
const {installer, reports} = createInstaller({
|
|
resumeFrom: 0,
|
|
respond: () => {
|
|
throw new Error('unexpected request');
|
|
},
|
|
});
|
|
|
|
await assert.rejects(installer.install(planItem({url}), 1, 1), /not https/u);
|
|
|
|
assert.equal(reports.length, 1);
|
|
assert.equal(reports[0].type, 'package-rejected');
|
|
assert.match(reports[0].message, /192\.168\.139\.107/u);
|
|
});
|
|
});
|