diff --git a/components/popout/__tests__/popout-host.test.tsx b/components/popout/__tests__/popout-host.test.tsx
index 239f3045c..dcdb2e036 100644
--- a/components/popout/__tests__/popout-host.test.tsx
+++ b/components/popout/__tests__/popout-host.test.tsx
@@ -122,28 +122,37 @@ describe('PopoutHost', () => {
expect(JSON.stringify(heard)).not.toContain('Bearer main');
});
- it('gives no credentials while the app is locked, also to a pop-out that never heard of the lock', async () => {
+ it('gives no credentials while the app is locked, to a pop-out that never heard of the lock', async () => {
render();
await settle();
setLockedHere(true);
- const channel = openPopoutChannel(() => {});
const opener = await createSessionOpener();
- channel?.post({ type: 'session-request', requestId: 'r1', publicKey: opener.publicKey });
+ popout.post({ type: 'session-request', requestId: 'r1', publicKey: opener.publicKey });
await settle();
- channel?.close();
expect(answers()).toEqual([]);
- await expect(requestWindowSession({}, 100)).resolves.toBeNull();
setLockedHere(false);
- await expect(requestWindowSession({}, 2000)).resolves.toMatchObject({ activeAccountId: 'alice' });
+ popout.post({ type: 'session-request', requestId: 'r2', publicKey: opener.publicKey });
+ await vi.waitFor(() => expect(answers()).toHaveLength(1));
});
- it('a locked pop-out takes no session from the cookies either', async () => {
+ it('a locked pop-out doesn\'t ask for a session', async () => {
+ setLockedHere(true);
+ await expect(requestWindowSession({}, 100)).resolves.toBeNull();
+ setLockedHere(false);
+ expect(heard.filter((message) => message.type === 'session-request')).toEqual([]);
+ });
+
+ it('a locked pop-out takes no session from the cookies either, and asks nobody', async () => {
+ const fetched = vi.fn(async () => new Response('{}'));
+ vi.stubGlobal('fetch', fetched);
setLockedHere(true);
try {
- await expect(useAuthStore.getState().startSharedSession({})).resolves.toBe('no-session');
+ await expect(useAuthStore.getState().startSharedSession({ accountId: 'alice' })).resolves.toBe('no-session');
} finally {
setLockedHere(false);
}
+ expect(fetched).not.toHaveBeenCalled();
+ expect(useAuthStore.getState().client).not.toBeNull();
});
it('says nothing while it has no session to lend, so a pop-out goes on to the cookies', async () => {
diff --git a/lib/app-lock/__tests__/controller.test.ts b/lib/app-lock/__tests__/controller.test.ts
index 1c6a03da4..958875a38 100644
--- a/lib/app-lock/__tests__/controller.test.ts
+++ b/lib/app-lock/__tests__/controller.test.ts
@@ -197,6 +197,14 @@ describe('while locked', () => {
expect(isAppLockedNow()).toBe(true);
});
+ it('a running page isn\'t held back while it saves: composers stay mounted until the reload', async () => {
+ await enableAppLock(PIN);
+ let holdDuringSave: Promise | null | undefined;
+ registerComposeFlush(async () => { holdDuringSave = appLockHold(); });
+ await lockedAndReloaded();
+ expect(holdDuringSave).toBeNull();
+ });
+
it('notifications go by it: the check the OS code asks says locked', async () => {
await enableAppLock(PIN);
asNewDocument();
@@ -286,15 +294,23 @@ describe('locking', () => {
});
describe('the store when a tab inherits it without the key', () => {
- it('locks everywhere instead of opening or writing an unwrapped key', async () => {
+ it('asks for the lock instead of opening, or writing a key outside the layer', async () => {
await enableAppLock(PIN);
+ // This tab has no key: the one that had it went.
resetAppLockForTests();
+ (window.navigator.locks.query as ReturnType).mockResolvedValue({ held: [{ name: 'bulwark-app-lock-holder' }], pending: [] });
+ (window.navigator.locks.request as ReturnType).mockImplementation(() => new Promise(() => {}));
bootAppLock();
- await vi.waitFor(() => expect(getAppLockPhase()).toBe('locked'));
- const layers = () => appLockKeyLayers();
- expect(layers).toThrow(expect.objectContaining({ name: 'StoreKeyLayerMissingError' }));
- // A new account can't get a key record outside the layer either.
+ await vi.waitFor(() => expect(getAppLockPhase()).toBe('open'));
+ const onKeyLocked = vi.fn();
+ configureStore({ ...hooks, onKeyLocked });
+ await expect(getStore().ensureOpen('new-account', { apiUrl: 'x' } as never).then(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 20));
+ await getStore().ensureOpen('new-account');
+ })).rejects.toMatchObject({ name: 'StoreKeyLayerMissingError' });
+ expect(onKeyLocked).toHaveBeenCalled();
expect(await getAccountRecord('new-account')).toBeNull();
+ expect(lastWorker().posted.some((m) => m.kind === 'open')).toBe(false);
});
it('a new account after unlock gets its key under the lock', async () => {
diff --git a/lib/app-lock/controller.ts b/lib/app-lock/controller.ts
index 46c71533c..cf1b5fb69 100644
--- a/lib/app-lock/controller.ts
+++ b/lib/app-lock/controller.ts
@@ -366,7 +366,6 @@ async function applyLockHere(): Promise {
const wasRunning = phase === 'open';
setLockedHere(true);
markDocument(true);
- hold ??= newHold();
setPhase('locking');
tellDesktopLocked();
tellServiceWorker('locked');
@@ -699,7 +698,7 @@ export function startAppLock(): () => void {
startIdle();
if (!heldKey) watchHolder();
}
- tellServiceWorker(readAppLockFile().on ? (isAppLockedNow() ? 'locked' : 'unlocked') : 'off');
+ if (readAppLockFile().on) tellServiceWorker(isAppLockedNow() ? 'locked' : 'unlocked');
return () => {
channel?.close();
window.removeEventListener('storage', onStorage);