Skip to main content
Glama
resetpassword.test.ts15.2 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { SendEmailCommand, SESv2Client } from '@aws-sdk/client-sesv2'; import { createReference, getReferenceString, Operator, resolveId } from '@medplum/core'; import type { DomainConfiguration, UserSecurityRequest } from '@medplum/fhirtypes'; import { randomUUID } from 'crypto'; import express from 'express'; import { pwnedPassword } from 'hibp'; import { simpleParser } from 'mailparser'; import fetch from 'node-fetch'; import request from 'supertest'; import { initApp, shutdownApp } from '../app'; import { getConfig, loadTestConfig } from '../config/loader'; import { getSystemRepo } from '../fhir/repo'; import { setupPwnedPasswordMock, setupRecaptchaMock, withTestContext } from '../test.setup'; import { registerNew } from './register'; jest.mock('@aws-sdk/client-sesv2'); jest.mock('hibp'); jest.mock('node-fetch'); describe('Reset Password', () => { const app = express(); const systemRepo = getSystemRepo(); const testRecaptchaSecretKey = 'testrecaptchasecretkey'; beforeAll(async () => { const config = await loadTestConfig(); config.emailProvider = 'awsses'; await initApp(app, config); }); afterAll(async () => { await shutdownApp(); }); beforeEach(() => { (SESv2Client as unknown as jest.Mock).mockClear(); (SendEmailCommand as unknown as jest.Mock).mockClear(); (fetch as unknown as jest.Mock).mockClear(); (pwnedPassword as unknown as jest.Mock).mockClear(); setupPwnedPasswordMock(pwnedPassword as unknown as jest.Mock, 0); setupRecaptchaMock(fetch as unknown as jest.Mock, true); getConfig().recaptchaSecretKey = testRecaptchaSecretKey; }); test('Blank email address', async () => { const res = await request(app).post('/auth/resetpassword').type('json').send({ email: '', recaptchaToken: 'xyz', }); expect(res.status).toBe(400); expect(res.body.issue[0].details.text).toBe('Valid email address between 3 and 72 characters is required'); expect(res.body.issue[0].expression[0]).toBe('email'); }); test('Missing recaptcha', async () => { const res = await request(app).post('/auth/resetpassword').type('json').send({ email: 'admin@example.com', recaptchaToken: '', }); expect(res.status).toBe(400); expect(res.body.issue[0].details.text).toBe('Recaptcha token is required'); }); test('Incorrect recaptcha', async () => { setupRecaptchaMock(fetch as unknown as jest.Mock, false); const res = await request(app).post('/auth/resetpassword').type('json').send({ email: 'admin@example.com', recaptchaToken: 'wrong', }); expect(res.status).toBe(400); expect(res.body.issue[0].details.text).toBe('Recaptcha failed'); }); test('User not found', async () => { const res = await request(app) .post('/auth/resetpassword') .type('json') .send({ email: `alex${randomUUID()}@example.com`, recaptchaToken: 'xyz', }); expect(res.status).toBe(200); expect(SESv2Client).not.toHaveBeenCalled(); expect(SendEmailCommand).not.toHaveBeenCalled(); }); test('Success', async () => { const email = `george${randomUUID()}@example.com`; await withTestContext(() => registerNew({ firstName: 'George', lastName: 'Washington', projectName: 'Washington Project', email, password: 'password!@#', }) ); const res2 = await request(app).post('/auth/resetpassword').type('json').send({ email, recaptchaToken: 'xyz', }); expect(res2.status).toBe(200); expect(SESv2Client).toHaveBeenCalledTimes(1); expect(SendEmailCommand).toHaveBeenCalledTimes(1); const args = (SendEmailCommand as unknown as jest.Mock).mock.calls[0][0]; expect(args.Destination.ToAddresses[0]).toBe(email); const parsed = await simpleParser(args.Content.Raw.Data); expect(parsed.subject).toBe('Medplum Password Reset'); }); test('Success no send email', async () => { const email = `george${randomUUID()}@example.com`; await withTestContext(() => registerNew({ firstName: 'George', lastName: 'Washington', projectName: 'Washington Project', email, password: 'password!@#', }) ); const res2 = await request(app).post('/auth/resetpassword').type('json').send({ email, recaptchaToken: 'xyz', sendEmail: false, }); expect(res2.status).toBe(200); expect(SESv2Client).toHaveBeenCalledTimes(0); expect(SendEmailCommand).toHaveBeenCalledTimes(0); }); test('Success with no recaptcha secret key and missing recaptchaToken', async () => { getConfig().recaptchaSecretKey = ''; const email = `george${randomUUID()}@example.com`; await withTestContext(() => registerNew({ firstName: 'George', lastName: 'Washington', projectName: 'Washington Project', email, password: 'password!@#', }) ); const res2 = await request(app).post('/auth/resetpassword').type('json').send({ email, recaptchaToken: '', }); expect(res2.status).toBe(200); expect(SESv2Client).toHaveBeenCalledTimes(1); expect(SendEmailCommand).toHaveBeenCalledTimes(1); const args = (SendEmailCommand as unknown as jest.Mock).mock.calls[0][0]; expect(args.Destination.ToAddresses[0]).toBe(email); const parsed = await simpleParser(args.Content.Raw.Data); expect(parsed.subject).toBe('Medplum Password Reset'); }); test('External auth', async () => { // Create a domain with external auth const domain = randomUUID() + '.example.com'; await withTestContext(() => systemRepo.createResource<DomainConfiguration>({ resourceType: 'DomainConfiguration', domain, identityProvider: { authorizeUrl: 'https://example.com/oauth2/authorize', tokenUrl: 'https://example.com/oauth2/token', userInfoUrl: 'https://example.com/oauth2/userinfo', clientId: '123', clientSecret: '456', }, }) ); const res = await request(app) .post('/auth/resetpassword') .type('json') .send({ email: `alice@${domain}`, recaptchaToken: 'xyz', }); expect(res.status).toBe(400); expect(res.body.issue[0].details.text).toBe( 'Cannot reset password for external auth. Contact your system administrator.' ); expect(SESv2Client).not.toHaveBeenCalled(); expect(SendEmailCommand).not.toHaveBeenCalled(); }); test('Custom reCAPTCHA site key success', async () => { const email = `recaptcha-client${randomUUID()}@example.com`; const password = 'password!@#'; const recaptchaSiteKey = 'recaptcha-site-key-' + randomUUID(); const recaptchaSecretKey = 'recaptcha-secret-key-' + randomUUID(); await withTestContext(async () => { // Register and create a project const { project } = await registerNew({ firstName: 'Reset', lastName: 'Reset', projectName: 'Reset Project', email, password, }); // As a super admin, set the recaptcha site key // and the default access policy await systemRepo.updateResource({ ...project, site: [ { name: 'Test Site', domain: ['example.com'], recaptchaSiteKey, recaptchaSecretKey, }, ], }); return project; }); const res = await request(app).post('/auth/resetpassword').type('json').send({ email, recaptchaSiteKey, recaptchaToken: 'xyz', }); expect(res.status).toBe(200); expect(SESv2Client).toHaveBeenCalledTimes(1); expect(SendEmailCommand).toHaveBeenCalledTimes(1); const args = (SendEmailCommand as unknown as jest.Mock).mock.calls[0][0]; expect(args.Destination.ToAddresses[0]).toBe(email); const parsed = await simpleParser(args.Content.Raw.Data); expect(parsed.subject).toBe('Medplum Password Reset'); }); test('Custom reCAPTCHA site key not found', async () => { const email = `recaptcha-client${randomUUID()}@example.com`; const password = 'password!@#'; const recaptchaSiteKey = 'recaptcha-site-key-' + randomUUID(); const project = await withTestContext(async () => { // Register and create a project const { project } = await registerNew({ firstName: 'Reset', lastName: 'Reset', projectName: 'Reset Project', email, password, }); // As a super admin, set the recaptcha site key // and the default access policy await systemRepo.updateResource({ ...project, site: [ { name: 'Test Site', domain: ['example.com'], recaptchaSiteKey, }, ], }); return project; }); const res = await request(app).post('/auth/resetpassword').type('json').send({ email, projectId: project.id, recaptchaSiteKey, recaptchaToken: 'xyz', }); expect(res.status).toBe(400); expect(res.body).toMatchObject({ issue: [{ code: 'invalid', details: { text: 'Invalid recaptchaSecretKey' } }] }); expect(SESv2Client).not.toHaveBeenCalled(); expect(SendEmailCommand).not.toHaveBeenCalled(); }); test('Custom reCAPTCHA site key not found', async () => { const email = `recaptcha-client${randomUUID()}@example.com`; const password = 'password!@#'; const recaptchaSiteKey = 'recaptcha-site-key-' + randomUUID(); const project = await withTestContext(async () => { // Register and create a project const { project } = await registerNew({ firstName: 'Reset', lastName: 'Reset', projectName: 'Reset Project', email, password, }); return project; }); const res = await request(app).post('/auth/resetpassword').type('json').send({ email, projectId: project.id, recaptchaSiteKey, recaptchaToken: 'xyz', }); expect(res.status).toBe(400); expect(res.body).toMatchObject({ issue: [{ code: 'invalid', details: { text: 'Invalid recaptchaSiteKey' } }] }); expect(SESv2Client).not.toHaveBeenCalled(); expect(SendEmailCommand).not.toHaveBeenCalled(); }); // User is present but project is not assigned to it. test('User without project', async () => { const email = `recaptcha-client${randomUUID()}@example.com`; const password = 'password!@#'; const project = await withTestContext(async () => { // Register and create a project const { project } = await registerNew({ firstName: 'Reset', lastName: 'Reset', projectName: 'Reset Project', email, password, }); return project; }); // Attempt to reset the password for the user without a project const res = await request(app).post('/auth/resetpassword').type('json').send({ email, projectId: project.id, recaptchaToken: 'xyz', }); // Verify the response and expectations expect(res.status).toBe(200); expect(res.body.issue[0].details.text).toBe('All OK'); expect(SESv2Client).not.toHaveBeenCalled(); // Ensure SESv2Client is not called expect(SendEmailCommand).not.toHaveBeenCalled(); // Ensure SendEmailCommand is not called }); test('User with the project success', async () => { const email = `recaptcha-client${randomUUID()}@example.com`; const password = 'password!@#'; const project = await withTestContext(async () => { // Register and create a project const { project, user } = await registerNew({ firstName: 'Reset', lastName: 'Reset', projectName: 'Reset Project', email, password, }); // Add the project to the user await systemRepo.patchResource('User', resolveId(user) as string, [ { path: '/project', op: 'add', value: createReference(project), }, ]); return project; }); // Attempt to reset the password for the user with a project const res = await request(app).post('/auth/resetpassword').type('json').send({ email, projectId: project.id, recaptchaToken: 'xyz', }); // Verify the response and expectations expect(res.status).toBe(200); expect(SESv2Client).toHaveBeenCalledTimes(1); // Ensure SESv2Client is called once expect(SendEmailCommand).toHaveBeenCalledTimes(1); // Ensure SendEmailCommand is called once // Verify email details const args = (SendEmailCommand as unknown as jest.Mock).mock.calls[0][0]; expect(args.Destination.ToAddresses[0]).toBe(email); // Verify parsed email content const parsed = await simpleParser(args.Content.Raw.Data); expect(parsed.subject).toBe('Medplum Password Reset'); }); test('Password change request with redirectUri', async () => { const email = `recaptcha-client${randomUUID()}@example.com`; const password = 'password!@#'; const { project, user } = await withTestContext(async () => { // Register and create a project const { project, user } = await registerNew({ firstName: 'Reset', lastName: 'Reset', projectName: 'Reset Project', email, password, }); // Add the project to the user await systemRepo.patchResource('User', resolveId(user) as string, [ { path: '/project', op: 'add', value: createReference(project), }, ]); return { project, user }; }); // Attempt to reset the password for the user with a project const res = await request(app).post('/auth/resetpassword').type('json').send({ email, projectId: project.id, recaptchaToken: 'xyz', redirectUri: 'http://example.com', }); // Verify the response and expectations expect(res.status).toBe(200); expect(SESv2Client).toHaveBeenCalledTimes(1); // Ensure SESv2Client is called once expect(SendEmailCommand).toHaveBeenCalledTimes(1); // Ensure SendEmailCommand is called once // Get newly created UserSecurityRequest const userSecurityRequest = (await withTestContext(async () => systemRepo.searchOne<UserSecurityRequest>({ resourceType: 'UserSecurityRequest', filters: [ { code: 'user', operator: Operator.EQUALS, value: getReferenceString(user), }, ], }) )) as UserSecurityRequest; // Verify UserSecurityRequest.redirectUri expect(userSecurityRequest.redirectUri).toBe('http://example.com'); // Verify email details const args = (SendEmailCommand as unknown as jest.Mock).mock.calls[0][0]; expect(args.Destination.ToAddresses[0]).toBe(email); // Verify parsed email content const parsed = await simpleParser(args.Content.Raw.Data); expect(parsed.subject).toBe('Medplum Password Reset'); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server