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.
165 lines
7.7 KiB
JavaScript
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');
|
|
});
|