Files
openless/openless-all/app/scripts/encrypted-sync-ui-bridge.test.mjs

313 lines
10 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
const app = fileURLToPath(new URL('../', import.meta.url));
const require = createRequire(import.meta.url);
const ts = require('typescript');
const source = readFileSync(app + '/src/lib/encryptedSyncUiBridge.ts', 'utf8');
const code = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
}).outputText;
const tick = () => new Promise((r) => setImmediate(r));
const drain = async () => {
for (let i = 0; i < 8; i++) await tick();
};
const defer = () => {
let resolve, reject;
const promise = new Promise((r, e) => {
resolve = r;
reject = e;
});
return { promise, resolve, reject };
};
function context({ fresh = false } = {}) {
const window = new EventTarget(),
handlers = new Map(),
statusQueue = [],
attempts = [];
let errors = 0;
window.addEventListener('openless:sync-ui-persistence-failed', () => errors++);
let locale = 'en',
fontScale = 'medium',
rev = fresh ? 0 : 1;
let preferences = fresh ? null : { locale, fontScale };
let status = {
sequence: '0',
account: fresh ? null : { githubId: '1' },
vaultId: fresh ? null : 'v1',
consentVersion: fresh ? null : 'yes',
taskId: null,
};
const revision = () => (rev ? `00000000-0000-4000-8000-${String(rev).padStart(12, '0')}` : null);
const event = (key, source) =>
window.dispatchEvent(
new CustomEvent('openless:ui-preferences-changed', { detail: { key, source } }),
);
const i18n = {
getLocalePreference: () => locale,
setLocalePreference: async (value, source = 'user') => {
locale = value;
event('locale', source);
},
SUPPORTED_LOCALES: ['en', 'fr', 'de'],
};
const fonts = {
readFontScale: () => fontScale,
setFontScale: (value, source = 'user') => {
fontScale = value;
event('fontScale', source);
},
};
const invoke = async (cmd, args) => {
if (cmd === 'cloud_sync_e2ee_status')
return statusQueue.length ? statusQueue.shift().promise : { ...status };
if (cmd === 'cloud_sync_e2ee_get_ui_preferences_snapshot')
return { preferences: preferences && { ...preferences }, revision: revision() };
if (
cmd === 'cloud_sync_e2ee_set_ui_preferences_checked' ||
cmd === 'cloud_sync_e2ee_set_ui_preferences'
) {
attempts.push({ cmd, ...args });
if (cmd.endsWith('_checked') && args.expectedRevision !== revision())
throw { details: { reason: 'stale_preview' } };
const next = { locale: args.locale, fontScale: args.fontScale };
if (JSON.stringify(next) !== JSON.stringify(preferences)) {
preferences = next;
rev++;
}
return;
}
throw new Error(cmd);
};
const exports = {},
shared = { isTauri: true, invokeOrMock: invoke };
const localRequire = (name) =>
name === './ipc/shared'
? shared
: name === '../i18n'
? i18n
: name === './fontScale'
? fonts
: name === '@tauri-apps/api/event'
? {
listen: async (name, fn) => {
handlers.set(name, fn);
return () => handlers.delete(name);
},
}
: null;
new Function('require', 'exports', 'window', 'location', 'Event', 'CustomEvent', code)(
localRequire,
exports,
window,
{ search: '' },
Event,
CustomEvent,
);
return {
install: exports.installEncryptedSyncUiBridge,
flush: exports.flushEncryptedSyncUiPreferences,
userLocale: i18n.setLocalePreference,
userFont: fonts.setFontScale,
statusQueue,
attempts,
setStatus: (value) => {
status = { ...status, ...value };
},
snapshot: () => ({ locale, fontScale, preferences, revision: revision(), errors }),
externalWrite: (value = {}) => {
preferences = { ...preferences, ...value };
rev++;
},
unchecked: () => invoke('cloud_sync_e2ee_set_ui_preferences', { locale, fontScale }),
restore: (sequence = '1', scope = {}) =>
handlers.get('cloud-sync-e2ee:restored')({
payload: {
sequence,
accountId: status.account.githubId,
vaultId: status.vaultId,
taskId: 'restore',
...scope,
},
}),
};
}
let failed = 0;
async function test(name, fn) {
try {
await fn();
console.log('PASS ' + name);
} catch (e) {
failed++;
console.log('FAIL ' + name + '\n' + e.message);
}
}
await test('old mirror queued before restore cannot overwrite its native value', async () => {
const c = context();
await c.install();
const wait = defer();
c.statusQueue.push(wait);
await c.userLocale('de');
await tick();
c.externalWrite({ locale: 'fr' });
c.restore();
await tick();
wait.resolve({ sequence: '2', account: { githubId: '1' }, vaultId: 'v1', consentVersion: 'yes' });
await drain();
assert.equal(c.snapshot().preferences.locale, 'fr');
assert.equal(c.snapshot().locale, 'fr');
});
await test('new choice after restore notification wins only its chosen field', async () => {
const c = context();
await c.install();
const wait = defer();
c.statusQueue.push(wait);
c.externalWrite({ locale: 'fr', fontScale: 'large' });
c.restore();
await tick();
await c.userLocale('de');
wait.resolve({ sequence: '2', account: { githubId: '1' }, vaultId: 'v1', consentVersion: 'yes' });
await drain();
assert.deepEqual(c.snapshot().preferences, { locale: 'de', fontScale: 'large' });
assert.equal(c.snapshot().locale, 'de');
assert.equal(c.snapshot().fontScale, 'large');
});
await test('prepare mirror must not permanently stale the live bridge revision', async () => {
const c = context({ fresh: true });
await c.install();
await c.flush();
c.setStatus({ account: { githubId: '1' }, vaultId: 'v1', consentVersion: 'yes' });
await c.userLocale('de');
await drain();
c.userFont('large');
await drain();
assert.deepEqual(
c.snapshot().preferences,
{ locale: 'de', fontScale: 'large' },
JSON.stringify(c.snapshot()),
);
});
await test('legitimate account switch must rebase the cached mirror scope', async () => {
const c = context();
await c.install();
c.setStatus({ sequence: '3', account: { githubId: '2' }, vaultId: 'v2' });
await c.userLocale('de');
await drain();
c.userFont('large');
await drain();
assert.deepEqual(
c.snapshot().preferences,
{ locale: 'de', fontScale: 'large' },
JSON.stringify(c.snapshot()),
);
});
await test('rollback-only UUID change must not permanently poison later user saves', async () => {
const c = context();
await c.install();
c.externalWrite({ locale: 'en' });
await c.userLocale('de');
await drain();
await c.userLocale('fr');
await drain();
assert.equal(c.snapshot().preferences.locale, 'fr', JSON.stringify(c.snapshot()));
});
await test('a new vault restore is hydrated after an account switch', async () => {
const c = context();
await c.install();
c.setStatus({ sequence: '3', account: { githubId: '2' }, vaultId: 'v2' });
c.externalWrite({ locale: 'fr', fontScale: 'large' });
c.restore('4');
await drain();
assert.equal(c.snapshot().locale, 'fr');
assert.equal(c.snapshot().fontScale, 'large');
await c.userLocale('de');
await drain();
assert.equal(c.snapshot().preferences.locale, 'de');
});
await test('an equal-value restore invalidates an older unsent user choice', async () => {
const c = context();
await c.install();
const wait = defer();
c.statusQueue.push(wait);
await c.userLocale('de');
await tick();
c.externalWrite({ locale: 'en' });
c.restore();
wait.resolve({ sequence: '2', account: { githubId: '1' }, vaultId: 'v1', consentVersion: 'yes' });
await drain();
assert.equal(c.snapshot().preferences.locale, 'en');
await c.userLocale('fr');
await drain();
assert.equal(c.snapshot().preferences.locale, 'fr');
});
await test('prepare and user edits use only the checked writer', async () => {
const c = context({ fresh: true });
await c.install();
await c.flush();
c.setStatus({ consentVersion: 'yes' });
await c.userLocale('de');
c.userFont('large');
await drain();
assert.deepEqual(c.snapshot().preferences, { locale: 'de', fontScale: 'large' });
assert(c.attempts.every((a) => a.cmd === 'cloud_sync_e2ee_set_ui_preferences_checked'));
});
await test('a late old-vault event cannot discard a new-account user choice', async () => {
const c = context();
await c.install();
c.setStatus({ account: { githubId: '2' }, vaultId: 'v2' });
const wait = defer();
c.statusQueue.push(wait);
await c.userLocale('de');
await tick();
c.restore('99', { accountId: '1', vaultId: 'v1' });
wait.resolve({ account: { githubId: '2' }, vaultId: 'v2', consentVersion: 'yes' });
await drain();
assert.equal(c.snapshot().preferences.locale, 'de');
c.externalWrite({ locale: 'fr' });
c.restore('5');
await drain();
assert.equal(c.snapshot().locale, 'fr');
});
await test('a failed old-scope notification read retains the newer local choice', async () => {
const c = context();
await c.install();
c.setStatus({ account: { githubId: '2' }, vaultId: 'v2' });
const userStatus = defer(),
restoreStatus = defer();
c.statusQueue.push(userStatus, restoreStatus);
await c.userLocale('de');
await tick();
c.restore('99', { accountId: '1', vaultId: 'v1' });
userStatus.resolve({ account: { githubId: '2' }, vaultId: 'v2', consentVersion: 'yes' });
await tick();
restoreStatus.reject(new Error('transient native status failure'));
await drain();
assert.equal(c.snapshot().preferences.locale, 'de', JSON.stringify(c.snapshot()));
assert.equal(c.snapshot().locale, 'de');
});
await test('overlapping valid and old-scope notifications preserve a later field choice', async () => {
const c = context();
await c.install();
c.setStatus({ account: { githubId: '2' }, vaultId: 'v2' });
const wait = defer();
c.statusQueue.push(wait);
c.externalWrite({ locale: 'fr', fontScale: 'large' });
c.restore('4');
await tick();
await c.userLocale('de');
c.restore('99', { accountId: '1', vaultId: 'v1' });
wait.resolve({ account: { githubId: '2' }, vaultId: 'v2', consentVersion: 'yes' });
await drain();
assert.deepEqual(
c.snapshot().preferences,
{ locale: 'de', fontScale: 'large' },
JSON.stringify(c.snapshot()),
);
assert.equal(c.snapshot().locale, 'de');
assert.equal(c.snapshot().fontScale, 'large');
});
console.log(`failures=${failed}`);
process.exitCode = failed ? 1 : 0;