mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-02 07:01:30 -08:00
ci(core): Add unit tests for "me" controller (no-changelog) (#5479)
This commit is contained in:
parent
a6c59fcbc2
commit
83505cb0d4
|
@ -47,6 +47,7 @@
|
||||||
"jest": "^29.4.2",
|
"jest": "^29.4.2",
|
||||||
"jest-environment-jsdom": "^29.4.2",
|
"jest-environment-jsdom": "^29.4.2",
|
||||||
"jest-mock": "^29.4.2",
|
"jest-mock": "^29.4.2",
|
||||||
|
"jest-mock-extended": "^3.0.1",
|
||||||
"nock": "^13.2.9",
|
"nock": "^13.2.9",
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
|
|
166
packages/cli/test/unit/controllers/me.controller.test.ts
Normal file
166
packages/cli/test/unit/controllers/me.controller.test.ts
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
import { CookieOptions, Response } from 'express';
|
||||||
|
import type { Repository } from 'typeorm';
|
||||||
|
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 { User } from '@db/entities/User';
|
||||||
|
import { MeController } from '@/controllers';
|
||||||
|
import { AUTH_COOKIE_NAME } from '@/constants';
|
||||||
|
import { BadRequestError } from '@/ResponseHelper';
|
||||||
|
import type { AuthenticatedRequest, MeRequest } from '@/requests';
|
||||||
|
|
||||||
|
describe('MeController', () => {
|
||||||
|
const logger = mock<ILogger>();
|
||||||
|
const externalHooks = mock<IExternalHooksClass>();
|
||||||
|
const internalHooks = mock<IInternalHooksClass>();
|
||||||
|
const userRepository = mock<Repository<User>>();
|
||||||
|
|
||||||
|
let controller: MeController;
|
||||||
|
beforeAll(() => {
|
||||||
|
controller = new MeController({
|
||||||
|
logger,
|
||||||
|
externalHooks,
|
||||||
|
internalHooks,
|
||||||
|
repositories: { User: userRepository },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateCurrentUser', () => {
|
||||||
|
it('should throw BadRequestError if email is missing in the payload', async () => {
|
||||||
|
const req = mock<MeRequest.Settings>({});
|
||||||
|
expect(controller.updateCurrentUser(req, mock())).rejects.toThrowError(
|
||||||
|
new BadRequestError('Email is mandatory'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw BadRequestError if email is invalid', async () => {
|
||||||
|
const req = mock<MeRequest.Settings>({ body: { email: 'invalid-email' } });
|
||||||
|
expect(controller.updateCurrentUser(req, mock())).rejects.toThrowError(
|
||||||
|
new BadRequestError('Invalid email address'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the user in the DB, and issue a new cookie', async () => {
|
||||||
|
const req = mock<MeRequest.Settings>({
|
||||||
|
user: mock({ id: '123', password: 'password', authIdentities: [] }),
|
||||||
|
body: { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' },
|
||||||
|
});
|
||||||
|
const res = mock<Response>();
|
||||||
|
userRepository.save.calledWith(anyObject()).mockResolvedValue(req.user);
|
||||||
|
jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token');
|
||||||
|
|
||||||
|
await controller.updateCurrentUser(req, res);
|
||||||
|
|
||||||
|
const cookieOptions = captor<CookieOptions>();
|
||||||
|
expect(res.cookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME, 'signed-token', cookieOptions);
|
||||||
|
expect(cookieOptions.value.httpOnly).toBe(true);
|
||||||
|
expect(cookieOptions.value.sameSite).toBe('lax');
|
||||||
|
|
||||||
|
expect(externalHooks.run).toHaveBeenCalledWith('user.profile.update', [
|
||||||
|
req.user.email,
|
||||||
|
anyObject(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updatePassword', () => {
|
||||||
|
const passwordHash = '$2a$10$ffitcKrHT.Ls.m9FfWrMrOod76aaI0ogKbc3S96Q320impWpCbgj6'; // Hashed 'old_password'
|
||||||
|
|
||||||
|
it('should throw if the user does not have a password set', async () => {
|
||||||
|
const req = mock<MeRequest.Password>({
|
||||||
|
user: mock({ password: undefined }),
|
||||||
|
body: { currentPassword: '', newPassword: '' },
|
||||||
|
});
|
||||||
|
expect(controller.updatePassword(req, mock())).rejects.toThrowError(
|
||||||
|
new BadRequestError('Requesting user not set up.'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if currentPassword does not match the user's password", async () => {
|
||||||
|
const req = mock<MeRequest.Password>({
|
||||||
|
user: mock({ password: passwordHash }),
|
||||||
|
body: { currentPassword: 'not_old_password', newPassword: '' },
|
||||||
|
});
|
||||||
|
expect(controller.updatePassword(req, mock())).rejects.toThrowError(
|
||||||
|
new BadRequestError('Provided current password is incorrect.'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should throw if newPassword is not valid', () => {
|
||||||
|
Object.entries({
|
||||||
|
pass: 'Password must be 8 to 64 characters long. Password must contain at least 1 number. Password must contain at least 1 uppercase letter.',
|
||||||
|
password:
|
||||||
|
'Password must contain at least 1 number. Password must contain at least 1 uppercase letter.',
|
||||||
|
password1: 'Password must contain at least 1 uppercase letter.',
|
||||||
|
}).forEach(([newPassword, errorMessage]) => {
|
||||||
|
it(newPassword, async () => {
|
||||||
|
const req = mock<MeRequest.Password>({
|
||||||
|
user: mock({ password: passwordHash }),
|
||||||
|
body: { currentPassword: 'old_password', newPassword },
|
||||||
|
});
|
||||||
|
expect(controller.updatePassword(req, mock())).rejects.toThrowError(
|
||||||
|
new BadRequestError(errorMessage),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the password in the DB, and issue a new cookie', async () => {
|
||||||
|
const req = mock<MeRequest.Password>({
|
||||||
|
user: mock({ password: passwordHash }),
|
||||||
|
body: { currentPassword: 'old_password', newPassword: 'NewPassword123' },
|
||||||
|
});
|
||||||
|
const res = mock<Response>();
|
||||||
|
userRepository.save.calledWith(req.user).mockResolvedValue(req.user);
|
||||||
|
jest.spyOn(jwt, 'sign').mockImplementation(() => 'new-signed-token');
|
||||||
|
|
||||||
|
await controller.updatePassword(req, res);
|
||||||
|
|
||||||
|
expect(req.user.password).not.toBe(passwordHash);
|
||||||
|
|
||||||
|
const cookieOptions = captor<CookieOptions>();
|
||||||
|
expect(res.cookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME, 'new-signed-token', cookieOptions);
|
||||||
|
expect(cookieOptions.value.httpOnly).toBe(true);
|
||||||
|
expect(cookieOptions.value.sameSite).toBe('lax');
|
||||||
|
|
||||||
|
expect(externalHooks.run).toHaveBeenCalledWith('user.password.update', [
|
||||||
|
req.user.email,
|
||||||
|
req.user.password,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(internalHooks.onUserUpdate).toHaveBeenCalledWith({
|
||||||
|
user: req.user,
|
||||||
|
fields_changed: ['password'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('API Key methods', () => {
|
||||||
|
let req: AuthenticatedRequest;
|
||||||
|
beforeAll(() => {
|
||||||
|
req = mock({ user: mock<Partial<User>>({ id: '123', apiKey: 'test-key' }) });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createAPIKey', () => {
|
||||||
|
it('should create and save an API key', async () => {
|
||||||
|
const { apiKey } = await controller.createAPIKey(req);
|
||||||
|
expect(userRepository.update).toHaveBeenCalledWith(req.user.id, { apiKey });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAPIKey', () => {
|
||||||
|
it('should return the users api key', async () => {
|
||||||
|
const { apiKey } = await controller.getAPIKey(req);
|
||||||
|
expect(apiKey).toEqual(req.user.apiKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteAPIKey', () => {
|
||||||
|
it('should delete the API key', async () => {
|
||||||
|
await controller.deleteAPIKey(req);
|
||||||
|
expect(userRepository.update).toHaveBeenCalledWith(req.user.id, { apiKey: null });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -36,6 +36,7 @@ importers:
|
||||||
jest: ^29.4.2
|
jest: ^29.4.2
|
||||||
jest-environment-jsdom: ^29.4.2
|
jest-environment-jsdom: ^29.4.2
|
||||||
jest-mock: ^29.4.2
|
jest-mock: ^29.4.2
|
||||||
|
jest-mock-extended: ^3.0.1
|
||||||
n8n: '*'
|
n8n: '*'
|
||||||
nock: ^13.2.9
|
nock: ^13.2.9
|
||||||
node-fetch: ^2.6.7
|
node-fetch: ^2.6.7
|
||||||
|
@ -61,6 +62,7 @@ importers:
|
||||||
jest: 29.4.2
|
jest: 29.4.2
|
||||||
jest-environment-jsdom: 29.4.2
|
jest-environment-jsdom: 29.4.2
|
||||||
jest-mock: 29.4.2
|
jest-mock: 29.4.2
|
||||||
|
jest-mock-extended: 3.0.1_47mstovy2thvxzoe7ouhdvjexm
|
||||||
nock: 13.2.9
|
nock: 13.2.9
|
||||||
node-fetch: 2.6.7
|
node-fetch: 2.6.7
|
||||||
p-limit: 3.1.0
|
p-limit: 3.1.0
|
||||||
|
@ -13117,6 +13119,17 @@ packages:
|
||||||
stack-utils: 2.0.6
|
stack-utils: 2.0.6
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/jest-mock-extended/3.0.1_47mstovy2thvxzoe7ouhdvjexm:
|
||||||
|
resolution: {integrity: sha512-RF4Ow8pXvbRuEcCTj56oYHmig5311BSFvbEGxPNYL51wGKGu93MvVQgx0UpFmjqyBXIcElkZo2Rke88kR1iSKQ==}
|
||||||
|
peerDependencies:
|
||||||
|
jest: ^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0
|
||||||
|
typescript: ^3.0.0 || ^4.0.0
|
||||||
|
dependencies:
|
||||||
|
jest: 29.4.2
|
||||||
|
ts-essentials: 7.0.3_typescript@4.9.4
|
||||||
|
typescript: 4.9.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
/jest-mock/29.4.2:
|
/jest-mock/29.4.2:
|
||||||
resolution: {integrity: sha512-x1FSd4Gvx2yIahdaIKoBjwji6XpboDunSJ95RpntGrYulI1ByuYQCKN/P7hvk09JB74IonU3IPLdkutEWYt++g==}
|
resolution: {integrity: sha512-x1FSd4Gvx2yIahdaIKoBjwji6XpboDunSJ95RpntGrYulI1ByuYQCKN/P7hvk09JB74IonU3IPLdkutEWYt++g==}
|
||||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||||
|
@ -19222,6 +19235,14 @@ packages:
|
||||||
engines: {node: '>=6.10'}
|
engines: {node: '>=6.10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/ts-essentials/7.0.3_typescript@4.9.4:
|
||||||
|
resolution: {integrity: sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=3.7.0'
|
||||||
|
dependencies:
|
||||||
|
typescript: 4.9.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
/ts-expect/1.3.0:
|
/ts-expect/1.3.0:
|
||||||
resolution: {integrity: sha512-e4g0EJtAjk64xgnFPD6kTBUtpnMVzDrMb12N1YZV0VvSlhnVT3SGxiYTLdGy8Q5cYHOIC/FAHmZ10eGrAguicQ==}
|
resolution: {integrity: sha512-e4g0EJtAjk64xgnFPD6kTBUtpnMVzDrMb12N1YZV0VvSlhnVT3SGxiYTLdGy8Q5cYHOIC/FAHmZ10eGrAguicQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
Loading…
Reference in a new issue