Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add password reset flow tests for user management backend #2807

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
5ae6b3f
:zap: Refactor users namespace
ivov Feb 9, 2022
eadbec0
:zap: Adjust fillout endpoint
ivov Feb 9, 2022
ba9a7ad
:zap: Refactor initTestServer arg
ivov Feb 9, 2022
4d17eb8
:pencil2: Specify agent type
ivov Feb 9, 2022
b251fe0
:pencil2: Specify role type
ivov Feb 9, 2022
a7b74db
:zap: Tighten `/users/:id` check
ivov Feb 9, 2022
ccea112
:sparkles: Add initial tests
ivov Feb 9, 2022
381f111
:truck: Reposition init server map
ivov Feb 9, 2022
27d2393
:zap: Set constants in `validatePassword()`
ivov Feb 10, 2022
4d613e7
:zap: Tighten `/users/:id` check
ivov Feb 10, 2022
9b55b2e
:zap: Improve checks in `/users/:id`
ivov Feb 10, 2022
3e65857
:sparkles: Add tests for `/users/:id`
ivov Feb 10, 2022
03cba0e
:package: Update package-lock.json
ivov Feb 10, 2022
9d6a47e
:zap: Simplify expectation
ivov Feb 10, 2022
1e0b94f
:zap: Reuse util for authless agent
ivov Feb 10, 2022
218f085
:truck: Make role names consistent
ivov Feb 10, 2022
ce93c66
:blue_book: Tighten namespaces map type
ivov Feb 10, 2022
0894f6e
:fire: Remove unneeded default arg
ivov Feb 10, 2022
2279efd
:sparkles: Add tests for `POST /users`
ivov Feb 10, 2022
d99c05a
:blue_book: Create test SMTP account type
ivov Feb 10, 2022
b857097
:pencil2: Improve wording
ivov Feb 10, 2022
673ee5d
:art: Formatting
ivov Feb 10, 2022
858ae42
:fire: Remove temp fix
ivov Feb 11, 2022
0adc929
:zap: Replace helper with config call
ivov Feb 11, 2022
9fbc5bb
:zap: Fix failing tests
ivov Feb 11, 2022
5667a34
:twisted_rightwards_arrows: Merge parent branch
ivov Feb 11, 2022
e021618
:fire: Remove outdated test
ivov Feb 11, 2022
b449690
:sparkles: Add tests for password reset flow
ivov Feb 11, 2022
e88e19b
:pencil2: Fix test wording
ivov Feb 11, 2022
458493c
:zap: Set password reset namespace
ivov Feb 11, 2022
0c904eb
:fire: Remove unused helper
ivov Feb 11, 2022
1ae3ebe
:zap: Increase readability of domain fetcher
ivov Feb 14, 2022
5c657e1
:zap: Refactor payload validation
ivov Feb 14, 2022
3639cda
:fire: Remove repetition
ivov Feb 14, 2022
14000a4
:rewind: Restore logging
ivov Feb 14, 2022
84d1ee0
:zap: Initialize logger in tests
ivov Feb 14, 2022
93c3e02
:fire: Remove redundancy from check
ivov Feb 14, 2022
b116981
:truck: Move `globalOwnerRole` fetching to global scope
ivov Feb 14, 2022
bc1c428
:fire: Remove unused imports
ivov Feb 14, 2022
8c96a51
:truck: Move random utils to own module
ivov Feb 14, 2022
ba2de0e
:truck: Move test types to own module
ivov Feb 14, 2022
af20438
:pencil2: Add dividers to utils
ivov Feb 14, 2022
403f198
:pencil2: Reorder `initTestServer` param docstring
ivov Feb 14, 2022
6169636
:pencil2: Add TODO comment
ivov Feb 14, 2022
273a36b
:zap: Dry up member creation
ivov Feb 14, 2022
771464b
:zap: Tighten search criteria
ivov Feb 14, 2022
fff3504
:test_tube: Add expectation to `GET /users`
ivov Feb 14, 2022
d21d63d
:zap: Create role fetcher utils
ivov Feb 14, 2022
ca46df3
:zap: Create one more role fetch util
ivov Feb 14, 2022
657a4aa
:fire: Remove unneeded DB query
ivov Feb 14, 2022
4e58847
:test_tube: Add expectation to `POST /users`
ivov Feb 14, 2022
3519c0f
:test_tube: Add expectation to `DELETE /users/:id`
ivov Feb 14, 2022
7c55e03
:test_tube: Add another expectation to `DELETE /users/:id`
ivov Feb 14, 2022
371e9ae
:test_tube: Add expectations to `DELETE /users/:id`
ivov Feb 14, 2022
856a96c
:test_tube: Adjust expectations in `POST /users/:id`
ivov Feb 14, 2022
e5fc785
:test_tube: Add expectations to `DELETE /users/:id`
ivov Feb 14, 2022
ed075ec
:twisted_rightwards_arrows: Merge parent branch
ivov Feb 14, 2022
3ceee82
:twisted_rightwards_arrows: Merge parent branch
ivov Feb 14, 2022
d9cace9
:blue_book: Add namespace name to type
ivov Feb 14, 2022
e90a71a
:truck: Adjust imports
ivov Feb 14, 2022
e317143
:zap: Optimize `globalOwnerRole` fetching
ivov Feb 14, 2022
fea0884
:test_tube: Add expectations
ivov Feb 14, 2022
cb460f7
:shirt: Fix build
ivov Feb 14, 2022
579867d
:shirt: Fix build
ivov Feb 14, 2022
a3813ff
:zap: Update method
ivov Feb 14, 2022
f4820af
:zap: Update method
ivov Feb 14, 2022
7273280
:twisted_rightwards_arrows: Merge parent branch
ivov Feb 14, 2022
182fce8
:twisted_rightwards_arrows: Merge master
ivov Feb 14, 2022
9786175
:test_tube: Fix `POST /change-password` test
ivov Feb 14, 2022
df7b4ca
:blue_book: Fix `userToDelete` type
ivov Feb 15, 2022
c32423f
:zap: Refactor `createAgent()`
ivov Feb 15, 2022
2dc4838
:zap: Make role fetching global
ivov Feb 15, 2022
2c4579b
:zap: Optimize roles fetching
ivov Feb 15, 2022
9e69323
:zap: Centralize member creation
ivov Feb 15, 2022
ded6663
:zap: Refactor truncation helper
ivov Feb 15, 2022
9136d67
:test_tube: Add teardown to `DELETE /users/:id`
ivov Feb 15, 2022
b17e9d1
:test_tube: Add DB expectations to users tests
ivov Feb 15, 2022
e777d68
:twisted_rightwards_arrows: Merge master
ivov Feb 15, 2022
ad2deef
:zap: Refactor as in users namespace
ivov Feb 15, 2022
753f1a3
:test_tube: Add expectation to `POST /change-password`
ivov Feb 15, 2022
aab2cd9
:fire: Remove pass validation due to hash
ivov Feb 15, 2022
45bfd12
:pencil2: Improve pass validation error message
ivov Feb 15, 2022
44e0fdc
:zap: Improve owner pass validation
ivov Feb 15, 2022
ab7753f
:zap: Create logger initialization helper
ivov Feb 15, 2022
c60226e
:zap: Optimize helpers
ivov Feb 15, 2022
89cabd5
:zap: Restructure `getAllRoles` helper
ivov Feb 15, 2022
ead1367
:twisted_rightwards_arrows: Merge parent branch
ivov Feb 15, 2022
1baa54e
:zap: Update `truncate` calls
ivov Feb 15, 2022
a6f96a4
:twisted_rightwards_arrows: Merge parent branch
ivov Feb 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/cli/src/UserManagement/routes/passwordReset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as UserManagementMailer from '../email';
import type { PasswordResetRequest } from '../../requests';
import { issueCookie } from '../auth/jwt';
import { getBaseUrl } from '../../GenericHelpers';
import config = require('../../../config');

