mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* feat: cloud prefs sync polish + encryption key rotation (#2906) 1. Add sync status indicator in Preferences panel — shows current state (synced/pending/error/offline) with colored dot, last-sync timestamp, and manual "Sync now" button for conflict/error recovery. 2. Support AES key rotation in scripts/lib/crypto.cjs — encrypt() always uses the latest ENCRYPTION_KEY_V{n} env var, decrypt() reads the version prefix to select the matching key. Fully backwards-compatible with the existing NOTIFICATION_ENCRYPTION_KEY single-key setup. 3. Add tests exercising the applyMigrations() schema-version plumbing and the new multi-version key rotation (12 tests, all passing). Closes #2906 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review feedback - Raise key version ceiling from 9 to 99 to avoid silent footgun - Extract SYNC_STATE_LABELS/COLORS to shared constants (no drift risk) - Gate Cloud Sync UI on isCloudSyncEnabled() so it doesn't render when VITE_CLOUD_PREFS_ENABLED is off Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
69 lines
2.1 KiB
JavaScript
69 lines
2.1 KiB
JavaScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
/**
|
|
* Exercises the applyMigrations() plumbing from cloud-prefs-sync.ts.
|
|
*
|
|
* The function is not exported (internal), so we replicate the algorithm here
|
|
* with a real migration map to prove the loop + fallthrough logic works before
|
|
* it's needed in production. (Issue #2906 item 3)
|
|
*/
|
|
|
|
function applyMigrations(data, fromVersion, currentVersion, migrations) {
|
|
let result = data;
|
|
for (let v = fromVersion + 1; v <= currentVersion; v++) {
|
|
result = migrations[v]?.(result) ?? result;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
describe('applyMigrations (cloud-prefs-sync plumbing)', () => {
|
|
const MIGRATIONS = {
|
|
2: (data) => {
|
|
// Simulate renaming a preference key
|
|
const out = { ...data };
|
|
if ('oldKey' in out) {
|
|
out.newKey = out.oldKey;
|
|
delete out.oldKey;
|
|
}
|
|
return out;
|
|
},
|
|
3: (data) => {
|
|
// Simulate adding a default for a new preference
|
|
const out = { ...data };
|
|
if (!('addedInV3' in out)) out.addedInV3 = 'default-value';
|
|
return out;
|
|
},
|
|
};
|
|
|
|
it('no-op when already at current version', () => {
|
|
const data = { foo: 'bar' };
|
|
const result = applyMigrations(data, 1, 1, MIGRATIONS);
|
|
assert.deepEqual(result, { foo: 'bar' });
|
|
});
|
|
|
|
it('applies a single v1 -> v2 migration', () => {
|
|
const data = { oldKey: 'hello', keep: 42 };
|
|
const result = applyMigrations(data, 1, 2, MIGRATIONS);
|
|
assert.deepEqual(result, { newKey: 'hello', keep: 42 });
|
|
});
|
|
|
|
it('chains v1 -> v2 -> v3 migrations', () => {
|
|
const data = { oldKey: 'hello' };
|
|
const result = applyMigrations(data, 1, 3, MIGRATIONS);
|
|
assert.deepEqual(result, { newKey: 'hello', addedInV3: 'default-value' });
|
|
});
|
|
|
|
it('skips missing migration versions gracefully', () => {
|
|
const data = { oldKey: 'x' };
|
|
const result = applyMigrations(data, 1, 4, MIGRATIONS);
|
|
assert.deepEqual(result, { newKey: 'x', addedInV3: 'default-value' });
|
|
});
|
|
|
|
it('handles empty migrations map', () => {
|
|
const data = { a: 1 };
|
|
const result = applyMigrations(data, 1, 5, {});
|
|
assert.deepEqual(result, { a: 1 });
|
|
});
|
|
});
|