mirror of
https://github.com/logto-io/logto.git
synced 2025-03-17 22:31:28 -05:00
test: session route (#283)
This commit is contained in:
parent
ddd695dd33
commit
94e583111d
4 changed files with 322 additions and 24 deletions
|
@ -46,7 +46,7 @@ export const getUserInfoByAuthCode = async (
|
|||
|
||||
export const getUserInfoFromInteractionResult = async (
|
||||
connectorId: string,
|
||||
interactionResult?: InteractionResults
|
||||
interactionResult: InteractionResults
|
||||
): Promise<SocialUserInfo> => {
|
||||
const parse = z
|
||||
.object({
|
||||
|
|
|
@ -5,38 +5,326 @@ import { createRequester } from '@/utils/test-utils';
|
|||
import sessionRoutes from './session';
|
||||
|
||||
jest.mock('oidc-provider');
|
||||
jest.mock('@/lib/user', () => ({
|
||||
async findUserByUsernameAndPassword(username: string) {
|
||||
if (username === 'notexistuser') {
|
||||
throw new Error(' ');
|
||||
}
|
||||
|
||||
return { id: 'user1' };
|
||||
},
|
||||
}));
|
||||
jest.mock('@/lib/social', () => ({
|
||||
...jest.requireActual('@/lib/social'),
|
||||
async findSocialRelatedUser() {
|
||||
return ['phone', { id: 'user1', identities: {} }];
|
||||
},
|
||||
}));
|
||||
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||
jest.mock('@/queries/user', () => ({
|
||||
findUserById: async () => ({ id: 'id ' }),
|
||||
findUserByPhone: async () => ({ id: 'id ' }),
|
||||
findUserByEmail: async () => ({ id: 'id ' }),
|
||||
updateUserById: async (...args: unknown[]) => updateUserById(...args),
|
||||
hasUserWithPhone: async (phone: string) => phone === '13000000000',
|
||||
hasUserWithEmail: async (email: string) => email === 'a@a.com',
|
||||
}));
|
||||
const sendPasscode = jest.fn();
|
||||
jest.mock('@/lib/passcode', () => ({
|
||||
createPasscode: async () => ({ id: 'id' }),
|
||||
sendPasscode: () => {
|
||||
sendPasscode();
|
||||
},
|
||||
verifyPasscode: async (_a: unknown, _b: unknown, code: string) => {
|
||||
if (code !== '1234') {
|
||||
throw new Error(' ');
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const MockedProvider = Provider as jest.MockedClass<typeof Provider>;
|
||||
|
||||
const getProvider = (): Provider => {
|
||||
const provider = MockedProvider.mock.instances[0];
|
||||
|
||||
if (!provider) {
|
||||
throw new Error('Provider is not initialized');
|
||||
}
|
||||
|
||||
return provider;
|
||||
};
|
||||
|
||||
const getInteractionDetails = () => {
|
||||
return getProvider().interactionDetails as unknown as jest.MockedFunction<() => Promise<any>>;
|
||||
};
|
||||
|
||||
describe('sessionRoutes', () => {
|
||||
const sessionRequest = createRequester({
|
||||
anonymousRoutes: sessionRoutes,
|
||||
provider: new Provider(''),
|
||||
middlewares: [
|
||||
async (ctx, next) => {
|
||||
ctx.userLog = {};
|
||||
|
||||
return next();
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
afterAll(() => jest.clearAllMocks());
|
||||
|
||||
it('POST /session with consent prompt name', async () => {
|
||||
(
|
||||
MockedProvider.mock.instances[0]?.interactionDetails as unknown as jest.MockedFunction<
|
||||
() => Promise<{ prompt: { name: string } }>
|
||||
>
|
||||
).mockResolvedValue({
|
||||
prompt: { name: 'consent' },
|
||||
});
|
||||
const response = await sessionRequest.post('/session');
|
||||
beforeAll(() => {
|
||||
const provider = getProvider();
|
||||
const { interactionResult } = provider;
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toHaveProperty('redirectTo', expect.stringContaining('/session/consent'));
|
||||
(interactionResult as jest.MockedFunction<typeof interactionResult>).mockResolvedValue(
|
||||
'redirectTo'
|
||||
);
|
||||
});
|
||||
|
||||
it('POST /session with invalid prompt name', async () => {
|
||||
(
|
||||
MockedProvider.mock.instances[0]?.interactionDetails as unknown as jest.MockedFunction<
|
||||
() => Promise<{ prompt: { name: string } }>
|
||||
>
|
||||
).mockResolvedValue({
|
||||
prompt: { name: 'invalid' },
|
||||
describe('POST /session', () => {
|
||||
it('should redirect to /session/consent with consent prompt name', async () => {
|
||||
getInteractionDetails().mockResolvedValue({
|
||||
prompt: { name: 'consent' },
|
||||
});
|
||||
const response = await sessionRequest.post('/session');
|
||||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.body).toHaveProperty(
|
||||
'redirectTo',
|
||||
expect.stringContaining('/session/consent')
|
||||
);
|
||||
});
|
||||
await expect(sessionRequest.post('/session').send({})).resolves.toHaveProperty('status', 400);
|
||||
|
||||
it('throw error with other prompt name', async () => {
|
||||
getInteractionDetails().mockResolvedValue({
|
||||
prompt: { name: 'invalid' },
|
||||
});
|
||||
await expect(sessionRequest.post('/session').send({})).resolves.toHaveProperty('status', 400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/sign-in/username-password', () => {
|
||||
it('assign result and redirect', async () => {
|
||||
const response = await sessionRequest.post('/session/sign-in/username-password').send({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
});
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.body).toHaveProperty('redirectTo');
|
||||
expect(getProvider().interactionResult).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({ login: { accountId: 'user1' } }),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('throw if user not found', async () => {
|
||||
const response = await sessionRequest.post('/session/sign-in/username-password').send({
|
||||
username: 'notexistuser',
|
||||
password: 'password',
|
||||
});
|
||||
expect(response.statusCode).toEqual(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/sign-in/passwordless/phone/send-passcode', () => {
|
||||
beforeAll(() => {
|
||||
getInteractionDetails().mockResolvedValue({
|
||||
jti: 'jti',
|
||||
});
|
||||
});
|
||||
it('call sendPasscode', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/sign-in/passwordless/phone/send-passcode')
|
||||
.send({ phone: '13000000000' });
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(sendPasscode).toHaveBeenCalled();
|
||||
});
|
||||
it('throw error if phone not exists', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/sign-in/passwordless/phone/send-passcode')
|
||||
.send({ phone: '13000000001' });
|
||||
expect(response.statusCode).toEqual(422);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/sign-in/passwordless/phone/verify-passcode', () => {
|
||||
it('assign result and redirect', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/sign-in/passwordless/phone/verify-passcode')
|
||||
.send({ phone: '13000000000', code: '1234' });
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.body).toHaveProperty('redirectTo');
|
||||
expect(getProvider().interactionResult).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({ login: { accountId: 'user1' } }),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
it('throw error if phone not exists', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/sign-in/passwordless/phone/send-passcode')
|
||||
.send({ phone: '13000000001' });
|
||||
expect(response.statusCode).toEqual(422);
|
||||
});
|
||||
it('throw error if verifyPasscode failed', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/sign-in/passwordless/phone/verify-passcode')
|
||||
.send({ phone: '13000000000', code: '1231' });
|
||||
expect(response.statusCode).toEqual(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/sign-in/passwordless/email/send-passcode', () => {
|
||||
beforeAll(() => {
|
||||
getInteractionDetails().mockResolvedValue({
|
||||
jti: 'jti',
|
||||
});
|
||||
});
|
||||
it('call sendPasscode', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/sign-in/passwordless/email/send-passcode')
|
||||
.send({ email: 'a@a.com' });
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(sendPasscode).toHaveBeenCalled();
|
||||
});
|
||||
it('throw error if email not exists', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/sign-in/passwordless/email/send-passcode')
|
||||
.send({ email: 'b@a.com' });
|
||||
expect(response.statusCode).toEqual(422);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/sign-in/passwordless/email/verify-passcode', () => {
|
||||
it('assign result and redirect', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/sign-in/passwordless/email/verify-passcode')
|
||||
.send({ email: 'a@a.com', code: '1234' });
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.body).toHaveProperty('redirectTo');
|
||||
expect(getProvider().interactionResult).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({ login: { accountId: 'user1' } }),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
it('throw error if verifyPasscode failed', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/sign-in/passwordless/email/verify-passcode')
|
||||
.send({ email: 'a@a.com', code: '1231' });
|
||||
expect(response.statusCode).toEqual(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/sign-in/bind-social-related-user', () => {
|
||||
it('throw if session is not authorized', async () => {
|
||||
await expect(
|
||||
sessionRequest
|
||||
.post('/session/sign-in/bind-social-related-user')
|
||||
.send({ connectorId: 'connectorId' })
|
||||
).resolves.toHaveProperty('statusCode', 400);
|
||||
});
|
||||
it('throw if no social info in session', async () => {
|
||||
getInteractionDetails().mockResolvedValue({
|
||||
result: { login: { accountId: 'user1' } },
|
||||
});
|
||||
await expect(
|
||||
sessionRequest
|
||||
.post('/session/sign-in/bind-social-related-user')
|
||||
.send({ connectorId: 'connectorId' })
|
||||
).resolves.toHaveProperty('statusCode', 400);
|
||||
});
|
||||
it('updates user identities and sign in', async () => {
|
||||
getInteractionDetails().mockResolvedValue({
|
||||
result: {
|
||||
login: { accountId: 'user1' },
|
||||
socialUserInfo: {
|
||||
connectorId: 'connectorId',
|
||||
userInfo: { id: 'connectorUser', phone: 'phone' },
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest.post('/session/sign-in/bind-social-related-user').send({
|
||||
connectorId: 'connectorId',
|
||||
});
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(updateUserById).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
identities: {
|
||||
connectorId: {
|
||||
details: { id: 'connectorUser', phone: 'phone' },
|
||||
userId: 'connectorUser',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(response.body).toHaveProperty('redirectTo');
|
||||
expect(getProvider().interactionResult).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({ login: { accountId: 'user1' } }),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/bind-social', () => {
|
||||
it('throw if session is not authorized', async () => {
|
||||
getInteractionDetails().mockResolvedValue({});
|
||||
await expect(
|
||||
sessionRequest.post('/session/bind-social').send({ connectorId: 'connectorId' })
|
||||
).resolves.toHaveProperty('statusCode', 400);
|
||||
});
|
||||
it('throw if no social info in session', async () => {
|
||||
getInteractionDetails().mockResolvedValue({
|
||||
result: { login: { accountId: 'user1' } },
|
||||
});
|
||||
await expect(
|
||||
sessionRequest.post('/session/bind-social').send({ connectorId: 'connectorId' })
|
||||
).resolves.toHaveProperty('statusCode', 400);
|
||||
});
|
||||
it('updates user identities', async () => {
|
||||
getInteractionDetails().mockResolvedValue({
|
||||
result: {
|
||||
login: { accountId: 'user1' },
|
||||
socialUserInfo: {
|
||||
connectorId: 'connectorId',
|
||||
userInfo: { id: 'connectorUser', phone: 'phone' },
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest.post('/session/bind-social').send({
|
||||
connectorId: 'connectorId',
|
||||
});
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(updateUserById).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
identities: {
|
||||
connectorId: {
|
||||
details: { id: 'connectorUser', phone: 'phone' },
|
||||
userId: 'connectorUser',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('DELETE /session', async () => {
|
||||
const response = await sessionRequest.delete('/session');
|
||||
expect(response.body).toHaveProperty('redirectTo');
|
||||
expect(getProvider().interactionResult).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({ error: 'oidc.aborted' }),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -85,7 +85,7 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
|
|||
|
||||
const passcode = await createPasscode(jti, PasscodeType.SignIn, { phone });
|
||||
await sendPasscode(passcode);
|
||||
ctx.state = 204;
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
|
@ -131,7 +131,7 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
|
|||
|
||||
const passcode = await createPasscode(jti, PasscodeType.SignIn, { email });
|
||||
await sendPasscode(passcode);
|
||||
ctx.state = 204;
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
|
@ -214,7 +214,7 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
|
|||
);
|
||||
|
||||
router.post(
|
||||
'/session/sign-in/bind-social-related-user-and-sign-in',
|
||||
'/session/sign-in/bind-social-related-user',
|
||||
koaGuard({
|
||||
body: object({ connectorId: string() }),
|
||||
}),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createMockContext, Options } from '@shopify/jest-koa-mocks';
|
||||
import Koa, { MiddlewareType, Context } from 'koa';
|
||||
import Koa, { MiddlewareType, Context, Middleware } from 'koa';
|
||||
import Router, { IRouterParamContext } from 'koa-router';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { createMockPool, createMockQueryResult, QueryResultRowType } from 'slonik';
|
||||
|
@ -72,12 +72,14 @@ export function createRequester(
|
|||
| {
|
||||
anonymousRoutes?: RouteLauncher<AnonymousRouter> | Array<RouteLauncher<AnonymousRouter>>;
|
||||
authedRoutes?: RouteLauncher<AuthedRouter> | Array<RouteLauncher<AuthedRouter>>;
|
||||
middlewares?: Middleware[];
|
||||
}
|
||||
| {
|
||||
anonymousRoutes?:
|
||||
| ProviderRouteLauncher<AnonymousRouter>
|
||||
| Array<ProviderRouteLauncher<AnonymousRouter>>;
|
||||
authedRoutes?: RouteLauncher<AuthedRouter> | Array<RouteLauncher<AuthedRouter>>;
|
||||
middlewares?: Middleware[];
|
||||
provider: Provider;
|
||||
}
|
||||
): request.SuperTest<request.Test>;
|
||||
|
@ -86,6 +88,7 @@ export function createRequester({
|
|||
anonymousRoutes,
|
||||
authedRoutes,
|
||||
provider,
|
||||
middlewares,
|
||||
}: {
|
||||
anonymousRoutes?:
|
||||
| RouteLauncher<AnonymousRouter>
|
||||
|
@ -94,9 +97,16 @@ export function createRequester({
|
|||
| Array<ProviderRouteLauncher<AnonymousRouter>>;
|
||||
authedRoutes?: RouteLauncher<AuthedRouter> | Array<RouteLauncher<AuthedRouter>>;
|
||||
provider?: Provider;
|
||||
middlewares?: Middleware[];
|
||||
}): request.SuperTest<request.Test> {
|
||||
const app = new Koa();
|
||||
|
||||
if (middlewares) {
|
||||
for (const middleware of middlewares) {
|
||||
app.use(middleware);
|
||||
}
|
||||
}
|
||||
|
||||
if (anonymousRoutes) {
|
||||
const anonymousRouter: AnonymousRouter = new Router();
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue