Skip to content

Commit cd03392

Browse files
committed
test(profile-sync-controller): test new pairing functionality
1 parent d90711c commit cd03392

File tree

4 files changed

+249
-6
lines changed

4 files changed

+249
-6
lines changed

packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.test.ts

Lines changed: 176 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import { Messenger } from '@metamask/base-controller';
22

3-
import AuthenticationController from './AuthenticationController';
43
import type {
54
AllowedActions,
65
AllowedEvents,
76
AuthenticationControllerState,
87
} from './AuthenticationController';
9-
import {
10-
MOCK_LOGIN_RESPONSE,
11-
MOCK_OATH_TOKEN_RESPONSE,
12-
} from './mocks/mockResponses';
8+
import AuthenticationController from './AuthenticationController';
9+
import { MOCK_LOGIN_RESPONSE, MOCK_OATH_TOKEN_RESPONSE } from './mocks';
1310
import type { LoginResponse } from '../../sdk';
1411
import { Platform } from '../../sdk';
1512
import { arrangeAuthAPIs } from '../../sdk/__fixtures__/auth';
@@ -54,6 +51,8 @@ describe('authentication/authentication-controller - constructor() tests', () =>
5451

5552
expect(controller.state.isSignedIn).toBe(false);
5653
expect(controller.state.srpSessionData).toBeUndefined();
54+
expect(controller.state.socialPairingDone).toBeUndefined();
55+
expect(controller.state.pairingInProgress).toBeUndefined();
5756
});
5857

5958
it('should initialize with override state', () => {
@@ -204,19 +203,181 @@ describe('authentication/authentication-controller - performSignIn() tests', ()
204203
}
205204
});
206205