export function passwordResetNamespace(this: N8nApp): void {
/**
Expand All @@ -22,6 +23,14 @@ export function passwordResetNamespace(this: N8nApp): void {
this.app.post(
`/${this.restEndpoint}/forgot-password`,
ResponseHelper.send(async (req: PasswordResetRequest.Email) => {
if (config.get('userManagement.emails.mode') === '') {
throw new ResponseHelper.ResponseError(
'Email sending must be set up in order to request a password reset email',
undefined,
500,
);
}

const { email } = req.body;

if (!email) {
Expand Down
241 changes: 241 additions & 0 deletions packages/cli/test/integration/passwordReset.endpoints.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import express = require('express');
import { getConnection } from 'typeorm';
import { v4 as uuid } from 'uuid';

import * as utils from './shared/utils';
import { Db } from '../../src';
import config = require('../../config');
import { compare } from 'bcryptjs';
import {
randomEmail,
randomInvalidPassword,
randomName,
randomValidPassword,
} from './shared/random';
import { Role } from '../../src/databases/entities/Role';

let app: express.Application;
let globalOwnerRole: Role;

beforeAll(async () => {
app = utils.initTestServer({ namespaces: ['passwordReset'], applyAuth: true });
await utils.initTestDb();
await utils.truncate(['User']);

globalOwnerRole = await Db.collections.Role!.findOneOrFail({
name: 'owner',
scope: 'global',
});
});

beforeEach(async () => {
jest.isolateModules(() => {
jest.mock('../../config');
});

config.set('userManagement.hasOwner', true);
config.set('userManagement.emails.mode', '');

await utils.createUser({
id: INITIAL_TEST_USER.id,
email: INITIAL_TEST_USER.email,
password: INITIAL_TEST_USER.password,
firstName: INITIAL_TEST_USER.firstName,
lastName: INITIAL_TEST_USER.lastName,
role: globalOwnerRole,
});
});

afterEach(async () => {
await utils.truncate(['User']);
});

afterAll(() => {
return getConnection().close();
});

test('POST /forgot-password should send password reset email', async () => {
const authlessAgent = await utils.createAgent(app);

const {
user,
pass,
smtp: { host, port, secure },
} = await utils.getSmtpTestAccount();

config.set('userManagement.emails.mode', 'smtp');
config.set('userManagement.emails.smtp.host', host);
config.set('userManagement.emails.smtp.port', port);
config.set('userManagement.emails.smtp.secure', secure);
config.set('userManagement.emails.smtp.auth.user', user);
config.set('userManagement.emails.smtp.auth.pass', pass);

const response = await authlessAgent
.post('/forgot-password')
.send({ email: INITIAL_TEST_USER.email });

expect(response.statusCode).toBe(200);
expect(response.body).toEqual({});
ivov marked this conversation as resolved.
Show resolved Hide resolved

const owner = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email });
expect(owner.resetPasswordToken).toBeDefined();
});

test('POST /forgot-password should fail if emailing is not set up', async () => {
const authlessAgent = await utils.createAgent(app);

const response = await authlessAgent
.post('/forgot-password')
.send({ email: INITIAL_TEST_USER.email });

expect(response.statusCode).toBe(500);
ivov marked this conversation as resolved.
Show resolved Hide resolved

const owner = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email });
expect(owner.resetPasswordToken).toBeNull();
});

test('POST /forgot-password should fail with invalid inputs', async () => {
const authlessAgent = await utils.createAgent(app);

config.set('userManagement.emails.mode', 'smtp');

const invalidPayloads = [
randomEmail(),
[randomEmail()],
{},
[{ name: randomName() }],
[{ email: randomName() }],
];

for (const invalidPayload of invalidPayloads) {
const response = await authlessAgent.post('/forgot-password').send(invalidPayload);
expect(response.statusCode).toBe(400);
ivov marked this conversation as resolved.
Show resolved Hide resolved

const owner = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email });
expect(owner.resetPasswordToken).toBeNull();
}
});

test('POST /forgot-password should fail if user is not found', async () => {
const authlessAgent = await utils.createAgent(app);

config.set('userManagement.emails.mode', 'smtp');

const response = await authlessAgent.post('/forgot-password').send({ email: randomEmail() });

expect(response.statusCode).toBe(404);
});

test('GET /resolve-password-token should succeed with valid inputs', async () => {
const authlessAgent = await utils.createAgent(app);

const resetPasswordToken = uuid();

await Db.collections.User!.update(INITIAL_TEST_USER.id, { resetPasswordToken });

const response = await authlessAgent
.get('/resolve-password-token')
.query({ userId: INITIAL_TEST_USER.id, token: resetPasswordToken });

expect(response.statusCode).toBe(200);
});

test('GET /resolve-password-token should fail with invalid inputs', async () => {
const authlessAgent = await utils.createAgent(app);

config.set('userManagement.emails.mode', 'smtp');

const first = await authlessAgent.get('/resolve-password-token').query({ token: uuid() });

const second = await authlessAgent
.get('/resolve-password-token')
.query({ userId: INITIAL_TEST_USER.id });

for (const response of [first, second]) {
expect(response.statusCode).toBe(400);
}
});

test('GET /resolve-password-token should fail if user is not found', async () => {
const authlessAgent = await utils.createAgent(app);

config.set('userManagement.emails.mode', 'smtp');

const response = await authlessAgent
.get('/resolve-password-token')
.query({ userId: INITIAL_TEST_USER.id, token: uuid() });

expect(response.statusCode).toBe(404);
});

test('POST /change-password should succeed with valid inputs', async () => {
const authlessAgent = await utils.createAgent(app);

const resetPasswordToken = uuid();
await Db.collections.User!.update(INITIAL_TEST_USER.id, { resetPasswordToken });

const passwordToStore = randomValidPassword();

const response = await authlessAgent.post('/change-password').send({
token: resetPasswordToken,
userId: INITIAL_TEST_USER.id,
password: passwordToStore,
});

expect(response.statusCode).toBe(200);

const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined();

const { password: storedPassword } = await Db.collections.User!.findOneOrFail(
INITIAL_TEST_USER.id,
);

const comparisonResult = await compare(passwordToStore, storedPassword!);
expect(comparisonResult).toBe(true);
ivov marked this conversation as resolved.
Show resolved Hide resolved
expect(storedPassword).not.toBe(passwordToStore);
});

test('POST /change-password should fail with invalid inputs', async () => {
const authlessAgent = await utils.createAgent(app);

const resetPasswordToken = uuid();
await Db.collections.User!.update(INITIAL_TEST_USER.id, { resetPasswordToken });

const invalidPayloads = [
{ token: uuid() },
{ id: INITIAL_TEST_USER.id },
{ password: randomValidPassword() },
{ token: uuid(), id: INITIAL_TEST_USER.id },
{ token: uuid(), password: randomValidPassword() },
{ id: INITIAL_TEST_USER.id, password: randomValidPassword() },
{
id: INITIAL_TEST_USER.id,
password: randomInvalidPassword(),
token: resetPasswordToken,
},
{
id: INITIAL_TEST_USER.id,
password: randomValidPassword(),
token: uuid(),
},
];

const { password: originalHashedPassword } = await Db.collections.User!.findOneOrFail();

for (const invalidPayload of invalidPayloads) {
const response = await authlessAgent.post('/change-password').query(invalidPayload);
expect(response.statusCode).toBe(400);

const { password: fetchedHashedPassword } = await Db.collections.User!.findOneOrFail();
expect(originalHashedPassword).toBe(fetchedHashedPassword);
}
});

const INITIAL_TEST_USER = {
id: uuid(),
email: randomEmail(),
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};
2 changes: 1 addition & 1 deletion packages/cli/test/integration/shared/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ export type SmtpTestAccount = {
};
};

export type EndpointNamespace = 'me' | 'users' | 'auth' | 'owner';
export type EndpointNamespace = 'me' | 'users' | 'auth' | 'owner' | 'passwordReset';

export type NamespacesMap = Readonly<Record<EndpointNamespace, (this: N8nApp) => void>>;
3 changes: 3 additions & 0 deletions packages/cli/test/integration/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { meNamespace as meEndpoints } from '../../../src/UserManagement/routes/m
import { usersNamespace as usersEndpoints } from '../../../src/UserManagement/routes/users';
import { authenticationMethods as authEndpoints } from '../../../src/UserManagement/routes/auth';
import { ownerNamespace as ownerEndpoints } from '../../../src/UserManagement/routes/owner';
import { passwordResetNamespace as passwordResetEndpoints } from '../../../src/UserManagement/routes/passwordReset';
import { getConnection } from 'typeorm';
import { issueJWT } from '../../../src/UserManagement/auth/jwt';
import { randomEmail, randomValidPassword, randomName } from './random';
Expand Down Expand Up @@ -67,6 +68,7 @@ export function initTestServer({
users: usersEndpoints,
auth: authEndpoints,
owner: ownerEndpoints,
passwordReset: passwordResetEndpoints,
};

for (const namespace of namespaces) {
Expand Down Expand Up @@ -176,6 +178,7 @@ export function getAllRoles() {
]);
}


// ----------------------------------
// request agent
// ----------------------------------
Expand Down