Files
Twitch-VOD-Manager/scripts/e2e-test-environment.js
T
Sucukdeluxe 66e84508c8 ci: add Windows quality and packaging gates
Teach ESLint the classic renderer and mixed CommonJS harness runtimes while retaining actionable rules. Add deterministic credential, lockfile, release-manifest, and lint contracts plus equivalent GitHub and Gitea Windows pipelines. Gate clean installs, focused offline smoke coverage, builds, packaged launches, and silent installer verification without enabling authenticated network tests.
2026-08-12 02:32:07 +02:00

243 lines
7.6 KiB
JavaScript

const fs = require('fs');
const os = require('os');
const path = require('path');
const OFFLINE_PROXY = 'http://127.0.0.1:1';
function buildSafeConfig(downloadsDir, overrides = {}) {
return {
theme: 'twitch',
download_mode: 'full',
part_minutes: 120,
language: 'en',
filename_template_vod: '{title}.mp4',
filename_template_parts: '{date}_Part{part_padded}.mp4',
filename_template_clip: '{date}_{part}.mp4',
smart_queue_scheduler: false,
performance_mode: 'balanced',
prevent_duplicate_downloads: true,
persist_queue_on_restart: false,
metadata_cache_minutes: 10,
parallel_downloads: 1,
downloaded_vod_ids: [],
streamlink_quality: 'best',
notify_on_each_completion: false,
streamlink_disable_ads: true,
auto_record_poll_seconds: 90,
auto_cleanup_days: 30,
auto_cleanup_target: 'live_only',
auto_cleanup_action: 'archive',
log_stream_events: false,
auto_vod_download_poll_minutes: 15,
auto_vod_max_age_hours: 24,
...overrides,
client_id: '',
download_path: downloadsDir,
streamers: [],
auto_resume_queue_on_startup: false,
auto_record_streamers: [],
download_chat_replay: false,
capture_live_chat: false,
discord_notify_live_start: false,
discord_notify_live_end: false,
discord_notify_vod_complete: false,
discord_notify_vod_auto_queued: false,
auto_cleanup_enabled: false,
auto_vod_download_streamers: [],
auto_resume_live_recording: false,
auto_merge_resumed_parts: false,
delete_parts_after_merge: false
};
}
function writeJson(filePath, value) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}
function createE2eEnvironment(name, configOverrides = {}) {
const safeName = String(name || 'smoke').replace(/[^a-z0-9_-]+/gi, '-').toLowerCase();
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), `twitch-vod-manager-${safeName}-`));
const programDataDir = path.join(rootDir, 'programdata');
const userDataDir = path.join(rootDir, 'userdata');
const downloadsDir = path.join(rootDir, 'downloads');
const appDataDir = path.join(programDataDir, 'Twitch_VOD_Manager');
const mediaDir = path.join(rootDir, 'media');
const configFile = path.join(appDataDir, 'config.json');
const queueFile = path.join(appDataDir, 'download_queue.json');
for (const directory of [programDataDir, userDataDir, downloadsDir, appDataDir, mediaDir]) {
fs.mkdirSync(directory, { recursive: true });
}
writeJson(configFile, buildSafeConfig(downloadsDir, configOverrides));
writeJson(queueFile, []);
return {
rootDir,
programDataDir,
userDataDir,
downloadsDir,
appDataDir,
mediaDir,
configFile,
queueFile
};
}
function writeE2eConfig(environment, overrides = {}) {
const config = buildSafeConfig(environment.downloadsDir, overrides);
writeJson(environment.configFile, config);
return config;
}
function readE2eConfig(environment) {
const databasePath = path.join(environment.appDataDir, 'app.db');
if (fs.existsSync(databasePath)) {
const Database = require('better-sqlite3');
const database = new Database(databasePath, { readonly: true });
try {
const rows = database.prepare('SELECT key, value FROM config_kv').all();
return Object.fromEntries(rows.map((row) => [row.key, JSON.parse(row.value)]));
} finally {
database.close();
}
}
return JSON.parse(fs.readFileSync(environment.configFile, 'utf8'));
}
function getElectronLaunchOptions(environment, extraArgs = []) {
return {
executablePath: require('electron'),
args: [
`--user-data-dir=${environment.userDataDir}`,
`--proxy-server=${OFFLINE_PROXY}`,
'--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE localhost',
...extraArgs,
'.'
],
cwd: path.resolve(__dirname, '..'),
env: {
...process.env,
PROGRAMDATA: environment.programDataDir,
HTTP_PROXY: OFFLINE_PROXY,
HTTPS_PROXY: OFFLINE_PROXY,
ALL_PROXY: OFFLINE_PROXY,
NO_PROXY: '',
http_proxy: OFFLINE_PROXY,
https_proxy: OFFLINE_PROXY,
all_proxy: OFFLINE_PROXY,
no_proxy: ''
}
};
}
function isSamePath(left, right) {
return path.resolve(left).toLowerCase() === path.resolve(right).toLowerCase();
}
async function verifyE2eIsolation(app, win, environment) {
const main = await app.evaluate(({ app: electronApp }) => ({
programData: process.env.PROGRAMDATA || '',
userData: electronApp.getPath('userData')
}));
const rendererDownloadPath = await win.evaluate(async () => {
const currentConfig = await window.api.getConfig();
return currentConfig.download_path;
});
const verification = {
rootDir: environment.rootDir,
programData: main.programData,
userData: main.userData,
downloadPath: rendererDownloadPath,
programDataIsolated: isSamePath(main.programData, environment.programDataDir),
userDataIsolated: isSamePath(main.userData, environment.userDataDir),
downloadPathIsolated: isSamePath(rendererDownloadPath, environment.downloadsDir)
};
if (!verification.programDataIsolated || !verification.userDataIsolated || !verification.downloadPathIsolated) {
throw new Error(`E2E isolation verification failed: ${JSON.stringify(verification)}`);
}
return verification;
}
async function installOfflineFixtures(app) {
return app.evaluate(({ app: electronApp, ipcMain }) => {
const offlineState = {
httpProxy: process.env.HTTP_PROXY || '',
httpsProxy: process.env.HTTPS_PROXY || '',
allProxy: process.env.ALL_PROXY || '',
noProxy: process.env.NO_PROXY ?? null,
chromiumProxy: electronApp.commandLine.getSwitchValue('proxy-server')
};
const guardsActive =
offlineState.httpProxy === offlineState.httpsProxy &&
offlineState.httpProxy === offlineState.allProxy &&
offlineState.httpProxy.startsWith('http://127.0.0.1:') &&
offlineState.noProxy === '' &&
offlineState.chromiumProxy === offlineState.httpProxy;
if (!guardsActive) {
throw new Error(`Offline bootstrap verification failed: ${JSON.stringify(offlineState || null)}`);
}
const vods = [{
id: '999999999999999',
title: 'Offline fixture VOD',
created_at: '2026-02-01T00:00:00Z',
duration: '1h0m0s',
thumbnail_url: '',
url: 'https://www.twitch.tv/videos/999999999999999',
view_count: 123,
stream_id: 'offline-fixture-stream'
}];
const replaceHandler = (channel, handler) => {
ipcMain.removeHandler(channel);
ipcMain.handle(channel, handler);
};
replaceHandler('get-user-id', async () => 'offline-fixture-user');
replaceHandler('get-vods', async () => vods);
replaceHandler('get-streamer-profile', async () => null);
replaceHandler('run-preflight', async () => ({
ok: true,
autoFixApplied: false,
checks: {
internet: true,
streamlink: true,
ffmpeg: true,
ffprobe: true,
downloadPathWritable: true
},
messages: ['Offline fixture'],
timestamp: '2026-01-01T00:00:00Z'
}));
replaceHandler('check-update', async () => ({ checking: true, offlineFixture: true }));
return {
network: 'blocked',
twitch: 'fixture',
updater: 'fixture',
guards: offlineState
};
});
}
function cleanupE2eEnvironment(environment) {
if (!environment?.rootDir) {
return;
}
fs.rmSync(environment.rootDir, { recursive: true, force: true });
}
module.exports = {
buildSafeConfig,
createE2eEnvironment,
writeE2eConfig,
readE2eConfig,
getElectronLaunchOptions,
verifyE2eIsolation,
installOfflineFixtures,
cleanupE2eEnvironment
};