Files
chimera-Public/test/server/functional-modules.test.js

257 lines
13 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { registerFunctionalModules } = require('../../src/server/register-functional-modules');
const github = require('../../src/github');
test('autoloader snapshots can be taken, exported, imported and restored', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-snapshots-'));
const paths = {
root,
backups: path.join(root, 'backups'),
cache: path.join(root, 'cache'),
tmp: path.join(root, 'tmp'),
};
for (const dir of Object.values(paths)) fs.mkdirSync(dir, { recursive: true });
const remote = new Map([
['/data/ps5_autoloader/etaHEN.elf', Buffer.from('payload')],
['/data/ps5_autoloader/autoload.txt', Buffer.from('etaHEN.elf\n')],
]);
const ftp = {
isConnected: () => true,
hasCredentials: () => true,
listDir: async () => [{ name: 'etaHEN.elf', isFile: true, isDir: false, size: 7 }, { name: 'autoload.txt', isFile: true, isDir: false, size: 11 }],
downloadFile: async (remotePath, localPath) => {
if (!remote.has(remotePath)) throw new Error('missing remote file');
fs.writeFileSync(localPath, remote.get(remotePath));
},
uploadFile: async (localPath, remotePath) => { remote.set(remotePath, fs.readFileSync(localPath)); },
};
const definitions = new Map();
registerFunctionalModules({
register: definition => definitions.set(definition.name, definition),
db: {}, ftp,
settings: { get: (key, fallback) => key === 'ftp.host' ? '192.168.1.105' : fallback },
secrets: { get: () => '', has: () => false },
paths,
logger: { warn() {}, info() {}, error() {} },
broadcast() {},
});
assert.equal(definitions.get('payload-manager:autoload-repair-plan').classification, 'sensitive');
assert.equal(definitions.get('payload-manager:autoload-repair-apply').classification, 'destructive');
assert.equal(definitions.get('payload-manager:autoload-repair-apply').confirmation, 'PERSIST PS5 PAYLOAD MANAGER 0.5.1');
assert.equal(definitions.get('payload-manager:autoload-repair-rollback-plan').classification, 'sensitive');
assert.equal(definitions.get('payload-manager:autoload-repair-rollback').classification, 'destructive');
assert.equal(definitions.get('payload-manager:autoload-repair-rollback').confirmation, 'HERSTEL ORIGINELE BOOTKETEN');
assert.equal(definitions.get('etahen:autoload-persistence-plan').classification, 'sensitive');
assert.equal(definitions.get('etahen:autoload-persistence-apply').confirmation, 'PERSIST DIRECT ETAHEN 2.6B');
assert.equal(definitions.get('etahen:direct-start').classification, 'destructive');
assert.equal(definitions.get('etahen:direct-start').confirmation, 'START DIRECT ETAHEN 2.6B');
assert.equal(definitions.get('payload-manager:verified-start').classification, 'destructive');
assert.equal(definitions.get('payload-manager:verified-start').confirmation, 'START VERIFIED PS5 PAYLOAD MANAGER 0.5.1');
assert.equal(definitions.get('etahen:autoload-persistence-rollback-plan').classification, 'sensitive');
assert.equal(definitions.get('etahen:autoload-persistence-rollback').confirmation, 'HERSTEL VOOR ETAHEN 2.6B');
const taken = await definitions.get('snapshot:autoloader:take').handler({ label: 'Werkend' });
assert.equal(taken.meta.label, 'Werkend');
assert.equal(taken.meta.files.length, 1);
assert.equal(taken.meta.hasAutoload, true);
const listed = await definitions.get('snapshot:autoloader:list').handler();
assert.equal(listed.length, 1);
assert.equal(listed[0].id, taken.meta.id);
const exported = await definitions.get('snapshot:autoloader:export').handler({ id: taken.meta.id });
const archivePath = path.join(paths.cache, decodeURIComponent(exported.downloadUrl.replace('/pork-cache/', '')));
assert.equal(fs.existsSync(archivePath), true);
const imported = await definitions.get('snapshot:autoloader:import').handler({ data: fs.readFileSync(archivePath), filename: exported.filename, label: 'Import' });
assert.equal(imported.meta.label, 'Import');
assert.equal(imported.meta.files[0].name, 'etaHEN.elf');
remote.clear();
const restored = await definitions.get('snapshot:autoloader:restore').handler({ id: imported.meta.id });
assert.equal(restored.ok, true);
assert.equal(restored.fileCount, 1);
assert.equal(remote.get('/data/ps5_autoloader/etaHEN.elf').toString(), 'payload');
assert.equal(remote.get('/data/ps5_autoloader/autoload.txt').toString(), 'etaHEN.elf\n');
});
test('Y2JB generator can use verified payloads from the Chimera catalog without a browser upload', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-y2jb-catalog-'));
const paths = { root, backups: path.join(root, 'backups'), cache: path.join(root, 'cache'), tmp: path.join(root, 'tmp') };
const payloadRoot = path.join(root, 'payloads');
for (const dir of [...Object.values(paths), payloadRoot]) fs.mkdirSync(dir, { recursive: true });
const payloadBytes = Buffer.from('verified catalog payload');
const payloadPath = path.join(payloadRoot, 'catalog-test.elf');
fs.writeFileSync(payloadPath, payloadBytes);
const payloadHash = crypto.createHash('sha256').update(payloadBytes).digest('hex');
const definitions = new Map();
registerFunctionalModules({
register: definition => definitions.set(definition.name, definition),
db: {
getPayloadCatalog: () => ({
files: [{
id: 42,
asset_name: 'catalog-test.elf',
local_path: payloadPath,
local_size: payloadBytes.length,
local_hash: payloadHash,
local_version: '1.2.3',
source_name: 'Fixture source',
}],
}),
},
ftp: { isConnected: () => false, hasCredentials: () => false },
settings: { get: (key, fallback) => key === 'payload.localPath' ? payloadRoot : fallback },
secrets: { get: () => '', has: () => false },
paths,
logger: { warn() {}, info() {}, error() {} },
broadcast() {},
});
try {
const catalog = definitions.get('y2jbgen:catalog').handler();
assert.deepEqual(catalog.items, [{
id: '42',
name: 'catalog-test.elf',
size: payloadBytes.length,
sha256: payloadHash,
version: '1.2.3',
source: 'Fixture source',
}]);
const result = await definitions.get('y2jbgen:build').handler({
catalogPayloadIds: ['42'],
entries: [{ type: 'payload', value: 'catalog-test.elf' }],
});
assert.equal(result.outputZip, 'y2jb_update.zip');
assert.match(result.updateInfo, /ps5_autoloader\/catalog-test\.elf\|24/);
fs.writeFileSync(payloadPath, 'tampered');
await assert.rejects(
() => definitions.get('y2jbgen:build').handler({
catalogPayloadIds: ['42'],
entries: [{ type: 'payload', value: 'catalog-test.elf' }],
}),
/Checksum/
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('media catalog persists deltas and reuses unchanged PS5 directories', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-media-catalog-'));
const paths = { root, backups: path.join(root, 'backups'), cache: path.join(root, 'cache'), tmp: path.join(root, 'tmp') };
for (const dir of Object.values(paths)) fs.mkdirSync(dir, { recursive: true });
let marker = new Date('2026-07-16T10:00:00Z');
let files = [{ name: '20260716_120000.jpg', isDir: false, size: 123, modifiedAt: marker }];
let failChild = false;
const calls = [];
const ftp = {
isConnected: () => true, hasCredentials: () => true,
listDir: async remotePath => {
calls.push(remotePath);
if (remotePath === '/user/av_contents/photo') return [{ name: 'CUSA12345', isDir: true, size: 0, modifiedAt: marker }];
if (remotePath === '/user/av_contents/photo/CUSA12345') { if (failChild) throw new Error('temporary FTP failure'); return files; }
if (remotePath === '/user/av_contents/video') return [];
return [];
},
};
const definitions = new Map();
registerFunctionalModules({
register: definition => definitions.set(definition.name, definition), db: {}, ftp,
settings: { get: (key, fallback) => key === 'ftp.host' ? '127.0.0.1' : fallback },
secrets: { get: () => '', has: () => false }, paths,
logger: { warn() {}, info() {}, error() {} }, broadcast() {},
});
try {
const scan = definitions.get('media:scan');
const first = await scan.handler({ force: true });
assert.equal(first.stats.total, 1);
assert.equal(first.delta.added, 1);
assert.equal(first.scan.mode, 'full');
const catalogFile = path.join(paths.cache, 'media', 'catalog.json');
const cached = JSON.parse(fs.readFileSync(catalogFile, 'utf8'));
cached.scannedAt = 1;
fs.writeFileSync(catalogFile, JSON.stringify(cached));
calls.length = 0;
const second = await scan.handler({ force: false });
assert.equal(second.scan.mode, 'incremental');
assert.equal(second.scan.reusedDirectories, 1);
assert.deepEqual(calls, ['/user/av_contents/photo', '/user/av_contents/video']);
marker = new Date('2026-07-16T10:05:00Z');
files = [...files, { name: '20260716_120500.png', isDir: false, size: 456, modifiedAt: marker }];
const stale = JSON.parse(fs.readFileSync(catalogFile, 'utf8'));
stale.scannedAt = 1;
fs.writeFileSync(catalogFile, JSON.stringify(stale));
const third = await scan.handler({ force: false });
assert.equal(third.stats.total, 2);
assert.equal(third.delta.added, 1);
assert.equal(third.scan.mode, 'incremental');
const loaded = definitions.get('media:catalog').handler();
assert.equal(loaded.items.length, 2);
assert.equal(Object.prototype.hasOwnProperty.call(loaded, 'directories'), true);
assert.equal(loaded.directories, undefined);
marker = new Date('2026-07-16T10:10:00Z');
failChild = true;
const beforeFailure = JSON.parse(fs.readFileSync(catalogFile, 'utf8'));
beforeFailure.scannedAt = 1;
fs.writeFileSync(catalogFile, JSON.stringify(beforeFailure));
const preserved = await scan.handler({ force: false });
assert.equal(preserved.stats.total, 2);
assert.equal(preserved.scan.errors.length, 1);
assert.equal(preserved.scan.errors[0].preservedItems, 2);
assert.equal(preserved.delta.removed, 0);
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});
test('payload synchronization stops API calls after a GitHub rate limit and preserves cached rows', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-payload-rate-limit-'));
const paths = { root, backups: path.join(root, 'backups'), cache: path.join(root, 'cache'), tmp: path.join(root, 'tmp') };
for (const dir of Object.values(paths)) fs.mkdirSync(dir, { recursive: true });
const sources = [1, 2, 3].map(id => ({ id, name: `Source ${id}`, github_url: `https://github.com/example/repo-${id}`, enabled: 1, built_in: 0 }));
const updates = [];
const progress = [];
let calls = 0;
const originalFetchLatestRelease = github.fetchLatestRelease;
github.fetchLatestRelease = async () => {
calls += 1;
const error = new Error('GitHub API-limiet bereikt; probeer later opnieuw.');
error.code = 'GITHUB_RATE_LIMIT';
error.retryAt = Date.now() + 60_000;
throw error;
};
const definitions = new Map();
try {
registerFunctionalModules({
register: definition => definitions.set(definition.name, definition),
db: {
upsertPayloadSource() {}, listPayloadSources: () => sources, removePayloadSource() {},
getPayloadFiles: sourceId => [{ id: sourceId, asset_name: `cached-${sourceId}.elf` }],
updatePayloadSourceError: (sourceId, error) => updates.push({ sourceId, error }),
},
ftp: { isConnected: () => false, hasCredentials: () => false },
settings: { get: (key, fallback) => fallback }, secrets: { get: () => '', has: () => false }, paths,
logger: { warn() {}, info() {}, error() {} }, broadcast: (name, value) => progress.push({ name, value }),
});
const result = await definitions.get('payload:check-updates').handler({});
assert.equal(calls, 1);
assert.equal(result.length, 3);
assert.equal(result[0].deferred, undefined);
assert.equal(result[1].deferred, true);
assert.equal(result[2].deferred, true);
assert.equal(updates.length, 1);
assert.equal(result[1].files[0].asset_name, 'cached-2.elf');
assert.equal(progress.filter(item => item.value.status === 'deferred').length, 2);
} finally {
github.fetchLatestRelease = originalFetchLatestRelease;
fs.rmSync(root, { recursive: true, force: true });
}
});