feat(core): Add filtering, selection and pagination to users (#6994)
https://linear.app/n8n/issue/PAY-646
This commit is contained in:
@@ -11,7 +11,6 @@ import { LdapManager } from '@/Ldap/LdapManager.ee';
|
||||
import { LdapService } from '@/Ldap/LdapService.ee';
|
||||
import { encryptPassword, saveLdapSynchronization } from '@/Ldap/helpers';
|
||||
import type { LdapConfig } from '@/Ldap/types';
|
||||
import { sanitizeUser } from '@/UserManagement/UserManagementHelper';
|
||||
import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
|
||||
|
||||
import { randomEmail, randomName, uniqueId } from './../shared/random';
|
||||
@@ -570,24 +569,3 @@ describe('Instance owner should able to delete LDAP users', () => {
|
||||
await authOwnerAgent.post(`/users/${member.id}?transferId=${owner.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Sign-type should be returned when listing users', async () => {
|
||||
const ldapConfig = await createLdapConfig();
|
||||
LdapManager.updateConfig(ldapConfig);
|
||||
|
||||
await testDb.createLdapUser(
|
||||
{
|
||||
globalRole: globalMemberRole,
|
||||
},
|
||||
uniqueId(),
|
||||
);
|
||||
|
||||
const allUsers = await testDb.getAllUsers();
|
||||
expect(allUsers.length).toBe(2);
|
||||
|
||||
const ownerUser = allUsers.find((u) => u.email === owner.email)!;
|
||||
expect(sanitizeUser(ownerUser).signInType).toStrictEqual('email');
|
||||
|
||||
const memberUser = allUsers.find((u) => u.email !== owner.email)!;
|
||||
expect(sanitizeUser(memberUser).signInType).toStrictEqual('ldap');
|
||||
});
|
||||
|
||||
@@ -246,6 +246,10 @@ export async function createOwner() {
|
||||
return createUser({ globalRole: await getGlobalOwnerRole() });
|
||||
}
|
||||
|
||||
export async function createMember() {
|
||||
return createUser({ globalRole: await getGlobalMemberRole() });
|
||||
}
|
||||
|
||||
export async function createUserShell(globalRole: Role): Promise<User> {
|
||||
if (globalRole.scope !== 'global') {
|
||||
throw new Error(`Invalid role received: ${JSON.stringify(globalRole)}`);
|
||||
|
||||
@@ -60,49 +60,6 @@ beforeEach(async () => {
|
||||
config.set('userManagement.emails.smtp.host', '');
|
||||
});
|
||||
|
||||
describe('GET /users', () => {
|
||||
test('should return all users (for owner)', async () => {
|
||||
await testDb.createUser({ globalRole: globalMemberRole });
|
||||
|
||||
const response = await authOwnerAgent.get('/users');
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.data.length).toBe(2);
|
||||
|
||||
response.body.data.map((user: User) => {
|
||||
const {
|
||||
id,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
personalizationAnswers,
|
||||
globalRole,
|
||||
password,
|
||||
isPending,
|
||||
apiKey,
|
||||
} = user;
|
||||
|
||||
expect(validator.isUUID(id)).toBe(true);
|
||||
expect(email).toBeDefined();
|
||||
expect(firstName).toBeDefined();
|
||||
expect(lastName).toBeDefined();
|
||||
expect(personalizationAnswers).toBeUndefined();
|
||||
expect(password).toBeUndefined();
|
||||
expect(isPending).toBe(false);
|
||||
expect(globalRole).toBeDefined();
|
||||
expect(apiKey).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test('should return all users (for member)', async () => {
|
||||
const member = await testDb.createUser({ globalRole: globalMemberRole });
|
||||
const response = await testServer.authAgentFor(member).get('/users');
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.data.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /users/:id', () => {
|
||||
test('should delete the user', async () => {
|
||||
const userToDelete = await testDb.createUser({ globalRole: globalMemberRole });
|
||||
|
||||
248
packages/cli/test/integration/users.controller.test.ts
Normal file
248
packages/cli/test/integration/users.controller.test.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import * as testDb from './shared/testDb';
|
||||
import { setupTestServer } from './shared/utils/';
|
||||
import type { User } from '@/databases/entities/User';
|
||||
import type { PublicUser } from '@/Interfaces';
|
||||
|
||||
const { any } = expect;
|
||||
|
||||
const testServer = setupTestServer({ endpointGroups: ['users'] });
|
||||
|
||||
let owner: User;
|
||||
let member: User;
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['User']);
|
||||
owner = await testDb.createOwner();
|
||||
member = await testDb.createMember();
|
||||
});
|
||||
|
||||
const validatePublicUser = (user: PublicUser) => {
|
||||
expect(typeof user.id).toBe('string');
|
||||
expect(user.email).toBeDefined();
|
||||
expect(user.firstName).toBeDefined();
|
||||
expect(user.lastName).toBeDefined();
|
||||
expect(typeof user.isOwner).toBe('boolean');
|
||||
expect(user.isPending).toBe(false);
|
||||
expect(user.signInType).toBe('email');
|
||||
expect(user.settings).toBe(null);
|
||||
expect(user.personalizationAnswers).toBeNull();
|
||||
expect(user.password).toBeUndefined();
|
||||
expect(user.globalRole).toBeDefined();
|
||||
};
|
||||
|
||||
describe('GET /users', () => {
|
||||
test('should return all users', async () => {
|
||||
const response = await testServer.authAgentFor(owner).get('/users').expect(200);
|
||||
|
||||
expect(response.body.data).toHaveLength(2);
|
||||
|
||||
response.body.data.forEach(validatePublicUser);
|
||||
|
||||
const _response = await testServer.authAgentFor(member).get('/users').expect(200);
|
||||
|
||||
expect(_response.body.data).toHaveLength(2);
|
||||
|
||||
_response.body.data.forEach(validatePublicUser);
|
||||
});
|
||||
|
||||
describe('filter', () => {
|
||||
test('should filter users by field: email', async () => {
|
||||
const secondMember = await testDb.createMember();
|
||||
|
||||
const response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query(`filter={ "email": "${secondMember.email}" }`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data).toHaveLength(1);
|
||||
|
||||
const [user] = response.body.data;
|
||||
|
||||
expect(user.email).toBe(secondMember.email);
|
||||
|
||||
const _response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query('filter={ "email": "non@existing.com" }')
|
||||
.expect(200);
|
||||
|
||||
expect(_response.body.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should filter users by field: firstName', async () => {
|
||||
const secondMember = await testDb.createMember();
|
||||
|
||||
const response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query(`filter={ "firstName": "${secondMember.firstName}" }`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data).toHaveLength(1);
|
||||
|
||||
const [user] = response.body.data;
|
||||
|
||||
expect(user.email).toBe(secondMember.email);
|
||||
|
||||
const _response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query('filter={ "firstName": "Non-Existing" }')
|
||||
.expect(200);
|
||||
|
||||
expect(_response.body.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should filter users by field: lastName', async () => {
|
||||
const secondMember = await testDb.createMember();
|
||||
|
||||
const response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query(`filter={ "lastName": "${secondMember.lastName}" }`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data).toHaveLength(1);
|
||||
|
||||
const [user] = response.body.data;
|
||||
|
||||
expect(user.email).toBe(secondMember.email);
|
||||
|
||||
const _response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query('filter={ "lastName": "Non-Existing" }')
|
||||
.expect(200);
|
||||
|
||||
expect(_response.body.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should filter users by computed field: isOwner', async () => {
|
||||
const response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query('filter={ "isOwner": true }')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data).toHaveLength(1);
|
||||
|
||||
const [user] = response.body.data;
|
||||
|
||||
expect(user.isOwner).toBe(true);
|
||||
|
||||
const _response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query('filter={ "isOwner": false }')
|
||||
.expect(200);
|
||||
|
||||
expect(_response.body.data).toHaveLength(1);
|
||||
|
||||
const [_user] = _response.body.data;
|
||||
|
||||
expect(_user.isOwner).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('select', () => {
|
||||
test('should select user field: id', async () => {
|
||||
const response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query('select=["id"]')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
data: [{ id: any(String) }, { id: any(String) }],
|
||||
});
|
||||
});
|
||||
|
||||
test('should select user field: email', async () => {
|
||||
const response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query('select=["email"]')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
data: [{ email: any(String) }, { email: any(String) }],
|
||||
});
|
||||
});
|
||||
|
||||
test('should select user field: firstName', async () => {
|
||||
const response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query('select=["firstName"]')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
data: [{ firstName: any(String) }, { firstName: any(String) }],
|
||||
});
|
||||
});
|
||||
|
||||
test('should select user field: lastName', async () => {
|
||||
const response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query('select=["lastName"]')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
data: [{ lastName: any(String) }, { lastName: any(String) }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('take', () => {
|
||||
test('should return n users or less, without skip', async () => {
|
||||
const response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query('take=2')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data).toHaveLength(2);
|
||||
|
||||
response.body.data.forEach(validatePublicUser);
|
||||
|
||||
const _response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query('take=1')
|
||||
.expect(200);
|
||||
|
||||
expect(_response.body.data).toHaveLength(1);
|
||||
|
||||
_response.body.data.forEach(validatePublicUser);
|
||||
});
|
||||
|
||||
test('should return n users or less, with skip', async () => {
|
||||
const response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query('take=1&skip=1')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data).toHaveLength(1);
|
||||
|
||||
response.body.data.forEach(validatePublicUser);
|
||||
});
|
||||
});
|
||||
|
||||
describe('combinations', () => {
|
||||
test('should support options that require auxiliary fields', async () => {
|
||||
// isOwner requires globalRole
|
||||
// id-less select with take requires id
|
||||
|
||||
const response = await testServer
|
||||
.authAgentFor(owner)
|
||||
.get('/users')
|
||||
.query('filter={ "isOwner": true }&select=["firstName"]&take=10')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toEqual({ data: [{ firstName: any(String) }] });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import type { CookieOptions, Response } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { mock, anyObject, captor } from 'jest-mock-extended';
|
||||
import type { ILogger } from 'n8n-workflow';
|
||||
import type { IExternalHooksClass, IInternalHooksClass } from '@/Interfaces';
|
||||
import type { IExternalHooksClass, IInternalHooksClass, PublicUser } from '@/Interfaces';
|
||||
import type { User } from '@db/entities/User';
|
||||
import { MeController } from '@/controllers';
|
||||
import { AUTH_COOKIE_NAME } from '@/constants';
|
||||
@@ -45,6 +45,7 @@ describe('MeController', () => {
|
||||
const res = mock<Response>();
|
||||
userService.findOneOrFail.mockResolvedValue(user);
|
||||
jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token');
|
||||
userService.toPublic.mockResolvedValue({} as unknown as PublicUser);
|
||||
|
||||
await controller.updateCurrentUser(req, res);
|
||||
|
||||
|
||||
@@ -134,12 +134,12 @@ describe('List query middleware', () => {
|
||||
expect(nextFn).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should ignore skip without take', () => {
|
||||
test('should throw on skip without take', () => {
|
||||
mockReq.query = { skip: '1' };
|
||||
paginationListQueryMiddleware(...args);
|
||||
|
||||
expect(mockReq.listQueryOptions).toBeUndefined();
|
||||
expect(nextFn).toBeCalledTimes(1);
|
||||
expect(sendErrorResponse).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should default skip to 0', () => {
|
||||
|
||||
Reference in New Issue
Block a user