191 lines
5.3 KiB
JavaScript
191 lines
5.3 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
|
|
const {
|
|
LATEST_RELEASE_URL,
|
|
RELEASES_PAGE_URL,
|
|
MAX_BODY_BYTES,
|
|
checkForUpdate,
|
|
compareSemver,
|
|
} = require('../src/update-policy');
|
|
|
|
function response(body, overrides = {}) {
|
|
return {
|
|
statusCode: 200,
|
|
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
body: Buffer.from(body),
|
|
url: LATEST_RELEASE_URL,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function rejectsWithCode(promise, code) {
|
|
await assert.rejects(promise, (error) => {
|
|
assert.equal(error.code, code);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
test('valid update returns only sanitized, check-only metadata', async () => {
|
|
let requestOptions;
|
|
const request = async (options) => {
|
|
requestOptions = options;
|
|
return response(JSON.stringify({
|
|
id: 42,
|
|
tag_name: '1.2.3',
|
|
name: ' Security <Release> ',
|
|
published_at: '2026-08-19T12:34:56Z',
|
|
body: 'untrusted release notes',
|
|
html_url: 'https://evil.example/download',
|
|
assets: [{ browser_download_url: 'https://evil.example/payload.exe' }],
|
|
}));
|
|
};
|
|
|
|
const result = await checkForUpdate({
|
|
currentVersion: '1.2.2',
|
|
request,
|
|
platform: 'win32',
|
|
arch: 'x64',
|
|
});
|
|
|
|
assert.deepEqual(requestOptions, {
|
|
url: LATEST_RELEASE_URL,
|
|
timeoutMs: 5000,
|
|
maxBodyBytes: MAX_BODY_BYTES,
|
|
redirects: 'error',
|
|
});
|
|
assert.deepEqual(result, {
|
|
status: 'update-available',
|
|
currentVersion: '1.2.2',
|
|
latestVersion: '1.2.3',
|
|
releaseName: 'Security <Release>',
|
|
publishedAt: '2026-08-19T12:34:56.000Z',
|
|
releasesPageUrl: RELEASES_PAGE_URL,
|
|
platform: 'win32',
|
|
arch: 'x64',
|
|
});
|
|
assert.equal(JSON.stringify(result).includes('payload.exe'), false);
|
|
assert.equal(JSON.stringify(result).includes('release notes'), false);
|
|
});
|
|
|
|
test('equal or older release is reported as up to date', async () => {
|
|
const request = async () => response(JSON.stringify({
|
|
tag_name: '2.0.0',
|
|
name: 'Current',
|
|
published_at: '2026-08-19T12:34:56Z',
|
|
}));
|
|
|
|
const result = await checkForUpdate({ currentVersion: '2.0.0', request });
|
|
assert.equal(result.status, 'up-to-date');
|
|
assert.equal(result.releasesPageUrl, RELEASES_PAGE_URL);
|
|
});
|
|
|
|
test('404 means there is no published release', async () => {
|
|
const request = async () => response('', { statusCode: 404 });
|
|
const result = await checkForUpdate({ currentVersion: '1.0.0', request });
|
|
|
|
assert.deepEqual(result, {
|
|
status: 'no-release',
|
|
currentVersion: '1.0.0',
|
|
releasesPageUrl: RELEASES_PAGE_URL,
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
});
|
|
});
|
|
|
|
test('malformed JSON and invalid response schema fail closed', async () => {
|
|
await rejectsWithCode(
|
|
checkForUpdate({ currentVersion: '1.0.0', request: async () => response('{') }),
|
|
'INVALID_RESPONSE',
|
|
);
|
|
await rejectsWithCode(
|
|
checkForUpdate({ currentVersion: '1.0.0', request: async () => response('[]') }),
|
|
'INVALID_RESPONSE',
|
|
);
|
|
await rejectsWithCode(
|
|
checkForUpdate({
|
|
currentVersion: '1.0.0',
|
|
request: async () => response(JSON.stringify({ tag_name: '1.1.0', name: 7 })),
|
|
}),
|
|
'INVALID_RESPONSE',
|
|
);
|
|
});
|
|
|
|
test('oversized bodies fail before parsing', async () => {
|
|
const oversized = Buffer.alloc(MAX_BODY_BYTES + 1, 0x20);
|
|
await rejectsWithCode(
|
|
checkForUpdate({
|
|
currentVersion: '1.0.0',
|
|
request: async () => response('', { body: oversized }),
|
|
}),
|
|
'RESPONSE_TOO_LARGE',
|
|
);
|
|
});
|
|
|
|
test('redirects and response host drift are rejected', async () => {
|
|
await rejectsWithCode(
|
|
checkForUpdate({
|
|
currentVersion: '1.0.0',
|
|
request: async () => response('', {
|
|
statusCode: 302,
|
|
headers: { location: 'https://evil.example/latest' },
|
|
}),
|
|
}),
|
|
'REDIRECT_REJECTED',
|
|
);
|
|
|
|
await rejectsWithCode(
|
|
checkForUpdate({
|
|
currentVersion: '1.0.0',
|
|
request: async () => response('{}', { url: 'https://evil.example/latest' }),
|
|
}),
|
|
'UNTRUSTED_RESPONSE_URL',
|
|
);
|
|
});
|
|
|
|
test('invalid or non-strict semver fails closed', async () => {
|
|
const invalidVersions = ['v1.2.3', '1.2', '01.2.3', '1.2.3.4', 'latest'];
|
|
|
|
for (const tag_name of invalidVersions) {
|
|
await rejectsWithCode(
|
|
checkForUpdate({
|
|
currentVersion: '1.0.0',
|
|
request: async () => response(JSON.stringify({ tag_name })),
|
|
}),
|
|
'INVALID_RELEASE_VERSION',
|
|
);
|
|
}
|
|
|
|
await rejectsWithCode(
|
|
checkForUpdate({ currentVersion: 'v1.0.0', request: async () => response('{}') }),
|
|
'INVALID_CURRENT_VERSION',
|
|
);
|
|
});
|
|
|
|
test('strict semver comparison handles prerelease precedence', () => {
|
|
assert.equal(compareSemver('1.0.0', '1.0.0'), 0);
|
|
assert.equal(compareSemver('1.0.1', '1.0.0'), 1);
|
|
assert.equal(compareSemver('1.0.0-alpha.2', '1.0.0-alpha.10'), -1);
|
|
assert.equal(compareSemver('1.0.0-rc.1', '1.0.0'), -1);
|
|
assert.equal(compareSemver('2.0.0+build.1', '2.0.0+build.2'), 0);
|
|
});
|
|
|
|
test('unexpected HTTP and content types fail closed', async () => {
|
|
await rejectsWithCode(
|
|
checkForUpdate({
|
|
currentVersion: '1.0.0',
|
|
request: async () => response('server error', { statusCode: 500 }),
|
|
}),
|
|
'HTTP_ERROR',
|
|
);
|
|
await rejectsWithCode(
|
|
checkForUpdate({
|
|
currentVersion: '1.0.0',
|
|
request: async () => response('{}', { headers: { 'content-type': 'text/html' } }),
|
|
}),
|
|
'INVALID_CONTENT_TYPE',
|
|
);
|
|
});
|