import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import test from 'node:test'; import JSZip from 'jszip'; import { loadConfig } from '../src/config.js'; import { ExtensionUpdateError, buildExtensionUpdatePowerShell, compareExtensionVersions, createExtensionDownloadToken, inspectExtensionPackage, interpretCloudAssistantInvocation, verifyExtensionDownloadToken } from '../src/extension-updates.js'; async function extensionZip(version = '0.5.167'): Promise { const zip = new JSZip(); zip.file('manifest.json', JSON.stringify({ manifest_version: 3, name: '联泰下单助手', version, background: { service_worker: 'background.js' }, content_scripts: [{ matches: ['https://business.example.test/*'], js: ['business-bridge.js'] }] })); zip.file('background.js', 'chrome.runtime.onMessage.addListener(() => {});'); zip.file('business-bridge.js', 'window.postMessage({ ok: true });'); return zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' }); } test('extension versions compare numerically rather than lexically', () => { assert.equal(compareExtensionVersions('0.5.167', '0.5.166'), 1); assert.equal(compareExtensionVersions('0.10.0', '0.9.99'), 1); assert.equal(compareExtensionVersions('1.0.0', '1.0.0.0'), 0); assert.equal(compareExtensionVersions('0.5.166', '0.5.167'), -1); assert.throws( () => compareExtensionVersions('latest', '0.5.167'), (error: unknown) => error instanceof ExtensionUpdateError && error.code === 'extension_version_invalid' ); }); test('extension release inspection binds identity, version, required files, and SHA-256', async () => { const content = await extensionZip(); const inspected = await inspectExtensionPackage(content); assert.equal(inspected.version, '0.5.167'); assert.equal(inspected.manifest.name, '联泰下单助手'); assert.equal(inspected.entryCount, 3); assert.equal(inspected.sha256, createHash('sha256').update(content).digest('hex')); const invalid = new JSZip(); invalid.file('manifest.json', JSON.stringify({ manifest_version: 3, name: '其他插件', version: '0.5.167', background: { service_worker: 'background.js' }, content_scripts: [{ matches: ['https://business.example.test/*'], js: ['business-bridge.js'] }] })); invalid.file('background.js', ''); invalid.file('business-bridge.js', ''); await assert.rejects( inspectExtensionPackage(await invalid.generateAsync({ type: 'nodebuffer' })), (error: unknown) => error instanceof ExtensionUpdateError && error.code === 'extension_manifest_identity_mismatch' ); const unsafePath = await extensionZip(); const unsafeZip = await JSZip.loadAsync(unsafePath); unsafeZip.file('asset.js:alternate-stream', 'forbidden'); await assert.rejects( inspectExtensionPackage(await unsafeZip.generateAsync({ type: 'nodebuffer' })), (error: unknown) => error instanceof ExtensionUpdateError && error.code === 'extension_package_path_invalid' ); }); test('short-lived host-scoped package tokens reject tampering and expiry', () => { const key = Buffer.alloc(32, 7); const payload = { releaseId: 'release-a', organizationId: 'organization-a', regionId: 'cn-hangzhou', instanceId: 'i-12345678', expiresAt: 2_000 }; const token = createExtensionDownloadToken(payload, key); assert.deepEqual(verifyExtensionDownloadToken(token, key, 1_999), payload); assert.throws( () => verifyExtensionDownloadToken(`${token}x`, key, 1_999), (error: unknown) => error instanceof ExtensionUpdateError && error.code === 'extension_download_token_invalid' ); assert.throws( () => verifyExtensionDownloadToken(token, key, 2_001), (error: unknown) => error instanceof ExtensionUpdateError && error.code === 'extension_download_token_expired' ); }); test('PowerShell updater carries encoded values, enforces ProgramData, hash, staging, and rollback', () => { const script = buildExtensionUpdatePowerShell({ downloadUrl: 'https://business.example.test/api/extension-updates/package/token', sha256: 'a'.repeat(64), version: '0.5.167', installPath: 'C:\\ProgramData\\LTJT\\chrome-extension\\ltjt-order-assistant', releaseId: 'release-a' }); assert.match(script, /Get-FileHash/); assert.match(script, /PackageHashMismatch/); assert.match(script, /InstallPathOutsideAllowedRoot/); assert.match(script, /LTJT_EXTENSION_NEWER_PRESENT/); assert.match(script, /\.previous/); assert.match(script, /Move-Item -LiteralPath \$backupPath -Destination \$installPath/); assert.ok(Buffer.byteLength(Buffer.from(script, 'utf8').toString('base64')) < 24 * 1024); assert.doesNotMatch(script, /business\.example\.test/); }); test('Cloud Assistant success requires exit zero and the updater completion marker', () => { assert.deepEqual( interpretCloudAssistantInvocation({ invocationStatus: 'Success', exitCode: 0, output: 'LTJT_EXTENSION_UPDATED 0.5.167\n' }), { status: 'success', exitCode: 0, output: 'LTJT_EXTENSION_UPDATED 0.5.167\n' } ); assert.equal( interpretCloudAssistantInvocation({ invocationStatus: 'Success', exitCode: 0, output: '' }).errorCode, 'extension_update_marker_missing' ); assert.equal( interpretCloudAssistantInvocation({ invocationStatus: 'Running', output: '' }).status, 'running' ); assert.equal( interpretCloudAssistantInvocation({ invocationStatus: 'Success', exitCode: 0, output: 'LTJT_EXTENSION_NEWER_PRESENT 0.5.168\r\n' }).status, 'success' ); assert.equal( interpretCloudAssistantInvocation({ invocationStatus: 'Aborted', errorCode: 'ClientNotRunning' }).status, 'failed' ); }); test('extension updater is off by default and enabled production config requires HTTPS plus OSS and ECS credentials', () => { const disabled = loadConfig({ NODE_ENV: 'test' }); assert.equal(disabled.EXTENSION_AUTO_UPDATE_ENABLED, false); assert.equal(disabled.EXTENSION_WINDOWS_INSTALL_PATH, 'C:\\ProgramData\\LTJT\\chrome-extension\\ltjt-order-assistant'); assert.throws(() => loadConfig({ NODE_ENV: 'production', FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 1).toString('base64'), APP_ORIGIN: 'https://business.example.test', EXTENSION_AUTO_UPDATE_ENABLED: 'true' }), /missing configuration/); assert.throws(() => loadConfig({ NODE_ENV: 'production', FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 1).toString('base64'), APP_ORIGIN: 'http://business.example.test', EXTENSION_AUTO_UPDATE_ENABLED: 'true', OSS_ACCESS_KEY_ID: 'oss-id', OSS_ACCESS_KEY_SECRET: 'oss-secret', OSS_ENDPOINT: 'oss-cn-hangzhou.aliyuncs.com', OSS_BUCKET_NAME: 'bucket', OSS_REGION: 'cn-hangzhou', ALIBABA_CLOUD_ACCESS_KEY_ID: 'ecs-id', ALIBABA_CLOUD_ACCESS_KEY_SECRET: 'ecs-secret' }), /must use HTTPS/); });