0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00
logto/packages/core/src/routes/admin-user.test.ts
2022-12-02 09:52:01 +08:00

429 lines
14 KiB
TypeScript

import type { CreateUser, Role, User } from '@logto/schemas';
import { userInfoSelectFields } from '@logto/schemas';
import pick from 'lodash.pick';
import {
mockUser,
mockUserList,
mockUserListResponse,
mockUserResponse,
} from '#src/__mocks__/index.js';
import { encryptUserPassword } from '#src/lib/user.js';
import { findRolesByRoleNames } from '#src/queries/roles.js';
import {
hasUser,
findUserById,
updateUserById,
deleteUserIdentity,
deleteUserById,
} from '#src/queries/user.js';
import { createRequester } from '#src/utils/test-utils.js';
import adminUserRoutes from './admin-user.js';
const filterUsersWithSearch = (users: User[], search: string) =>
users.filter((user) =>
[user.username, user.primaryEmail, user.primaryPhone, user.name].some((value) =>
value ? !value.includes(search) : false
)
);
const mockFindDefaultSignInExperience = jest.fn(async () => ({
signUp: {
identifiers: [],
password: false,
verify: false,
},
}));
jest.mock('#src/queries/sign-in-experience.js', () => ({
findDefaultSignInExperience: jest.fn(async () => mockFindDefaultSignInExperience()),
}));
const mockHasUser = jest.fn(async () => false);
const mockHasUserWithEmail = jest.fn(async () => false);
const mockHasUserWithPhone = jest.fn(async () => false);
jest.mock('#src/queries/user.js', () => ({
countUsers: jest.fn(async (search) => ({
count: search ? filterUsersWithSearch(mockUserList, search).length : mockUserList.length,
})),
findUsers: jest.fn(
async (limit, offset, search): Promise<User[]> =>
search ? filterUsersWithSearch(mockUserList, search) : mockUserList
),
findUserById: jest.fn(async (): Promise<User> => mockUser),
hasUser: jest.fn(async () => mockHasUser()),
hasUserWithEmail: jest.fn(async () => mockHasUserWithEmail()),
hasUserWithPhone: jest.fn(async () => mockHasUserWithPhone()),
updateUserById: jest.fn(
async (_, data: Partial<CreateUser>): Promise<User> => ({
...mockUser,
...data,
})
),
deleteUserById: jest.fn(),
deleteUserIdentity: jest.fn(),
}));
jest.mock('#src/lib/user.js', () => ({
generateUserId: jest.fn(() => 'fooId'),
encryptUserPassword: jest.fn(() => ({
passwordEncrypted: 'password',
passwordEncryptionMethod: 'Argon2i',
})),
insertUser: jest.fn(
async (user: CreateUser): Promise<User> => ({
...mockUser,
...user,
})
),
}));
jest.mock('#src/queries/roles.js', () => ({
findRolesByRoleNames: jest.fn(
async (): Promise<Role[]> => [{ id: 'role_id', name: 'admin', description: 'none' }]
),
}));
const revokeInstanceByUserId = jest.fn();
jest.mock('#src/queries/oidc-model-instance.js', () => ({
revokeInstanceByUserId: async (modelName: string, userId: string) =>
revokeInstanceByUserId(modelName, userId),
}));
describe('adminUserRoutes', () => {
const userRequest = createRequester({ authedRoutes: adminUserRoutes });
afterEach(() => {
jest.clearAllMocks();
});
it('GET /users', async () => {
const response = await userRequest.get('/users');
expect(response.status).toEqual(200);
expect(response.body).toEqual(mockUserListResponse);
expect(response.header).toHaveProperty('total-number', `${mockUserList.length}`);
});
it('GET /users should return matched data', async () => {
const search = 'foo';
const response = await userRequest.get('/users').send({ search });
expect(response.status).toEqual(200);
expect(response.body).toEqual(
filterUsersWithSearch(mockUserList, search).map((user) => pick(user, ...userInfoSelectFields))
);
expect(response.header).toHaveProperty(
'total-number',
`${filterUsersWithSearch(mockUserList, search).length}`
);
});
it('GET /users/:userId', async () => {
const response = await userRequest.get('/users/foo');
expect(response.status).toEqual(200);
expect(response.body).toEqual(mockUserResponse);
});
it('POST /users', async () => {
const username = 'MJAtLogto';
const password = 'PASSWORD';
const name = 'Michael';
const primaryEmail = 'foo@logto.io';
const response = await userRequest
.post('/users')
.send({ primaryEmail, username, password, name });
expect(response.status).toEqual(200);
expect(response.body).toEqual({
...mockUserResponse,
id: 'fooId',
username,
name,
});
});
it('POST /users should throw with invalid input params', async () => {
const username = 'MJAtLogto';
const password = 'PASSWORD';
const name = 'Michael';
// Invalid input format
await expect(
userRequest.post('/users').send({ username, password: 'abc', name })
).resolves.toHaveProperty('status', 400);
});
it('POST /users should throw if username exists', async () => {
const mockHasUser = hasUser as jest.Mock;
mockHasUser.mockImplementationOnce(async () => true);
const username = 'MJAtLogto';
const password = 'PASSWORD';
const name = 'Michael';
await expect(
userRequest.post('/users').send({ username, password, name })
).resolves.toHaveProperty('status', 422);
});
it('PATCH /users/:userId', async () => {
const name = 'Michael';
const avatar = 'http://www.michael.png';
const primaryEmail = 'bar@logto.io';
const primaryPhone = '222222';
const username = 'bar';
const response = await userRequest
.patch('/users/foo')
.send({ username, name, avatar, primaryEmail, primaryPhone });
expect(response.status).toEqual(200);
expect(response.body).toEqual({
...mockUserResponse,
primaryEmail,
primaryPhone,
username,
name,
avatar,
});
});
it('PATCH /users/:userId should allow empty string for clearable fields', async () => {
const response = await userRequest
.patch('/users/foo')
.send({ name: '', avatar: '', primaryEmail: '' });
expect(response.status).toEqual(200);
expect(response.body).toEqual({
...mockUserResponse,
name: '',
avatar: '',
primaryEmail: '',
});
});
it('PATCH /users/:userId should allow null values for clearable fields', async () => {
const response = await userRequest
.patch('/users/foo')
.send({ name: null, username: null, primaryPhone: null });
expect(response.status).toEqual(200);
expect(response.body).toEqual({
...mockUserResponse,
name: null,
username: null,
primaryPhone: null,
});
});
it('PATCH /users/:userId should allow partial update', async () => {
const name = 'Michael';
const updateNameResponse = await userRequest.patch('/users/foo').send({ name });
expect(updateNameResponse.status).toEqual(200);
expect(updateNameResponse.body).toEqual({
...mockUserResponse,
name,
});
const avatar = 'https://www.michael.png';
const updateAvatarResponse = await userRequest.patch('/users/foo').send({ avatar });
expect(updateAvatarResponse.status).toEqual(200);
expect(updateAvatarResponse.body).toEqual({
...mockUserResponse,
avatar,
});
});
it('PATCH /users/:userId should throw when avatar URL is invalid', async () => {
const name = 'Michael';
const avatar = 'http://www.michael.png';
await expect(userRequest.patch('/users/foo').send({ avatar })).resolves.toHaveProperty(
'status',
200
);
await expect(
userRequest.patch('/users/foo').send({ name, avatar: 'non url' })
).resolves.toHaveProperty('status', 400);
});
it('PATCH /users/:userId should throw if user cannot be found', async () => {
const name = 'Michael';
const avatar = 'http://www.michael.png';
const mockFindUserById = findUserById as jest.Mock;
mockFindUserById.mockImplementationOnce(() => {
throw new Error(' ');
});
await expect(userRequest.patch('/users/foo').send({ name, avatar })).resolves.toHaveProperty(
'status',
500
);
expect(updateUserById).not.toBeCalled();
});
it('PATCH /users/:userId should throw if new username is already in use', async () => {
mockHasUser.mockImplementationOnce(async () => true);
await expect(
userRequest.patch('/users/foo').send({ username: 'test' })
).resolves.toHaveProperty('status', 422);
});
it('PATCH /users/:userId should throw if new email has already linked to other accounts', async () => {
mockHasUserWithEmail.mockImplementationOnce(async () => true);
await expect(
userRequest.patch('/users/foo').send({ primaryEmail: 'test@email.com' })
).resolves.toHaveProperty('status', 422);
});
it('PATCH /users/:userId should throw if new phone number has already linked to other accounts', async () => {
mockHasUserWithPhone.mockImplementationOnce(async () => true);
await expect(
userRequest.patch('/users/foo').send({ primaryPhone: '18688886666' })
).resolves.toHaveProperty('status', 422);
});
it('PATCH /users/:userId should throw if role names are invalid', async () => {
const mockedFindRolesByRoleNames = findRolesByRoleNames as jest.Mock;
mockedFindRolesByRoleNames.mockImplementationOnce(
async (): Promise<Role[]> => [
{ id: 'role_id1', name: 'worker', description: 'none' },
{ id: 'role_id2', name: 'cleaner', description: 'none' },
]
);
await expect(
userRequest.patch('/users/foo').send({ roleNames: ['admin'] })
).resolves.toHaveProperty('status', 400);
expect(findUserById).toHaveBeenCalledTimes(1);
expect(updateUserById).not.toHaveBeenCalled();
});
it('PATCH /users/:userId should update if roleNames field is an empty array', async () => {
const roleNames: string[] = [];
const response = await userRequest.patch('/users/foo').send({ roleNames });
expect(response.status).toEqual(200);
expect(response.body).toEqual({
...mockUserResponse,
roleNames,
});
});
it('PATCH /users/:userId/password', async () => {
const mockedUserId = 'foo';
const password = '123456';
const response = await userRequest.patch(`/users/${mockedUserId}/password`).send({ password });
expect(encryptUserPassword).toHaveBeenCalledWith(password);
expect(updateUserById).toHaveBeenCalledTimes(1);
expect(response.status).toEqual(200);
expect(response.body).toEqual({
...mockUserResponse,
});
});
it('PATCH /users/:userId/password should throw if user cannot be found', async () => {
const notExistedUserId = 'notExistedUserId';
const dummyPassword = '123456';
const mockedFindUserById = findUserById as jest.Mock;
mockedFindUserById.mockImplementationOnce((userId) => {
if (userId === notExistedUserId) {
throw new Error(' ');
}
});
await expect(
userRequest.patch(`/users/${notExistedUserId}/password`).send({ password: dummyPassword })
).resolves.toHaveProperty('status', 500);
expect(encryptUserPassword).not.toHaveBeenCalled();
expect(updateUserById).not.toHaveBeenCalled();
});
it('PATCH /users/:userId/is-suspended', async () => {
const mockedUserId = 'foo';
const response = await userRequest
.patch(`/users/${mockedUserId}/is-suspended`)
.send({ isSuspended: true });
expect(updateUserById).toHaveBeenCalledWith(mockedUserId, { isSuspended: true });
expect(revokeInstanceByUserId).toHaveBeenCalledWith('refreshToken', mockedUserId);
expect(response.status).toEqual(200);
expect(response.body).toEqual({
...mockUserResponse,
isSuspended: true,
});
});
it('DELETE /users/:userId', async () => {
const userId = 'fooUser';
const response = await userRequest.delete(`/users/${userId}`);
expect(response.status).toEqual(204);
});
it('DELETE /users/:userId should throw if user is deleting self', async () => {
const userId = 'foo';
const response = await userRequest.delete(`/users/${userId}`);
expect(response.status).toEqual(400);
expect(deleteUserIdentity).not.toHaveBeenCalled();
});
it('DELETE /users/:userId should throw if user cannot be found', async () => {
const notExistedUserId = 'notExistedUserId';
const mockedFindUserById = findUserById as jest.Mock;
mockedFindUserById.mockImplementationOnce((userId) => {
if (userId === notExistedUserId) {
throw new Error(' ');
}
});
await expect(userRequest.delete(`/users/${notExistedUserId}`)).resolves.toHaveProperty(
'status',
500
);
expect(deleteUserById).not.toHaveBeenCalled();
});
it('DELETE /users/:userId/identities/:target should throw if user cannot be found', async () => {
const notExistedUserId = 'notExistedUserId';
const arbitraryTarget = 'arbitraryTarget';
const mockedFindUserById = findUserById as jest.Mock;
mockedFindUserById.mockImplementationOnce((userId) => {
if (userId === notExistedUserId) {
throw new Error(' ');
}
});
await expect(
userRequest.delete(`/users/${notExistedUserId}/identities/${arbitraryTarget}`)
).resolves.toHaveProperty('status', 500);
expect(deleteUserIdentity).not.toHaveBeenCalled();
});
it('DELETE /users/:userId/identities/:target should throw if user is found but connector cannot be found', async () => {
const arbitraryUserId = 'arbitraryUserId';
const nonExistedTarget = 'nonExistedTarget';
const mockedFindUserById = findUserById as jest.Mock;
mockedFindUserById.mockImplementationOnce((userId) => {
if (userId === arbitraryUserId) {
return { identities: { connector1: {}, connector2: {} } };
}
});
await expect(
userRequest.delete(`/users/${arbitraryUserId}/identities/${nonExistedTarget}`)
).resolves.toHaveProperty('status', 404);
expect(deleteUserIdentity).not.toHaveBeenCalled();
});
it('DELETE /users/:userId/identities/:target', async () => {
const arbitraryUserId = 'arbitraryUserId';
const arbitraryTarget = 'arbitraryTarget';
const mockedFindUserById = findUserById as jest.Mock;
mockedFindUserById.mockImplementationOnce((userId) => {
if (userId === arbitraryUserId) {
return { identities: { connectorTarget1: {}, connectorTarget2: {}, arbitraryTarget: {} } };
}
});
await userRequest.delete(`/users/${arbitraryUserId}/identities/${arbitraryTarget}`);
expect(deleteUserIdentity).toHaveBeenCalledWith(arbitraryUserId, arbitraryTarget);
});
});