mirror of
https://github.com/tompro/sattle.git
synced 2026-08-26 23:05:58 +00:00
fix: consume owner monitor transition failures
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
import { MINT_KEY, OTHER_OWNER_ID, PASSWORD } from './wallet.lifecycle.testHarness';
|
import { MINT_KEY, mocks, OTHER_OWNER_ID, PASSWORD } from './wallet.lifecycle.testHarness';
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { savedKeyOwnerId } from '@/lnurlcash/keys';
|
import { savedKeyOwnerId } from '@/lnurlcash/keys';
|
||||||
import { readNwcEnabled, writeNwcConnections, writeNwcEnabled } from '@/lnurlcash/nwc';
|
import { readNwcEnabled, writeNwcConnections, writeNwcEnabled } from '@/lnurlcash/nwc';
|
||||||
import type { NwcConnectionRecord } from '@/lnurlcash/nwc';
|
import type { NwcConnectionRecord } from '@/lnurlcash/nwc';
|
||||||
import { addTrustedMint, readTrustedMints } from '@/lnurlcash/trustedMints';
|
import { addTrustedMint, readTrustedMints } from '@/lnurlcash/trustedMints';
|
||||||
|
import { useNwcStore } from './nwc';
|
||||||
import { useWalletStore } from './wallet';
|
import { useWalletStore } from './wallet';
|
||||||
|
|
||||||
describe('cross-tab owner invalidation', () => {
|
describe('cross-tab owner invalidation', () => {
|
||||||
@@ -41,6 +42,36 @@ describe('cross-tab owner invalidation', () => {
|
|||||||
expect(readNwcEnabled(OTHER_OWNER_ID)).toBe(false);
|
expect(readNwcEnabled(OTHER_OWNER_ID)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('surfaces a failed stale-tab drain without an unhandled rejection', async () => {
|
||||||
|
// Given an unlocked old-owner tab whose live NWC service rejects shutdown
|
||||||
|
const events = new EventTarget();
|
||||||
|
vi.stubGlobal('window', events);
|
||||||
|
const wallet = useWalletStore();
|
||||||
|
const nwc = useNwcStore();
|
||||||
|
await wallet.create(PASSWORD);
|
||||||
|
const serviceStop = vi.fn().mockRejectedValue(new Error('stale drain failed'));
|
||||||
|
mocks.startService.mockResolvedValue({ connections: [], stop: serviceStop });
|
||||||
|
await nwc.setEnabled(true);
|
||||||
|
|
||||||
|
// When another tab replaces the saved wallet owner
|
||||||
|
localStorage.setItem(
|
||||||
|
'sattle_linking_key',
|
||||||
|
JSON.stringify({ enc: false, value: '09'.repeat(32), ownerId: OTHER_OWNER_ID, version: 1 }),
|
||||||
|
);
|
||||||
|
events.dispatchEvent(
|
||||||
|
Object.defineProperties(new Event('storage'), {
|
||||||
|
key: { value: 'sattle_linking_key' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await vi.waitFor(() => expect(wallet.state).toBe('locked'));
|
||||||
|
|
||||||
|
// Then the stale runtime is cleared and the queue surfaces the drain failure
|
||||||
|
expect(wallet.lifecycleError).toMatch(/stale drain failed/i);
|
||||||
|
expect(() => wallet.requireLinkingKey()).toThrow('Wallet is locked.');
|
||||||
|
expect(nwc.running).toBe(false);
|
||||||
|
expect(serviceStop).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects old-owner trust and NWC writes during the markerless forget gap', async () => {
|
it('rejects old-owner trust and NWC writes during the markerless forget gap', async () => {
|
||||||
// Given wallet A was active and an old tab retained only its owner identifier
|
// Given wallet A was active and an old tab retained only its owner identifier
|
||||||
const wallet = useWalletStore();
|
const wallet = useWalletStore();
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { linkingPubKeyHex } from '@/lnurlcash/keys';
|
||||||
|
import { stubLocalStorage } from '@/lnurlcash/test-utils';
|
||||||
|
import { startWalletOwnerMonitor } from './walletOwnerMonitor';
|
||||||
|
|
||||||
|
const OWNER_ID = linkingPubKeyHex(new Uint8Array(32).fill(7));
|
||||||
|
const OTHER_OWNER_ID = linkingPubKeyHex(new Uint8Array(32).fill(9));
|
||||||
|
|
||||||
|
describe('wallet owner monitor', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
stubLocalStorage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('consumes a background transition rejection after an owner replacement', () => {
|
||||||
|
// Given an unlocked stale owner and a transition promise observed by the queue
|
||||||
|
const events = new EventTarget();
|
||||||
|
vi.stubGlobal('window', events);
|
||||||
|
localStorage.setItem(
|
||||||
|
'sattle_linking_key',
|
||||||
|
JSON.stringify({
|
||||||
|
enc: false,
|
||||||
|
value: '09'.repeat(32),
|
||||||
|
ownerId: OTHER_OWNER_ID,
|
||||||
|
version: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const transition = Promise.resolve();
|
||||||
|
const catchRejection = vi.spyOn(transition, 'catch');
|
||||||
|
startWalletOwnerMonitor({
|
||||||
|
snapshot: () => ({ token: 1, state: 'unlocked', ownerId: OWNER_ID }),
|
||||||
|
deactivate: vi.fn().mockResolvedValue(undefined),
|
||||||
|
runTransition: vi.fn().mockReturnValue(transition),
|
||||||
|
});
|
||||||
|
|
||||||
|
// When the browser reports that the saved owner changed
|
||||||
|
events.dispatchEvent(
|
||||||
|
Object.defineProperties(new Event('storage'), {
|
||||||
|
key: { value: 'sattle_linking_key' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then the fire-and-forget transition has a rejection consumer
|
||||||
|
expect(catchRejection).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -24,16 +24,18 @@ export const startWalletOwnerMonitor = (monitor: WalletOwnerMonitor): (() => voi
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void monitor.runTransition(async () => {
|
void monitor
|
||||||
const current = monitor.snapshot();
|
.runTransition(async () => {
|
||||||
if (
|
const current = monitor.snapshot();
|
||||||
current.token !== expected.token ||
|
if (
|
||||||
current.state !== 'unlocked' ||
|
current.token !== expected.token ||
|
||||||
current.ownerId !== expected.ownerId ||
|
current.state !== 'unlocked' ||
|
||||||
savedKeyOwnerId() === expected.ownerId
|
current.ownerId !== expected.ownerId ||
|
||||||
) {
|
savedKeyOwnerId() === expected.ownerId
|
||||||
return;
|
) {
|
||||||
}
|
return;
|
||||||
await monitor.deactivate();
|
}
|
||||||
});
|
await monitor.deactivate();
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user