0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -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, findUserById,
hasUser, hasUser,
updateUserById, updateUserById,
hasUserWithEmail,
} from '@/queries/user'; } from '@/queries/user';
import assertThat from '@/utils/assert-that'; import assertThat from '@/utils/assert-that';
@ -121,15 +122,24 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
'/users', '/users',
koaGuard({ koaGuard({
body: object({ body: object({
username: string().regex(usernameRegEx), primaryEmail: string().regex(emailRegEx).optional(),
username: string().regex(usernameRegEx).optional(),
password: string().regex(passwordRegEx), password: string().regex(passwordRegEx),
name: string(), name: string(),
}), }),
}), }),
async (ctx, next) => { async (ctx, next) => {
const { username, password, name } = ctx.guard.body; const { primaryEmail, username, password, name } = ctx.guard.body;
assertThat( assertThat(
!(await hasUser(username)), !username || !(await hasUser(username)),
new RequestError({
code: 'user.username_exists_register',
status: 422,
})
);
assertThat(
!primaryEmail || !(await hasUserWithEmail(primaryEmail)),
new RequestError({ new RequestError({
code: 'user.username_exists_register', code: 'user.username_exists_register',
status: 422, status: 422,
@ -142,6 +152,7 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
const user = await insertUser({ const user = await insertUser({
id, id,
primaryEmail,
username, username,
passwordEncrypted, passwordEncrypted,
passwordEncryptionMethod, passwordEncryptionMethod,

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import { managementResource } from '@logto/schemas/lib/seeds';
import { assert } from '@silverhand/essentials'; import { assert } from '@silverhand/essentials';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import { signInWithUsernameAndPassword } from '@/api'; import { signInWithPassword } from '@/api';
import MockClient, { defaultConfig } from '@/client'; import MockClient, { defaultConfig } from '@/client';
import { logtoUrl } from '@/constants'; import { logtoUrl } from '@/constants';
import { createUserByAdmin } from '@/helpers'; import { createUserByAdmin } from '@/helpers';
@ -24,11 +24,11 @@ describe('get access token', () => {
await client.initSession(); await client.initSession();
assert(client.interactionCookie, new Error('Session not found')); assert(client.interactionCookie, new Error('Session not found'));
const { redirectTo } = await signInWithUsernameAndPassword( const { redirectTo } = await signInWithPassword({
username, username,
password, password,
client.interactionCookie interactionCookie: client.interactionCookie,
); });
await client.processSession(redirectTo); await client.processSession(redirectTo);
@ -47,11 +47,11 @@ describe('get access token', () => {
await client.initSession(); await client.initSession();
assert(client.interactionCookie, new Error('Session not found')); assert(client.interactionCookie, new Error('Session not found'));
const { redirectTo } = await signInWithUsernameAndPassword( const { redirectTo } = await signInWithPassword({
username, username,
password, password,
client.interactionCookie interactionCookie: client.interactionCookie,
); });
await client.processSession(redirectTo); await client.processSession(redirectTo);
assert(client.isAuthenticated, new Error('Sign in get get access token failed')); 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 { adminConsoleApplicationId } from '@logto/schemas/lib/seeds';
import { assert } from '@silverhand/essentials';
import { import {
mockEmailConnectorId, mockEmailConnectorId,
@ -9,26 +9,27 @@ import {
mockSmsConnectorConfig, mockSmsConnectorConfig,
} from '@/__mocks__/connectors-mock'; } from '@/__mocks__/connectors-mock';
import { import {
sendRegisterUserWithEmailPasscode, createUser,
verifyRegisterUserWithEmailPasscode,
sendSignInUserWithEmailPasscode,
verifySignInUserWithEmailPasscode,
sendRegisterUserWithSmsPasscode,
verifyRegisterUserWithSmsPasscode,
sendSignInUserWithSmsPasscode,
verifySignInUserWithSmsPasscode,
disableConnector, disableConnector,
signInWithUsernameAndPassword, sendRegisterUserWithEmailPasscode,
sendRegisterUserWithSmsPasscode,
sendSignInUserWithEmailPasscode,
sendSignInUserWithSmsPasscode,
signInWithPassword,
verifyRegisterUserWithEmailPasscode,
verifyRegisterUserWithSmsPasscode,
verifySignInUserWithEmailPasscode,
verifySignInUserWithSmsPasscode,
} from '@/api'; } from '@/api';
import MockClient from '@/client'; import MockClient from '@/client';
import { import {
registerNewUser,
signIn,
setUpConnector,
readPasscode,
createUserByAdmin, createUserByAdmin,
setSignUpIdentifier, readPasscode,
registerNewUser,
setSignInMethod, setSignInMethod,
setSignUpIdentifier,
setUpConnector,
signIn,
} from '@/helpers'; } from '@/helpers';
import { generateUsername, generatePassword, generateEmail, generatePhone } from '@/utils'; import { generateUsername, generatePassword, generateEmail, generatePhone } from '@/utils';
@ -36,12 +37,43 @@ describe('username and password flow', () => {
const username = generateUsername(); const username = generateUsername();
const password = generatePassword(); 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(registerNewUser(username, password)).resolves.not.toThrow();
await expect(signIn({ username, password })).resolves.not.toThrow();
});
}); });
it('sign-in with username & password', async () => { describe('email and password flow', () => {
await expect(signIn(username, password)).resolves.not.toThrow(); 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('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')); assert(client.interactionCookie, new Error('Session not found'));
const { redirectTo } = await signInWithUsernameAndPassword( const { redirectTo } = await signInWithPassword({
username, username,
password, password,
client.interactionCookie interactionCookie: client.interactionCookie,
); });
await client.processSession(redirectTo); 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')); assert(client.interactionCookie, new Error('Session not found'));
const { redirectTo } = await signInWithUsernameAndPassword( const { redirectTo } = await signInWithPassword({
username, username,
password, password,
client.interactionCookie interactionCookie: client.interactionCookie,
); });
await client.processSession(redirectTo); await client.processSession(redirectTo);

View file

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