206+
describe('authentication/authentication-controller - performSignIn() with pairing functionality', () => {
207+
it('triggers social pairing when social token is available', async () => {
208+
const metametrics = createMockAuthMetaMetrics();
209+
const mockEndpoints = arrangeAuthAPIs();
210+
const { messenger, mockSeedlessOnboardingGetState } =
211+
createMockAuthenticationMessenger();
212+
213+
// Mock social token is available
214+
mockSeedlessOnboardingGetState.mockReturnValue({
215+
accessToken: 'MOCK_SOCIAL_TOKEN',
216+
});
217+
218+
const controller = new AuthenticationController({ messenger, metametrics });
219+
220+
const result = await controller.performSignIn();
221+
222+
// Verify sign-in still works normally
223+
expect(result).toStrictEqual([
224+
MOCK_OATH_TOKEN_RESPONSE.access_token,
225+
MOCK_OATH_TOKEN_RESPONSE.access_token,
226+
]);
227+
228+
// Verify SeedlessOnboardingController was called
229+
expect(mockSeedlessOnboardingGetState).toHaveBeenCalled();
230+
231+
// Note: Pairing happens asynchronously, so we need to wait longer
232+
await new Promise((resolve) => setTimeout(resolve, 100));
233+
234+
// Should have attempted pairing
235+
expect(controller.state.isSignedIn).toBe(true);
236+
expect(controller.state.socialPairingDone).toBe(true);
237+
expect(controller.state.pairingInProgress).toBe(false);
238+
mockEndpoints.mockNonceUrl.done();
239+
mockEndpoints.mockSrpLoginUrl.done();
240+
mockEndpoints.mockOAuth2TokenUrl.done();
241+
mockEndpoints.mockPairSocialIdentifierUrl.done();
242+
});
243+
244+
it('does not attempt pairing when social token is unavailable', async () => {
245+
const mockEndpoints = arrangeAuthAPIs();
246+
const { messenger, mockSeedlessOnboardingGetState } =
247+
createMockAuthenticationMessenger();
248+
249+
mockSeedlessOnboardingGetState.mockReturnValue({
250+
accessToken: null,
251+
});
252+
253+
const controller = new AuthenticationController({
254+
messenger,
255+
metametrics: createMockAuthMetaMetrics(),
256+
});
257+
258+
await controller.performSignIn();
259+
await new Promise((resolve) => setTimeout(resolve, 50));
260+
261+
expect(controller.state.socialPairingDone).toBeUndefined();
262+
expect(controller.state.pairingInProgress).toBe(false);
263+
expect(mockEndpoints.mockPairSocialIdentifierUrl.isDone()).toBe(false);
264+
mockEndpoints.mockNonceUrl.done();
265+
mockEndpoints.mockSrpLoginUrl.done();
266+
mockEndpoints.mockOAuth2TokenUrl.done();
267+
});
268+
269+
it('does not attempt pairing when already done', async () => {
270+
const mockEndpoints = arrangeAuthAPIs();
271+
const { messenger, mockSeedlessOnboardingGetState } =
272+
createMockAuthenticationMessenger();
273+
274+
mockSeedlessOnboardingGetState.mockReturnValue({
275+
accessToken: 'MOCK_SOCIAL_TOKEN',
276+
});
277+
278+
const controller = new AuthenticationController({
279+
messenger,
280+
metametrics: createMockAuthMetaMetrics(),
281+
state: {
282+
isSignedIn: false,
283+
socialPairingDone: true,
284+
},
285+
});
286+
287+
await controller.performSignIn();
288+
await new Promise((resolve) => setTimeout(resolve, 50));
289+
290+
// Should not update state since pairing was already done
291+
expect(controller.state.socialPairingDone).toBe(true);
292+
expect(controller.state.pairingInProgress).toBe(false);
293+
expect(mockEndpoints.mockPairSocialIdentifierUrl.isDone()).toBe(false);
294+
mockEndpoints.mockNonceUrl.done();
295+
mockEndpoints.mockSrpLoginUrl.done();
296+
mockEndpoints.mockOAuth2TokenUrl.done();
297+
});
298+
299+
it('does not attempt pairing when pairing is already in progress', async () => {
300+
const mockEndpoints = arrangeAuthAPIs();
301+
const { messenger, mockSeedlessOnboardingGetState } =
302+
createMockAuthenticationMessenger();
303+
304+
mockSeedlessOnboardingGetState.mockReturnValue({
305+
accessToken: 'MOCK_SOCIAL_TOKEN',
306+
});
307+
308+
const controller = new AuthenticationController({
309+
messenger,
310+
metametrics: createMockAuthMetaMetrics(),
311+
state: {
312+
isSignedIn: false,
313+
pairingInProgress: true,
314+
},
315+
});
316+
317+
await controller.performSignIn();
318+
await new Promise((resolve) => setTimeout(resolve, 50));
319+
320+
// Should not change pairing state since it was already in progress
321+
expect(controller.state.pairingInProgress).toBe(true);
322+
expect(mockEndpoints.mockPairSocialIdentifierUrl.isDone()).toBe(false);
323+
mockEndpoints.mockNonceUrl.done();
324+
mockEndpoints.mockSrpLoginUrl.done();
325+
mockEndpoints.mockOAuth2TokenUrl.done();
326+
});
327+
328+
it('handles pairing failures gracefully', async () => {
329+
const metametrics = createMockAuthMetaMetrics();
330+
const mockEndpoints = arrangeAuthAPIs({
331+
mockPairSocialIdentifier: { status: 400 },
332+
});
333+
const { messenger, mockSeedlessOnboardingGetState } =
334+
createMockAuthenticationMessenger();
335+
336+
mockSeedlessOnboardingGetState.mockReturnValue({
337+
accessToken: 'MOCK_SOCIAL_TOKEN',
338+
});
339+
340+
const controller = new AuthenticationController({ messenger, metametrics });
341+
342+
const result = await controller.performSignIn();
343+
344+
// Sign-in should still succeed even if pairing fails
345+
expect(result).toStrictEqual([
346+
MOCK_OATH_TOKEN_RESPONSE.access_token,
347+
MOCK_OATH_TOKEN_RESPONSE.access_token,
348+
]);
349+
expect(controller.state.isSignedIn).toBe(true);
350+
351+
await new Promise((resolve) => setTimeout(resolve, 100));
352+
353+
// Pairing should have been attempted but failed
354+
expect(controller.state.socialPairingDone).toBeUndefined();
355+
expect(controller.state.pairingInProgress).toBe(false);
356+
mockEndpoints.mockNonceUrl.done();
357+
mockEndpoints.mockSrpLoginUrl.done();
358+
mockEndpoints.mockOAuth2TokenUrl.done();
359+
mockEndpoints.mockPairSocialIdentifierUrl.done();
360+
});
361+
});
362+
207363
describe('authentication/authentication-controller - performSignOut() tests', () => {
208364
it('should remove signed in user and any access tokens', () => {
209365
const metametrics = createMockAuthMetaMetrics();
210366
const { messenger } = createMockAuthenticationMessenger();
211367
const controller = new AuthenticationController({
212368
messenger,
213-
state: mockSignedInState(),
369+
state: {
370+
...mockSignedInState(),
371+
socialPairingDone: true,
372+
pairingInProgress: false,
373+
},
214374
metametrics,
215375
});
216376

217377
controller.performSignOut();
218378
expect(controller.state.isSignedIn).toBe(false);
219379
expect(controller.state.srpSessionData).toBeUndefined();
380+
expect(controller.state.socialPairingDone).toBe(false);
220381
});
221382
});
222383

