Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
66 lines
3.0 KiB
TypeScript
66 lines
3.0 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import React from 'react';
|
|
import { createDomRenderer, flushEffects, installDomEnvironment } from '../../components/test-support/renderReactDom';
|
|
import { EncryptionService } from '../../infrastructure/services/EncryptionService';
|
|
import type { SyncManagerState } from '../../infrastructure/services/CloudSyncManager';
|
|
import type { SyncPayload } from '../../domain/sync';
|
|
|
|
test('manual sync does not clear a newly shared password when an older unlock is superseded', async (t) => {
|
|
const env = installDomEnvironment();
|
|
const previousStorage = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
|
|
Object.defineProperty(globalThis, "localStorage", { configurable: true, value: env.window.localStorage });
|
|
t.after(() => {
|
|
if (previousStorage) Object.defineProperty(globalThis, "localStorage", previousStorage);
|
|
else Reflect.deleteProperty(globalThis, "localStorage");
|
|
});
|
|
let sharedPassword: string | undefined;
|
|
let clears = 0;
|
|
Object.defineProperty(env.window, 'netcatty', { configurable: true, value: {
|
|
cloudSyncGetSessionPassword: async () => sharedPassword,
|
|
cloudSyncClearSessionPassword: async () => { clears += 1; sharedPassword = undefined; },
|
|
} });
|
|
const { getCloudSyncManager } = await import('../../infrastructure/services/CloudSyncManager');
|
|
const manager = getCloudSyncManager();
|
|
await manager.setupMasterKey('old-fixture-password');
|
|
manager.lock();
|
|
const newConfig = await EncryptionService.createMasterKeyConfig('new-fixture-password');
|
|
const { useCloudSync } = await import('./useCloudSync');
|
|
let current!: ReturnType<typeof useCloudSync>;
|
|
const Probe = () => { current = useCloudSync(); return null; };
|
|
const renderer = await createDomRenderer(env.document);
|
|
const originalUnlock = EncryptionService.unlockMasterKey;
|
|
let release!: () => void;
|
|
let started!: () => void;
|
|
const gate = new Promise<void>((resolve) => { release = resolve; });
|
|
const entered = new Promise<void>((resolve) => { started = resolve; });
|
|
try {
|
|
await renderer.render(<Probe />);
|
|
await flushEffects();
|
|
EncryptionService.unlockMasterKey = async (...args) => {
|
|
started();
|
|
await gate;
|
|
return originalUnlock(...args);
|
|
};
|
|
sharedPassword = 'old-fixture-password';
|
|
const result = current.syncNow({} as SyncPayload).then(() => null, (error: unknown) => error);
|
|
await entered;
|
|
const state = Reflect.get(manager, 'state') as SyncManagerState;
|
|
state.masterKeyConfig = newConfig;
|
|
Reflect.get(manager, 'bumpSyncSecurityGeneration').call(manager);
|
|
sharedPassword = 'new-fixture-password';
|
|
release();
|
|
const error = await result;
|
|
assert.equal(clears, 0, 'stale work must not erase the new shared password');
|
|
assert.equal(sharedPassword, 'new-fixture-password');
|
|
assert.ok(error instanceof Error);
|
|
assert.match(error.message, /changed while unlocking/i);
|
|
} finally {
|
|
release();
|
|
EncryptionService.unlockMasterKey = originalUnlock;
|
|
await renderer.unmount();
|
|
manager.destroy();
|
|
env.cleanup();
|
|
}
|
|
});
|