feat: add safe GitPeji update checks
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
'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',
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user