0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(core,test): allow create user with email and add tests

- Allow create user with email or username for admin user route
- Add integration tests for email and password sign-in
This commit is contained in:
Gao Sun 2022-11-09 16:25:54 +08:00
parent f82d2a8c4f
commit 4b6d3c584a
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
8 changed files with 138 additions and 61 deletions

View file

@ -18,6 +18,7 @@ import {
findUserById,
hasUser,
updateUserById,
hasUserWithEmail,
} from '@/queries/user';
import assertThat from '@/utils/assert-that';
@ -121,15 +122,24 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
'/users',
koaGuard({
body: object({
username: string().regex(usernameRegEx),
primaryEmail: string().regex(emailRegEx).optional(),
username: string().regex(usernameRegEx).optional(),
password: string().regex(passwordRegEx),
name: string(),
}),
}),
async (ctx, next) => {
const { username, password, name } = ctx.guard.body;
const { primaryEmail, username, password, name } = ctx.guard.body;
assertThat(
!(await hasUser(username)),
!username || !(await hasUser(username)),
new RequestError({
code: 'user.username_exists_register',
status: 422,
})
);
assertThat(
!primaryEmail || !(await hasUserWithEmail(primaryEmail)),
new RequestError({
code: 'user.username_exists_register',
status: 422,
@ -142,6 +152,7 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
const user = await insertUser({
id,
primaryEmail,
username,
passwordEncrypted,
passwordEncryptionMethod,

View file

@ -3,6 +3,7 @@ import type { User } from '@logto/schemas';
import { authedAdminApi } from './api';
type CreateUserPayload = {
primaryEmail?: string;
username: string;
password: string;
name: string;

View file

@ -24,17 +24,27 @@ export const registerUserWithUsernameAndPassword = async (
})
.json<RedirectResponse>();
export const signInWithUsernameAndPassword = async (
username: string,
password: string,
interactionCookie: string
) =>
export type SignInWithPassword = {
username?: string;
email?: string;
password: string;
interactionCookie: string;
};
export const signInWithPassword = async ({
email,
username,
password,
interactionCookie,
}: SignInWithPassword) =>
api
.post('session/sign-in/password/username', {
// This route in core needs to be refactored
.post('session/sign-in/password/' + (username ? 'username' : 'email'), {
headers: {
cookie: interactionCookie,
},
json: {
email,
username,
password,
},
@ -42,6 +52,24 @@ export const signInWithUsernameAndPassword = async (
})
.json<RedirectResponse>();
export const signInWithEmailAndPassword = async (
email: string,
password: string,
interactionCookie: string
) =>
api
.post('session/sign-in/email/username', {
headers: {
cookie: interactionCookie,
},
json: {
email,
password,
},
followRedirect: false,
})
.json<RedirectResponse>();
export const consent = async (interactionCookie: string) =>
api
.post('session/consent', {

View file

@ -8,7 +8,7 @@ import { HTTPError } from 'got';
import {
createUser,
registerUserWithUsernameAndPassword,
signInWithUsernameAndPassword,
signInWithPassword,
updateConnectorConfig,
enableConnector,
bindWithSocial,
@ -21,14 +21,12 @@ import { generateUsername, generatePassword } from '@/utils';
import { mockSocialConnectorId } from './__mocks__/connectors-mock';
export const createUserByAdmin = (_username?: string, _password?: string) => {
const username = _username ?? generateUsername();
const password = _password ?? generatePassword();
export const createUserByAdmin = (username?: string, password?: string, primaryEmail?: string) => {
return createUser({
username,
password,
name: username,
username: username ?? generateUsername(),
password: password ?? generatePassword(),
name: username ?? 'John',
primaryEmail,
}).json<User>();
};
@ -49,17 +47,24 @@ export const registerNewUser = async (username: string, password: string) => {
assert(client.isAuthenticated, new Error('Sign in failed'));
};
export const signIn = async (username: string, password: string) => {
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 signInWithUsernameAndPassword(
const { redirectTo } = await signInWithPassword({
username,
email,
password,
client.interactionCookie
);
interactionCookie: client.interactionCookie,
});
await client.processSession(redirectTo);
@ -135,11 +140,11 @@ export const bindSocialToNewCreatedUser = async () => {
new Error('Auth with social failed')
);
const { redirectTo } = await signInWithUsernameAndPassword(
const { redirectTo } = await signInWithPassword({
username,
password,
client.interactionCookie
);
interactionCookie: client.interactionCookie,
});
await bindWithSocial(mockSocialConnectorId, client.interactionCookie);

View file

@ -11,7 +11,7 @@ export const generatePassword = () => `pwd_${crypto.randomUUID()}`;
export const generateResourceName = () => `res_${crypto.randomUUID()}`;
export const generateResourceIndicator = () => `https://${crypto.randomUUID()}.logto.io`;
export const generateEmail = () => `${crypto.randomUUID()}@logto.io`;
export const generateEmail = () => `${crypto.randomUUID().toLowerCase()}@logto.io`;
export const generatePhone = () => {
const array = new Uint32Array(1);

View file

@ -5,7 +5,7 @@ import { managementResource } from '@logto/schemas/lib/seeds';
import { assert } from '@silverhand/essentials';
import fetch from 'node-fetch';
import { signInWithUsernameAndPassword } from '@/api';
import { signInWithPassword } from '@/api';
import MockClient, { defaultConfig } from '@/client';
import { logtoUrl } from '@/constants';
import { createUserByAdmin } from '@/helpers';
@ -24,11 +24,11 @@ describe('get access token', () => {
await client.initSession();
assert(client.interactionCookie, new Error('Session not found'));
const { redirectTo } = await signInWithUsernameAndPassword(
const { redirectTo } = await signInWithPassword({
username,
password,
client.interactionCookie
);
interactionCookie: client.interactionCookie,
});
await client.processSession(redirectTo);
@ -47,11 +47,11 @@ describe('get access token', () => {
await client.initSession();
assert(client.interactionCookie, new Error('Session not found'));
const { redirectTo } = await signInWithUsernameAndPassword(
const { redirectTo } = await signInWithPassword({
username,
password,
client.interactionCookie
);
interactionCookie: client.interactionCookie,
});
await client.processSession(redirectTo);
assert(client.isAuthenticated, new Error('Sign in get get access token failed'));

View file

@ -1,6 +1,6 @@
import { SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
import assert from 'assert';
import { adminConsoleApplicationId } from '@logto/schemas/lib/seeds';
import { assert } from '@silverhand/essentials';
import {
mockEmailConnectorId,
@ -9,26 +9,27 @@ import {
mockSmsConnectorConfig,
} from '@/__mocks__/connectors-mock';
import {
sendRegisterUserWithEmailPasscode,
verifyRegisterUserWithEmailPasscode,
sendSignInUserWithEmailPasscode,
verifySignInUserWithEmailPasscode,
sendRegisterUserWithSmsPasscode,
verifyRegisterUserWithSmsPasscode,
sendSignInUserWithSmsPasscode,
verifySignInUserWithSmsPasscode,
createUser,
disableConnector,
signInWithUsernameAndPassword,
sendRegisterUserWithEmailPasscode,
sendRegisterUserWithSmsPasscode,
sendSignInUserWithEmailPasscode,
sendSignInUserWithSmsPasscode,
signInWithPassword,
verifyRegisterUserWithEmailPasscode,
verifyRegisterUserWithSmsPasscode,
verifySignInUserWithEmailPasscode,
verifySignInUserWithSmsPasscode,
} from '@/api';
import MockClient from '@/client';
import {
registerNewUser,
signIn,
setUpConnector,
readPasscode,
createUserByAdmin,
setSignUpIdentifier,
readPasscode,
registerNewUser,
setSignInMethod,
setSignUpIdentifier,
setUpConnector,
signIn,
} from '@/helpers';
import { generateUsername, generatePassword, generateEmail, generatePhone } from '@/utils';
@ -36,12 +37,43 @@ describe('username and password flow', () => {
const username = generateUsername();
const password = generatePassword();
it('register with username & password', async () => {
it('register and sign in with username & password', async () => {
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);
beforeAll(async () => {
await setSignInMethod([
{
identifier: SignInIdentifier.Email,
password: true,
verificationCode: false,
isPasswordPrimary: false,
},
]);
});
it('sign-in with username & password', async () => {
await expect(signIn(username, password)).resolves.not.toThrow();
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();
});
});
@ -236,11 +268,11 @@ describe('sign-in and sign-out', () => {
assert(client.interactionCookie, new Error('Session not found'));
const { redirectTo } = await signInWithUsernameAndPassword(
const { redirectTo } = await signInWithPassword({
username,
password,
client.interactionCookie
);
interactionCookie: client.interactionCookie,
});
await client.processSession(redirectTo);
@ -266,11 +298,11 @@ describe('sign-in to demo app and revisit Admin Console', () => {
assert(client.interactionCookie, new Error('Session not found'));
const { redirectTo } = await signInWithUsernameAndPassword(
const { redirectTo } = await signInWithPassword({
username,
password,
client.interactionCookie
);
interactionCookie: client.interactionCookie,
});
await client.processSession(redirectTo);

View file

@ -12,7 +12,7 @@ import {
getAuthWithSocial,
registerWithSocial,
bindWithSocial,
signInWithUsernameAndPassword,
signInWithPassword,
getUser,
} from '@/api';
import MockClient from '@/client';
@ -126,11 +126,11 @@ describe('social bind account', () => {
// User with social does not exist
expect(response instanceof HTTPError && response.response.statusCode === 422).toBe(true);
const { redirectTo } = await signInWithUsernameAndPassword(
const { redirectTo } = await signInWithPassword({
username,
password,
client.interactionCookie
);
interactionCookie: client.interactionCookie,
});
await expect(
bindWithSocial(mockSocialConnectorId, client.interactionCookie)