mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
test: interaction api counter cases and response status guard (#4306)
* test: add interaction api counter cases and response status guard * fix(core): fix koa router method type definition
This commit is contained in:
parent
0fcea5ae5e
commit
3c903b4778
9 changed files with 730 additions and 20 deletions
16
packages/core/src/include.d/koa-router.d.ts
vendored
16
packages/core/src/include.d/koa-router.d.ts
vendored
|
@ -274,11 +274,21 @@ declare module 'koa-router' {
|
|||
middleware2: Koa.Middleware<StateT & T1, CustomT & U1>,
|
||||
routeHandler: Router.IMiddleware<StateT & T1 & T2, CustomT & U1 & U2>
|
||||
): Router<StateT & T1 & T2, CustomT & U1 & U2>;
|
||||
/**
|
||||
* Note: When the middleware type has more than 3 generic types, TypeScript infers it as `unknown`.
|
||||
* Here, we ensure that the input types of these 3 middleware don't depend on preceding types,
|
||||
* only on the router's provided types.
|
||||
*
|
||||
* P.S. This type might not handle cases where later middleware depends on preceding middleware.
|
||||
* While imperfect, this definition works for most cases.
|
||||
* When there is a genuine need for dependencies between middlewares,
|
||||
* consider taking inspiration from `interactionRoutes` to define types for the Router.
|
||||
*/
|
||||
post<T1, U1, T2, U2, T3, U3>(
|
||||
path: string | RegExp | Array<string | RegExp>,
|
||||
middleware1: Koa.Middleware<T1, U1>,
|
||||
middleware2: Koa.Middleware<StateT & T1, CustomT & U1>,
|
||||
middleware3: Koa.Middleware<StateT & T1 & T2, CustomT & U1 & U2>,
|
||||
middleware1: Koa.Middleware<StateT & T1, CustomT & U1>,
|
||||
middleware2: Koa.Middleware<StateT & T2, CustomT & U2>,
|
||||
middleware3: Koa.Middleware<StateT & T3, CustomT & U3>,
|
||||
routeHandler: Router.IMiddleware<StateT & T1 & T2 & T3, CustomT & U1 & U2 & U3>
|
||||
): Router<StateT & T1 & T2 & T3, CustomT & U1 & U2 & U3>;
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
identifier: identifierPayloadGuard.optional(),
|
||||
profile: profileGuard.optional(),
|
||||
}),
|
||||
status: [204, 400, 401, 403, 422],
|
||||
}),
|
||||
koaInteractionSie(queries),
|
||||
async (ctx, next) => {
|
||||
|
@ -108,17 +109,24 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
);
|
||||
|
||||
// Delete Interaction
|
||||
router.delete(interactionPrefix, async (ctx, next) => {
|
||||
const error: LogtoErrorCode = 'oidc.aborted';
|
||||
await assignInteractionResults(ctx, provider, { error });
|
||||
router.delete(
|
||||
interactionPrefix,
|
||||
koaGuard({
|
||||
status: [204, 400],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const error: LogtoErrorCode = 'oidc.aborted';
|
||||
await assignInteractionResults(ctx, provider, { error });
|
||||
|
||||
return next();
|
||||
});
|
||||
ctx.status = 204;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
// Update Interaction Event
|
||||
router.put(
|
||||
`${interactionPrefix}/event`,
|
||||
koaGuard({ body: z.object({ event: eventGuard }) }),
|
||||
koaGuard({ body: z.object({ event: eventGuard }), status: [204, 400, 403, 404] }),
|
||||
koaInteractionSie(queries),
|
||||
async (ctx, next) => {
|
||||
const { event } = ctx.guard.body;
|
||||
|
@ -156,6 +164,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
`${interactionPrefix}/identifiers`,
|
||||
koaGuard({
|
||||
body: identifierPayloadGuard,
|
||||
status: [204, 400, 401, 404, 422],
|
||||
}),
|
||||
koaInteractionSie(queries),
|
||||
async (ctx, next) => {
|
||||
|
@ -193,6 +202,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
`${interactionPrefix}/profile`,
|
||||
koaGuard({
|
||||
body: profileGuard,
|
||||
status: [204, 400, 404],
|
||||
}),
|
||||
koaInteractionSie(queries),
|
||||
async (ctx, next) => {
|
||||
|
@ -230,6 +240,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
`${interactionPrefix}/profile`,
|
||||
koaGuard({
|
||||
body: profileGuard,
|
||||
status: [204, 400, 404],
|
||||
}),
|
||||
koaInteractionSie(queries),
|
||||
async (ctx, next) => {
|
||||
|
@ -265,25 +276,39 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
);
|
||||
|
||||
// Delete Interaction Profile
|
||||
router.delete(`${interactionPrefix}/profile`, async (ctx, next) => {
|
||||
const { interactionDetails, createLog } = ctx;
|
||||
const interactionStorage = getInteractionStorage(interactionDetails.result);
|
||||
router.delete(
|
||||
`${interactionPrefix}/profile`,
|
||||
koaGuard({
|
||||
status: [204, 400, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { interactionDetails, createLog } = ctx;
|
||||
const interactionStorage = getInteractionStorage(interactionDetails.result);
|
||||
|
||||
const log = createLog(`Interaction.${interactionStorage.event}.Profile.Delete`);
|
||||
log.append({ interactionStorage });
|
||||
const log = createLog(`Interaction.${interactionStorage.event}.Profile.Delete`);
|
||||
log.append({ interactionStorage });
|
||||
|
||||
const { profile, ...rest } = interactionStorage;
|
||||
const { profile, ...rest } = interactionStorage;
|
||||
|
||||
await storeInteractionResult(rest, ctx, provider);
|
||||
await storeInteractionResult(rest, ctx, provider);
|
||||
|
||||
ctx.status = 204;
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
});
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
// Submit Interaction
|
||||
router.post(
|
||||
`${interactionPrefix}/submit`,
|
||||
koaGuard({
|
||||
status: [200, 204, 400, 401, 404, 422],
|
||||
response: z
|
||||
.object({
|
||||
redirectTo: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
koaInteractionSie(queries),
|
||||
koaInteractionHooks(libraries),
|
||||
async (ctx, next) => {
|
||||
|
@ -312,7 +337,13 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
// Create social authorization url interaction verification
|
||||
router.post(
|
||||
`${interactionPrefix}/${verificationPath}/social-authorization-uri`,
|
||||
koaGuard({ body: socialAuthorizationUrlPayloadGuard }),
|
||||
koaGuard({
|
||||
body: socialAuthorizationUrlPayloadGuard,
|
||||
status: [200, 400, 404],
|
||||
response: z.object({
|
||||
redirectTo: z.string(),
|
||||
}),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
// Check interaction exists
|
||||
const { event } = getInteractionStorage(ctx.interactionDetails.result);
|
||||
|
@ -335,6 +366,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
`${interactionPrefix}/${verificationPath}/verification-code`,
|
||||
koaGuard({
|
||||
body: requestVerificationCodePayloadGuard,
|
||||
status: [204, 400, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { interactionDetails, guard, createLog } = ctx;
|
||||
|
|
|
@ -27,6 +27,14 @@ export const putInteraction = async (cookie: string, payload: InteractionPayload
|
|||
})
|
||||
.json();
|
||||
|
||||
export const deleteInteraction = async (cookie: string) =>
|
||||
api
|
||||
.delete('interaction', {
|
||||
headers: { cookie },
|
||||
followRedirect: false,
|
||||
})
|
||||
.json();
|
||||
|
||||
export const putInteractionEvent = async (cookie: string, payload: { event: InteractionEvent }) =>
|
||||
api
|
||||
.put('interaction/event', { headers: { cookie }, json: payload, followRedirect: false })
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
import { InteractionEvent } from '@logto/schemas';
|
||||
|
||||
import {
|
||||
putInteractionEvent,
|
||||
patchInteractionIdentifiers,
|
||||
putInteractionProfile,
|
||||
patchInteractionProfile,
|
||||
deleteInteractionProfile,
|
||||
createSocialAuthorizationUri,
|
||||
sendVerificationCode,
|
||||
putInteraction,
|
||||
deleteInteraction,
|
||||
} from '#src/api/interaction.js';
|
||||
import MockClient from '#src/client/index.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import { generateUsername, generatePassword, generateEmail } from '#src/utils.js';
|
||||
|
||||
/**
|
||||
* Note: These test cases are designed to cover exceptional scenarios of API calls that
|
||||
* cannot be covered within the auth flow.
|
||||
*/
|
||||
describe('Interaction details guard checking', () => {
|
||||
// Create a client without interaction cookies
|
||||
const client = new MockClient();
|
||||
|
||||
it('PUT /interaction', async () => {
|
||||
await expectRejects(
|
||||
client.send(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
}),
|
||||
{
|
||||
code: 'session.not_found',
|
||||
statusCode: 400,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('DELETE /interaction', async () => {
|
||||
await expectRejects(client.send(deleteInteraction), {
|
||||
code: 'session.not_found',
|
||||
statusCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('PUT /interaction/event', async () => {
|
||||
await expectRejects(
|
||||
client.send(putInteractionEvent, {
|
||||
event: InteractionEvent.SignIn,
|
||||
}),
|
||||
{
|
||||
code: 'session.not_found',
|
||||
statusCode: 400,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('PATCH /interaction/identifier', async () => {
|
||||
await expectRejects(
|
||||
client.send(patchInteractionIdentifiers, {
|
||||
username: generateUsername(),
|
||||
password: generatePassword(),
|
||||
}),
|
||||
{
|
||||
code: 'session.not_found',
|
||||
statusCode: 400,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('PUT /interaction/profile', async () => {
|
||||
await expectRejects(
|
||||
client.send(putInteractionProfile, {
|
||||
username: generateUsername(),
|
||||
password: generatePassword(),
|
||||
}),
|
||||
{
|
||||
code: 'session.not_found',
|
||||
statusCode: 400,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('PATCH /interaction/profile', async () => {
|
||||
await expectRejects(
|
||||
client.send(patchInteractionProfile, {
|
||||
username: generateUsername(),
|
||||
password: generatePassword(),
|
||||
}),
|
||||
{
|
||||
code: 'session.not_found',
|
||||
statusCode: 400,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('DELETE /interaction/profile', async () => {
|
||||
await expectRejects(client.send(deleteInteractionProfile), {
|
||||
code: 'session.not_found',
|
||||
statusCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('POST /interaction/submit', async () => {
|
||||
await expectRejects(client.submitInteraction(), {
|
||||
code: 'session.not_found',
|
||||
statusCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('POST /interaction/verification/social-authorization-uri', async () => {
|
||||
await expectRejects(
|
||||
client.send(createSocialAuthorizationUri, {
|
||||
state: 'fake_state',
|
||||
redirectUri: 'https://logto.dev',
|
||||
connectorId: 'fake_connector_id',
|
||||
}),
|
||||
{
|
||||
code: 'session.not_found',
|
||||
statusCode: 400,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('POST /interaction/verification/verification-code', async () => {
|
||||
await expectRejects(
|
||||
client.send(sendVerificationCode, {
|
||||
email: generateEmail(),
|
||||
}),
|
||||
{
|
||||
code: 'session.not_found',
|
||||
statusCode: 400,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,120 @@
|
|||
import { InteractionEvent } from '@logto/schemas';
|
||||
|
||||
import {
|
||||
putInteractionEvent,
|
||||
patchInteractionIdentifiers,
|
||||
putInteractionProfile,
|
||||
patchInteractionProfile,
|
||||
deleteInteractionProfile,
|
||||
createSocialAuthorizationUri,
|
||||
sendVerificationCode,
|
||||
} from '#src/api/interaction.js';
|
||||
import { initClient } from '#src/helpers/client.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import { generateUsername, generatePassword, generateEmail } from '#src/utils.js';
|
||||
|
||||
/**
|
||||
* Note: These test cases are designed to cover exceptional scenarios of API calls that
|
||||
* cannot be covered within the auth flow.
|
||||
*/
|
||||
describe('Interaction details results checking', () => {
|
||||
it('PUT /interaction/event', async () => {
|
||||
const client = await initClient();
|
||||
await expectRejects(
|
||||
client.send(putInteractionEvent, {
|
||||
event: InteractionEvent.SignIn,
|
||||
}),
|
||||
{
|
||||
code: 'session.verification_session_not_found',
|
||||
statusCode: 404,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('PATCH /interaction/identifier', async () => {
|
||||
const client = await initClient();
|
||||
|
||||
await expectRejects(
|
||||
client.send(patchInteractionIdentifiers, {
|
||||
username: generateUsername(),
|
||||
password: generatePassword(),
|
||||
}),
|
||||
{
|
||||
code: 'session.verification_session_not_found',
|
||||
statusCode: 404,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('PUT /interaction/profile', async () => {
|
||||
const client = await initClient();
|
||||
await expectRejects(
|
||||
client.send(putInteractionProfile, {
|
||||
username: generateUsername(),
|
||||
password: generatePassword(),
|
||||
}),
|
||||
{
|
||||
code: 'session.verification_session_not_found',
|
||||
statusCode: 404,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('PATCH /interaction/profile', async () => {
|
||||
const client = await initClient();
|
||||
await expectRejects(
|
||||
client.send(patchInteractionProfile, {
|
||||
username: generateUsername(),
|
||||
password: generatePassword(),
|
||||
}),
|
||||
{
|
||||
code: 'session.verification_session_not_found',
|
||||
statusCode: 404,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('DELETE /interaction/profile', async () => {
|
||||
const client = await initClient();
|
||||
await expectRejects(client.send(deleteInteractionProfile), {
|
||||
code: 'session.verification_session_not_found',
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
|
||||
it('POST /interaction/submit', async () => {
|
||||
const client = await initClient();
|
||||
await expectRejects(client.submitInteraction(), {
|
||||
code: 'session.verification_session_not_found',
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
|
||||
it('POST /interaction/verification/social-authorization-uri', async () => {
|
||||
const client = await initClient();
|
||||
await expectRejects(
|
||||
client.send(createSocialAuthorizationUri, {
|
||||
state: 'fake_state',
|
||||
redirectUri: 'https://logto.dev',
|
||||
connectorId: 'fake_connector_id',
|
||||
}),
|
||||
{
|
||||
code: 'session.verification_session_not_found',
|
||||
statusCode: 404,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('POST /interaction/verification/verification-code', async () => {
|
||||
const client = await initClient();
|
||||
await expectRejects(
|
||||
client.send(sendVerificationCode, {
|
||||
email: generateEmail(),
|
||||
}),
|
||||
{
|
||||
code: 'session.verification_session_not_found',
|
||||
statusCode: 404,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import { InteractionEvent } from '@logto/schemas';
|
||||
|
||||
import { suspendUser } from '#src/api/admin-user.js';
|
||||
import { patchInteractionIdentifiers, putInteraction } from '#src/api/interaction.js';
|
||||
import { initClient } from '#src/helpers/client.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUser } from '#src/helpers/user.js';
|
||||
|
||||
/**
|
||||
* Note: These test cases are designed to cover exceptional scenarios of API calls that
|
||||
* cannot be covered within the auth flow.
|
||||
*/
|
||||
describe('PATCH /interaction/identifiers', () => {
|
||||
it('Should fail to update identifiers with username and password if related user is suspended', async () => {
|
||||
// Init a valid sign-in experience config
|
||||
await enableAllPasswordSignInMethods();
|
||||
const { user, userProfile } = await generateNewUser({ username: true, password: true });
|
||||
await suspendUser(user.id, true);
|
||||
|
||||
const client = await initClient();
|
||||
|
||||
await client.successSend(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
});
|
||||
|
||||
await expectRejects(
|
||||
client.send(patchInteractionIdentifiers, {
|
||||
username: userProfile.username,
|
||||
password: userProfile.password,
|
||||
}),
|
||||
{
|
||||
code: 'user.suspended',
|
||||
statusCode: 401,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
import { InteractionEvent } from '@logto/schemas';
|
||||
|
||||
import { putInteraction, sendVerificationCode } from '#src/api/interaction.js';
|
||||
import { initClient } from '#src/helpers/client.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import { generateEmail, generatePhone } from '#src/utils.js';
|
||||
|
||||
/**
|
||||
* Note: These test cases are designed to cover exceptional scenarios of API calls that
|
||||
* cannot be covered within the auth flow.
|
||||
*/
|
||||
describe('POST /interaction/verification/verification-code', () => {
|
||||
it('Should fail to send email verification code if related connector is not found', async () => {
|
||||
const client = await initClient();
|
||||
|
||||
await client.successSend(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
});
|
||||
|
||||
await expectRejects(
|
||||
client.send(sendVerificationCode, {
|
||||
email: generateEmail(),
|
||||
}),
|
||||
{
|
||||
code: 'connector.not_found',
|
||||
statusCode: 400,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('Should fail to send phone verification code if related connector is not found', async () => {
|
||||
const client = await initClient();
|
||||
|
||||
await client.successSend(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
});
|
||||
|
||||
await expectRejects(
|
||||
client.send(sendVerificationCode, {
|
||||
phone: generatePhone(),
|
||||
}),
|
||||
{
|
||||
code: 'connector.not_found',
|
||||
statusCode: 400,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,118 @@
|
|||
import { SignInMode, InteractionEvent } from '@logto/schemas';
|
||||
|
||||
import { putInteractionEvent, putInteraction } from '#src/api/interaction.js';
|
||||
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
|
||||
import { initClient } from '#src/helpers/client.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||
|
||||
/**
|
||||
* Note: These test cases are designed to cover exceptional scenarios of API calls that
|
||||
* cannot be covered within the auth flow.
|
||||
*/
|
||||
describe('PUT /interaction/event', () => {
|
||||
it('Should fail to update interaction event when the related sign-in mode is not enabled', async () => {
|
||||
// Init a valid sign-in experience config
|
||||
await enableAllPasswordSignInMethods();
|
||||
|
||||
const client = await initClient();
|
||||
|
||||
await updateSignInExperience({
|
||||
signInMode: SignInMode.Register,
|
||||
});
|
||||
|
||||
await expectRejects(
|
||||
client.send(putInteractionEvent, {
|
||||
event: InteractionEvent.SignIn,
|
||||
}),
|
||||
{
|
||||
code: 'auth.forbidden',
|
||||
statusCode: 403,
|
||||
}
|
||||
);
|
||||
|
||||
await updateSignInExperience({
|
||||
signInMode: SignInMode.SignIn,
|
||||
});
|
||||
|
||||
await expectRejects(
|
||||
client.send(putInteractionEvent, {
|
||||
event: InteractionEvent.Register,
|
||||
}),
|
||||
{
|
||||
code: 'auth.forbidden',
|
||||
statusCode: 403,
|
||||
}
|
||||
);
|
||||
|
||||
// Reset
|
||||
await enableAllPasswordSignInMethods();
|
||||
});
|
||||
|
||||
it('Should fail to change interaction event to another event when the initial event is forgot password', async () => {
|
||||
// Init a valid sign-in experience config
|
||||
await enableAllPasswordSignInMethods();
|
||||
|
||||
const client = await initClient();
|
||||
|
||||
await client.successSend(putInteraction, {
|
||||
event: InteractionEvent.ForgotPassword,
|
||||
});
|
||||
|
||||
await expectRejects(
|
||||
client.send(putInteractionEvent, {
|
||||
event: InteractionEvent.Register,
|
||||
}),
|
||||
{
|
||||
code: 'session.interaction_not_found',
|
||||
statusCode: 404,
|
||||
}
|
||||
);
|
||||
|
||||
await expectRejects(
|
||||
client.send(putInteractionEvent, {
|
||||
event: InteractionEvent.SignIn,
|
||||
}),
|
||||
{
|
||||
code: 'session.interaction_not_found',
|
||||
statusCode: 404,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('Should fail to change interaction event to forgot password if the initial event is not forgot password', async () => {
|
||||
// Init a valid sign-in experience config
|
||||
await enableAllPasswordSignInMethods();
|
||||
|
||||
const client = await initClient();
|
||||
|
||||
await client.successSend(putInteraction, {
|
||||
event: InteractionEvent.Register,
|
||||
});
|
||||
|
||||
await expectRejects(
|
||||
client.send(putInteractionEvent, {
|
||||
event: InteractionEvent.ForgotPassword,
|
||||
}),
|
||||
{
|
||||
code: 'session.interaction_not_found',
|
||||
statusCode: 404,
|
||||
}
|
||||
);
|
||||
|
||||
// Change event to sign-in
|
||||
await client.successSend(putInteractionEvent, {
|
||||
event: InteractionEvent.SignIn,
|
||||
});
|
||||
|
||||
await expectRejects(
|
||||
client.send(putInteractionEvent, {
|
||||
event: InteractionEvent.ForgotPassword,
|
||||
}),
|
||||
{
|
||||
code: 'session.interaction_not_found',
|
||||
statusCode: 404,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,201 @@
|
|||
import { SignInIdentifier, InteractionEvent, ConnectorType } from '@logto/schemas';
|
||||
|
||||
import { putInteraction } from '#src/api/interaction.js';
|
||||
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
|
||||
import { initClient } from '#src/helpers/client.js';
|
||||
import {
|
||||
clearConnectorsByTypes,
|
||||
setEmailConnector,
|
||||
setSmsConnector,
|
||||
} from '#src/helpers/connector.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import {
|
||||
enableAllPasswordSignInMethods,
|
||||
enableAllVerificationCodeSignInMethods,
|
||||
} from '#src/helpers/sign-in-experience.js';
|
||||
import { generateEmail, generatePhone } from '#src/utils.js';
|
||||
|
||||
/**
|
||||
* Note: These test cases are designed to cover exceptional scenarios of API calls that
|
||||
* cannot be covered within the auth flow.
|
||||
*/
|
||||
describe('PUT /interaction', () => {
|
||||
/**
|
||||
* Note: only test email & phone identifier here, since other cases are covered in the auth flow.
|
||||
*/
|
||||
it('Should fail to create a sign-in interaction with identifiers when related sign-in methods are not enabled', async () => {
|
||||
// Init a valid sign-in experience config
|
||||
await enableAllPasswordSignInMethods();
|
||||
|
||||
// Remove email & phone identifier from sign-in methods
|
||||
await updateSignInExperience({
|
||||
signIn: {
|
||||
methods: [
|
||||
{
|
||||
identifier: SignInIdentifier.Username,
|
||||
password: true,
|
||||
verificationCode: false,
|
||||
isPasswordPrimary: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const client = await initClient();
|
||||
|
||||
// Email
|
||||
await expectRejects(
|
||||
client.send(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
identifier: {
|
||||
email: generateEmail(),
|
||||
verificationCode: '123456',
|
||||
},
|
||||
}),
|
||||
{
|
||||
code: 'user.sign_in_method_not_enabled',
|
||||
statusCode: 422,
|
||||
}
|
||||
);
|
||||
|
||||
// Phone
|
||||
await expectRejects(
|
||||
client.send(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
identifier: {
|
||||
phone: generatePhone(),
|
||||
verificationCode: '123456',
|
||||
},
|
||||
}),
|
||||
{
|
||||
code: 'user.sign_in_method_not_enabled',
|
||||
statusCode: 422,
|
||||
}
|
||||
);
|
||||
|
||||
// Reset
|
||||
await enableAllPasswordSignInMethods();
|
||||
});
|
||||
|
||||
/**
|
||||
* Note: only test email & phone identifier here, since other cases are covered in the auth flow.
|
||||
*/
|
||||
it('Should fail to create a register interaction with profile when related sign-up identifiers are not enabled', async () => {
|
||||
// Init a valid sign-in experience config
|
||||
await enableAllPasswordSignInMethods({
|
||||
identifiers: [SignInIdentifier.Username],
|
||||
password: true,
|
||||
verify: false,
|
||||
});
|
||||
|
||||
const client = await initClient();
|
||||
|
||||
// Email
|
||||
await expectRejects(
|
||||
client.send(putInteraction, {
|
||||
event: InteractionEvent.Register,
|
||||
profile: {
|
||||
email: generateEmail(),
|
||||
},
|
||||
}),
|
||||
{
|
||||
code: 'user.sign_in_method_not_enabled',
|
||||
statusCode: 422,
|
||||
}
|
||||
);
|
||||
|
||||
// Phone
|
||||
await expectRejects(
|
||||
client.send(putInteraction, {
|
||||
event: InteractionEvent.Register,
|
||||
profile: {
|
||||
phone: generatePhone(),
|
||||
},
|
||||
}),
|
||||
{
|
||||
code: 'user.sign_in_method_not_enabled',
|
||||
statusCode: 422,
|
||||
}
|
||||
);
|
||||
|
||||
// Reset
|
||||
await enableAllPasswordSignInMethods();
|
||||
});
|
||||
|
||||
it('Should fail to create an interaction when verification code is provided in the identifier but failed to verified', async () => {
|
||||
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
|
||||
await setEmailConnector();
|
||||
await setSmsConnector();
|
||||
await enableAllVerificationCodeSignInMethods();
|
||||
|
||||
const client = await initClient();
|
||||
// Email
|
||||
await expectRejects(
|
||||
client.send(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
identifier: {
|
||||
email: generateEmail(),
|
||||
verificationCode: '123456',
|
||||
},
|
||||
}),
|
||||
{
|
||||
code: 'verification_code.not_found',
|
||||
statusCode: 400,
|
||||
}
|
||||
);
|
||||
|
||||
// Email
|
||||
await expectRejects(
|
||||
client.send(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
identifier: {
|
||||
phone: generatePhone(),
|
||||
verificationCode: '123456',
|
||||
},
|
||||
}),
|
||||
{
|
||||
code: 'verification_code.not_found',
|
||||
statusCode: 400,
|
||||
}
|
||||
);
|
||||
|
||||
// Clear
|
||||
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
|
||||
});
|
||||
|
||||
it('Should fail to create an interaction when connector id and connector data is provided but failed to verified', async () => {
|
||||
const client = await initClient();
|
||||
await expectRejects(
|
||||
client.send(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
identifier: {
|
||||
connectorId: 'fake_connector_id',
|
||||
connectorData: {
|
||||
email: generateEmail(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
code: 'session.invalid_connector_id',
|
||||
statusCode: 422,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('Should fail to create an interaction when connector id is provided but failed to verified', async () => {
|
||||
const client = await initClient();
|
||||
await expectRejects(
|
||||
client.send(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
identifier: {
|
||||
connectorId: 'fake_connector_id',
|
||||
email: generateEmail(),
|
||||
},
|
||||
}),
|
||||
{
|
||||
code: 'session.connector_session_not_found',
|
||||
statusCode: 400,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue