Files
chimera-Public/test/server/ch-0003-registry-queue-completion.test.js
T

410 lines
15 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const modulePathCache = new Map();
let activeTransferState = null;
function mockModule(resolvedPath, exportsValue) {
const current = require.cache[resolvedPath];
modulePathCache.set(resolvedPath, current);
require.cache[resolvedPath] = {
id: resolvedPath,
filename: resolvedPath,
loaded: true,
exports: exportsValue,
};
}
function restoreModules() {
for (const [resolvedPath, original] of modulePathCache) {
if (original === undefined) delete require.cache[resolvedPath];
else require.cache[resolvedPath] = original;
}
modulePathCache.clear();
}
function installRegistryMocks(rootDir) {
const createOperationResult = () => ({
status: 'ok',
services: [],
metadata: {},
scans: [],
health: {},
});
mockModule(path.resolve(rootDir, 'src/platform/data-paths.js'), {
mountedRoots: () => [],
normalizeAllowedRoots: (roots, fallback) => (Array.isArray(roots) && roots.length ? roots : [fallback]),
resolveAllowedPath: (candidate, _roots, options = {}) => {
if (!candidate) return options.fallbackRoot || '';
return path.isAbsolute(candidate) ? candidate : path.join(options.fallbackRoot || '', candidate);
},
});
mockModule(path.resolve(rootDir, 'src/hashing.js'), {
hashFile: async () => Buffer.from('fake-hash'),
});
mockModule(path.resolve(rootDir, 'src/services/elf-arsenal-client.js'), {
createElfArsenalClient: () => ({}),
});
mockModule(path.resolve(rootDir, 'src/services/local-library-scanner.js'), {
scanLocalLibraries: async () => ({ games: [], added: 0, total: 0 }),
});
mockModule(path.resolve(rootDir, 'src/services/transfer-service.js'), {
createTransferService: ({ broadcast, getCredentials }) => {
const executors = new Map();
const state = {
jobs: [],
jobsById: new Map(),
registerExecutor: (kind, factory) => {
executors.set(kind, factory);
},
enqueue: (type, options = {}, runner) => {
const jobId = `job-${state.jobs.length + 1}`;
let status = 'queued';
const progress = {};
const throwIfCancelled = () => {
if (status === 'cancelled') {
throw new Error('Taak geannuleerd');
}
};
const context = {
progress: () => {},
throwIfCancelled,
client: {
uploadFile: async () => ({ success: true }),
uploadDirectory: async () => ({ success: true }),
downloadFile: async () => ({ success: true }),
},
};
const job = {
jobId,
id: jobId,
type,
progress,
status,
promise: Promise.resolve(),
deduplicated: false,
};
const execute = async () => {
status = 'active';
if (runner) {
await runner(context);
} else {
const executor = executors.get(String((options.descriptor || {}).kind || type));
if (!executor) throw new Error(`No executor for ${options.descriptor?.kind || type}`);
await executor(options.descriptor || {})(context);
}
return { success: true, remotePath: options.remotePath };
};
const credentials = getCredentials?.();
if (credentials) {
context.credentials = credentials;
}
const resolved = execute()
.then(result => { status = 'done'; broadcast?.('transfer:update', state.state()); return result; })
.catch(error => {
status = 'failed';
throw error;
});
job.promise = resolved;
job.status = status;
state.jobs.push(job);
state.jobsById.set(jobId, job);
broadcast?.('transfer:update', state.state());
return job;
},
pause: () => {},
resume: () => {},
clearDone: () => {
state.jobs = [];
state.jobsById = new Map();
},
setConcurrent: () => 4,
setPolicy: options => options,
retry: () => false,
cancel: () => false,
state: () => ({ jobs: state.jobs.slice(), policy: { bandwidthLimitSupported: false } }),
close: () => {},
};
activeTransferState = state;
return state;
},
});
mockModule(path.resolve(rootDir, 'src/services/conversion-service.js'), {
createConversionService: () => ({}),
});
mockModule(path.resolve(rootDir, 'src/services/operations-service.js'), {
createOperationsService: () => new Proxy({}, {
get: (_target, prop) => {
if (prop === 'start' || prop === 'stop') return () => {};
return () => createOperationResult();
},
}),
});
mockModule(path.resolve(rootDir, 'src/services/ecosystem-service.js'), {
createEcosystemService: () => ({
stop: () => {},
payloads: {},
listRemoteSources: () => [],
classification: {},
downloads: () => [],
}),
});
mockModule(path.resolve(rootDir, 'src/services/ecosystem-intelligence-service.js'), {
createEcosystemIntelligenceService: () => ({ latestHealth: () => ({}) }),
});
mockModule(path.resolve(rootDir, 'src/services/game-update-service.js'), {
createGameUpdateService: () => ({}),
});
mockModule(path.resolve(rootDir, 'src/services/ps5debug-readonly-client.js'), {
createPs5DebugReadonlyClient: () => ({}),
});
mockModule(path.resolve(rootDir, 'src/services/remote-play-service.js'), {
createRemotePlayService: () => ({}),
});
mockModule(path.resolve(rootDir, 'src/services/backup-replication-service.js'), {
createBackupReplicationService: () => ({}),
});
mockModule(path.resolve(rootDir, 'src/services/payload-runtime-service.js'), {
createPayloadRuntimeService: () => ({
watchdog: () => ({}),
updatePlan: () => ({}),
refreshRepositories: () => ({}),
installUpdate: () => ({}),
}),
});
mockModule(path.resolve(rootDir, 'src/services/chimera-beacon-client.js'), {
createChimeraBeaconClient: () => ({}),
});
mockModule(path.resolve(rootDir, 'src/services/console-profile-service.js'), {
createConsoleProfileService: () => {
const secrets = new Map();
return {
activeId: () => 'console-1',
getSecret: key => secrets.get(String(key)),
setSecret: (key, value) => { secrets.set(String(key), value); },
syncActiveFromLegacy: () => {},
list: () => [],
comparison: () => ({ local: [], remote: [] }),
save: () => ({}),
activate: () => ({}),
remove: () => ({}),
storageTopology: () => ({ roots: [], games: [] }),
};
},
});
mockModule(path.resolve(rootDir, 'src/services/companion-worker-service.js'), {
createCompanionWorkerService: () => ({}),
});
mockModule(path.resolve(rootDir, 'src/services/audit-log-service.js'), {
createAuditLogService: () => ({
query: () => [],
health: () => ({ ok: true }),
exportAudit: () => Buffer.from('[]'),
}),
normalizeEntry: () => ({}),
LEVELS: {},
CLASSIFICATIONS: {},
});
mockModule(path.resolve(rootDir, 'src/services/ps5-klog-service.js'), {
createPs5KlogService: () => ({ start: () => {}, stop: () => {} }),
});
mockModule(path.resolve(rootDir, 'src/security/totp.js'), {
createTotpService: () => ({}),
});
mockModule(path.resolve(rootDir, 'src/services/notification-service.js'), {
createNotificationService: () => ({ emit: async () => ({}) }),
});
mockModule(path.resolve(rootDir, 'src/services/source-classification-service.js'), {
createSourceClassificationService: () => ({}),
});
mockModule(path.resolve(rootDir, 'src/services/game-metadata-view.js'), {
sanitizeOverride: value => value,
annotateRegionalVariants: value => value,
});
mockModule(path.resolve(rootDir, 'src/security/read-only-sql.js'), {
assertReadOnlySql: value => value,
});
mockModule(path.resolve(rootDir, 'src/server/register-functional-modules.js'), {
registerFunctionalModules: () => ({
metadata: {
queue: () => {},
queueDue: () => [],
status: () => ({}),
sync: () => [],
snapshot: () => ({}),
},
payloadCatalog: {
sync: async () => ({ count: 0 }),
seed: () => ({ count: 0 }),
snapshot: () => ({}),
},
payloadManager: {},
cheatsSync: async () => ({}),
}),
});
mockModule(path.resolve(rootDir, 'src/services/ezremote-server-client.js'), {
createEzRemoteServerClient: () => ({
probe: () => ({ status: 'mock' }),
downloads: () => [],
waitFor: () => ({}),
ensureDownload: async () => ({ completed: true, download: { transferred: 123, size: 123, path: '' } }),
}),
buildDownloadRequest: input => input,
DEFAULT_DESTINATION_ROOTS: ['/games/ps4', '/games/ps5'],
});
mockModule(path.resolve(rootDir, 'src/services/console-scan-reconciler.js'), {
reconcileConsoleScan: () => ({ applied: true, upserted: 0, cleared: 0 }),
});
}
function withMockedRegistry(callback) {
const rootDir = path.resolve(__dirname, '../..');
let originalCreateRegistry;
const registryPath = path.resolve(rootDir, 'src/server/create-registry.js');
let active = true;
const cleanup = () => {
if (!active) return;
active = false;
restoreModules();
delete require.cache[registryPath];
};
try {
installRegistryMocks(rootDir);
delete require.cache[registryPath];
originalCreateRegistry = require(registryPath);
const result = callback(originalCreateRegistry.createRegistry);
if (result && typeof result.then === 'function') {
return result.finally(cleanup);
}
cleanup();
return result;
} finally {
if (active === false) return;
cleanup();
}
}
function createDbWithTracking() {
const updates = [];
const games = new Map();
const db = new Proxy({
__updates: updates,
updateGame: (gameId, fields) => {
updates.push({ gameId, fields });
const current = games.get(String(gameId)) || { game_id: String(gameId), ftp_path: '' };
games.set(String(gameId), { ...current, ...fields });
},
getGame: gameId => games.get(String(gameId)) || null,
upsertGame: game => { if (game?.game_id) games.set(String(game.game_id), { ...(games.get(String(game.game_id)) || {}), ...game }); },
setGamePorked: () => {},
listBackporkFolders: () => [],
listBackups: () => [],
upsertGameMinimal: () => {},
setGameFirmwareLabel: () => {},
upsertBackup: () => {},
pruneGameFirmwareForFolder: () => {},
pruneOrphanScanGames: () => {},
syncBackedUpFlags: () => {},
beginBulkUpdate: () => {},
endBulkUpdate: () => {},
}, {
get(target, property) {
return Reflect.get(target, property) || (() => {});
},
});
return { db, updates };
}
function waitForJob(transferState, jobId) {
const job = transferState.jobsById.get(jobId);
if (!job) throw new Error(`Missing job ${jobId}`);
return job.promise;
}
test('FTP upload queue completion does not set installed', async () => {
await withMockedRegistry(async (createRegistry) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ch0003-upload-'));
try {
const localFile = path.join(root, 'game.pkg');
fs.writeFileSync(localFile, 'payload');
const { db, updates } = createDbWithTracking();
db.upsertGame({ game_id: 'PPSA12345', ftp_path: '/existing/path', installed: 1 });
const settings = { get: () => '127.0.0.1', set: () => {} };
const secrets = { get: () => '', has: () => false };
const ftp = { isConnected: () => true, hasCredentials: () => true };
const paths = { root, cache: root, backups: root, tmp: root, config: root };
const logger = { info() {}, warn() {}, error() {}, audit() {} };
const registry = createRegistry({
db,
ftp,
settings,
secrets,
paths,
logger,
capabilities: { features: {} },
broadcast: () => {},
appRoot: root,
});
const upload = registry.commands.get('ftp:upload');
assert.ok(upload);
const result = upload.handler({ localPath: localFile, remotePath: '/mnt/sandbox' });
await waitForJob(activeTransferState, result.jobId);
assert.equal(result.success, true);
assert.equal(updates.length, 0, 'no db.updateGame calls expected after upload completion');
} finally {
fs.rmSync(root, { recursive: true, force: true });
activeTransferState = null;
}
});
});
test('Backpork queue completion writes ftp_path but never flips installed', async () => {
await withMockedRegistry(async (createRegistry) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ch0003-pork-'));
try {
const backporkRoot = path.join(root, 'backporks');
fs.mkdirSync(backporkRoot, { recursive: true });
const gameFolder = path.join(backporkRoot, 'PPSA54321-pack');
fs.mkdirSync(gameFolder, { recursive: true });
const { db, updates } = createDbWithTracking();
db.upsertGame({ game_id: 'PPSA54321', ftp_path: '/mnt/sandbox/pfsmnt/PPSA54321', installed: 1 });
const settings = { get: () => '127.0.0.1', set: () => {} };
const secrets = { get: () => '', has: () => false };
const ftp = { isConnected: () => true, hasCredentials: () => true };
const paths = { root, cache: root, backups: root, tmp: root, config: root };
const logger = { info() {}, warn() {}, error() {}, audit() {} };
const registry = createRegistry({
db,
ftp,
settings,
secrets,
paths,
logger,
capabilities: { features: {} },
broadcast: () => {},
appRoot: root,
});
const pork = registry.commands.get('backporks:pork');
assert.ok(pork);
const queued = pork.handler({ game_id: 'PPSA54321', folder_name: '11.50.0', folder_path: backporkRoot });
await waitForJob(activeTransferState, queued.jobId);
assert.equal(updates.length >= 1, true);
assert.equal(updates.some(item => item.fields && Object.prototype.hasOwnProperty.call(item.fields, 'ftp_path')), true);
const hasInstalled = updates.some(item => item.fields && Object.prototype.hasOwnProperty.call(item.fields, 'installed'));
assert.equal(hasInstalled, false, 'backpork completion must not set installed');
} finally {
fs.rmSync(root, { recursive: true, force: true });
activeTransferState = null;
}
});
});