Files
Multi-Hoster-Upload/tests/upload-audit.test.js
T
Sucukdeluxe 28a475401e fix: preserve rotation logs during quit fallback
Route synchronous account-rotation shutdown flushes through the internal log writer so failures under userData/logs retry userData/internal-logs in the same order as runtime writes.

Keep buffered lines until a target accepts the full chunk, report the successful fallback as the active path, and retain the buffer when every internal target fails.

Cover the real EISDIR primary-path case, complete fallback failure, and main-process quit delegation without launching Electron.
2026-08-26 12:37:17 +02:00

165 lines
7.7 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
createInternalLogPathResolver,
createInternalLogWriter,
createUploadAuditWriter,
getLogOpenDirectory
} = require('../lib/upload-audit');
function createTempDirectory(t, prefix) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return directory;
}
test('internal audit and account rotation paths share the userData logs directory', (t) => {
const directory = createTempDirectory(t, 'mhu-internal-log-paths-');
const userDataPath = path.join(directory, 'user-data');
const resolveInternalLogPath = createInternalLogPathResolver({ fs, path, userDataPath });
assert.equal(resolveInternalLogPath('upload-audit.log'), path.join(userDataPath, 'logs', 'upload-audit.log'));
assert.equal(resolveInternalLogPath('account-rotation.log'), path.join(userDataPath, 'logs', 'account-rotation.log'));
});
test('internal log paths fall back to another directory below userData without touching Desktop files', (t) => {
const directory = createTempDirectory(t, 'mhu-internal-log-fallback-');
const userDataPath = path.join(directory, 'user-data');
const desktopPath = path.join(directory, 'Desktop');
const desktopAuditPath = path.join(desktopPath, 'upload-audit.log');
const desktopRotationPath = path.join(desktopPath, 'account-rotation.log');
fs.mkdirSync(userDataPath, { recursive: true });
fs.mkdirSync(desktopPath, { recursive: true });
fs.writeFileSync(path.join(userDataPath, 'logs'), 'blocked');
fs.writeFileSync(desktopAuditPath, 'existing audit');
fs.writeFileSync(desktopRotationPath, 'existing rotation');
const resolveInternalLogPath = createInternalLogPathResolver({ fs, path, userDataPath });
assert.equal(resolveInternalLogPath('upload-audit.log'), path.join(userDataPath, 'internal-logs', 'upload-audit.log'));
assert.equal(resolveInternalLogPath('account-rotation.log'), path.join(userDataPath, 'internal-logs', 'account-rotation.log'));
assert.equal(fs.readFileSync(desktopAuditPath, 'utf8'), 'existing audit');
assert.equal(fs.readFileSync(desktopRotationPath, 'utf8'), 'existing rotation');
});
test('internal rotation writer retries inside userData and reports the file that accepted the write', async (t) => {
const directory = createTempDirectory(t, 'mhu-internal-log-writer-');
const userDataPath = path.join(directory, 'user-data');
const primaryPath = path.join(userDataPath, 'logs', 'account-rotation.log');
const fallbackPath = path.join(userDataPath, 'internal-logs', 'account-rotation.log');
const appendTargets = [];
const rotationTargets = [];
const testFs = {
mkdirSync: fs.mkdirSync,
appendFileSync: fs.appendFileSync,
promises: {
appendFile: async (targetPath, ...args) => {
appendTargets.push(targetPath);
if (targetPath === primaryPath) throw Object.assign(new Error('blocked primary'), { code: 'EACCES' });
return fs.promises.appendFile(targetPath, ...args);
}
}
};
const targets = [primaryPath, fallbackPath];
const resolveInternalLogPath = (_fileName, excludedPaths = new Set()) => targets.find(targetPath => !excludedPaths.has(targetPath)) || null;
const writer = createInternalLogWriter({
fs: testFs,
path,
fileName: 'account-rotation.log',
resolveInternalLogPath,
rotateLogFile: targetPath => rotationTargets.push(targetPath),
reportError: () => {},
retryDelays: [0, 0]
});
assert.equal(await writer.append('[rotation]\n', 'rot-log'), true);
assert.deepEqual(appendTargets, [primaryPath, fallbackPath]);
assert.deepEqual(rotationTargets, [primaryPath, fallbackPath]);
assert.equal(writer.getActivePath(), fallbackPath);
assert.equal(fs.readFileSync(fallbackPath, 'utf8'), '[rotation]\n');
});
test('synchronous rotation flush falls back completely and retains buffered lines when every target fails', (t) => {
const directory = createTempDirectory(t, 'mhu-internal-log-sync-writer-');
const userDataPath = path.join(directory, 'user-data');
const primaryPath = path.join(userDataPath, 'logs', 'account-rotation.log');
const fallbackPath = path.join(userDataPath, 'internal-logs', 'account-rotation.log');
fs.mkdirSync(primaryPath, { recursive: true });
const resolveInternalLogPath = createInternalLogPathResolver({ fs, path, userDataPath });
const writer = createInternalLogWriter({
fs,
path,
fileName: 'account-rotation.log',
resolveInternalLogPath,
rotateLogFile: () => {},
reportError: () => {}
});
const buffer = ['[rotation 1]\n', '[rotation 2]\n'];
assert.equal(writer.flushSync(buffer, 'rot-log'), true);
assert.deepEqual(buffer, []);
assert.equal(writer.getActivePath(), fallbackPath);
assert.equal(fs.readFileSync(fallbackPath, 'utf8'), '[rotation 1]\n[rotation 2]\n');
const failedUserDataPath = path.join(directory, 'failed-user-data');
const failedPrimaryPath = path.join(failedUserDataPath, 'logs', 'account-rotation.log');
const failedFallbackPath = path.join(failedUserDataPath, 'internal-logs', 'account-rotation.log');
fs.mkdirSync(failedPrimaryPath, { recursive: true });
fs.mkdirSync(failedFallbackPath, { recursive: true });
const failedWriter = createInternalLogWriter({
fs,
path,
fileName: 'account-rotation.log',
resolveInternalLogPath: createInternalLogPathResolver({ fs, path, userDataPath: failedUserDataPath }),
rotateLogFile: () => {},
reportError: () => {}
});
const retainedBuffer = ['[retained 1]\n', '[retained 2]\n'];
assert.equal(failedWriter.flushSync(retainedBuffer, 'rot-log'), false);
assert.deepEqual(retainedBuffer, ['[retained 1]\n', '[retained 2]\n']);
assert.equal(failedWriter.getActivePath(), null);
});
test('quit flush delegates the rotation buffer to the synchronous internal writer', () => {
const mainSource = fs.readFileSync(path.join(__dirname, '..', 'main.js'), 'utf8');
assert.match(mainSource, /_rotLogWriter\.flushSync\(_rotLogBuffer, 'rot-log'\);/);
assert.doesNotMatch(mainSource, /appendFileSync\(getRotLogPath\(\), _rotLogBuffer\.join\(''\)/);
});
test('upload audit writer leaves the configured fileuploader log contract unchanged', async (t) => {
const directory = createTempDirectory(t, 'mhu-upload-log-contract-');
const userDataPath = path.join(directory, 'user-data');
const customUploadDirectory = path.join(directory, 'custom-upload-logs');
const uploadLogPath = path.join(customUploadDirectory, '13-08-2026-mdu-session-14-20-123456.log');
fs.mkdirSync(customUploadDirectory, { recursive: true });
fs.writeFileSync(uploadLogPath, 'existing upload entry\n');
const resolveInternalLogPath = () => path.join(userDataPath, 'logs', 'upload-audit.log');
const writer = createUploadAuditWriter({
fs,
path,
resolveInternalLogPath,
rotateLogFile: () => {},
reportError: () => {},
retryDelays: [0]
});
assert.equal(await writer.append('# UPLOAD-PLAN {"plannedUploadCount":4}\r\n', 'upload-plan'), true);
assert.equal(writer.getActivePath(), path.join(userDataPath, 'logs', 'upload-audit.log'));
assert.equal(fs.readFileSync(uploadLogPath, 'utf8'), 'existing upload entry\n');
assert.equal(fs.readFileSync(writer.getActivePath(), 'utf8'), '# UPLOAD-PLAN {"plannedUploadCount":4}\r\n');
});
test('log opening uses the reported internal file directory before the general fallback directory', () => {
assert.equal(
getLogOpenDirectory('C:\\AppData\\Multi-Hoster\\logs\\upload-audit.log', 'C:\\Program Files\\Multi-Hoster', path.win32),
'C:\\AppData\\Multi-Hoster\\logs'
);
assert.equal(getLogOpenDirectory(null, 'C:\\Program Files\\Multi-Hoster', path.win32), 'C:\\Program Files\\Multi-Hoster');
});