From a2e4729aaab8d8a50f101d03abe7d1cb279e7474 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 1 Aug 2022 09:51:27 +0800 Subject: [PATCH] test(test): add passwordless integration test (#1697) * test(test): add paswordless integration test add passwordless integration test * fix(test): add clean up add clean up --- packages/connector-mock-mail/src/index.ts | 8 + packages/connector-mock-sms/src/index.ts | 8 + packages/integration-tests/src/api/session.ts | 108 +++++++++++ packages/integration-tests/src/helpers.ts | 27 +++ packages/integration-tests/src/utils.ts | 7 + .../integration-tests/tests/connector.test.ts | 10 -- .../integration-tests/tests/session.test.ts | 170 +++++++++++++++++- 7 files changed, 326 insertions(+), 12 deletions(-) diff --git a/packages/connector-mock-mail/src/index.ts b/packages/connector-mock-mail/src/index.ts index 717507195..99bed289e 100644 --- a/packages/connector-mock-mail/src/index.ts +++ b/packages/connector-mock-mail/src/index.ts @@ -1,3 +1,6 @@ +import fs from 'fs/promises'; +import path from 'path'; + import { ConnectorError, ConnectorErrorCodes, @@ -71,6 +74,11 @@ export default class MockMailConnector implements EmailConnectorInstance followRedirect: false, }) .json(); + +export const sendRegisterUserWithEmailPasscode = (email: string, interactionCookie: string) => + api.post('session/register/passwordless/email/send-passcode', { + headers: { + cookie: interactionCookie, + }, + json: { + email, + }, + }); + +export const verifyRegisterUserWithEmailPasscode = ( + email: string, + code: string, + interactionCookie: string +) => + api + .post('session/register/passwordless/email/verify-passcode', { + headers: { + cookie: interactionCookie, + }, + json: { + email, + code, + }, + }) + .json(); + +export const sendSignInUserWithEmailPasscode = (email: string, interactionCookie: string) => + api.post('session/sign-in/passwordless/email/send-passcode', { + headers: { + cookie: interactionCookie, + }, + json: { + email, + }, + }); + +export const verifySignInUserWithEmailPasscode = ( + email: string, + code: string, + interactionCookie: string +) => + api + .post('session/sign-in/passwordless/email/verify-passcode', { + headers: { + cookie: interactionCookie, + }, + json: { + email, + code, + }, + }) + .json(); + +export const sendRegisterUserWithSmsPasscode = (phone: string, interactionCookie: string) => + api.post('session/register/passwordless/sms/send-passcode', { + headers: { + cookie: interactionCookie, + }, + json: { + phone, + }, + }); + +export const verifyRegisterUserWithSmsPasscode = ( + phone: string, + code: string, + interactionCookie: string +) => + api + .post('session/register/passwordless/sms/verify-passcode', { + headers: { + cookie: interactionCookie, + }, + json: { + phone, + code, + }, + }) + .json(); + +export const sendSignInUserWithSmsPasscode = (phone: string, interactionCookie: string) => + api.post('session/sign-in/passwordless/sms/send-passcode', { + headers: { + cookie: interactionCookie, + }, + json: { + phone, + }, + }); + +export const verifySignInUserWithSmsPasscode = ( + phone: string, + code: string, + interactionCookie: string +) => + api + .post('session/sign-in/passwordless/sms/verify-passcode', { + headers: { + cookie: interactionCookie, + }, + json: { + phone, + code, + }, + }) + .json(); diff --git a/packages/integration-tests/src/helpers.ts b/packages/integration-tests/src/helpers.ts index 566621230..f77a45600 100644 --- a/packages/integration-tests/src/helpers.ts +++ b/packages/integration-tests/src/helpers.ts @@ -1,3 +1,6 @@ +import fs from 'fs/promises'; +import path from 'path'; + import { User } from '@logto/schemas'; import { assert } from '@silverhand/essentials'; @@ -5,6 +8,8 @@ import { createUser, registerUserWithUsernameAndPassword, signInWithUsernameAndPassword, + updateConnectorConfig, + enableConnector, } from '@/api'; import MockClient from '@/client'; import { generateUsername, generatePassword } from '@/utils'; @@ -61,3 +66,25 @@ export const signIn = async (username: string, password: string) => { assert(client.isAuthenticated, new Error('Sign in failed')); }; + +export const setUpConnector = async (connectorId: string, config: Record) => { + await updateConnectorConfig(connectorId, config); + const connector = await enableConnector(connectorId); + assert(connector.enabled, new Error('Connector Setup Failed')); +}; + +type PasscodeRecord = { + phone?: string; + address?: string; + code: string; + type: string; +}; + +export const readPasscode = async (): Promise => { + const buffer = await fs.readFile(path.join('/tmp', 'logto_mock_passcode_record.txt')); + const content = buffer.toString(); + + // For test use only + // eslint-disable-next-line no-restricted-syntax + return JSON.parse(content) as PasscodeRecord; +}; diff --git a/packages/integration-tests/src/utils.ts b/packages/integration-tests/src/utils.ts index 0c629ee7a..2f1024853 100644 --- a/packages/integration-tests/src/utils.ts +++ b/packages/integration-tests/src/utils.ts @@ -11,3 +11,10 @@ export const generatePassword = () => `pwd_${crypto.randomUUID()}`; export const generateResourceName = () => `res_${crypto.randomUUID()}`; export const generateResourceIndicator = () => `https://${crypto.randomUUID()}.logto.io`; +export const generateEmail = () => `${crypto.randomUUID()}@logto.io`; + +export const generatePhone = () => { + const array = new Uint32Array(1); + + return crypto.getRandomValues(array).join(''); +}; diff --git a/packages/integration-tests/tests/connector.test.ts b/packages/integration-tests/tests/connector.test.ts index 40ebb2e3d..202129974 100644 --- a/packages/integration-tests/tests/connector.test.ts +++ b/packages/integration-tests/tests/connector.test.ts @@ -29,16 +29,6 @@ import { * for testing updating configs and enabling/disabling for now. */ test('connector set-up flow', async () => { - /* - * List connectors after initializing a new Logto instance - */ - const allConnectors = await listConnectors(); - - // There should be no connectors, or all connectors should be disabled. - for (const connectorDto of allConnectors) { - expect(connectorDto.enabled).toBeFalsy(); - } - /* * Set up social/SMS/email connectors */ diff --git a/packages/integration-tests/tests/session.test.ts b/packages/integration-tests/tests/session.test.ts index 47e9606b2..80f94a013 100644 --- a/packages/integration-tests/tests/session.test.ts +++ b/packages/integration-tests/tests/session.test.ts @@ -1,5 +1,25 @@ -import { registerNewUser, signIn } from '@/helpers'; -import { generateUsername, generatePassword } from '@/utils'; +import { assert } from '@silverhand/essentials'; + +import { + mockEmailConnectorId, + mockEmailConnectorConfig, + mockSmsConnectorId, + mockSmsConnectorConfig, +} from '@/__mocks__/connectors-mock'; +import { + sendRegisterUserWithEmailPasscode, + verifyRegisterUserWithEmailPasscode, + sendSignInUserWithEmailPasscode, + verifySignInUserWithEmailPasscode, + sendRegisterUserWithSmsPasscode, + verifyRegisterUserWithSmsPasscode, + sendSignInUserWithSmsPasscode, + verifySignInUserWithSmsPasscode, + disableConnector, +} from '@/api'; +import MockClient from '@/client'; +import { registerNewUser, signIn, setUpConnector, readPasscode } from '@/helpers'; +import { generateUsername, generatePassword, generateEmail, generatePhone } from '@/utils'; describe('username and password flow', () => { const username = generateUsername(); @@ -13,3 +33,149 @@ describe('username and password flow', () => { await expect(signIn(username, password)).resolves.not.toThrow(); }); }); + +describe('email passwordless flow', () => { + beforeAll(async () => { + await setUpConnector(mockEmailConnectorId, mockEmailConnectorConfig); + }); + + // Since we can not create a email register user throw admin. Have to run the register then sign-in concurrently. + const email = generateEmail(); + + it('register with email', async () => { + const client = new MockClient(); + + await client.initSession(); + assert(client.interactionCookie, new Error('Session not found')); + + await expect( + sendRegisterUserWithEmailPasscode(email, client.interactionCookie) + ).resolves.not.toThrow(); + + const passcodeRecord = await readPasscode(); + + expect(passcodeRecord).toMatchObject({ + address: email, + type: 'Register', + }); + + const { code } = passcodeRecord; + + const { redirectTo } = await verifyRegisterUserWithEmailPasscode( + email, + code, + client.interactionCookie + ); + + await client.processSession(redirectTo); + + expect(client.isAuthenticated).toBeTruthy(); + }); + + it('sign-in with email', async () => { + const client = new MockClient(); + + await client.initSession(); + assert(client.interactionCookie, new Error('Session not found')); + + await expect( + sendSignInUserWithEmailPasscode(email, client.interactionCookie) + ).resolves.not.toThrow(); + + const passcodeRecord = await readPasscode(); + + expect(passcodeRecord).toMatchObject({ + address: email, + type: 'SignIn', + }); + + const { code } = passcodeRecord; + + const { redirectTo } = await verifySignInUserWithEmailPasscode( + email, + code, + client.interactionCookie + ); + + await client.processSession(redirectTo); + + expect(client.isAuthenticated).toBeTruthy(); + }); + + afterAll(async () => { + void disableConnector(mockEmailConnectorId); + }); +}); + +describe('sms passwordless flow', () => { + beforeAll(async () => { + await setUpConnector(mockSmsConnectorId, mockSmsConnectorConfig); + }); + + // Since we can not create a sms register user throw admin. Have to run the register then sign-in concurrently. + const phone = generatePhone(); + + it('register with sms', async () => { + const client = new MockClient(); + + await client.initSession(); + assert(client.interactionCookie, new Error('Session not found')); + + await expect( + sendRegisterUserWithSmsPasscode(phone, client.interactionCookie) + ).resolves.not.toThrow(); + + const passcodeRecord = await readPasscode(); + + expect(passcodeRecord).toMatchObject({ + phone, + type: 'Register', + }); + + const { code } = passcodeRecord; + + const { redirectTo } = await verifyRegisterUserWithSmsPasscode( + phone, + code, + client.interactionCookie + ); + + await client.processSession(redirectTo); + + expect(client.isAuthenticated).toBeTruthy(); + }); + + it('sign-in with email', async () => { + const client = new MockClient(); + + await client.initSession(); + assert(client.interactionCookie, new Error('Session not found')); + + await expect( + sendSignInUserWithSmsPasscode(phone, client.interactionCookie) + ).resolves.not.toThrow(); + + const passcodeRecord = await readPasscode(); + + expect(passcodeRecord).toMatchObject({ + phone, + type: 'SignIn', + }); + + const { code } = passcodeRecord; + + const { redirectTo } = await verifySignInUserWithSmsPasscode( + phone, + code, + client.interactionCookie + ); + + await client.processSession(redirectTo); + + expect(client.isAuthenticated).toBeTruthy(); + }); + + afterAll(async () => { + void disableConnector(mockSmsConnectorId); + }); +});