This commit is contained in:
@@ -44,7 +44,8 @@ function fixture(options = {}) {
|
||||
encryptField: options.encryptField || encrypt,
|
||||
decryptField: options.decryptField || decrypt,
|
||||
isEncrypted: options.isEncrypted || isCanonicalEnvelope,
|
||||
fsImpl: options.fsImpl
|
||||
fsImpl: options.fsImpl,
|
||||
now: options.now
|
||||
})
|
||||
};
|
||||
}
|
||||
@@ -82,12 +83,14 @@ describe('encrypted online backup keyring', () => {
|
||||
assert.equal(document.version, 2);
|
||||
assert.equal(document.generation, 1);
|
||||
assert.equal(document.keys.length, 1);
|
||||
assert.equal(document.keys[0].expiresAt, null);
|
||||
assert.equal(fs.readFileSync(filePath, 'utf8').includes(key), false);
|
||||
assert.deepEqual(snapshot.issues, []);
|
||||
assert.deepEqual(snapshot.entries, [{
|
||||
id: parseOnlineBackupKey(key).id,
|
||||
displayKey: `${key.slice(0, 9)}…${key.slice(-4)}`,
|
||||
createdAt: timestamp
|
||||
createdAt: timestamp,
|
||||
expiresAt: null
|
||||
}]);
|
||||
assert.equal(Object.isFrozen(snapshot), true);
|
||||
assert.equal(Object.isFrozen(snapshot.entries), true);
|
||||
@@ -95,6 +98,25 @@ describe('encrypted online backup keyring', () => {
|
||||
assert.equal(await keyring.getKey(prepared.id), key);
|
||||
});
|
||||
|
||||
it('removes expired local keys from the managed list and encrypted keyring', async () => {
|
||||
let currentTime = new Date(timestamp).getTime();
|
||||
const { filePath, keyring } = fixture({ now: () => currentTime });
|
||||
const finite = validKey();
|
||||
const unlimited = validKey();
|
||||
const expiresAt = '2026-08-23T10:00:00.000Z';
|
||||
await keyring.commit(keyring.prepare(finite, timestamp, expiresAt));
|
||||
await keyring.commit(keyring.prepare(unlimited, timestamp, null));
|
||||
|
||||
assert.deepEqual((await keyring.list()).entries.map(entry => entry.expiresAt), [expiresAt, null]);
|
||||
currentTime = new Date(expiresAt).getTime();
|
||||
const snapshot = await keyring.list();
|
||||
|
||||
assert.deepEqual(snapshot.entries.map(entry => entry.id), [parseOnlineBackupKey(unlimited).id]);
|
||||
assert.equal(await keyring.getKey(parseOnlineBackupKey(finite).id), null);
|
||||
const document = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
assert.equal(document.keys.some(entry => entry.id === parseOnlineBackupKey(finite).id), false);
|
||||
});
|
||||
|
||||
it('keeps a valid v1 primary authoritative over an older v1 backup and migrates to v2', async () => {
|
||||
const { filePath, backupPath, keyring } = fixture();
|
||||
const backupKey = validKey();
|
||||
@@ -342,7 +364,8 @@ describe('encrypted online backup keyring', () => {
|
||||
assert.deepEqual((await keyring.list()).entries, [{
|
||||
id: parseOnlineBackupKey(key).id,
|
||||
displayKey: `${key.slice(0, 9)}…${key.slice(-4)}`,
|
||||
createdAt: timestamp
|
||||
createdAt: timestamp,
|
||||
expiresAt: null
|
||||
}]);
|
||||
});
|
||||
|
||||
|
||||
@@ -28,10 +28,12 @@ function createFixture(overrides = {}) {
|
||||
entries.set(id, {
|
||||
id,
|
||||
displayKey: `${key.slice(0, 9)}…${key.slice(-4)}`,
|
||||
createdAt: '2026-08-22T10:00:00.000Z'
|
||||
createdAt: '2026-08-22T10:00:00.000Z',
|
||||
expiresAt: null
|
||||
});
|
||||
}
|
||||
let createdAt;
|
||||
let expiresAt;
|
||||
let createArguments;
|
||||
const removalPlans = new WeakMap();
|
||||
const keyring = {
|
||||
@@ -43,11 +45,12 @@ function createFixture(overrides = {}) {
|
||||
issues: overrides.listIssues || []
|
||||
};
|
||||
},
|
||||
prepare: (value, timestamp) => {
|
||||
prepare: (value, timestamp, expiration) => {
|
||||
events.push('prepare');
|
||||
if (overrides.prepareError) throw overrides.prepareError;
|
||||
createdAt = timestamp;
|
||||
return { id, encryptedKey: 'encrypted-key', createdAt: timestamp };
|
||||
expiresAt = expiration ?? null;
|
||||
return { id, encryptedKey: 'encrypted-key', createdAt: timestamp, expiresAt };
|
||||
},
|
||||
commit: async (entry) => {
|
||||
events.push('commit');
|
||||
@@ -56,7 +59,8 @@ function createFixture(overrides = {}) {
|
||||
entries.set(entry.id, {
|
||||
id: entry.id,
|
||||
displayKey: `${key.slice(0, 9)}…${key.slice(-4)}`,
|
||||
createdAt: entry.createdAt
|
||||
createdAt: entry.createdAt,
|
||||
expiresAt: entry.expiresAt ?? null
|
||||
});
|
||||
},
|
||||
getKey: async (entryId) => {
|
||||
@@ -90,7 +94,8 @@ function createFixture(overrides = {}) {
|
||||
appVersion: () => '2.1.31',
|
||||
createBackup: (...args) => {
|
||||
createArguments = args;
|
||||
return { key, record };
|
||||
const expiration = args[3] === 'forever' ? null : new Date(new Date(args[2]).getTime() + 7 * 24 * 60 * 60 * 1000).toISOString();
|
||||
return { key, record, expiresAt: expiration };
|
||||
},
|
||||
uploadBackup: async (uploadedRecord) => {
|
||||
events.push('upload');
|
||||
@@ -112,6 +117,9 @@ function createFixture(overrides = {}) {
|
||||
get createdAt() {
|
||||
return createdAt;
|
||||
},
|
||||
get expiresAt() {
|
||||
return expiresAt;
|
||||
},
|
||||
get createArguments() {
|
||||
return createArguments;
|
||||
}
|
||||
@@ -174,14 +182,15 @@ describe('transactional online backup manager', () => {
|
||||
const result = await fixture.manager.createManaged();
|
||||
|
||||
assert.deepEqual(fixture.events, ['prepare', 'upload', 'commit']);
|
||||
assert.deepEqual(fixture.createArguments, [settings, '2.1.31', fixture.createdAt]);
|
||||
assert.deepEqual(fixture.createArguments, [settings, '2.1.31', fixture.createdAt, '7d']);
|
||||
assert.match(fixture.createdAt, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
|
||||
assert.deepEqual(result, {
|
||||
ok: true,
|
||||
entry: {
|
||||
id,
|
||||
displayKey: `${key.slice(0, 9)}…${key.slice(-4)}`,
|
||||
createdAt: fixture.createdAt
|
||||
createdAt: fixture.createdAt,
|
||||
expiresAt: fixture.expiresAt
|
||||
}
|
||||
});
|
||||
assert.equal(JSON.stringify(result).includes(key), false);
|
||||
@@ -202,7 +211,8 @@ describe('transactional online backup manager', () => {
|
||||
entry: {
|
||||
id,
|
||||
displayKey: `${key.slice(0, 9)}…${key.slice(-4)}`,
|
||||
createdAt: fixture.createdAt
|
||||
createdAt: fixture.createdAt,
|
||||
expiresAt: fixture.expiresAt
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -445,7 +455,8 @@ describe('transactional online backup manager', () => {
|
||||
entries: [{
|
||||
id,
|
||||
displayKey: `${key.slice(0, 9)}…${key.slice(-4)}`,
|
||||
createdAt: '2026-08-22T10:00:00.000Z'
|
||||
createdAt: '2026-08-22T10:00:00.000Z',
|
||||
expiresAt: null
|
||||
}]
|
||||
});
|
||||
fixture.keyring.list = async () => {
|
||||
@@ -464,7 +475,8 @@ describe('transactional online backup manager', () => {
|
||||
entries: [{
|
||||
id,
|
||||
displayKey: `${key.slice(0, 9)}…${key.slice(-4)}`,
|
||||
createdAt: '2026-08-22T10:00:00.000Z'
|
||||
createdAt: '2026-08-22T10:00:00.000Z',
|
||||
expiresAt: null
|
||||
}],
|
||||
warningCode: 'KEYRING_DECRYPT_FAILED',
|
||||
warning: 'Gespeicherter Online-Sicherungsschlüssel konnte nicht entschlüsselt werden'
|
||||
|
||||
@@ -49,7 +49,28 @@ describe('online backup key', () => {
|
||||
assert.equal(serialized.includes('secret-api-key'), false);
|
||||
assert.equal(serialized.includes('private-webhook'), false);
|
||||
assert.equal(serialized.includes(parsed.masterKey.toString('base64url')), false);
|
||||
assert.deepEqual(Object.keys(created.record).sort(), ['blob', 'deleteVerifier', 'id']);
|
||||
assert.deepEqual(Object.keys(created.record).sort(), ['blob', 'deleteVerifier', 'expiresInSeconds', 'id']);
|
||||
assert.equal(created.record.expiresInSeconds, 604_800);
|
||||
});
|
||||
|
||||
it('supports the allowed validity periods with seven days as the default', () => {
|
||||
const { createOnlineBackup, normalizeOnlineBackupRetention } = require('../lib/online-backup');
|
||||
const createdAt = '2026-08-09T00:00:00.000Z';
|
||||
const expected = new Map([
|
||||
['1d', [86_400, '2026-08-10T00:00:00.000Z']],
|
||||
['3d', [259_200, '2026-08-12T00:00:00.000Z']],
|
||||
['7d', [604_800, '2026-08-16T00:00:00.000Z']],
|
||||
['31d', [2_678_400, '2026-09-09T00:00:00.000Z']],
|
||||
['forever', [null, null]]
|
||||
]);
|
||||
|
||||
assert.equal(createOnlineBackup(settings(), '2.1.41', createdAt).record.expiresInSeconds, 604_800);
|
||||
for (const [retention, [seconds, expiresAt]] of expected) {
|
||||
const created = createOnlineBackup(settings(), '2.1.41', createdAt, retention);
|
||||
assert.equal(created.record.expiresInSeconds, seconds);
|
||||
assert.equal(created.expiresAt, expiresAt);
|
||||
}
|
||||
assert.throws(() => normalizeOnlineBackupRetention('30d'), /Gültigkeitsdauer/i);
|
||||
});
|
||||
|
||||
it('rejects corrupted keys, ciphertext and oversized settings', () => {
|
||||
|
||||
@@ -329,7 +329,7 @@ test('exposes managed online backup operations through narrow IPC boundaries', (
|
||||
const mainSource = fs.readFileSync(path.join(projectRoot, 'main.js'), 'utf8');
|
||||
|
||||
assert.match(preloadSource, /listManagedOnlineBackups:\s*\(\)\s*=>\s*ipcRenderer\.invoke\('online-backup:list-managed'\)/u);
|
||||
assert.match(preloadSource, /createManagedOnlineBackup:\s*\(\)\s*=>\s*ipcRenderer\.invoke\('online-backup:create-managed'\)/u);
|
||||
assert.match(preloadSource, /createManagedOnlineBackup:\s*\(retention\)\s*=>\s*ipcRenderer\.invoke\('online-backup:create-managed',\s*retention\)/u);
|
||||
assert.match(preloadSource, /copyManagedOnlineBackup:\s*\(id\)\s*=>\s*ipcRenderer\.invoke\('online-backup:copy-managed',\s*id\)/u);
|
||||
assert.match(preloadSource, /deleteManagedOnlineBackup:\s*\(id\)\s*=>\s*ipcRenderer\.invoke\('online-backup:delete-managed',\s*id\)/u);
|
||||
assert.match(preloadSource, /restoreOnlineBackup:\s*\(key\)\s*=>\s*ipcRenderer\.invoke\('online-backup:restore',\s*key\)/u);
|
||||
@@ -344,7 +344,7 @@ test('exposes managed online backup operations through narrow IPC boundaries', (
|
||||
assert.match(mainSource, /decoded\.toString\('base64url'\)\s*!==\s*id/u);
|
||||
assert.doesNotMatch(mainSource, /ipcMain\.handle\('online-backup:create'/u);
|
||||
assert.match(mainSource, /ipcMain\.handle\('online-backup:list-managed',[\s\S]*?onlineBackupManager\.listManaged\(\)/u);
|
||||
assert.match(mainSource, /ipcMain\.handle\('online-backup:create-managed',[\s\S]*?onlineBackupManager\.createManaged\(\)/u);
|
||||
assert.match(mainSource, /ipcMain\.handle\('online-backup:create-managed',[\s\S]*?onlineBackupManager\.createManaged\(normalizeOnlineBackupRetention\(retention\)\)/u);
|
||||
assert.match(mainSource, /ipcMain\.handle\('online-backup:copy-managed',[\s\S]*?onlineBackupManager\.copyManaged\(/u);
|
||||
assert.match(mainSource, /ipcMain\.handle\('online-backup:delete-managed',[\s\S]*?onlineBackupManager\.deleteManaged\(/u);
|
||||
});
|
||||
@@ -393,6 +393,7 @@ test('managed online backup handlers reject every sender outside the local main
|
||||
ipcMain: { handle: (channel, handler) => handlers.set(channel, handler) },
|
||||
mainWindow,
|
||||
onlineBackupManager,
|
||||
normalizeOnlineBackupRetention: value => value || '7d',
|
||||
path,
|
||||
pathToFileURL
|
||||
};
|
||||
|
||||
@@ -7,7 +7,22 @@ describe('settings backup snapshot', () => {
|
||||
const input = {
|
||||
hosters: { 'voe.sx': [{ id: 'v1', username: 'user', password: 'secret', enabled: true }] },
|
||||
hosterSettings: { 'voe.sx': { retries: 7 } },
|
||||
globalSettings: { alwaysOnTop: true, pendingQueue: [{ file: 'private.mkv' }] },
|
||||
globalSettings: {
|
||||
language: 'de',
|
||||
autoHealthCheckEnabled: false,
|
||||
alwaysOnTop: true,
|
||||
pendingQueue: [{ file: 'private.mkv' }],
|
||||
uploadRecovery: { jobs: ['private.mkv'] },
|
||||
lastBrowseDirectory: 'Z:\\private',
|
||||
folderMonitor: {
|
||||
enabled: true,
|
||||
folderPath: 'D:\\watch',
|
||||
recursive: true,
|
||||
paused: true,
|
||||
pausedAt: 123,
|
||||
telemetry: { detected: 8 }
|
||||
}
|
||||
},
|
||||
history: [{ file: 'done.mkv' }],
|
||||
rotationCursors: { 'voe.sx': 4 }
|
||||
};
|
||||
@@ -17,13 +32,27 @@ describe('settings backup snapshot', () => {
|
||||
assert.deepEqual(snapshot, {
|
||||
hosters: input.hosters,
|
||||
hosterSettings: input.hosterSettings,
|
||||
globalSettings: { alwaysOnTop: true, pendingQueue: null },
|
||||
globalSettings: {
|
||||
language: 'de',
|
||||
autoHealthCheckEnabled: false,
|
||||
alwaysOnTop: true,
|
||||
pendingQueue: null,
|
||||
uploadRecovery: null,
|
||||
lastBrowseDirectory: '',
|
||||
folderMonitor: {
|
||||
enabled: true,
|
||||
folderPath: 'D:\\watch',
|
||||
recursive: true,
|
||||
paused: false,
|
||||
pausedAt: null
|
||||
}
|
||||
},
|
||||
history: []
|
||||
});
|
||||
assert.notEqual(snapshot.hosters, input.hosters);
|
||||
});
|
||||
|
||||
it('validates imports and clears only source-machine paths that do not exist locally', () => {
|
||||
it('preserves configured paths, disables a missing monitored folder and reports both missing paths', () => {
|
||||
const { prepareImportedSettings } = require('../lib/settings-backup');
|
||||
const snapshot = {
|
||||
hosters: { 'byse.sx': [{ id: 'b1', apiKey: 'secret', enabled: true }] },
|
||||
@@ -36,12 +65,19 @@ describe('settings backup snapshot', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const imported = prepareImportedSettings(snapshot, { pathExists: () => false, pathDirname: (value) => value });
|
||||
const warnings = [];
|
||||
const imported = prepareImportedSettings(snapshot, { pathExists: () => false, pathDirname: (value) => value, warnings });
|
||||
|
||||
assert.equal(imported.globalSettings.logFilePath, '');
|
||||
assert.deepEqual(imported.globalSettings.folderMonitor, { enabled: false, folderPath: '' });
|
||||
assert.equal(imported.globalSettings.logFilePath, 'Z:\\missing\\upload.log');
|
||||
assert.deepEqual(imported.globalSettings.folderMonitor, { enabled: false, folderPath: 'Z:\\missing\\watch', paused: false, pausedAt: null });
|
||||
assert.equal(imported.globalSettings.pendingQueue, null);
|
||||
assert.equal(imported.globalSettings.uploadRecovery, null);
|
||||
assert.equal(imported.globalSettings.lastBrowseDirectory, '');
|
||||
assert.deepEqual(imported.history, []);
|
||||
assert.deepEqual(warnings, [
|
||||
'Log-Dateipfad (Ordner nicht gefunden)',
|
||||
'Ordnerüberwachung (Ordner nicht gefunden und deaktiviert)'
|
||||
]);
|
||||
assert.throws(() => prepareImportedSettings({ hosters: {} }), /ungültige Struktur/i);
|
||||
});
|
||||
});
|
||||
|
||||
+90
-8
@@ -63,18 +63,21 @@ let initialConfigReadDelayed = false;
|
||||
let startupLanguagePendingSnapshot = null;
|
||||
let managedOnlineBackupEntries = [
|
||||
{ id: 'AAAAAAAAAAAAAAAAAAAAAA', displayKey: 'MHU2-ABCD…1234', createdAt: '2026-08-20T08:00:00.000Z' },
|
||||
{ id: 'AQEBAQEBAQEBAQEBAQEBAQ', displayKey: 'MHU2-ZYXW…9876', createdAt: '2026-08-22T10:00:00.000Z' }
|
||||
{ id: 'AQEBAQEBAQEBAQEBAQEBAQ', displayKey: 'MHU2-ZYXW…9876', createdAt: '2026-08-22T10:00:00.000Z', expiresAt: '2026-09-22T10:00:00.000Z' },
|
||||
{ id: 'AwMDAwMDAwMDAwMDAwMDAw', displayKey: 'MHU2-OLDK…0000', createdAt: '2026-08-01T10:00:00.000Z', expiresAt: '2026-08-02T10:00:00.000Z' }
|
||||
];
|
||||
const managedOnlineBackupCopyIds = [];
|
||||
const managedOnlineBackupDeleteIds = [];
|
||||
let managedOnlineBackupCreateCalls = 0;
|
||||
const managedOnlineBackupCreateRetentions = [];
|
||||
let managedOnlineBackupDeleteMode = 'failure';
|
||||
let releaseManagedOnlineBackupDelete = null;
|
||||
const managedOnlineBackupHandlers = {
|
||||
'online-backup:list-managed': async () => ({ ok: true, entries: managedOnlineBackupEntries.map(entry => ({ ...entry })) }),
|
||||
'online-backup:create-managed': async () => {
|
||||
'online-backup:create-managed': async (_event, retention) => {
|
||||
managedOnlineBackupCreateCalls++;
|
||||
const entry = { id: 'AgICAgICAgICAgICAgICAg', displayKey: 'MHU2-QWER…4321', createdAt: '2026-08-23T12:00:00.000Z' };
|
||||
managedOnlineBackupCreateRetentions.push(retention);
|
||||
const entry = { id: 'AgICAgICAgICAgICAgICAg', displayKey: 'MHU2-QWER…4321', createdAt: '2026-08-23T12:00:00.000Z', expiresAt: '2026-09-23T12:00:00.000Z' };
|
||||
managedOnlineBackupEntries = [...managedOnlineBackupEntries, entry];
|
||||
return { ok: true, entry: { ...entry } };
|
||||
},
|
||||
@@ -1191,6 +1194,48 @@ setTimeout(async () => {
|
||||
check('Completed account checks expose their timestamp and release generation tokens', completedAccountCheckState.status === 'ok' && completedAccountCheckState.checkedAt === '2026-08-20T12:34:00.000Z' && completedAccountCheckState.subtitle.includes('geprüft') && completedAccountCheckState.generations === 0);
|
||||
restoreInitialIpcHandler('run-health-check');
|
||||
|
||||
ipcMain.removeHandler('run-health-check');
|
||||
ipcMain.handle('run-health-check', (event, payload) => new Promise(resolve => {
|
||||
const [first, second] = payload.hosters;
|
||||
setTimeout(() => {
|
||||
event.sender.send('health-check:result', {
|
||||
requestId: payload.requestId,
|
||||
checkedAt: '2026-08-20T12:35:00.000Z',
|
||||
result: { accountId: first.accountId, hoster: first.hoster, status: 'ok', message: 'First ready' }
|
||||
});
|
||||
}, 20);
|
||||
setTimeout(() => {
|
||||
const secondResult = { accountId: second.accountId, hoster: second.hoster, status: 'error', message: 'Second failed' };
|
||||
event.sender.send('health-check:result', {
|
||||
requestId: payload.requestId,
|
||||
checkedAt: '2026-08-20T12:35:01.000Z',
|
||||
result: secondResult
|
||||
});
|
||||
resolve({
|
||||
checkedAt: '2026-08-20T12:35:01.000Z',
|
||||
results: [
|
||||
{ accountId: first.accountId, hoster: first.hoster, status: 'ok', message: 'First ready' },
|
||||
secondResult
|
||||
]
|
||||
});
|
||||
}, 180);
|
||||
}));
|
||||
const progressiveCheck = wc.executeJavaScript(\`(() => {
|
||||
HOSTERS.forEach(name => { config.hosters[name] = []; });
|
||||
config.hosters['byse.sx'] = [
|
||||
{ id: 'ui-progress-first', enabled: true, authType: 'api', apiKey: 'first-key' },
|
||||
{ id: 'ui-progress-second', enabled: true, authType: 'api', apiKey: 'second-key' }
|
||||
];
|
||||
accountStatuses = {};
|
||||
healthCheckRunning = false;
|
||||
renderAccounts();
|
||||
return runHealthCheck('manual');
|
||||
})()\`);
|
||||
const progressiveIntermediate = await waitUntil(() => wc.executeJavaScript(\`accountStatuses['ui-progress-first']?.status === 'ok' && accountStatuses['ui-progress-second']?.status === 'checking'\`));
|
||||
const progressiveFinal = await progressiveCheck.then(() => wc.executeJavaScript(\`({ first: accountStatuses['ui-progress-first']?.status, second: accountStatuses['ui-progress-second']?.status, running: healthCheckRunning, generations: accountStatusGenerations.size })\`));
|
||||
check('Batch account checks update each card as soon as that account finishes', progressiveIntermediate === true && progressiveFinal.first === 'ok' && progressiveFinal.second === 'error' && progressiveFinal.running === false && progressiveFinal.generations === 0);
|
||||
restoreInitialIpcHandler('run-health-check');
|
||||
|
||||
let resolveStaleAccountCheck = null;
|
||||
ipcMain.removeHandler('run-health-check');
|
||||
ipcMain.handle('run-health-check', () => new Promise(resolve => { resolveStaleAccountCheck = resolve; }));
|
||||
@@ -1251,6 +1296,27 @@ setTimeout(async () => {
|
||||
const settingsActive = await wc.executeJavaScript('document.getElementById("settings-view")?.classList.contains("active")');
|
||||
check('Settings tab active', settingsActive);
|
||||
|
||||
const importedLanguageState = await wc.executeJavaScript(\`(() => {
|
||||
const original = structuredClone(config);
|
||||
const target = document.documentElement.lang === 'de' ? 'en' : 'de';
|
||||
const targetAutoCheck = config.globalSettings.autoHealthCheckEnabled === false;
|
||||
const imported = structuredClone(config);
|
||||
imported.globalSettings.language = target;
|
||||
imported.globalSettings.autoHealthCheckEnabled = targetAutoCheck;
|
||||
applyImportedConfig(imported, 'Importiert');
|
||||
const state = {
|
||||
target,
|
||||
documentLanguage: document.documentElement.lang,
|
||||
urlLanguage: new URL(window.location.href).searchParams.get('language'),
|
||||
inputLanguage: document.getElementById('languageInput')?.value,
|
||||
targetAutoCheck,
|
||||
importedAutoCheck: autoHealthCheckEnabled
|
||||
};
|
||||
applyImportedConfig(original, 'Zurückgesetzt');
|
||||
return state;
|
||||
})()\`);
|
||||
check('Imported backup language and automatic account check apply immediately', importedLanguageState.documentLanguage === importedLanguageState.target && importedLanguageState.urlLanguage === importedLanguageState.target && importedLanguageState.inputLanguage === importedLanguageState.target && importedLanguageState.importedAutoCheck === importedLanguageState.targetAutoCheck);
|
||||
|
||||
let updateCheckCallCount = 0;
|
||||
const updateCheckResolvers = [];
|
||||
ipcMain.removeHandler('app:check-updates');
|
||||
@@ -1405,9 +1471,12 @@ setTimeout(async () => {
|
||||
check('Global parallel uploads default 0', parallel === '0');
|
||||
|
||||
await wc.executeJavaScript('document.querySelector("[data-settings-page=\\'backup\\']")?.click()');
|
||||
const onlineBackupControls = await wc.executeJavaScript('["createOnlineBackupBtn", "managedOnlineBackupHeading", "managedOnlineBackupList", "managedOnlineBackupRefreshStatus", "reloadManagedOnlineBackupsBtn", "onlineBackupKeyInput", "restoreOnlineBackupBtn", "onlineBackupStatus"].every(id => Boolean(document.getElementById(id))) && !document.getElementById("onlineBackupKeyOutput") && !document.getElementById("copyOnlineBackupKeyBtn")');
|
||||
const onlineBackupControls = await wc.executeJavaScript('["createOnlineBackupBtn", "onlineBackupRetentionSelect", "managedOnlineBackupHeading", "managedOnlineBackupList", "managedOnlineBackupRefreshStatus", "reloadManagedOnlineBackupsBtn", "onlineBackupKeyInput", "restoreOnlineBackupBtn", "onlineBackupStatus"].every(id => Boolean(document.getElementById(id))) && !document.getElementById("onlineBackupKeyOutput") && !document.getElementById("copyOnlineBackupKeyBtn")');
|
||||
check('Online backup controls exist', onlineBackupControls);
|
||||
|
||||
const onlineBackupRetentionState = await wc.executeJavaScript('(() => { const select = document.getElementById("onlineBackupRetentionSelect"); const wrapper = select.closest(".online-backup-retention-select"); return { value: select.value, options: [...select.options].map(option => option.value + ":" + option.textContent), arrowRight: getComputedStyle(wrapper, "::after").right, fontSize: getComputedStyle(select).fontSize }; })()');
|
||||
check('Online backup validity defaults to seven days and offers every requested duration', onlineBackupRetentionState.value === '7d' && onlineBackupRetentionState.options.join('|') === '1d:24 Stunden|3d:3 Tage|7d:7 Tage (Standard)|31d:31 Tage|forever:Unbegrenzt' && onlineBackupRetentionState.arrowRight === '14px' && parseFloat(onlineBackupRetentionState.fontSize) >= 10);
|
||||
|
||||
const onlineBackupKeyContract = await wc.executeJavaScript('document.getElementById("onlineBackupKeyInput")?.maxLength + "|" + document.getElementById("onlineBackupKeyInput")?.getAttribute("pattern")');
|
||||
check('Online backup input enforces the 75-character MHU key format', onlineBackupKeyContract === '75|MHU2-[A-Za-z0-9_-]{70}');
|
||||
|
||||
@@ -1415,8 +1484,21 @@ setTimeout(async () => {
|
||||
check('Online backup uses a narrow managed preload bridge', onlineBackupBridge === 'function|function|function|function|function|undefined');
|
||||
|
||||
const managedOnlineBackupLoaded = await waitUntil(() => wc.executeJavaScript('document.querySelectorAll(".online-backup-managed-row").length === 2'));
|
||||
const managedOnlineBackupInitialState = await wc.executeJavaScript('(() => ({ keys: [...document.querySelectorAll(".online-backup-managed-key")].map(element => element.textContent), described: [...document.querySelectorAll(".online-backup-managed-row")].every(row => [...row.querySelectorAll("button")].every(button => button.getAttribute("aria-describedby") === row.querySelector(".online-backup-managed-key").id)), secretInBody: /MHU2-[A-Za-z0-9_-]{70}/.test(document.body.textContent) }))()');
|
||||
check('Managed online backups render canonical masked IDs with accessible actions', managedOnlineBackupLoaded === true && managedOnlineBackupInitialState.keys.join('|') === 'MHU2-ZYXW…9876|MHU2-ABCD…1234' && managedOnlineBackupInitialState.described === true && managedOnlineBackupInitialState.secretInBody === false);
|
||||
const managedOnlineBackupInitialState = await wc.executeJavaScript('(() => ({ keys: [...document.querySelectorAll(".online-backup-managed-key")].map(element => element.textContent), metadata: [...document.querySelectorAll(".online-backup-managed-created")].map(element => element.textContent), described: [...document.querySelectorAll(".online-backup-managed-row")].every(row => [...row.querySelectorAll("button")].every(button => button.getAttribute("aria-describedby") === row.querySelector(".online-backup-managed-key").id)), secretInBody: /MHU2-[A-Za-z0-9_-]{70}/.test(document.body.textContent) }))()');
|
||||
check('Managed online backups render validity, hide expired entries and keep accessible actions', managedOnlineBackupLoaded === true && managedOnlineBackupInitialState.keys.join('|') === 'MHU2-ZYXW…9876|MHU2-ABCD…1234' && managedOnlineBackupInitialState.metadata[0].includes('Gültig bis') && managedOnlineBackupInitialState.metadata[1].includes('Unbegrenzt gültig') && managedOnlineBackupInitialState.described === true && managedOnlineBackupInitialState.secretInBody === false);
|
||||
|
||||
const expiringUiEntry = {
|
||||
id: 'BAQEBAQEBAQEBAQEBAQEBA',
|
||||
displayKey: 'MHU2-TEMP…5555',
|
||||
createdAt: new Date(Date.now() - 1_000).toISOString(),
|
||||
expiresAt: new Date(Date.now() + 400).toISOString()
|
||||
};
|
||||
managedOnlineBackupEntries = [...managedOnlineBackupEntries, expiringUiEntry];
|
||||
await wc.executeJavaScript('loadManagedOnlineBackups()');
|
||||
const expiringUiEntryAppeared = await waitUntil(() => wc.executeJavaScript('[...document.querySelectorAll(".online-backup-managed-row")].some(element => element.dataset.managedOnlineBackupId === "BAQEBAQEBAQEBAQEBAQEBA")'));
|
||||
const expiringUiEntryDisappeared = await waitUntil(() => wc.executeJavaScript('![...document.querySelectorAll(".online-backup-managed-row")].some(element => element.dataset.managedOnlineBackupId === "BAQEBAQEBAQEBAQEBAQEBA")'), 2000);
|
||||
managedOnlineBackupEntries = managedOnlineBackupEntries.filter(entry => entry.id !== expiringUiEntry.id);
|
||||
check('A managed online key disappears automatically when its validity expires', expiringUiEntryAppeared === true && expiringUiEntryDisappeared === true);
|
||||
|
||||
await wc.executeJavaScript('document.querySelector(".online-backup-managed-row .online-backup-copy-btn").click()');
|
||||
await waitUntil(() => managedOnlineBackupCopyIds.length === 1);
|
||||
@@ -1443,10 +1525,10 @@ setTimeout(async () => {
|
||||
const managedDeleteSuccessState = await wc.executeJavaScript('(() => ({ key: document.querySelector(".online-backup-managed-key")?.textContent, status: document.getElementById("onlineBackupStatus")?.textContent, focusAction: document.activeElement?.dataset.managedOnlineBackupAction, focusId: document.activeElement?.dataset.managedOnlineBackupId, secretInBody: /MHU2-[A-Za-z0-9_-]{70}/.test(document.body.textContent) }))()');
|
||||
check('Managed online backup row disappears only after successful deletion and focuses the neighboring action', managedDeletePendingState.count === 2 && managedDeletePendingState.controlsDisabled === true && managedDeleteSucceeded === true && managedDeleteSuccessState.key === 'MHU2-ABCD…1234' && managedDeleteSuccessState.status === 'Schlüssel gelöscht' && managedDeleteSuccessState.focusAction === 'delete' && managedDeleteSuccessState.focusId === 'AAAAAAAAAAAAAAAAAAAAAA' && managedDeleteSuccessState.secretInBody === false);
|
||||
|
||||
await wc.executeJavaScript('document.getElementById("createOnlineBackupBtn").click()');
|
||||
await wc.executeJavaScript('document.getElementById("onlineBackupRetentionSelect").value = "31d"; document.getElementById("createOnlineBackupBtn").click()');
|
||||
const managedCreateSucceeded = await waitUntil(() => wc.executeJavaScript('document.querySelectorAll(".online-backup-managed-row").length === 2'));
|
||||
const managedCreateState = await wc.executeJavaScript('(() => ({ first: document.querySelector(".online-backup-managed-key")?.textContent, status: document.getElementById("onlineBackupStatus")?.textContent, secretInBody: /MHU2-[A-Za-z0-9_-]{70}/.test(document.body.textContent) }))()');
|
||||
check('Creating a managed online backup inserts the returned entry with the exact success text', managedOnlineBackupCreateCalls === 1 && managedCreateSucceeded === true && managedCreateState.first === 'MHU2-QWER…4321' && managedCreateState.status === 'Neuer Schlüssel erstellt.' && managedCreateState.secretInBody === false);
|
||||
check('Creating a managed online backup passes the chosen validity and inserts the returned entry', managedOnlineBackupCreateCalls === 1 && managedOnlineBackupCreateRetentions.join('|') === '31d' && managedCreateSucceeded === true && managedCreateState.first === 'MHU2-QWER…4321' && managedCreateState.status === 'Neuer Schlüssel erstellt.' && managedCreateState.secretInBody === false);
|
||||
|
||||
const invalidOnlineBackup = await wc.executeJavaScript('document.getElementById("onlineBackupKeyInput").value = "MHU2-short"; document.getElementById("onlineBackupKeyInput").dispatchEvent(new Event("input", { bubbles: true })); document.getElementById("restoreOnlineBackupBtn").disabled + "|" + document.getElementById("onlineBackupStatus").textContent');
|
||||
check('Invalid online backup keys stay blocked with visible guidance', invalidOnlineBackup === 'true|Der Schlüssel muss exakt 75 Zeichen lang sein.');
|
||||
|
||||
Reference in New Issue
Block a user