Files
Alta-Proxy-Tool/test/update-policy.test.js
T

225 lines
6.7 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const { EventEmitter } = require('node:events');
const {
LATEST_RELEASE_URL,
RELEASES_PAGE_URL,
MAX_BODY_BYTES,
checkForUpdate,
compareSemver,
defaultRequest,
} = 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 &lt;Release&gt;',
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',
);
});
test('default transport enforces an absolute deadline despite trickled response bytes', async () => {
const request = new EventEmitter();
const responseStream = new EventEmitter();
request.destroyedWith = null;
responseStream.destroyed = false;
request.destroy = (error) => { request.destroyedWith = error; request.emit('error', error); };
request.setTimeout = () => { throw new Error('inactivity timeout must not be used'); };
responseStream.destroy = () => { responseStream.destroyed = true; };
responseStream.headers = { 'content-type': 'application/json' };
responseStream.statusCode = 200;
let deadline;
let cleared = false;
const pending = defaultRequest({
url: LATEST_RELEASE_URL,
timeoutMs: 5000,
maxBodyBytes: MAX_BODY_BYTES,
httpsGet: (_url, _options, onResponse) => {
onResponse(responseStream);
return request;
},
setTimer: (callback, milliseconds) => { assert.equal(milliseconds, 5000); deadline = callback; return 7; },
clearTimer: (timer) => { assert.equal(timer, 7); cleared = true; },
});
responseStream.emit('data', Buffer.from('{'));
responseStream.emit('data', Buffer.from(' '));
deadline();
await rejectsWithCode(pending, 'REQUEST_TIMEOUT');
assert.equal(responseStream.destroyed, true);
assert.equal(request.destroyedWith.code, 'REQUEST_TIMEOUT');
assert.equal(cleared, true);
});