@@ -555,6 +716,10 @@ function createMockAuthenticationMessenger() {
555716
.fn()
556717
.mockReturnValue({ isUnlocked: true });
557718

719+
const mockSeedlessOnboardingGetState = jest.fn().mockReturnValue({
720+
accessToken: 'MOCK_SOCIAL_TOKEN',
721+
});
722+
558723
mockCall.mockImplementation((...args) => {
559724
const [actionType, params] = args;
560725
if (actionType === 'SnapController:handleRequest') {
@@ -581,6 +746,10 @@ function createMockAuthenticationMessenger() {
581746
return mockKeyringControllerGetState();
582747
}
583748

749+
if (actionType === 'SeedlessOnboardingController:getState') {
750+
return mockSeedlessOnboardingGetState();
751+
}
752+
584753
throw new Error(
585754
`MOCK_FAIL - unsupported messenger call: ${actionType as string}`,
586755
);
@@ -593,6 +762,7 @@ function createMockAuthenticationMessenger() {
593762
mockSnapGetAllPublicKeys,
594763
mockSnapSignMessage,
595764
mockKeyringControllerGetState,
765+
mockSeedlessOnboardingGetState,
596766
};
597767
}
598768

packages/profile-sync-controller/src/sdk/__fixtures__/auth.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
MOCK_OIDC_TOKEN_RESPONSE,
77
MOCK_OIDC_TOKEN_URL,
88
MOCK_PAIR_IDENTIFIERS_URL,
9+
MOCK_PAIR_SOCIAL_IDENTIFIER_URL,
910
MOCK_PROFILE_METAMETRICS_URL,
1011
MOCK_SIWE_LOGIN_RESPONSE,
1112
MOCK_SIWE_LOGIN_URL,
@@ -51,6 +52,16 @@ export const handleMockPairIdentifiers = (mockReply?: MockReply) => {
5152
return mockPairIdentifiersEndpoint;
5253
};
5354

55+
export const handleMockPairSocialIdentifier = (mockReply?: MockReply) => {
56+
const reply = mockReply ?? { status: 200 };
57+
const mockPairSocialIdentifierEndpoint = nock(MOCK_PAIR_SOCIAL_IDENTIFIER_URL)
58+
.persist()
59+
.post('')
60+
.reply(reply.status, reply.body);
61+
62+
return mockPairSocialIdentifierEndpoint;
63+
};
64+
5465
export const handleMockSrpLogin = (mockReply?: MockReply) => {
5566
const reply = mockReply ?? { status: 200, body: MOCK_SRP_LOGIN_RESPONSE };
5667
const mockLoginEndpoint = nock(MOCK_SRP_LOGIN_URL)
@@ -91,6 +102,7 @@ export const arrangeAuthAPIs = (options?: {
91102
mockSrpLoginUrl?: MockReply;
92103
mockSiweLoginUrl?: MockReply;
93104
mockPairIdentifiers?: MockReply;
105+
mockPairSocialIdentifier?: MockReply;
94106
mockUserProfileMetaMetrics?: MockReply;
95107
}) => {
96108
const mockNonceUrl = handleMockNonce(options?.mockNonceUrl);
@@ -100,6 +112,9 @@ export const arrangeAuthAPIs = (options?: {
100112
const mockPairIdentifiersUrl = handleMockPairIdentifiers(
101113
options?.mockPairIdentifiers,
102114
);
115+
const mockPairSocialIdentifierUrl = handleMockPairSocialIdentifier(
116+
options?.mockPairSocialIdentifier,
117+
);
103118
const mockUserProfileMetaMetricsUrl = handleMockUserProfileMetaMetrics(
104119
options?.mockUserProfileMetaMetrics,
105120
);
@@ -110,6 +125,7 @@ export const arrangeAuthAPIs = (options?: {
110125
mockSrpLoginUrl,
111126
mockSiweLoginUrl,
112127
mockPairIdentifiersUrl,
128+
mockPairSocialIdentifierUrl,
113129
mockUserProfileMetaMetricsUrl,
114130
};
115131
};

packages/profile-sync-controller/src/sdk/authentication.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,61 @@ describe('Identifier Pairing', () => {
129129
});
130130
});
131131

132+
describe('Social Identifier Pairing', () => {
133+
it('successfully pairs with social identifier', async () => {
134+
const { auth } = arrangeAuth('SRP', MOCK_SRP);
135+
const { mockNonceUrl, mockOAuth2TokenUrl, mockPairSocialIdentifierUrl } =
136+
arrangeAuthAPIs();
137+
138+
const result = await auth.pairSocialIdentifier('MOCK_JWT_TOKEN');
139+
140+
expect(result).toBe(true);
141+
expect(mockNonceUrl.isDone()).toBe(true);
142+
expect(mockOAuth2TokenUrl.isDone()).toBe(true);
143+
expect(mockPairSocialIdentifierUrl.isDone()).toBe(true);
144+
});
145+
146+
it('handles social pairing failures', async () => {
147+
const { auth } = arrangeAuth('SRP', MOCK_SRP);
148+
const { mockNonceUrl, mockOAuth2TokenUrl, mockPairSocialIdentifierUrl } =
149+
arrangeAuthAPIs({
150+
mockPairSocialIdentifier: { status: 400 },
151+
});
152+
153+
const result = await auth.pairSocialIdentifier('INVALID_TOKEN');
154+
155+
expect(result).toBe(false);
156+
expect(mockNonceUrl.isDone()).toBe(true);
157+
expect(mockOAuth2TokenUrl.isDone()).toBe(true);
158+
expect(mockPairSocialIdentifierUrl.isDone()).toBe(true);
159+
});
160+
161+
it('handles social pairing authentication failures', async () => {
162+
const { auth } = arrangeAuth('SRP', MOCK_SRP);
163+
const { mockOAuth2TokenUrl } = arrangeAuthAPIs({
164+
mockOAuth2TokenUrl: { status: 401 },
165+
});
166+
167+
await expect(auth.pairSocialIdentifier('INVALID_TOKEN')).rejects.toThrow(
168+
/unable to get access token.*/u,
169+
);
170+
expect(mockOAuth2TokenUrl.isDone()).toBe(true);
171+
});
172+
173+
it('handles social pairing nonce failures', async () => {
174+
const { auth } = arrangeAuth('SRP', MOCK_SRP);
175+
const { mockNonceUrl, mockOAuth2TokenUrl } = arrangeAuthAPIs({
176+
mockNonceUrl: { status: 400 },
177+
});
178+
179+
await expect(auth.pairSocialIdentifier('MOCK_JWT_TOKEN')).rejects.toThrow(
180+
/failed to generate nonce.*/u,
181+
);
182+
expect(mockOAuth2TokenUrl.isDone()).toBe(true);
183+
expect(mockNonceUrl.isDone()).toBe(true);
184+
});
185+
});
186+
132187
describe('Authentication - constructor()', () => {
133188
it('errors on invalid auth type', async () => {
134189
expect(() => {

packages/profile-sync-controller/src/sdk/mocks/auth.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
SRP_LOGIN_URL,
66
OIDC_TOKEN_URL,
77
PAIR_IDENTIFIERS,
8+
PAIR_SOCIAL_IDENTIFIER,
89
PROFILE_METAMETRICS_URL,
910
} from '../authentication-jwt-bearer/services';
1011

@@ -13,6 +14,7 @@ export const MOCK_SRP_LOGIN_URL = SRP_LOGIN_URL(Env.PRD);
1314
export const MOCK_OIDC_TOKEN_URL = OIDC_TOKEN_URL(Env.PRD);
1415
export const MOCK_SIWE_LOGIN_URL = SIWE_LOGIN_URL(Env.PRD);
1516
export const MOCK_PAIR_IDENTIFIERS_URL = PAIR_IDENTIFIERS(Env.PRD);
17+
export const MOCK_PAIR_SOCIAL_IDENTIFIER_URL = PAIR_SOCIAL_IDENTIFIER(Env.PRD);
1618
export const MOCK_PROFILE_METAMETRICS_URL = PROFILE_METAMETRICS_URL(Env.PRD);
1719

1820
export const MOCK_JWT =

0 commit comments

Comments
 (0)