0
Fork 0
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:
Wang Sijie 2022-02-25 15:09:20 +08:00 committed by GitHub
parent ddd695dd33
commit 94e583111d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 322 additions and 24 deletions

View file

@ -46,7 +46,7 @@ export const getUserInfoByAuthCode = async (
export const getUserInfoFromInteractionResult = async (
connectorId: string,
interactionResult?: InteractionResults
interactionResult: InteractionResults
): Promise<SocialUserInfo> => {
const parse = z
.object({

View file

@ -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()
);
});
});

View file

@ -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() }),
}),

View file

@ -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();