Harden update, system-check, queue, cutter, streamer and shutdown state transitions. Add multi-user installer recovery, secret-safe config migration, provider fallback handling, managed-tool validation and real media export coverage. Refresh the English public documentation, release notes and 1.0.18 product screenshot.
705 lines
26 KiB
JavaScript
705 lines
26 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const nodeCrypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
|
|
const {
|
|
assertOwnedPath,
|
|
buildStreamlinkArguments,
|
|
compareVersions,
|
|
parseGateMode,
|
|
parseLatestYaml,
|
|
readLiveConfiguration,
|
|
redactDiagnostic,
|
|
sanitizeChildEnvironment,
|
|
validateMediaProbe,
|
|
validateDownloadedReleaseArtifact,
|
|
validateProductionRelease,
|
|
validateUpdateCacheRecord,
|
|
validateTwitchToken
|
|
} = require('./smoke-test-live-integration-contract');
|
|
const {
|
|
checkPackagedUpdate,
|
|
closeElectronApp,
|
|
createUpdaterDownloadReadySummary,
|
|
getPackagedVersion,
|
|
inspectProductionRelease,
|
|
requestProductTwitchToken,
|
|
runWithElectronAppCleanup,
|
|
runWithOwnedRoot,
|
|
startUpdaterDownload,
|
|
verifyProductTwitchProviderFallbacks
|
|
} = require('./smoke-test-live-integration');
|
|
const PACKAGE_VERSION = require('../package.json').version;
|
|
|
|
const TWITCH_ENVIRONMENT = {
|
|
TWITCH_VOD_MANAGER_LIVE_INTEGRATION: '1',
|
|
TWITCH_VOD_MANAGER_LIVE_TWITCH_CLIENT_ID: 'client-id-value',
|
|
TWITCH_VOD_MANAGER_LIVE_TWITCH_CLIENT_SECRET: 'client-secret-value',
|
|
TWITCH_VOD_MANAGER_LIVE_TWITCH_LOGIN: 'example_channel',
|
|
TWITCH_VOD_MANAGER_LIVE_TWITCH_VOD_ID: '1234567890'
|
|
};
|
|
|
|
const UPDATE_SHA512 = Buffer.alloc(64, 7).toString('base64');
|
|
const UPDATE_COMMIT_SHA = 'b'.repeat(40);
|
|
|
|
function releaseYaml(version = PACKAGE_VERSION) {
|
|
return [
|
|
`version: ${version}`,
|
|
'files:',
|
|
` - url: Twitch-VOD-Manager-Setup-${version}.exe`,
|
|
` sha512: ${UPDATE_SHA512}`,
|
|
' size: 120000000',
|
|
`path: Twitch-VOD-Manager-Setup-${version}.exe`,
|
|
`sha512: ${UPDATE_SHA512}`,
|
|
"releaseDate: '2026-08-13T10:00:00.000Z'"
|
|
].join('\n');
|
|
}
|
|
|
|
function releaseInspectionDependencies(commitSha = UPDATE_COMMIT_SHA) {
|
|
const calls = [];
|
|
return {
|
|
calls,
|
|
requestJson: async (url) => {
|
|
calls.push(String(url));
|
|
if (String(url).endsWith('/releases/latest')) {
|
|
return {
|
|
assets: [
|
|
{ name: `Twitch-VOD-Manager-Setup-${PACKAGE_VERSION}.exe`, size: 120000000 },
|
|
{ name: 'latest.yml', size: 1024 }
|
|
],
|
|
draft: false,
|
|
prerelease: false,
|
|
tag_name: `v${PACKAGE_VERSION}`
|
|
};
|
|
}
|
|
if (String(url).endsWith(`/commits/v${PACKAGE_VERSION}`)) return { sha: commitSha };
|
|
throw new Error(`Unexpected JSON request: ${url}`);
|
|
},
|
|
requestLatestYaml: async (url) => {
|
|
calls.push(String(url));
|
|
return releaseYaml();
|
|
}
|
|
};
|
|
}
|
|
|
|
function updaterEnvironment(overrides = {}) {
|
|
return {
|
|
GITHUB_REF: `refs/tags/v${PACKAGE_VERSION}`,
|
|
GITHUB_SHA: UPDATE_COMMIT_SHA,
|
|
TWITCH_VOD_MANAGER_LIVE_INTEGRATION: '1',
|
|
TWITCH_VOD_MANAGER_LIVE_SOURCE_VERSION: '0.0.1',
|
|
TWITCH_VOD_MANAGER_LIVE_SOURCE_SHA256: 'a'.repeat(64),
|
|
TWITCH_VOD_MANAGER_LIVE_UPDATE_COMMIT_SHA: UPDATE_COMMIT_SHA,
|
|
TWITCH_VOD_MANAGER_LIVE_UPDATE_VERSION: PACKAGE_VERSION,
|
|
TWITCH_VOD_MANAGER_LIVE_UPDATE_SHA512: UPDATE_SHA512,
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
test('refuses every live mode until the explicit opt-in is set', () => {
|
|
assert.throws(
|
|
() => readLiveConfiguration('twitch', { ...TWITCH_ENVIRONMENT, TWITCH_VOD_MANAGER_LIVE_INTEGRATION: undefined }),
|
|
/TWITCH_VOD_MANAGER_LIVE_INTEGRATION=1/
|
|
);
|
|
assert.throws(
|
|
() => readLiveConfiguration('updater', {}),
|
|
/TWITCH_VOD_MANAGER_LIVE_INTEGRATION=1/
|
|
);
|
|
});
|
|
|
|
test('keeps Twitch credentials scoped to the Twitch gate while updater requires explicit release pins', () => {
|
|
const twitch = readLiveConfiguration('twitch', TWITCH_ENVIRONMENT);
|
|
assert.equal(twitch.twitch.clientId, 'client-id-value');
|
|
assert.equal(twitch.twitch.clientSecret, 'client-secret-value');
|
|
assert.equal(twitch.twitch.login, 'example_channel');
|
|
assert.equal(twitch.twitch.vodId, '1234567890');
|
|
|
|
assert.throws(
|
|
() => readLiveConfiguration('updater', { TWITCH_VOD_MANAGER_LIVE_INTEGRATION: '1' }),
|
|
/TWITCH_VOD_MANAGER_LIVE_SOURCE_VERSION/
|
|
);
|
|
|
|
const updater = readLiveConfiguration('updater', updaterEnvironment());
|
|
assert.equal(updater.twitch, undefined);
|
|
assert.equal(updater.updater.sourceVersion, '0.0.1');
|
|
assert.equal(updater.updater.sourceSha256, 'a'.repeat(64));
|
|
assert.equal(updater.updater.expectedVersion, PACKAGE_VERSION);
|
|
assert.equal(updater.updater.expectedSha512, UPDATE_SHA512);
|
|
assert.equal(updater.updater.expectedCommitSha, UPDATE_COMMIT_SHA);
|
|
assert.equal(updater.updater.packagedAppPath, undefined);
|
|
|
|
const override = readLiveConfiguration('updater', updaterEnvironment({
|
|
TWITCH_VOD_MANAGER_LIVE_PACKAGED_APP_PATH: 'C:\\fixtures\\Twitch VOD Manager.exe',
|
|
}));
|
|
assert.equal(override.updater.packagedAppPath, 'C:\\fixtures\\Twitch VOD Manager.exe');
|
|
assert.equal(override.updater.sourceVersion, '0.0.1');
|
|
assert.equal(override.updater.expectedVersion, PACKAGE_VERSION);
|
|
assert.equal(override.updater.expectedSha512, UPDATE_SHA512);
|
|
});
|
|
|
|
test('rejects every missing updater pin and binds the target to package version and release tag', () => {
|
|
for (const name of [
|
|
'TWITCH_VOD_MANAGER_LIVE_SOURCE_VERSION',
|
|
'TWITCH_VOD_MANAGER_LIVE_SOURCE_SHA256',
|
|
'TWITCH_VOD_MANAGER_LIVE_UPDATE_COMMIT_SHA',
|
|
'TWITCH_VOD_MANAGER_LIVE_UPDATE_VERSION',
|
|
'TWITCH_VOD_MANAGER_LIVE_UPDATE_SHA512'
|
|
]) {
|
|
assert.throws(
|
|
() => readLiveConfiguration('updater', updaterEnvironment({ [name]: undefined })),
|
|
(error) => error instanceof Error && error.message.includes(name)
|
|
);
|
|
}
|
|
assert.throws(
|
|
() => readLiveConfiguration('updater', updaterEnvironment({ TWITCH_VOD_MANAGER_LIVE_UPDATE_VERSION: '99.0.0', GITHUB_REF: 'refs/tags/v99.0.0' })),
|
|
/package version/
|
|
);
|
|
assert.throws(
|
|
() => readLiveConfiguration('updater', updaterEnvironment({ GITHUB_REF: 'refs/heads/main' })),
|
|
/release tag/
|
|
);
|
|
assert.throws(
|
|
() => readLiveConfiguration('updater', updaterEnvironment({ TWITCH_VOD_MANAGER_LIVE_UPDATE_COMMIT_SHA: 'abc' })),
|
|
/UPDATE_COMMIT_SHA/
|
|
);
|
|
assert.throws(
|
|
() => readLiveConfiguration('updater', updaterEnvironment({ GITHUB_SHA: 'c'.repeat(40) })),
|
|
/current workflow commit/
|
|
);
|
|
for (const version of ['1.0.18-alpha', '1.0.18+build', '01.0.18', '1.00.18', '1.0.18.0', '1234567890.0.0']) {
|
|
assert.throws(
|
|
() => readLiveConfiguration('updater', updaterEnvironment({
|
|
GITHUB_REF: `refs/tags/v${version}`,
|
|
TWITCH_VOD_MANAGER_LIVE_UPDATE_VERSION: version
|
|
})),
|
|
/numeric release version/
|
|
);
|
|
}
|
|
});
|
|
|
|
test('obtains the live credential through the built Twitch token product path', async () => {
|
|
const product = require('../dist/main/twitch');
|
|
assert.equal(requestProductTwitchToken.length, 1);
|
|
assert.equal(typeof product.TwitchAppTokenService, 'function');
|
|
assert.equal(typeof product.requestTwitchAppAccessToken, 'function');
|
|
let requests = 0;
|
|
const request = async (url, options, timeoutMs) => {
|
|
requests += 1;
|
|
assert.equal(url.origin + url.pathname, 'https://id.twitch.tv/oauth2/token');
|
|
assert.deepEqual(Object.fromEntries(url.searchParams), {
|
|
client_id: 'client-id-value',
|
|
client_secret: 'client-secret-value',
|
|
grant_type: 'client_credentials'
|
|
});
|
|
assert.deepEqual(options, { method: 'POST' });
|
|
assert.equal(timeoutMs, 30000);
|
|
return { access_token: 'live-product-token', expires_in: 3600, token_type: 'bearer' };
|
|
};
|
|
const token = await requestProductTwitchToken(
|
|
{ clientId: 'client-id-value', clientSecret: 'client-secret-value' },
|
|
request
|
|
);
|
|
assert.deepEqual(token, {
|
|
accessToken: 'live-product-token',
|
|
tokenPayload: { access_token: 'live-product-token', expires_in: 3600, token_type: 'bearer' }
|
|
});
|
|
assert.equal(requests, 1);
|
|
});
|
|
|
|
test('runs Helix, public GQL, and offline last-good through the built Twitch provider product paths', async () => {
|
|
const calls = [];
|
|
const client = {
|
|
async get(url, config) {
|
|
calls.push({ config, method: 'GET', url });
|
|
assert.deepEqual(config.headers, {
|
|
'Client-ID': 'client-id-value',
|
|
Authorization: 'Bearer live-product-token'
|
|
});
|
|
assert.equal(config.timeout, 30000);
|
|
if (url === 'https://api.twitch.tv/helix/users') {
|
|
assert.deepEqual(config.params, { login: 'example_channel' });
|
|
return {
|
|
data: {
|
|
data: [{
|
|
broadcaster_type: 'partner',
|
|
description: 'Example broadcaster',
|
|
display_name: 'Example Channel',
|
|
id: '42',
|
|
login: 'example_channel',
|
|
profile_image_url: 'https://static-cdn.example.test/profile.png'
|
|
}]
|
|
}
|
|
};
|
|
}
|
|
assert.equal(url, 'https://api.twitch.tv/helix/videos');
|
|
assert.deepEqual(config.params, { first: 100, type: 'archive', user_id: '42' });
|
|
return {
|
|
data: {
|
|
data: [{
|
|
created_at: '2026-08-13T10:00:00Z',
|
|
duration: '2h3m4s',
|
|
id: '1234567890',
|
|
stream_id: 'stream-1',
|
|
thumbnail_url: 'https://static-cdn.example.test/vod.jpg',
|
|
title: 'A VOD',
|
|
url: 'https://www.twitch.tv/videos/1234567890',
|
|
user_login: 'example_channel',
|
|
view_count: 123
|
|
}],
|
|
pagination: {}
|
|
}
|
|
};
|
|
},
|
|
async post(url, body, config) {
|
|
calls.push({ body, config, method: 'POST', url });
|
|
if (config.timeout !== 30000) {
|
|
const error = new Error('Public product wrapper did not own its request signature');
|
|
error.response = { status: 400 };
|
|
throw error;
|
|
}
|
|
assert.equal(url, 'https://gql.twitch.tv/gql');
|
|
assert.equal(typeof body.query, 'string');
|
|
assert.deepEqual(body.variables, { first: 100, login: 'example_channel' });
|
|
assert.equal(config.headers['Content-Type'], 'application/json');
|
|
assert.equal(typeof config.headers['Client-ID'], 'string');
|
|
assert.notEqual(config.headers['Client-ID'], 'client-id-value');
|
|
return {
|
|
data: {
|
|
data: {
|
|
user: {
|
|
videos: {
|
|
edges: [{
|
|
node: {
|
|
id: '1234567890',
|
|
lengthSeconds: 7384,
|
|
previewThumbnailURL: 'https://static-cdn.example.test/vod.jpg',
|
|
publishedAt: '2026-08-13T10:00:00Z',
|
|
title: 'A VOD',
|
|
viewCount: 123
|
|
}
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
};
|
|
const originalFetch = global.fetch;
|
|
global.fetch = async () => { throw new Error('provider contract bypassed its injected transport'); };
|
|
try {
|
|
const result = await verifyProductTwitchProviderFallbacks({
|
|
clientId: 'client-id-value',
|
|
login: 'example_channel',
|
|
vodId: '1234567890'
|
|
}, 'live-product-token', client);
|
|
assert.deepEqual(result, {
|
|
helix: { duration: '2h3m4s', source: 'helix', userId: '42', vodId: '1234567890' },
|
|
lastGood: { restoredFrom: 'helix', restoredVodId: '1234567890', source: 'last-good', stale: true },
|
|
public: { duration: '2h3m4s', login: 'example_channel', source: 'public', vodId: '1234567890' }
|
|
});
|
|
assert.deepEqual(calls.map((call) => `${call.method} ${call.url}`), [
|
|
'GET https://api.twitch.tv/helix/users',
|
|
'POST https://gql.twitch.tv/gql',
|
|
'GET https://api.twitch.tv/helix/videos'
|
|
]);
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test('bounds the packaged version product call', async () => {
|
|
const window = { evaluate: async () => await new Promise(() => {}) };
|
|
await assert.rejects(getPackagedVersion(window, 10), /version query timed out/i);
|
|
});
|
|
|
|
test('bounds the packaged update-check product call independently', async () => {
|
|
const window = { evaluate: async () => await new Promise(() => {}) };
|
|
await assert.rejects(checkPackagedUpdate(window, 10), /update check timed out/i);
|
|
});
|
|
|
|
test('closes the packaged app gracefully without a hard kill', async () => {
|
|
const calls = [];
|
|
const process = { exitCode: null };
|
|
const app = {
|
|
close: async () => {
|
|
calls.push('close');
|
|
process.exitCode = 0;
|
|
}
|
|
};
|
|
await closeElectronApp(app, process, {
|
|
terminate: () => calls.push('terminate'),
|
|
timeoutMs: 10,
|
|
waitForExit: async () => calls.push('wait')
|
|
});
|
|
assert.deepEqual(calls, ['close']);
|
|
});
|
|
|
|
test('hard-kills the packaged app after graceful close times out', async () => {
|
|
const calls = [];
|
|
const process = { exitCode: null };
|
|
const app = { close: async () => { calls.push('close'); return await new Promise(() => {}); } };
|
|
await closeElectronApp(app, process, {
|
|
terminate: () => { calls.push('terminate'); process.exitCode = 1; },
|
|
timeoutMs: 10,
|
|
waitForExit: async () => calls.push('wait')
|
|
});
|
|
assert.deepEqual(calls, ['close', 'terminate', 'wait']);
|
|
});
|
|
|
|
test('hard-kills the packaged app after graceful close rejects', async () => {
|
|
const calls = [];
|
|
const process = { exitCode: null };
|
|
const app = { close: async () => { calls.push('close'); throw new Error('close rejected'); } };
|
|
await closeElectronApp(app, process, {
|
|
terminate: () => { calls.push('terminate'); process.exitCode = 1; },
|
|
timeoutMs: 10,
|
|
waitForExit: async () => calls.push('wait')
|
|
});
|
|
assert.deepEqual(calls, ['close', 'terminate', 'wait']);
|
|
});
|
|
|
|
test('closes the packaged app when the updater body fails', async () => {
|
|
const calls = [];
|
|
const process = { exitCode: null };
|
|
const lifecycle = {
|
|
electronApp: { close: async () => { calls.push('close'); process.exitCode = 0; } },
|
|
electronProcess: process
|
|
};
|
|
await assert.rejects(
|
|
runWithElectronAppCleanup(lifecycle, async () => { throw new Error('updater body failed'); }, { timeoutMs: 10 }),
|
|
/updater body failed/
|
|
);
|
|
assert.deepEqual(calls, ['close']);
|
|
});
|
|
|
|
test('opens the update popover before clicking its download action', async () => {
|
|
const calls = [];
|
|
const locators = {
|
|
'#workspaceUpdateButton': { hover: async () => { calls.push('hover'); } },
|
|
'#updateButton': {
|
|
waitFor: async (options) => { calls.push(`wait:${options.state}`); },
|
|
click: async () => { calls.push('click'); }
|
|
}
|
|
};
|
|
await startUpdaterDownload({ locator: (selector) => locators[selector] });
|
|
assert.deepEqual(calls, ['hover', 'wait:visible', 'click']);
|
|
});
|
|
|
|
test('rejects malformed public fixture identities before making network requests', () => {
|
|
assert.throws(
|
|
() => readLiveConfiguration('twitch', { ...TWITCH_ENVIRONMENT, TWITCH_VOD_MANAGER_LIVE_TWITCH_LOGIN: 'https://twitch.tv/name' }),
|
|
/LIVE_TWITCH_LOGIN/
|
|
);
|
|
assert.throws(
|
|
() => readLiveConfiguration('twitch', { ...TWITCH_ENVIRONMENT, TWITCH_VOD_MANAGER_LIVE_TWITCH_VOD_ID: '../123' }),
|
|
/LIVE_TWITCH_VOD_ID/
|
|
);
|
|
});
|
|
|
|
test('redacts credentials and access tokens from nested external errors', () => {
|
|
const diagnostic = redactDiagnostic(
|
|
new Error('request failed for client-id-value client-secret-value bearer-token-value'),
|
|
['client-id-value', 'client-secret-value', 'bearer-token-value']
|
|
);
|
|
assert.equal(diagnostic.includes('client-id-value'), false);
|
|
assert.equal(diagnostic.includes('client-secret-value'), false);
|
|
assert.equal(diagnostic.includes('bearer-token-value'), false);
|
|
assert.match(diagnostic, /\[REDACTED\]/);
|
|
});
|
|
|
|
test('validates a real client-credentials token contract without exposing the token', () => {
|
|
const result = validateTwitchToken(
|
|
{ access_token: 'bearer-token-value', expires_in: 3600, token_type: 'bearer' },
|
|
{ client_id: 'client-id-value', expires_in: 3590 },
|
|
'client-id-value'
|
|
);
|
|
assert.deepEqual(result, { expiresInSeconds: 3590, tokenType: 'bearer' });
|
|
assert.throws(
|
|
() => validateTwitchToken(
|
|
{ access_token: 'bearer-token-value', expires_in: 3600, token_type: 'bearer' },
|
|
{ client_id: 'other-client', expires_in: 3590 },
|
|
'client-id-value'
|
|
),
|
|
/client id/
|
|
);
|
|
});
|
|
|
|
test('builds a bounded lowest-quality Streamlink download command', () => {
|
|
const output = path.join('C:\\runner\\temp', 'sample.ts');
|
|
assert.deepEqual(buildStreamlinkArguments('1234567890', output, 8), [
|
|
'--no-config',
|
|
'--no-plugin-cache',
|
|
'--no-plugin-sideloading',
|
|
'--http-timeout',
|
|
'20',
|
|
'--stream-timeout',
|
|
'30',
|
|
'--stream-segment-attempts',
|
|
'2',
|
|
'--stream-segment-timeout',
|
|
'20',
|
|
'--stream-segmented-duration',
|
|
'8',
|
|
'--output',
|
|
output,
|
|
'https://www.twitch.tv/videos/1234567890',
|
|
'worst'
|
|
]);
|
|
assert.throws(() => buildStreamlinkArguments('abc', output, 8), /VOD id/);
|
|
assert.throws(() => buildStreamlinkArguments('1234567890', output, 61), /duration/);
|
|
});
|
|
|
|
test('accepts only a bounded ffprobe-confirmed video artifact', () => {
|
|
const result = validateMediaProbe({
|
|
format: { duration: '8.25', size: '1048576' },
|
|
streams: [{ codec_type: 'video', codec_name: 'h264' }, { codec_type: 'audio', codec_name: 'aac' }]
|
|
}, 1048576);
|
|
assert.deepEqual(result, { bytes: 1048576, codec: 'h264', durationSeconds: 8.25 });
|
|
assert.throws(
|
|
() => validateMediaProbe({ format: { duration: '0.4', size: '40' }, streams: [{ codec_type: 'audio', codec_name: 'aac' }] }, 40),
|
|
/video stream/
|
|
);
|
|
assert.throws(
|
|
() => validateMediaProbe({ format: { duration: '8', size: String(40 * 1024 * 1024) }, streams: [{ codec_type: 'video', codec_name: 'h264' }] }, 40 * 1024 * 1024),
|
|
/32 MiB/
|
|
);
|
|
});
|
|
|
|
test('parses and pins the production GitHub release feed metadata', () => {
|
|
const yaml = [
|
|
'version: 1.0.18',
|
|
'files:',
|
|
' - url: Twitch-VOD-Manager-Setup-1.0.18.exe',
|
|
` sha512: ${UPDATE_SHA512}`,
|
|
' size: 120000000',
|
|
'path: Twitch-VOD-Manager-Setup-1.0.18.exe',
|
|
`sha512: ${UPDATE_SHA512}`,
|
|
"releaseDate: '2026-08-13T10:00:00.000Z'"
|
|
].join('\n');
|
|
const metadata = parseLatestYaml(yaml);
|
|
const result = validateProductionRelease(metadata, {
|
|
expectedVersion: '1.0.18',
|
|
expectedSha512: UPDATE_SHA512,
|
|
latestTag: 'v1.0.18'
|
|
});
|
|
assert.deepEqual(result, {
|
|
artifactName: 'Twitch-VOD-Manager-Setup-1.0.18.exe',
|
|
artifactSize: 120000000,
|
|
feedUrl: 'https://github.com/Sucukdeluxe/Twitch-VOD-Manager/releases/download/v1.0.18/',
|
|
version: '1.0.18'
|
|
});
|
|
assert.throws(
|
|
() => validateProductionRelease({ ...metadata, path: '../outside.exe' }, {
|
|
expectedVersion: '1.0.18',
|
|
expectedSha512: UPDATE_SHA512,
|
|
latestTag: 'v1.0.18'
|
|
}),
|
|
/artifact path/
|
|
);
|
|
assert.throws(
|
|
() => validateProductionRelease(metadata, {
|
|
expectedVersion: '1.0.19',
|
|
expectedSha512: UPDATE_SHA512,
|
|
latestTag: 'v1.0.18'
|
|
}),
|
|
/version/
|
|
);
|
|
});
|
|
|
|
test('resolves the public release tag to the pinned workflow commit', async () => {
|
|
const dependencies = releaseInspectionDependencies();
|
|
const originalFetch = global.fetch;
|
|
global.fetch = async () => { throw new Error('inspectProductionRelease bypassed its injected transport'); };
|
|
try {
|
|
const result = await inspectProductionRelease({
|
|
expectedCommitSha: UPDATE_COMMIT_SHA,
|
|
expectedSha512: UPDATE_SHA512,
|
|
expectedVersion: PACKAGE_VERSION
|
|
}, dependencies);
|
|
assert.equal(result.commitSha, UPDATE_COMMIT_SHA);
|
|
assert.deepEqual(dependencies.calls, [
|
|
'https://api.github.com/repos/Sucukdeluxe/Twitch-VOD-Manager/releases/latest',
|
|
`https://api.github.com/repos/Sucukdeluxe/Twitch-VOD-Manager/commits/v${PACKAGE_VERSION}`,
|
|
`https://github.com/Sucukdeluxe/Twitch-VOD-Manager/releases/download/v${PACKAGE_VERSION}/latest.yml`
|
|
]);
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test('rejects a public release tag that resolves to another commit', async () => {
|
|
const dependencies = releaseInspectionDependencies('c'.repeat(40));
|
|
const originalFetch = global.fetch;
|
|
global.fetch = async () => { throw new Error('inspectProductionRelease bypassed its injected transport'); };
|
|
try {
|
|
await assert.rejects(
|
|
inspectProductionRelease({
|
|
expectedCommitSha: UPDATE_COMMIT_SHA,
|
|
expectedSha512: UPDATE_SHA512,
|
|
expectedVersion: PACKAGE_VERSION
|
|
}, dependencies),
|
|
/release tag commit/i
|
|
);
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test('describes updater success as download-ready without claiming installation', () => {
|
|
const result = createUpdaterDownloadReadySummary({
|
|
artifact: { bytes: 120000000, sha512Verified: true },
|
|
downloadedVersion: PACKAGE_VERSION,
|
|
progressEvents: 4,
|
|
release: {
|
|
artifactName: `Twitch-VOD-Manager-Setup-${PACKAGE_VERSION}.exe`,
|
|
commitSha: UPDATE_COMMIT_SHA,
|
|
feedUrl: `https://github.com/Sucukdeluxe/Twitch-VOD-Manager/releases/download/v${PACKAGE_VERSION}/`
|
|
},
|
|
source: { sourceInstallerVerified: true },
|
|
sourceVersion: '0.0.1'
|
|
});
|
|
assert.deepEqual(result, {
|
|
artifact: {
|
|
bytes: 120000000,
|
|
name: `Twitch-VOD-Manager-Setup-${PACKAGE_VERSION}.exe`,
|
|
sha512Verified: true
|
|
},
|
|
feed: `https://github.com/Sucukdeluxe/Twitch-VOD-Manager/releases/download/v${PACKAGE_VERSION}/`,
|
|
scope: 'download-ready',
|
|
source: { installerDigestVerified: true, version: '0.0.1' },
|
|
target: {
|
|
commitSha: UPDATE_COMMIT_SHA,
|
|
downloadReady: true,
|
|
progressEvents: 4,
|
|
version: PACKAGE_VERSION
|
|
}
|
|
});
|
|
});
|
|
|
|
test('independently hashes the downloaded PE artifact from the isolated updater cache', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tvm-live-update-artifact-'));
|
|
try {
|
|
const artifact = path.join(root, 'Twitch-VOD-Manager-Setup-1.0.18.exe');
|
|
const contents = Buffer.alloc(1024 * 1024, 9);
|
|
contents[0] = 0x4d;
|
|
contents[1] = 0x5a;
|
|
fs.writeFileSync(artifact, contents);
|
|
const expectedSha512 = nodeCrypto.createHash('sha512').update(contents).digest('base64');
|
|
const result = await validateDownloadedReleaseArtifact(artifact, {
|
|
artifactName: path.basename(artifact),
|
|
artifactSize: contents.length,
|
|
expectedSha512
|
|
});
|
|
assert.deepEqual(result, { bytes: contents.length, sha512Verified: true });
|
|
|
|
contents[0] = 0;
|
|
fs.writeFileSync(artifact, contents);
|
|
await assert.rejects(
|
|
validateDownloadedReleaseArtifact(artifact, {
|
|
artifactName: path.basename(artifact),
|
|
artifactSize: contents.length,
|
|
expectedSha512: nodeCrypto.createHash('sha512').update(contents).digest('base64')
|
|
}),
|
|
/Windows executable/
|
|
);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('requires the packaged source to be older than the pinned release', () => {
|
|
assert.equal(compareVersions('1.0.17', '1.0.18'), -1);
|
|
assert.equal(compareVersions('1.0.18', '1.0.18'), 0);
|
|
assert.equal(compareVersions('1.0.19', '1.0.18'), 1);
|
|
for (const invalid of ['nightly', 'v1.0.18', '1.0.18-alpha', '1.0.18+build', '01.0.18', '1.00.18', '1.0.18.0', '1234567890.0.0']) {
|
|
assert.throws(() => compareVersions(invalid, '1.0.18'), /invalid/);
|
|
}
|
|
});
|
|
|
|
test('binds the updater cache record to the pinned artifact and digest', () => {
|
|
assert.deepEqual(validateUpdateCacheRecord({
|
|
fileName: 'Twitch-VOD-Manager-Setup-1.0.17.exe',
|
|
sha512: 'MkTghoBxIOnhP77tHV8szr8S1dbhItJId0atllZjWVrPwNLcvwnCyYjoUVEWV1czTqb5I+CUvqLiIjaamwglgw==',
|
|
isAdminRightsRequired: false
|
|
}, {
|
|
artifactName: 'Twitch-VOD-Manager-Setup-1.0.17.exe',
|
|
expectedSha512: 'MkTghoBxIOnhP77tHV8szr8S1dbhItJId0atllZjWVrPwNLcvwnCyYjoUVEWV1czTqb5I+CUvqLiIjaamwglgw=='
|
|
}), {
|
|
fileName: 'Twitch-VOD-Manager-Setup-1.0.17.exe',
|
|
sha512Verified: true
|
|
});
|
|
assert.throws(() => validateUpdateCacheRecord({
|
|
fileName: '..\\outside.exe',
|
|
sha512: UPDATE_SHA512
|
|
}, {
|
|
artifactName: 'Twitch-VOD-Manager-Setup-1.0.17.exe',
|
|
expectedSha512: UPDATE_SHA512
|
|
}), /file name/);
|
|
});
|
|
|
|
test('refuses cleanup at or outside the owned temporary root', () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tvm-live-contract-'));
|
|
try {
|
|
const child = path.join(root, 'owned');
|
|
fs.mkdirSync(child);
|
|
assert.equal(assertOwnedPath(child, root), path.resolve(child));
|
|
assert.throws(() => assertOwnedPath(root, root), /outside/);
|
|
assert.throws(() => assertOwnedPath(path.dirname(root), root), /outside/);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('removes the owned runner root after a successful operation', async () => {
|
|
let ownedRoot = '';
|
|
const result = await runWithOwnedRoot('tvm-live-success-', async (context) => {
|
|
ownedRoot = context.ownedRoot;
|
|
assert.equal(fs.existsSync(ownedRoot), true);
|
|
fs.writeFileSync(path.join(ownedRoot, 'artifact.tmp'), 'owned');
|
|
return 'completed';
|
|
});
|
|
assert.equal(result, 'completed');
|
|
assert.equal(fs.existsSync(ownedRoot), false);
|
|
});
|
|
|
|
test('removes the owned runner root when the operation fails', async () => {
|
|
let ownedRoot = '';
|
|
await assert.rejects(
|
|
runWithOwnedRoot('tvm-live-failure-', async (context) => {
|
|
ownedRoot = context.ownedRoot;
|
|
fs.writeFileSync(path.join(ownedRoot, 'artifact.tmp'), 'owned');
|
|
throw new Error('runner failed');
|
|
}),
|
|
/runner failed/
|
|
);
|
|
assert.equal(fs.existsSync(ownedRoot), false);
|
|
});
|
|
|
|
test('accepts only the three explicit execution modes', () => {
|
|
assert.equal(parseGateMode([]), 'all');
|
|
assert.equal(parseGateMode(['twitch']), 'twitch');
|
|
assert.equal(parseGateMode(['updater']), 'updater');
|
|
assert.throws(() => parseGateMode(['local-feed']), /mode/);
|
|
});
|
|
|
|
test('removes live credentials and injection variables from the packaged app environment', () => {
|
|
const result = sanitizeChildEnvironment({
|
|
PATH: 'C:\\Windows',
|
|
SystemRoot: 'C:\\Windows',
|
|
GITHUB_TOKEN: 'github-secret',
|
|
HTTP_PROXY: ['http://user', ':', 'password', '@', 'proxy.example.test'].join(''),
|
|
NODE_OPTIONS: '--require malicious.js',
|
|
TWITCH_VOD_MANAGER_LIVE_TWITCH_CLIENT_SECRET: 'twitch-secret',
|
|
SAFE_SETTING: 'kept'
|
|
}, { LOCALAPPDATA: 'C:\\isolated' });
|
|
assert.deepEqual(result, {
|
|
LOCALAPPDATA: 'C:\\isolated',
|
|
PATH: 'C:\\Windows',
|
|
SYSTEMROOT: 'C:\\Windows'
|
|
});
|
|
});
|