From f257e4deb2efb13aeae85bc51f768c48809aee38 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Wed, 3 Aug 2022 13:43:28 +0800 Subject: [PATCH] test(test): add social integration tests (#1721) * test(test): add social integration tests add social integration tests * fix(test): align format align format * fix(test): cr update cr update * fix(test): cr update cr update * fix(test): cr update cr update --- packages/connector-mock-social/src/index.ts | 17 +- packages/integration-tests/src/api/session.ts | 39 +++++ .../integration-tests/src/client/index.ts | 4 + .../tests/social-session.test.ts | 145 ++++++++++++++++++ 4 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 packages/integration-tests/tests/social-session.test.ts diff --git a/packages/connector-mock-social/src/index.ts b/packages/connector-mock-social/src/index.ts index 6ab1a1abc..e8c75df7a 100644 --- a/packages/connector-mock-social/src/index.ts +++ b/packages/connector-mock-social/src/index.ts @@ -10,6 +10,7 @@ import { SocialConnectorInstance, GetConnectorConfig, } from '@logto/connector-types'; +import { z } from 'zod'; import { defaultMetadata } from './constant'; import { mockSocialConfigGuard, MockSocialConfig } from './types'; @@ -46,7 +47,17 @@ export default class MockSocialConnector implements SocialConnectorInstance randomUUID(); - public getUserInfo: GetUserInfo = async () => ({ - id: `mock-social-sub-${randomUUID()}`, - }); + public getUserInfo: GetUserInfo = async (data) => { + const dataGuard = z.object({ code: z.string(), userId: z.optional(z.string()) }); + const result = dataGuard.safeParse(data); + + if (!result.success) { + throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, JSON.stringify(data)); + } + + // For mock use only. Use to track the created user entity + return { + id: result.data.userId ?? `mock-social-sub-${randomUUID()}`, + }; + }; } diff --git a/packages/integration-tests/src/api/session.ts b/packages/integration-tests/src/api/session.ts index 9dccfbd73..b0b637f51 100644 --- a/packages/integration-tests/src/api/session.ts +++ b/packages/integration-tests/src/api/session.ts @@ -157,3 +157,42 @@ export const verifySignInUserWithSmsPasscode = ( }, }) .json(); + +export const signInWithSocial = ( + payload: { + connectorId: string; + state: string; + redirectUri: string; + }, + interactionCookie: string +) => + api + .post('session/sign-in/social', { headers: { cookie: interactionCookie }, json: payload }) + .json(); + +export const getAuthWithSocial = ( + payload: { connectorId: string; data: unknown }, + interactionCookie: string +) => + api + .post('session/sign-in/social/auth', { + headers: { cookie: interactionCookie }, + json: payload, + }) + .json(); + +export const registerWithSocial = (connectorId: string, interactionCookie: string) => + api + .post('session/register/social', { + headers: { cookie: interactionCookie }, + json: { connectorId }, + }) + .json(); + +export const bindWithSocial = (connectorId: string, interactionCookie: string) => + api + .post('session/bind-social', { + headers: { cookie: interactionCookie }, + json: { connectorId }, + }) + .json(); diff --git a/packages/integration-tests/src/client/index.ts b/packages/integration-tests/src/client/index.ts index c0bc41d9e..705581761 100644 --- a/packages/integration-tests/src/client/index.ts +++ b/packages/integration-tests/src/client/index.ts @@ -95,6 +95,10 @@ export default class MockClient { return this.logto.isAuthenticated; } + public getIdTokenClaims() { + return this.logto.getIdTokenClaims(); + } + private readonly consent = async () => { // Note: If sign in action completed successfully, we will get `_session.sig` in the cookie. assert(this.interactionCookie, new Error('Session not found')); diff --git a/packages/integration-tests/tests/social-session.test.ts b/packages/integration-tests/tests/social-session.test.ts new file mode 100644 index 000000000..c7116c737 --- /dev/null +++ b/packages/integration-tests/tests/social-session.test.ts @@ -0,0 +1,145 @@ +import { assert } from '@silverhand/essentials'; +import { HTTPError } from 'got'; + +import { + mockSocialConnectorId, + mockSocialConnectorTarget, + mockSocialConnectorConfig, +} from '@/__mocks__/connectors-mock'; +import { + signInWithSocial, + getAuthWithSocial, + registerWithSocial, + bindWithSocial, + signInWithUsernameAndPassword, + getUser, +} from '@/api'; +import MockClient from '@/client'; +import { setUpConnector, createUserByAdmin } from '@/helpers'; +import { generateUsername, generatePassword } from '@/utils'; + +const state = 'foo_state'; +const redirectUri = 'http://foo.dev/callback'; +const code = 'auth_code_foo'; + +describe('social sign-in and register', () => { + const socialUserId = crypto.randomUUID(); + + beforeAll(async () => { + await setUpConnector(mockSocialConnectorId, mockSocialConnectorConfig); + }); + + it('register with social', async () => { + const client = new MockClient(); + + await client.initSession(); + assert(client.interactionCookie, new Error('Session not found')); + + await expect( + signInWithSocial( + { state, connectorId: mockSocialConnectorId, redirectUri }, + client.interactionCookie + ) + ).resolves.toBeTruthy(); + + const response = await getAuthWithSocial( + { + connectorId: mockSocialConnectorId, + data: { state, redirectUri, code, userId: socialUserId }, + }, + client.interactionCookie + ).catch((error: unknown) => error); + + // User with social does not exist + expect(response instanceof HTTPError && response.response.statusCode === 422).toBe(true); + + // Register with social + const { redirectTo } = await registerWithSocial( + mockSocialConnectorId, + client.interactionCookie + ); + + await client.processSession(redirectTo); + + expect(client.isAuthenticated).toBeTruthy(); + }); + + /* + * Note: As currently we can not prepare a social identities through admin api. + * The sign-in test case MUST run concurrently after the register test case + */ + it('Sign-In with social', async () => { + const client = new MockClient(); + + await client.initSession(); + assert(client.interactionCookie, new Error('Session not found')); + + await expect( + signInWithSocial( + { state, connectorId: mockSocialConnectorId, redirectUri }, + client.interactionCookie + ) + ).resolves.toBeTruthy(); + + const { redirectTo } = await getAuthWithSocial( + { + connectorId: mockSocialConnectorId, + data: { state, redirectUri, code, userId: socialUserId }, + }, + client.interactionCookie + ); + + await client.processSession(redirectTo); + + expect(client.isAuthenticated).toBeTruthy(); + }); +}); + +describe('social bind account', () => { + const username = generateUsername(); + const password = generatePassword(); + + beforeAll(async () => { + await createUserByAdmin(username, password); + }); + + it('bind new social account', async () => { + const client = new MockClient(); + + await client.initSession(); + assert(client.interactionCookie, new Error('Session not found')); + + await expect( + signInWithSocial( + { state, connectorId: mockSocialConnectorId, redirectUri }, + client.interactionCookie + ) + ).resolves.toBeTruthy(); + + const response = await getAuthWithSocial( + { connectorId: mockSocialConnectorId, data: { state, redirectUri, code } }, + client.interactionCookie + ).catch((error: unknown) => error); + + // User with social does not exist + expect(response instanceof HTTPError && response.response.statusCode === 422).toBe(true); + + const { redirectTo } = await signInWithUsernameAndPassword( + username, + password, + client.interactionCookie + ); + + await expect( + bindWithSocial(mockSocialConnectorId, client.interactionCookie) + ).resolves.not.toThrow(); + + await client.processSession(redirectTo); + + // User should bind with social identities + const { sub } = client.getIdTokenClaims(); + const user = await getUser(sub); + + expect(user.identities).toHaveProperty(mockSocialConnectorTarget); + }); +});