diff --git a/packages/integration-tests/src/api/index.ts b/packages/integration-tests/src/api/index.ts index d0012206c..7faf8bd7b 100644 --- a/packages/integration-tests/src/api/index.ts +++ b/packages/integration-tests/src/api/index.ts @@ -3,7 +3,6 @@ export * from './connector.js'; export * from './application.js'; export * from './sign-in-experience.js'; export * from './admin-user.js'; -export * from './session.js'; export * from './logs.js'; export * from './dashboard.js'; export * from './me.js'; diff --git a/packages/integration-tests/src/api/session.ts b/packages/integration-tests/src/api/session.ts deleted file mode 100644 index 637574665..000000000 --- a/packages/integration-tests/src/api/session.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { VerificationCodeType } from '@logto/connector-kit'; - -import api from './api.js'; - -type RedirectResponse = { - redirectTo: string; -}; - -export const registerUserWithUsernameAndPassword = async ( - username: string, - password: string, - interactionCookie: string -) => - api - .post('session/register/password/username', { - headers: { - cookie: interactionCookie, - }, - json: { - username, - password, - }, - followRedirect: false, - }) - .json(); - -export type SignInWithPassword = { - username?: string; - email?: string; - password: string; - interactionCookie: string; -}; - -export const signInWithPassword = async ({ - email, - username, - password, - interactionCookie, -}: SignInWithPassword) => - api - // This route in core needs to be refactored - .post('session/sign-in/password/' + (username ? 'username' : 'email'), { - headers: { - cookie: interactionCookie, - }, - json: { - email, - username, - password, - }, - followRedirect: false, - }) - .json(); - -export const sendRegisterUserWithEmailPasscode = (email: string, interactionCookie: string) => - api.post('session/passwordless/email/send', { - headers: { - cookie: interactionCookie, - }, - json: { - email, - flow: VerificationCodeType.Register, - }, - }); - -export const verifyRegisterUserWithEmailPasscode = ( - email: string, - code: string, - interactionCookie: string -) => - api - .post('session/passwordless/email/verify', { - headers: { - cookie: interactionCookie, - }, - json: { - email, - code, - flow: VerificationCodeType.Register, - }, - }) - .json(); - -export const checkVerificationSessionAndRegisterWithEmail = (interactionCookie: string) => - api - .post('session/register/passwordless/email', { - headers: { - cookie: interactionCookie, - }, - }) - .json(); - -export const sendSignInUserWithEmailPasscode = (email: string, interactionCookie: string) => - api.post('session/passwordless/email/send', { - headers: { - cookie: interactionCookie, - }, - json: { - email, - flow: VerificationCodeType.SignIn, - }, - }); - -export const verifySignInUserWithEmailPasscode = ( - email: string, - code: string, - interactionCookie: string -) => - api - .post('session/passwordless/email/verify', { - headers: { - cookie: interactionCookie, - }, - json: { - email, - code, - flow: VerificationCodeType.SignIn, - }, - }) - .json(); - -export const checkVerificationSessionAndSignInWithEmail = (interactionCookie: string) => - api - .post('session/sign-in/passwordless/email', { - headers: { - cookie: interactionCookie, - }, - }) - .json(); - -export const sendRegisterUserWithSmsPasscode = (phone: string, interactionCookie: string) => - api.post('session/passwordless/sms/send', { - headers: { - cookie: interactionCookie, - }, - json: { - phone, - flow: VerificationCodeType.Register, - }, - }); - -export const verifyRegisterUserWithSmsPasscode = ( - phone: string, - code: string, - interactionCookie: string -) => - api - .post('session/passwordless/sms/verify', { - headers: { - cookie: interactionCookie, - }, - json: { - phone, - code, - flow: VerificationCodeType.Register, - }, - }) - .json(); - -export const checkVerificationSessionAndRegisterWithSms = (interactionCookie: string) => - api - .post('session/register/passwordless/sms', { - headers: { - cookie: interactionCookie, - }, - }) - .json(); - -export const sendSignInUserWithSmsPasscode = (phone: string, interactionCookie: string) => - api.post('session/passwordless/sms/send', { - headers: { - cookie: interactionCookie, - }, - json: { - phone, - flow: VerificationCodeType.SignIn, - }, - }); - -export const verifySignInUserWithSmsPasscode = ( - phone: string, - code: string, - interactionCookie: string -) => - api - .post('session/passwordless/sms/verify', { - headers: { - cookie: interactionCookie, - }, - json: { - phone, - code, - flow: VerificationCodeType.SignIn, - }, - }) - .json(); - -export const checkVerificationSessionAndSignInWithSms = (interactionCookie: string) => - api - .post('session/sign-in/passwordless/sms', { - headers: { - cookie: interactionCookie, - }, - }) - .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/helpers.ts b/packages/integration-tests/src/helpers.ts deleted file mode 100644 index 81f5bcbfb..000000000 --- a/packages/integration-tests/src/helpers.ts +++ /dev/null @@ -1,214 +0,0 @@ -import fs from 'fs/promises'; -import { createServer } from 'http'; -import path from 'path'; - -import type { User, SignIn, SignInIdentifier } from '@logto/schemas'; -import { assert } from '@silverhand/essentials'; -import { HTTPError, RequestError } from 'got'; - -import { - createUser, - registerUserWithUsernameAndPassword, - signInWithPassword, - bindWithSocial, - getAuthWithSocial, - signInWithSocial, - updateSignInExperience, -} from '#src/api/index.js'; -import MockClient from '#src/client/index.js'; -import { generateUsername, generatePassword } from '#src/utils.js'; - -import { enableAllPasswordSignInMethods } from './tests/api/interaction/utils/sign-in-experience.js'; - -export const createUserByAdmin = ( - username?: string, - password?: string, - primaryEmail?: string, - primaryPhone?: string, - name?: string, - isAdmin = false -) => { - return createUser({ - username: username ?? generateUsername(), - password, - name: name ?? username ?? 'John', - primaryEmail, - primaryPhone, - isAdmin, - }).json(); -}; - -export const registerNewUser = async (username: string, password: string) => { - const client = new MockClient(); - await client.initSession(); - - assert(client.interactionCookie, new Error('Session not found')); - - const { redirectTo } = await registerUserWithUsernameAndPassword( - username, - password, - client.interactionCookie - ); - - await client.processSession(redirectTo); - - assert(client.isAuthenticated, new Error('Sign in failed')); -}; - -export type SignInHelper = { - username?: string; - email?: string; - password: string; -}; - -export const signIn = async ({ username, email, password }: SignInHelper) => { - const client = new MockClient(); - await client.initSession(); - - assert(client.interactionCookie, new Error('Session not found')); - - const { redirectTo } = await signInWithPassword({ - username, - email, - password, - interactionCookie: client.interactionCookie, - }); - - await client.processSession(redirectTo); - - assert(client.isAuthenticated, new Error('Sign in failed')); -}; - -export const setSignUpIdentifier = async ( - identifiers: SignInIdentifier[], - password = true, - verify = true -) => { - await updateSignInExperience({ signUp: { identifiers, password, verify } }); -}; - -export const setSignInMethod = async (methods: SignIn['methods']) => { - await updateSignInExperience({ - signIn: { - methods, - }, - }); -}; - -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; -}; - -export const bindSocialToNewCreatedUser = async (connectorId: string) => { - const username = generateUsername(); - const password = generatePassword(); - - await enableAllPasswordSignInMethods(); - await createUserByAdmin(username, password); - - const state = 'mock_state'; - const redirectUri = 'http://mock.com/callback'; - const code = 'mock_code'; - - const client = new MockClient(); - - await client.initSession(); - assert(client.interactionCookie, new Error('Session not found')); - - await signInWithSocial({ state, connectorId, redirectUri }, client.interactionCookie); - - const response = await getAuthWithSocial( - { connectorId, data: { state, redirectUri, code } }, - client.interactionCookie - ).catch((error: unknown) => error); - - // User with social does not exist - assert( - response instanceof HTTPError && response.response.statusCode === 422, - new Error('Auth with social failed') - ); - - const { redirectTo } = await signInWithPassword({ - username, - password, - interactionCookie: client.interactionCookie, - }); - - await bindWithSocial(connectorId, client.interactionCookie); - - await client.processSession(redirectTo); - - const { sub } = await client.getIdTokenClaims(); - - return sub; -}; - -export const expectRejects = async ( - promise: Promise, - code: string, - messageIncludes?: string -) => { - try { - await promise; - } catch (error: unknown) { - expectRequestError(error, code, messageIncludes); - - return; - } - - fail(); -}; - -export const expectRequestError = (error: unknown, code: string, messageIncludes?: string) => { - if (!(error instanceof RequestError)) { - fail('Error should be an instance of RequestError'); - } - - // JSON.parse returns `any`. Directly use `as` since we've already know the response body structure. - // eslint-disable-next-line no-restricted-syntax - const body = JSON.parse(String(error.response?.body)) as { - code: string; - message: string; - }; - - expect(body.code).toEqual(code); - - if (messageIncludes) { - expect(body.message.includes(messageIncludes)).toBeTruthy(); - } -}; - -export const createMockServer = (port: number) => { - const server = createServer((request, response) => { - // eslint-disable-next-line @silverhand/fp/no-mutation - response.statusCode = 204; - response.end(); - }); - - return { - listen: async () => - new Promise((resolve) => { - server.listen(port, () => { - resolve(true); - }); - }), - close: async () => - new Promise((resolve) => { - server.close(() => { - resolve(true); - }); - }), - }; -}; diff --git a/packages/integration-tests/src/tests/api/interaction/utils/client.ts b/packages/integration-tests/src/helpers/client.ts similarity index 100% rename from packages/integration-tests/src/tests/api/interaction/utils/client.ts rename to packages/integration-tests/src/helpers/client.ts diff --git a/packages/integration-tests/src/tests/api/interaction/utils/connector.ts b/packages/integration-tests/src/helpers/connector.ts similarity index 100% rename from packages/integration-tests/src/tests/api/interaction/utils/connector.ts rename to packages/integration-tests/src/helpers/connector.ts diff --git a/packages/integration-tests/src/helpers/index.ts b/packages/integration-tests/src/helpers/index.ts new file mode 100644 index 000000000..3ad23492c --- /dev/null +++ b/packages/integration-tests/src/helpers/index.ts @@ -0,0 +1,101 @@ +import fs from 'fs/promises'; +import { createServer } from 'http'; +import path from 'path'; + +import type { User } from '@logto/schemas'; +import { RequestError } from 'got'; + +import { createUser } from '#src/api/index.js'; +import { generateUsername } from '#src/utils.js'; + +export const createUserByAdmin = ( + username?: string, + password?: string, + primaryEmail?: string, + primaryPhone?: string, + name?: string, + isAdmin = false +) => { + return createUser({ + username: username ?? generateUsername(), + password, + name: name ?? username ?? 'John', + primaryEmail, + primaryPhone, + isAdmin, + }).json(); +}; + +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; +}; + +export const expectRejects = async ( + promise: Promise, + code: string, + messageIncludes?: string +) => { + try { + await promise; + } catch (error: unknown) { + expectRequestError(error, code, messageIncludes); + + return; + } + + fail(); +}; + +export const expectRequestError = (error: unknown, code: string, messageIncludes?: string) => { + if (!(error instanceof RequestError)) { + fail('Error should be an instance of RequestError'); + } + + // JSON.parse returns `any`. Directly use `as` since we've already know the response body structure. + // eslint-disable-next-line no-restricted-syntax + const body = JSON.parse(String(error.response?.body)) as { + code: string; + message: string; + }; + + expect(body.code).toEqual(code); + + if (messageIncludes) { + expect(body.message.includes(messageIncludes)).toBeTruthy(); + } +}; + +export const createMockServer = (port: number) => { + const server = createServer((request, response) => { + // eslint-disable-next-line @silverhand/fp/no-mutation + response.statusCode = 204; + response.end(); + }); + + return { + listen: async () => + new Promise((resolve) => { + server.listen(port, () => { + resolve(true); + }); + }), + close: async () => + new Promise((resolve) => { + server.close(() => { + resolve(true); + }); + }), + }; +}; diff --git a/packages/integration-tests/src/helpers/interactions.ts b/packages/integration-tests/src/helpers/interactions.ts new file mode 100644 index 000000000..225923549 --- /dev/null +++ b/packages/integration-tests/src/helpers/interactions.ts @@ -0,0 +1,86 @@ +import type { + UsernamePasswordPayload, + EmailPasswordPayload, + PhonePasswordPayload, +} from '@logto/schemas'; +import { InteractionEvent } from '@logto/schemas'; + +import { + putInteraction, + createSocialAuthorizationUri, + patchInteractionIdentifiers, + putInteractionProfile, +} from '#src/api/index.js'; +import { generateUserId } from '#src/utils.js'; + +import { initClient, processSession, logoutClient } from './client.js'; +import { expectRejects } from './index.js'; +import { enableAllPasswordSignInMethods } from './sign-in-experience.js'; +import { generateNewUser } from './user.js'; + +export const registerNewUser = async (username: string, password: string) => { + const client = await initClient(); + + await client.send(putInteraction, { + event: InteractionEvent.Register, + profile: { + username, + password, + }, + }); + + const { redirectTo } = await client.submitInteraction(); + await processSession(client, redirectTo); + await logoutClient(client); +}; + +export const signInWithPassword = async ( + payload: UsernamePasswordPayload | EmailPasswordPayload | PhonePasswordPayload +) => { + const client = await initClient(); + + await client.successSend(putInteraction, { + event: InteractionEvent.SignIn, + identifier: payload, + }); + + const { redirectTo } = await client.submitInteraction(); + + await processSession(client, redirectTo); + await logoutClient(client); +}; + +export const createNewSocialUserWithUsernameAndPassword = async (connectorId: string) => { + const state = 'foo_state'; + const redirectUri = 'http://foo.dev/callback'; + const code = 'auth_code_foo'; + const socialUserId = generateUserId(); + + const { + userProfile: { username, password }, + user, + } = await generateNewUser({ username: true, password: true }); + + await enableAllPasswordSignInMethods(); + + const client = await initClient(); + + await client.successSend(putInteraction, { + event: InteractionEvent.SignIn, + }); + + await client.successSend(createSocialAuthorizationUri, { state, redirectUri, connectorId }); + + await client.successSend(patchInteractionIdentifiers, { + connectorId, + connectorData: { state, redirectUri, code, userId: socialUserId }, + }); + + await expectRejects(client.submitInteraction(), 'user.identity_not_exist'); + await client.successSend(patchInteractionIdentifiers, { username, password }); + await client.successSend(putInteractionProfile, { connectorId }); + + const { redirectTo } = await client.submitInteraction(); + + return processSession(client, redirectTo); +}; diff --git a/packages/integration-tests/src/tests/api/interaction/utils/sign-in-experience.ts b/packages/integration-tests/src/helpers/sign-in-experience.ts similarity index 100% rename from packages/integration-tests/src/tests/api/interaction/utils/sign-in-experience.ts rename to packages/integration-tests/src/helpers/sign-in-experience.ts diff --git a/packages/integration-tests/src/tests/api/interaction/utils/user.ts b/packages/integration-tests/src/helpers/user.ts similarity index 100% rename from packages/integration-tests/src/tests/api/interaction/utils/user.ts rename to packages/integration-tests/src/helpers/user.ts diff --git a/packages/integration-tests/src/tests/api/admin-user.search.test.ts b/packages/integration-tests/src/tests/api/admin-user.search.test.ts index 54683dd6f..4b523340b 100644 --- a/packages/integration-tests/src/tests/api/admin-user.search.test.ts +++ b/packages/integration-tests/src/tests/api/admin-user.search.test.ts @@ -3,7 +3,7 @@ import type { IncomingHttpHeaders } from 'http'; import type { User } from '@logto/schemas'; import { authedAdminApi, deleteUser } from '#src/api/index.js'; -import { createUserByAdmin, expectRejects } from '#src/helpers.js'; +import { createUserByAdmin, expectRejects } from '#src/helpers/index.js'; const getUsers = async ( init: string[][] | Record | URLSearchParams diff --git a/packages/integration-tests/src/tests/api/admin-user.test.ts b/packages/integration-tests/src/tests/api/admin-user.test.ts index 1d1e9aab5..64ef17ecd 100644 --- a/packages/integration-tests/src/tests/api/admin-user.test.ts +++ b/packages/integration-tests/src/tests/api/admin-user.test.ts @@ -16,7 +16,8 @@ import { updateConnectorConfig, deleteConnectorById, } from '#src/api/index.js'; -import { createUserByAdmin, bindSocialToNewCreatedUser } from '#src/helpers.js'; +import { createUserByAdmin } from '#src/helpers/index.js'; +import { createNewSocialUserWithUsernameAndPassword } from '#src/helpers/interactions.js'; describe('admin console user management', () => { it('should create user successfully', async () => { @@ -72,7 +73,7 @@ describe('admin console user management', () => { const { id } = await postConnector({ connectorId: mockSocialConnectorId }); await updateConnectorConfig(id, mockSocialConnectorConfig); - const createdUserId = await bindSocialToNewCreatedUser(id); + const createdUserId = await createNewSocialUserWithUsernameAndPassword(id); const userInfo = await getUser(createdUserId); expect(userInfo.identities).toHaveProperty(mockSocialConnectorTarget); diff --git a/packages/integration-tests/src/tests/api/audit-logs/index.test.ts b/packages/integration-tests/src/tests/api/audit-logs/index.test.ts index 350bc2d90..22d727af6 100644 --- a/packages/integration-tests/src/tests/api/audit-logs/index.test.ts +++ b/packages/integration-tests/src/tests/api/audit-logs/index.test.ts @@ -5,9 +5,8 @@ import { deleteUser } from '#src/api/admin-user.js'; import { putInteraction } from '#src/api/interaction.js'; import { getLogs } from '#src/api/logs.js'; import MockClient from '#src/client/index.js'; - -import { enableAllPasswordSignInMethods } from '../interaction/utils/sign-in-experience.js'; -import { generateNewUserProfile } from '../interaction/utils/user.js'; +import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js'; +import { generateNewUserProfile } from '#src/helpers/user.js'; describe('audit logs for interaction', () => { beforeAll(async () => { diff --git a/packages/integration-tests/src/tests/api/dashboard.test.ts b/packages/integration-tests/src/tests/api/dashboard.test.ts index 235abd3a5..5a0f9d1ba 100644 --- a/packages/integration-tests/src/tests/api/dashboard.test.ts +++ b/packages/integration-tests/src/tests/api/dashboard.test.ts @@ -1,12 +1,19 @@ +import { SignInIdentifier } from '@logto/schemas'; + import type { StatisticsData } from '#src/api/index.js'; import { getTotalUsersCount, getNewUsersData, getActiveUsersData } from '#src/api/index.js'; -import { signUpIdentifiers } from '#src/constants.js'; -import { createUserByAdmin, registerNewUser, setSignUpIdentifier, signIn } from '#src/helpers.js'; +import { createUserByAdmin } from '#src/helpers/index.js'; +import { registerNewUser, signInWithPassword } from '#src/helpers/interactions.js'; +import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js'; import { generateUsername, generatePassword } from '#src/utils.js'; describe('admin console dashboard', () => { beforeAll(async () => { - await setSignUpIdentifier(signUpIdentifiers.username); + await enableAllPasswordSignInMethods({ + identifiers: [SignInIdentifier.Username], + password: true, + verify: false, + }); }); it('should get total user count successfully', async () => { @@ -43,7 +50,7 @@ describe('admin console dashboard', () => { const username = generateUsername(); await createUserByAdmin(username, password); - await signIn({ username, password }); + await signInWithPassword({ username, password }); const newActiveUserStatistics = await getActiveUsersData(); diff --git a/packages/integration-tests/src/tests/api/get-access-token.test.ts b/packages/integration-tests/src/tests/api/get-access-token.test.ts index 74b7055d1..82f75e4bd 100644 --- a/packages/integration-tests/src/tests/api/get-access-token.test.ts +++ b/packages/integration-tests/src/tests/api/get-access-token.test.ts @@ -1,18 +1,18 @@ import path from 'path'; import { fetchTokenByRefreshToken } from '@logto/js'; -import { managementResource } from '@logto/schemas'; +import { managementResource, InteractionEvent } from '@logto/schemas'; import { assert } from '@silverhand/essentials'; import fetch from 'node-fetch'; -import { signInWithPassword } from '#src/api/index.js'; +import { putInteraction } from '#src/api/index.js'; import MockClient, { defaultConfig } from '#src/client/index.js'; import { logtoUrl } from '#src/constants.js'; -import { createUserByAdmin } from '#src/helpers.js'; +import { processSession } from '#src/helpers/client.js'; +import { createUserByAdmin } from '#src/helpers/index.js'; +import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js'; import { generateUsername, generatePassword } from '#src/utils.js'; -import { enableAllPasswordSignInMethods } from './interaction/utils/sign-in-experience.js'; - describe('get access token', () => { const username = generateUsername(); const password = generatePassword(); @@ -24,18 +24,17 @@ describe('get access token', () => { it('sign-in and getAccessToken', async () => { const client = new MockClient({ resources: [managementResource.indicator] }); - await client.initSession(); - assert(client.interactionCookie, new Error('Session not found')); - const { redirectTo } = await signInWithPassword({ - username, - password, - interactionCookie: client.interactionCookie, + await client.initSession(); + + await client.successSend(putInteraction, { + event: InteractionEvent.SignIn, + identifier: { username, password }, }); - await client.processSession(redirectTo); + const { redirectTo } = await client.submitInteraction(); - assert(client.isAuthenticated, new Error('Sign in get get access token failed')); + await processSession(client, redirectTo); const accessToken = await client.getAccessToken(managementResource.indicator); @@ -47,19 +46,20 @@ describe('get access token', () => { it('sign-in and get multiple Access Token by the same Refresh Token within refreshTokenReuseInterval', async () => { const client = new MockClient({ resources: [managementResource.indicator] }); - await client.initSession(); - assert(client.interactionCookie, new Error('Session not found')); - const { redirectTo } = await signInWithPassword({ - username, - password, - interactionCookie: client.interactionCookie, + await client.initSession(); + + await client.successSend(putInteraction, { + event: InteractionEvent.SignIn, + identifier: { username, password }, }); - await client.processSession(redirectTo); - assert(client.isAuthenticated, new Error('Sign in get get access token failed')); + const { redirectTo } = await client.submitInteraction(); + + await processSession(client, redirectTo); const refreshToken = await client.getRefreshToken(); + assert(refreshToken, new Error('No Refresh Token found')); const getAccessTokenByRefreshToken = async () => diff --git a/packages/integration-tests/src/tests/api/hooks.test.ts b/packages/integration-tests/src/tests/api/hooks.test.ts index 787f6f466..fb6ec2019 100644 --- a/packages/integration-tests/src/tests/api/hooks.test.ts +++ b/packages/integration-tests/src/tests/api/hooks.test.ts @@ -5,13 +5,12 @@ import { HookEvent } from '@logto/schemas/models'; import type { InferModelType } from '@withtyped/server'; import { authedAdminApi, deleteUser, getLogs, putInteraction } from '#src/api/index.js'; -import { createMockServer } from '#src/helpers.js'; +import { initClient, processSession } from '#src/helpers/client.js'; +import { createMockServer } from '#src/helpers/index.js'; +import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js'; +import { generateNewUser, generateNewUserProfile } from '#src/helpers/user.js'; import { waitFor } from '#src/utils.js'; -import { initClient, processSession } from './interaction/utils/client.js'; -import { enableAllPasswordSignInMethods } from './interaction/utils/sign-in-experience.js'; -import { generateNewUser, generateNewUserProfile } from './interaction/utils/user.js'; - type Hook = InferModelType; const createPayload = (event: HookEvent, url = 'not_work_url'): Partial => ({ diff --git a/packages/integration-tests/src/tests/api/interaction/forgot-password.test.ts b/packages/integration-tests/src/tests/api/interaction/forgot-password.test.ts index 67f5db81a..a055e146a 100644 --- a/packages/integration-tests/src/tests/api/interaction/forgot-password.test.ts +++ b/packages/integration-tests/src/tests/api/interaction/forgot-password.test.ts @@ -8,14 +8,17 @@ import { putInteractionProfile, patchInteractionProfile, } from '#src/api/index.js'; -import { expectRejects, readPasscode } from '#src/helpers.js'; +import { initClient, processSession, logoutClient } from '#src/helpers/client.js'; +import { + clearConnectorsByTypes, + setEmailConnector, + setSmsConnector, +} from '#src/helpers/connector.js'; +import { expectRejects, readPasscode } from '#src/helpers/index.js'; +import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js'; +import { generateNewUser } from '#src/helpers/user.js'; import { generatePassword } from '#src/utils.js'; -import { initClient, processSession, logoutClient } from './utils/client.js'; -import { clearConnectorsByTypes, setEmailConnector, setSmsConnector } from './utils/connector.js'; -import { enableAllVerificationCodeSignInMethods } from './utils/sign-in-experience.js'; -import { generateNewUser } from './utils/user.js'; - describe('reset password', () => { beforeAll(async () => { await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]); diff --git a/packages/integration-tests/src/tests/api/interaction/register-with-identifier.test.ts b/packages/integration-tests/src/tests/api/interaction/register-with-identifier.test.ts index 4561fff24..20eb612ed 100644 --- a/packages/integration-tests/src/tests/api/interaction/register-with-identifier.test.ts +++ b/packages/integration-tests/src/tests/api/interaction/register-with-identifier.test.ts @@ -11,15 +11,18 @@ import { deleteInteractionProfile, putInteractionEvent, } from '#src/api/index.js'; -import { readPasscode, expectRejects } from '#src/helpers.js'; - -import { initClient, processSession, logoutClient } from './utils/client.js'; -import { clearConnectorsByTypes, setEmailConnector, setSmsConnector } from './utils/connector.js'; +import { initClient, processSession, logoutClient } from '#src/helpers/client.js'; +import { + clearConnectorsByTypes, + setEmailConnector, + setSmsConnector, +} from '#src/helpers/connector.js'; +import { readPasscode, expectRejects } from '#src/helpers/index.js'; import { enableAllVerificationCodeSignInMethods, enableAllPasswordSignInMethods, -} from './utils/sign-in-experience.js'; -import { generateNewUserProfile, generateNewUser } from './utils/user.js'; +} from '#src/helpers/sign-in-experience.js'; +import { generateNewUserProfile, generateNewUser } from '#src/helpers/user.js'; describe('Register with username and password', () => { it('register with username and password', async () => { diff --git a/packages/integration-tests/src/tests/api/interaction/sign-in-with-passcode-identifier.test.ts b/packages/integration-tests/src/tests/api/interaction/sign-in-with-passcode-identifier.test.ts index a607a5cb7..1ea9e3e11 100644 --- a/packages/integration-tests/src/tests/api/interaction/sign-in-with-passcode-identifier.test.ts +++ b/packages/integration-tests/src/tests/api/interaction/sign-in-with-passcode-identifier.test.ts @@ -9,14 +9,17 @@ import { deleteUser, updateSignInExperience, } from '#src/api/index.js'; -import { expectRejects, readPasscode } from '#src/helpers.js'; +import { initClient, processSession, logoutClient } from '#src/helpers/client.js'; +import { + clearConnectorsByTypes, + setEmailConnector, + setSmsConnector, +} from '#src/helpers/connector.js'; +import { expectRejects, readPasscode } from '#src/helpers/index.js'; +import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js'; +import { generateNewUser, generateNewUserProfile } from '#src/helpers/user.js'; import { generateEmail, generatePhone } from '#src/utils.js'; -import { initClient, processSession, logoutClient } from './utils/client.js'; -import { clearConnectorsByTypes, setEmailConnector, setSmsConnector } from './utils/connector.js'; -import { enableAllVerificationCodeSignInMethods } from './utils/sign-in-experience.js'; -import { generateNewUser, generateNewUserProfile } from './utils/user.js'; - describe('Sign-In flow using verification-code identifiers', () => { beforeAll(async () => { await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]); diff --git a/packages/integration-tests/src/tests/api/interaction/sign-in-with-password-identifier.test.ts b/packages/integration-tests/src/tests/api/interaction/sign-in-with-password-identifier.test.ts index adf2cbc50..39318c25c 100644 --- a/packages/integration-tests/src/tests/api/interaction/sign-in-with-password-identifier.test.ts +++ b/packages/integration-tests/src/tests/api/interaction/sign-in-with-password-identifier.test.ts @@ -7,15 +7,18 @@ import { putInteractionProfile, deleteUser, } from '#src/api/index.js'; -import { readPasscode, expectRejects } from '#src/helpers.js'; - -import { initClient, processSession, logoutClient } from './utils/client.js'; -import { clearConnectorsByTypes, setSmsConnector, setEmailConnector } from './utils/connector.js'; +import { initClient, processSession, logoutClient } from '#src/helpers/client.js'; +import { + clearConnectorsByTypes, + setSmsConnector, + setEmailConnector, +} from '#src/helpers/connector.js'; +import { readPasscode, expectRejects } from '#src/helpers/index.js'; import { enableAllPasswordSignInMethods, enableAllVerificationCodeSignInMethods, -} from './utils/sign-in-experience.js'; -import { generateNewUser, generateNewUserProfile } from './utils/user.js'; +} from '#src/helpers/sign-in-experience.js'; +import { generateNewUser, generateNewUserProfile } from '#src/helpers/user.js'; describe('Sign-In flow using password identifiers', () => { beforeAll(async () => { diff --git a/packages/integration-tests/src/tests/api/interaction/social-interaction.test.ts b/packages/integration-tests/src/tests/api/interaction/social-interaction.test.ts index 98ecf3e69..4b92c57d3 100644 --- a/packages/integration-tests/src/tests/api/interaction/social-interaction.test.ts +++ b/packages/integration-tests/src/tests/api/interaction/social-interaction.test.ts @@ -9,17 +9,16 @@ import { patchInteractionIdentifiers, putInteractionProfile, } from '#src/api/index.js'; -import { expectRejects } from '#src/helpers.js'; -import { generateUserId } from '#src/utils.js'; - -import { initClient, logoutClient, processSession } from './utils/client.js'; +import { initClient, logoutClient, processSession } from '#src/helpers/client.js'; import { clearConnectorsByTypes, clearConnectorById, setSocialConnector, -} from './utils/connector.js'; -import { enableAllPasswordSignInMethods } from './utils/sign-in-experience.js'; -import { generateNewUser } from './utils/user.js'; +} from '#src/helpers/connector.js'; +import { expectRejects } from '#src/helpers/index.js'; +import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js'; +import { generateNewUser } from '#src/helpers/user.js'; +import { generateUserId } from '#src/utils.js'; const state = 'foo_state'; const redirectUri = 'http://foo.dev/callback'; @@ -102,6 +101,7 @@ describe('Social Identifier Interactions', () => { const { userProfile: { username, password }, } = await generateNewUser({ username: true, password: true }); + const client = await initClient(); const connectorId = connectorIdMap.get(mockSocialConnectorId) ?? ''; diff --git a/packages/integration-tests/src/tests/api/logs-legacy.test.ts b/packages/integration-tests/src/tests/api/logs-legacy.test.ts deleted file mode 100644 index 1ba6f4e23..000000000 --- a/packages/integration-tests/src/tests/api/logs-legacy.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { assert } from '@silverhand/essentials'; - -import { getLogs, getLog } from '#src/api/index.js'; -import { signUpIdentifiers } from '#src/constants.js'; -import { registerNewUser, setSignUpIdentifier } from '#src/helpers.js'; -import { generateUsername, generatePassword } from '#src/utils.js'; - -/** @deprecated This will be removed soon. */ -describe('admin console logs (legacy)', () => { - const username = generateUsername(); - const password = generatePassword(); - - beforeAll(async () => { - await setSignUpIdentifier(signUpIdentifiers.username); - }); - - it('should get logs and visit log details successfully', async () => { - await registerNewUser(username, password); - - const logs = await getLogs(); - - const registerLog = logs.filter( - ({ key, payload }) => key === 'RegisterUsernamePassword' && payload.username === username - ); - - expect(registerLog.length).toBeGreaterThan(0); - - assert(registerLog[0], new Error('Log is not valid')); - - const logDetails = await getLog(registerLog[0].id); - - expect(logDetails).toMatchObject(registerLog[0]); - }); -}); diff --git a/packages/integration-tests/src/tests/api/session.test.ts b/packages/integration-tests/src/tests/api/session.test.ts deleted file mode 100644 index c1e507def..000000000 --- a/packages/integration-tests/src/tests/api/session.test.ts +++ /dev/null @@ -1,356 +0,0 @@ -import { SignInIdentifier, adminConsoleApplicationId } from '@logto/schemas'; -import { assert } from '@silverhand/essentials'; - -import { - mockEmailConnectorId, - mockEmailConnectorConfig, - mockSmsConnectorId, - mockSmsConnectorConfig, -} from '#src/__mocks__/connectors-mock.js'; -import { - sendRegisterUserWithEmailPasscode, - verifyRegisterUserWithEmailPasscode, - sendSignInUserWithEmailPasscode, - verifySignInUserWithEmailPasscode, - sendRegisterUserWithSmsPasscode, - verifyRegisterUserWithSmsPasscode, - sendSignInUserWithSmsPasscode, - verifySignInUserWithSmsPasscode, - signInWithPassword, - createUser, - listConnectors, - deleteConnectorById, - postConnector, - updateConnectorConfig, -} from '#src/api/index.js'; -import MockClient from '#src/client/index.js'; -import { signUpIdentifiers } from '#src/constants.js'; -import { - registerNewUser, - signIn, - readPasscode, - createUserByAdmin, - setSignUpIdentifier, - setSignInMethod, -} from '#src/helpers.js'; -import { generateUsername, generatePassword, generateEmail, generatePhone } from '#src/utils.js'; - -const connectorIdMap = new Map(); - -describe('username and password flow', () => { - beforeAll(async () => { - await setSignUpIdentifier(signUpIdentifiers.username, true); - await setSignInMethod([ - { - identifier: SignInIdentifier.Username, - password: true, - verificationCode: false, - isPasswordPrimary: false, - }, - ]); - }); - - it('register and sign in with username & password', async () => { - const username = generateUsername(); - const password = generatePassword(); - await expect(registerNewUser(username, password)).resolves.not.toThrow(); - await expect(signIn({ username, password })).resolves.not.toThrow(); - }); -}); - -describe('email and password flow', () => { - const email = generateEmail(); - const [localPart, domain] = email.split('@'); - const password = generatePassword(); - - assert(localPart && domain, new Error('Email address local part or domain is empty')); - - beforeAll(async () => { - await setSignUpIdentifier(signUpIdentifiers.none, true); - await setSignInMethod([ - { - identifier: SignInIdentifier.Email, - password: true, - verificationCode: false, - isPasswordPrimary: false, - }, - ]); - }); - - it('can sign in with email & password', async () => { - await createUser({ password, primaryEmail: email, username: generateUsername(), name: 'John' }); - await expect( - Promise.all([ - signIn({ email, password }), - signIn({ email: localPart.toUpperCase() + '@' + domain, password }), - signIn({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - email: localPart[0]! + localPart.toUpperCase().slice(1) + '@' + domain, - password, - }), - ]) - ).resolves.not.toThrow(); - }); -}); - -describe('email passwordless flow', () => { - beforeAll(async () => { - const connectors = await listConnectors(); - await Promise.all( - connectors.map(async ({ id }) => { - await deleteConnectorById(id); - }) - ); - connectorIdMap.clear(); - - const { id } = await postConnector({ connectorId: mockEmailConnectorId }); - await updateConnectorConfig(id, mockEmailConnectorConfig); - connectorIdMap.set(mockEmailConnectorId, id); - - await setSignUpIdentifier(signUpIdentifiers.email, false); - await setSignInMethod([ - { - identifier: SignInIdentifier.Username, - password: true, - verificationCode: false, - isPasswordPrimary: true, - }, - { - identifier: SignInIdentifier.Email, - password: false, - verificationCode: true, - isPasswordPrimary: false, - }, - ]); - }); - - // 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); - - await expect(client.isAuthenticated()).resolves.toBe(true); - }); - - 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); - - await expect(client.isAuthenticated()).resolves.toBe(true); - }); - - afterAll(async () => { - await deleteConnectorById(connectorIdMap.get(mockEmailConnectorId)); - }); -}); - -describe('sms passwordless flow', () => { - beforeAll(async () => { - const connectors = await listConnectors(); - await Promise.all( - connectors.map(async ({ id }) => { - await deleteConnectorById(id); - }) - ); - connectorIdMap.clear(); - - const { id } = await postConnector({ connectorId: mockSmsConnectorId }); - await updateConnectorConfig(id, mockSmsConnectorConfig); - connectorIdMap.set(mockSmsConnectorId, id); - - await setSignUpIdentifier(signUpIdentifiers.sms, false); - await setSignInMethod([ - { - identifier: SignInIdentifier.Username, - password: true, - verificationCode: false, - isPasswordPrimary: true, - }, - { - identifier: SignInIdentifier.Phone, - password: false, - verificationCode: true, - isPasswordPrimary: false, - }, - ]); - }); - - // 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); - - await expect(client.isAuthenticated()).resolves.toBe(true); - }); - - it('sign-in with sms', 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); - - await expect(client.isAuthenticated()).resolves.toBe(true); - }); - - afterAll(async () => { - await deleteConnectorById(connectorIdMap.get(mockSmsConnectorId)); - }); -}); - -describe('sign-in and sign-out', () => { - const username = generateUsername(); - const password = generatePassword(); - - beforeAll(async () => { - await createUserByAdmin(username, password); - await setSignUpIdentifier(signUpIdentifiers.username); - }); - - it('verify sign-in and then sign-out', async () => { - const client = new MockClient(); - await client.initSession(); - - assert(client.interactionCookie, new Error('Session not found')); - - const { redirectTo } = await signInWithPassword({ - username, - password, - interactionCookie: client.interactionCookie, - }); - - await client.processSession(redirectTo); - - await expect(client.isAuthenticated()).resolves.toBe(true); - - await client.signOut(); - - await expect(client.isAuthenticated()).resolves.toBe(false); - }); -}); - -describe('sign-in to demo app and revisit Admin Console', () => { - const username = generateUsername(); - const password = generatePassword(); - - beforeAll(async () => { - await createUserByAdmin(username, password); - }); - - it('should throw in Admin Console consent step if a logged in user does not have admin role', async () => { - const client = new MockClient(); - await client.initSession(); - - assert(client.interactionCookie, new Error('Session not found')); - - const { redirectTo } = await signInWithPassword({ - username, - password, - interactionCookie: client.interactionCookie, - }); - - await client.processSession(redirectTo); - - await expect(client.isAuthenticated()).resolves.toBe(true); - - const { interactionCookie } = client; - const acClient = new MockClient({ appId: adminConsoleApplicationId }); - - acClient.assignCookie(interactionCookie); - - await expect(acClient.initSession()).rejects.toThrow(); - }); -}); diff --git a/packages/integration-tests/src/tests/api/social-session.test.ts b/packages/integration-tests/src/tests/api/social-session.test.ts deleted file mode 100644 index 70e320ba1..000000000 --- a/packages/integration-tests/src/tests/api/social-session.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { assert } from '@silverhand/essentials'; -import { HTTPError } from 'got'; - -import { - mockSocialConnectorId, - mockSocialConnectorTarget, - mockSocialConnectorConfig, -} from '#src/__mocks__/connectors-mock.js'; -import { - signInWithSocial, - getAuthWithSocial, - registerWithSocial, - bindWithSocial, - signInWithPassword, - getUser, - postConnector, - updateConnectorConfig, - deleteConnectorById, -} from '#src/api/index.js'; -import MockClient from '#src/client/index.js'; -import { signUpIdentifiers } from '#src/constants.js'; -import { createUserByAdmin, setSignUpIdentifier } from '#src/helpers.js'; -import { generateUsername, generatePassword } from '#src/utils.js'; - -const state = 'foo_state'; -const redirectUri = 'http://foo.dev/callback'; -const code = 'auth_code_foo'; - -const connectorIdMap = new Map(); - -describe('social sign-in and register', () => { - const socialUserId = crypto.randomUUID(); - - beforeAll(async () => { - const { id } = await postConnector({ connectorId: mockSocialConnectorId }); - connectorIdMap.set(mockSocialConnectorId, id); - await updateConnectorConfig(id, mockSocialConnectorConfig); - - await setSignUpIdentifier(signUpIdentifiers.none, false); - }); - - 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: connectorIdMap.get(mockSocialConnectorId) ?? '', redirectUri }, - client.interactionCookie - ) - ).resolves.toBeTruthy(); - - const response = await getAuthWithSocial( - { - connectorId: connectorIdMap.get(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( - connectorIdMap.get(mockSocialConnectorId) ?? '', - client.interactionCookie - ); - - await client.processSession(redirectTo); - - await expect(client.isAuthenticated()).resolves.toBe(true); - }); - - /* - * 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: connectorIdMap.get(mockSocialConnectorId) ?? '', redirectUri }, - client.interactionCookie - ) - ).resolves.toBeTruthy(); - - const { redirectTo } = await getAuthWithSocial( - { - connectorId: connectorIdMap.get(mockSocialConnectorId) ?? '', - data: { state, redirectUri, code, userId: socialUserId }, - }, - client.interactionCookie - ); - - await client.processSession(redirectTo); - - await expect(client.isAuthenticated()).resolves.toBe(true); - }); -}); - -describe('social bind account', () => { - const username = generateUsername(); - const password = generatePassword(); - - beforeAll(async () => { - await createUserByAdmin(username, password); - }); - - afterAll(async () => { - for (const [_connectorId, id] of connectorIdMap.entries()) { - // eslint-disable-next-line no-await-in-loop - await deleteConnectorById(id); - } - }); - - 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: connectorIdMap.get(mockSocialConnectorId) ?? '', redirectUri }, - client.interactionCookie - ) - ).resolves.toBeTruthy(); - - const response = await getAuthWithSocial( - { - connectorId: connectorIdMap.get(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 signInWithPassword({ - username, - password, - interactionCookie: client.interactionCookie, - }); - - await expect( - bindWithSocial(connectorIdMap.get(mockSocialConnectorId) ?? '', client.interactionCookie) - ).resolves.not.toThrow(); - - await client.processSession(redirectTo); - - // User should bind with social identities - const { sub } = await client.getIdTokenClaims(); - const user = await getUser(sub); - - expect(user.identities).toHaveProperty(mockSocialConnectorTarget); - }); -});