Skip to content

Commit 6803d3f

Browse files
committed
Added tests
1 parent c42c8b2 commit 6803d3f

10 files changed

Lines changed: 493 additions & 41 deletions

File tree

applications/pass-extension/src/app/worker/services/activation.spec.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe('Activation service - `CLIENT_INIT`', () => {
3434

3535
authStore = createAuthStore(createMemoryStore());
3636
authStore.setLocalID(123);
37+
authStore.setLockPasswordOnLaunch(false);
3738
exposeAuthStore(authStore);
3839

3940
connectivity = {
@@ -153,10 +154,12 @@ describe('Activation service - `CLIENT_INIT`', () => {
153154
test('should pass `retryable: false`: CLIENT_INIT must never bootstrap the alarm chain', async () => {
154155
ctx.status = AppStatus.IDLE;
155156
await handler(initMessage('popup'), {});
156-
expect(ctx.service.auth.init).toHaveBeenCalledWith({
157-
forceLock: expect.any(Boolean),
158-
retryable: false,
159-
});
157+
expect(ctx.service.auth.init).toHaveBeenCalledWith(
158+
expect.objectContaining({
159+
forceLock: expect.any(Boolean),
160+
retryable: false,
161+
})
162+
);
160163
});
161164
});
162165

applications/pass-extension/src/app/worker/services/auth/auth.service.spec.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ describe('Extension AuthService', () => {
4040
api.subscribe = jest.fn();
4141
api.idle = jest.fn().mockResolvedValue(undefined);
4242
authStore = createAuthStore(createMemoryStore());
43+
authStore.setLockPasswordOnLaunch(false);
4344

4445
connectivity = {
4546
online: true,
@@ -736,6 +737,7 @@ describe('Extension AuthService', () => {
736737
});
737738
jest.spyOn(auth.alarms, 'scheduleAutoResume').mockResolvedValue(undefined);
738739
auth.init = jest.fn().mockResolvedValue(true);
740+
ctx.service.auth = auth;
739741
authStore.setLocalID(123);
740742
auth.listen();
741743
WorkerMessageBroker.ports.query.mockReturnValue([]);
@@ -786,11 +788,13 @@ describe('Extension AuthService', () => {
786788
await alarmListener();
787789

788790
expect(ctx.service.store.dispatch).not.toHaveBeenCalled();
789-
expect(auth.init).toHaveBeenCalledWith({
790-
forceLock: expect.any(Boolean),
791-
retryable: true,
792-
silence: true,
793-
});
791+
expect(auth.init).toHaveBeenCalledWith(
792+
expect.objectContaining({
793+
forceLock: expect.any(Boolean),
794+
retryable: true,
795+
silence: true,
796+
})
797+
);
794798
});
795799

796800
test('should call `auth.init` when client is ERRORED', async () => {
@@ -842,11 +846,13 @@ describe('Extension AuthService', () => {
842846
connectivity.status = ConnectivityStatus.ONLINE;
843847
});
844848
await alarmListener();
845-
expect(auth.init).toHaveBeenCalledWith({
846-
forceLock: expect.any(Boolean),
847-
retryable: true,
848-
silence: true,
849-
});
849+
expect(auth.init).toHaveBeenCalledWith(
850+
expect.objectContaining({
851+
forceLock: expect.any(Boolean),
852+
retryable: true,
853+
silence: true,
854+
})
855+
);
850856
expect(auth.alarms.scheduleAutoResume).not.toHaveBeenCalled();
851857
});
852858
});

applications/pass-extension/src/app/worker/services/auth/auth.utils.spec.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { WorkerContextInterface } from 'proton-pass-extension/app/worker/co
44
import { PassFeature } from '@proton/pass/types/api/features';
55
import { epochToMs, getEpoch } from '@proton/pass/utils/time/epoch';
66

7-
import { isOfflineModeEnabled, shouldForceLock } from './auth.utils';
7+
import { getForceLockOptions, isOfflineModeEnabled, shouldForceLock } from './auth.utils';
88

99
describe('auth.utils', () => {
1010
let ctx: WorkerContextInterface;
@@ -127,4 +127,61 @@ describe('auth.utils', () => {
127127
expect(await shouldForceLock()).toBe(false);
128128
});
129129
});
130+
131+
describe('`getForceLockOptions`', () => {
132+
const setup = (persistedSession: any, forceLock = false) => {
133+
ctx = {
134+
authStore: {
135+
getLocalID: jest.fn().mockReturnValue(0),
136+
getLockPasswordOnLaunch: jest.fn().mockReturnValue(undefined),
137+
getLockLastExtendTime: jest.fn().mockReturnValue(undefined),
138+
getLockTTL: jest.fn().mockReturnValue(undefined),
139+
},
140+
service: {
141+
auth: {
142+
config: { getPersistedSession: jest.fn().mockResolvedValue(persistedSession) },
143+
alarms: { autoLockAlarm: { when: jest.fn().mockResolvedValue(undefined) } },
144+
},
145+
storage: {
146+
local: {
147+
getItem: jest.fn(async (key) => (key === 'forceLock' ? forceLock : undefined)),
148+
setItem: jest.fn(),
149+
},
150+
},
151+
},
152+
} as any;
153+
WorkerContext.set(ctx);
154+
};
155+
156+
test.each([
157+
{
158+
name: 'password launch lock is flagged',
159+
persistedSession: {
160+
lockPasswordOnLaunch: true,
161+
offlineConfig: {},
162+
offlineVerifier: 'offline-verifier',
163+
},
164+
forceLock: true,
165+
expected: { forceLock: true, forcePasswordLock: true },
166+
},
167+
{
168+
name: 'password launch lock state is unknown',
169+
persistedSession: null,
170+
expected: { forceLock: true, forcePasswordLock: true },
171+
},
172+
{
173+
name: 'password launch lock is explicitly disabled',
174+
persistedSession: { lockPasswordOnLaunch: false },
175+
expected: { forceLock: false },
176+
},
177+
{
178+
name: 'launch blob exists even if the outer flag is disabled',
179+
persistedSession: { launchPasswordBlob: 'password-blob', lockPasswordOnLaunch: false },
180+
expected: { forceLock: true, forcePasswordLock: true },
181+
},
182+
])('should handle $name', async ({ persistedSession, forceLock, expected }) => {
183+
setup(persistedSession, forceLock);
184+
expect(await getForceLockOptions()).toEqual(expected);
185+
});
186+
});
130187
});

applications/pass/src/lib/auth.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ jest.useFakeTimers();
118118

119119
describe('AuthService', () => {
120120
beforeEach(() => {
121+
(global as any).DESKTOP_BUILD = false;
122+
(global as any).EXTENSION_BUILD = false;
121123
authStore.clear();
122124
jest.clearAllMocks();
123125

@@ -250,6 +252,42 @@ describe('AuthService', () => {
250252
expect(app.setStatus).toHaveBeenCalledWith(AppStatus.PASSWORD_LOCKED);
251253
});
252254

255+
test('desktop launch password lock uses auth session flag', async () => {
256+
(global as any).DESKTOP_BUILD = true;
257+
258+
getPersistedSession.mockImplementationOnce(async () => ({
259+
...MOCK_PERSISTED_SESSION,
260+
lockMode: LockMode.NONE,
261+
lockPasswordOnLaunch: true,
262+
}));
263+
264+
const options: AuthOptions = {};
265+
const result = await authService.init(options);
266+
267+
expect(result).toBe(false);
268+
expect(options.forceLock).toBe(true);
269+
expect(resumeSession).not.toHaveBeenCalled();
270+
expect(app.setStatus).toHaveBeenCalledWith(AppStatus.PASSWORD_LOCKED);
271+
});
272+
273+
test('desktop launch password lock uses protected blob over outer flag', async () => {
274+
(global as any).DESKTOP_BUILD = true;
275+
276+
getPersistedSession.mockImplementationOnce(async () => ({
277+
...MOCK_PERSISTED_SESSION,
278+
launchPasswordBlob: 'password-blob',
279+
lockMode: LockMode.NONE,
280+
lockPasswordOnLaunch: false,
281+
}));
282+
283+
const options: AuthOptions = {};
284+
const result = await authService.init(options);
285+
286+
expect(result).toBe(false);
287+
expect(resumeSession).not.toHaveBeenCalled();
288+
expect(app.setStatus).toHaveBeenCalledWith(AppStatus.PASSWORD_LOCKED);
289+
});
290+
253291
test('should set `forceLock` to true with empty localID manipulation', async () => {
254292
/** When user has valid session but URL has empty/malformed localID,
255293
* clear auth store and force lock to prevent unauthorized access */

packages/pass/lib/auth/integrity.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
SESSION_DIGEST_VERSION,
33
SESSION_INTEGRITY_KEYS_V1,
4+
SESSION_INTEGRITY_KEYS_V2,
45
digestSession,
56
getSessionDigestVersion,
67
getSessionIntegrityKeys,
@@ -28,6 +29,11 @@ describe('Session integrity', () => {
2829
expect(getSessionIntegrityKeys(1)).toEqual(SESSION_INTEGRITY_KEYS_V1);
2930
});
3031

32+
it('should return correct keys for version 2', () => {
33+
expect(getSessionIntegrityKeys(2)).toEqual(SESSION_INTEGRITY_KEYS_V2);
34+
expect(getSessionIntegrityKeys(2)).toContain('lockPasswordOnLaunch');
35+
});
36+
3137
it('should return an empty array for unknown versions', () => {
3238
expect(getSessionIntegrityKeys(-1)).toEqual([]);
3339
expect(getSessionIntegrityKeys(999)).toEqual([]);
@@ -63,5 +69,12 @@ describe('Session integrity', () => {
6369

6470
expect(digestA).not.toBe(digestB);
6571
});
72+
73+
it('should protect lockPasswordOnLaunch in version 2', async () => {
74+
const digestA = await digestSession({ ...MOCK_SESSION, lockPasswordOnLaunch: true }, 2);
75+
const digestB = await digestSession({ ...MOCK_SESSION, lockPasswordOnLaunch: false }, 2);
76+
77+
expect(digestA).not.toBe(digestB);
78+
});
6679
});
6780
});

packages/pass/lib/auth/lock/session/adapter.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ jest.mock('./lock.requests');
1212
jest.mock('@proton/pass/utils/time/epoch');
1313
jest.mock('@proton/pass/lib/auth/session', () => ({
1414
...jest.requireActual('@proton/pass/lib/auth/session'),
15+
decryptLaunchPasswordSessionBlob: jest.fn(),
1516
decryptSessionBlob: jest.fn(),
1617
getPersistedSessionKey: jest.fn(),
1718
}));
1819

1920
const unlockSessionMock = lockRequests.unlockSession as jest.Mock;
2021
const checkSessionLockMock = lockRequests.checkSessionLock as jest.Mock;
2122
const getEpochMock = getEpoch as jest.Mock;
23+
const decryptLaunchPasswordSessionBlobMock = authSession.decryptLaunchPasswordSessionBlob as jest.Mock;
2224
const decryptSessionBlobMock = authSession.decryptSessionBlob as jest.Mock;
2325
const getPersistedSessionKeyMock = authSession.getPersistedSessionKey as jest.Mock;
2426

@@ -45,6 +47,7 @@ describe('SessionLock adapter', () => {
4547
beforeEach(() => {
4648
getEpochMock.mockReturnValue(1000);
4749
getPersistedSessionKeyMock.mockResolvedValue('client-key');
50+
decryptLaunchPasswordSessionBlobMock.mockResolvedValue({ sessionLockToken: TOKEN });
4851
decryptSessionBlobMock.mockResolvedValue({ sessionLockToken: TOKEN });
4952
});
5053

@@ -147,6 +150,20 @@ describe('SessionLock adapter', () => {
147150
expect(auth.logout).toHaveBeenCalledWith({ soft: false, broadcast: true });
148151
});
149152

153+
test('should read the persisted token from launch password blob when present', async () => {
154+
const { adapter, authStore, config } = setupAdapter();
155+
config.getPersistedSession.mockResolvedValue({ blob: 'blob', launchPasswordBlob: 'password-blob' });
156+
unlockSessionMock.mockResolvedValue(TOKEN);
157+
authStore.setOfflineKD('offline-kd');
158+
authStore.setLockMode(LockMode.SESSION);
159+
160+
await adapter.unlock('123456');
161+
162+
expect(decryptLaunchPasswordSessionBlobMock).toHaveBeenCalledWith('offline-kd', 'password-blob');
163+
expect(getPersistedSessionKeyMock).not.toHaveBeenCalled();
164+
expect(decryptSessionBlobMock).not.toHaveBeenCalled();
165+
});
166+
150167
test('should notify and reset lock state when API returns 400 + SESSION_LOCKED', async () => {
151168
const { adapter, authStore, config } = setupAdapter();
152169
const apiError: any = new Error('lock removed');

packages/pass/lib/auth/service.spec.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ describe('Core AuthService', () => {
1919
let onLockUpdate: jest.Mock;
2020

2121
beforeEach(() => {
22+
(global as any).DESKTOP_BUILD = false;
23+
(global as any).EXTENSION_BUILD = true;
2224
jest.clearAllMocks();
2325
api = jest.fn() as unknown as Api;
2426
api.subscribe = jest.fn();
@@ -45,6 +47,32 @@ describe('Core AuthService', () => {
4547
});
4648
});
4749

50+
const registerPasswordLock = () => {
51+
const passwordLock = jest.fn().mockResolvedValue({ mode: LockMode.PASSWORD, locked: true });
52+
auth.registerLockAdapter({
53+
type: LockMode.PASSWORD,
54+
check: jest.fn(),
55+
create: jest.fn(),
56+
delete: jest.fn(),
57+
lock: passwordLock,
58+
unlock: jest.fn(),
59+
});
60+
return passwordLock;
61+
};
62+
63+
const offlineSession = {
64+
AccessToken: 'access-token',
65+
keyPassword: 'key-password',
66+
lockMode: LockMode.NONE,
67+
offlineConfig: { salt: 'salt', params: {} as any },
68+
offlineKD: 'offline-kd',
69+
offlineVerifier: 'offline-verifier',
70+
RefreshToken: 'refresh-token',
71+
sso: false,
72+
UID: 'uid',
73+
UserID: 'user-id',
74+
};
75+
4876
describe('AuthService::resumeSession', () => {
4977
test('should halt resume when `onResumeStart` returns `false`', async () => {
5078
onResumeStart.mockResolvedValueOnce(false);
@@ -63,6 +91,34 @@ describe('Core AuthService', () => {
6391
expect(result).toBe(true);
6492
expect(onResumeStart).toHaveBeenCalledWith({ hasSession: true, memorySession: { LocalID: 0 } });
6593
});
94+
95+
test('should stop before decrypting a launch password protected session', async () => {
96+
const passwordLock = registerPasswordLock();
97+
getPersistedSession.mockResolvedValueOnce({
98+
...offlineSession,
99+
blob: 'client-key-blob',
100+
launchPasswordBlob: 'password-blob',
101+
lockPasswordOnLaunch: false,
102+
});
103+
104+
const result = await auth.resumeSession(0, {});
105+
106+
expect(result).toBe(false);
107+
expect(passwordLock).toHaveBeenCalled();
108+
expect(api).not.toHaveBeenCalled();
109+
});
110+
});
111+
112+
describe('AuthService::login', () => {
113+
test('should use password lock override when force-locking with offline components', async () => {
114+
const passwordLock = registerPasswordLock();
115+
authStore.setLockMode(LockMode.NONE);
116+
117+
const result = await auth.login(offlineSession, { forceLock: true, forcePasswordLock: true });
118+
119+
expect(result).toBe(false);
120+
expect(passwordLock).toHaveBeenCalled();
121+
});
66122
});
67123

68124
describe('AuthService::syncPersistedSession', () => {

0 commit comments

Comments
 (0)