Skip to main content
Glama
auth.test.ts6.57 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { MedplumClient } from '@medplum/core'; import { ContentType } from '@medplum/core'; import { MockClient } from '@medplum/mock'; import cp from 'node:child_process'; import fs from 'node:fs'; import http from 'node:http'; import { main } from '.'; import { FileSystemStorage } from './storage'; import { createMedplumClient } from './util/client'; jest.mock('node:child_process'); jest.mock('node:http'); jest.mock('./util/client'); jest.mock('node:fs', () => ({ existsSync: jest.fn(), mkdirSync: jest.fn(), readFileSync: jest.fn(), writeFileSync: jest.fn(), constants: { O_CREAT: 0, }, promises: { readFile: jest.fn(async () => '{}'), }, })); describe('CLI auth', () => { const env = process.env; let medplum: MedplumClient; let processError: jest.SpyInstance; beforeAll(() => { process.exit = jest.fn().mockImplementation(function exit(exitCode: number) { if (exitCode === 0) { return; } throw new Error(`Process exited with exit code ${exitCode}`); }) as unknown as typeof process.exit; processError = jest.spyOn(process.stderr, 'write').mockImplementation(jest.fn()); }); beforeEach(() => { jest.resetModules(); jest.clearAllMocks(); process.env = { ...env }; medplum = new MockClient(); console.log = jest.fn(); console.error = jest.fn(); (createMedplumClient as unknown as jest.Mock).mockImplementation(async () => medplum); }); afterEach(() => { process.env = env; }); test('Login success', async () => { (cp.exec as unknown as jest.Mock).mockImplementation( (_, callback: (error: Error | null, stdout: string, stderr: string) => void) => { if (callback) { callback(null, '', ''); } return true; } ); (http.createServer as unknown as jest.Mock).mockReturnValue({ listen: () => ({ close: () => undefined, }), }); // Expect no active login to start expect(medplum.getActiveLogin()).toBeUndefined(); // Start the login await main(['node', 'index.js', 'login']); // Get the handler const handler = (http.createServer as unknown as jest.Mock).mock.calls[0][0]; // Simulate a favicon.ico request, don't crash const req1 = { method: 'GET', url: '/favicon.ico' }; const res1 = { writeHead: jest.fn(), end: jest.fn() }; await handler(req1, res1); expect(res1.writeHead).toHaveBeenCalledWith(404, { 'Content-Type': ContentType.TEXT }); expect(res1.end).toHaveBeenCalledWith('Not found'); // Simulate an OPTIONS request, don't process the code const req2 = { method: 'OPTIONS', url: '/?code=123' }; const res2 = { writeHead: jest.fn(), end: jest.fn() }; await handler(req2, res2); expect(res2.writeHead).toHaveBeenCalledWith(200, { Allow: 'GET, POST', 'Content-Type': ContentType.TEXT, }); expect(res2.end).toHaveBeenCalledWith('OK'); // Simulate the redirect const req3 = { method: 'GET', url: '/?code=123' }; const res3 = { writeHead: jest.fn(), end: jest.fn() }; await handler(req3, res3); expect(res3.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': ContentType.TEXT }); expect(res3.end).toHaveBeenCalledWith('Signed in as Alice Smith. You may close this window.'); expect(medplum.getActiveLogin()).toBeDefined(); }); test('Login unsupported auth type', async () => { await expect(main(['node', 'index.js', 'login', '--auth-type', 'foo'])).rejects.toThrow( 'Process exited with exit code 1' ); expect(processError).toHaveBeenCalledWith( expect.stringContaining( "error: option '--auth-type <authType>' argument 'foo' is invalid. Allowed choices are basic, client-credentials, authorization-code, jwt-bearer, token-exchange, jwt-assertion" ) ); }); test('Login basic auth', async () => { expect(medplum.getActiveLogin()).toBeUndefined(); await main(['node', 'index.js', 'login', '--auth-type', 'basic']); expect(processError).not.toHaveBeenCalled(); }); test('Login client credentials', async () => { expect(medplum.getActiveLogin()).toBeUndefined(); await main([ 'node', 'index.js', 'login', '--auth-type', 'client-credentials', '--client-id', '123', '--client-secret', 'abc', ]); expect(processError).not.toHaveBeenCalled(); }); test('Load credentials from disk', async () => { medplum = new MockClient({ storage: new FileSystemStorage('default') }); (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ activeLogin: JSON.stringify({ accessToken: 'abc', refreshToken: 'xyz', profile: { reference: 'Practitioner/123', display: 'Alice Smith', }, project: { reference: 'Project/456', display: 'My Project', }, }), }) ); await main(['node', 'index.js', 'whoami']); expect((console.log as unknown as jest.Mock).mock.calls).toStrictEqual([ ['Server: https://example.com/'], ['Profile: Alice Smith (Practitioner/123)'], ['Project: My Project (Project/456)'], ]); }); test('Get access token -- logged in', async () => { medplum = new MockClient({ storage: new FileSystemStorage('default') }); (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ activeLogin: JSON.stringify({ accessToken: 'abc', refreshToken: 'xyz', profile: { reference: 'Practitioner/123', display: 'Alice Smith', }, project: { reference: 'Project/456', display: 'My Project', }, }), }) ); await main(['node', 'index.js', 'token']); expect((console.log as unknown as jest.Mock).mock.calls).toStrictEqual([[expect.any(String)]]); }); test('Get access token -- needs auth (expired or not logged in)', async () => { medplum = new MockClient({ profile: null }); await expect(main(['node', 'index.js', 'token'])).rejects.toThrow('Process exited with exit code 1'); expect(processError).toHaveBeenLastCalledWith(expect.stringContaining('Error: Not logged in')); }); });

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