Skip to main content
Glama
fhirquota.test.ts7.16 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { createReference, sleep } from '@medplum/core'; import type { Bundle, ProjectMembership, UserConfiguration } from '@medplum/fhirtypes'; import type { Express } from 'express'; import express from 'express'; import { randomUUID } from 'node:crypto'; import request from 'supertest'; import { inviteUser } from '../admin/invite'; import { initApp, shutdownApp } from '../app'; import { loadTestConfig } from '../config/loader'; import type { MedplumServerConfig } from '../config/types'; import { getRedis } from '../redis'; import type { TestRedisConfig } from '../test.setup'; import { createTestProject, deleteRedisKeys } from '../test.setup'; describe('FHIR Rate Limits', () => { let app: Express; let config: MedplumServerConfig; let redisConfig: TestRedisConfig; let accessToken: string; beforeAll(async () => { config = await loadTestConfig(); redisConfig = config.redis as TestRedisConfig; }); beforeEach(async () => { app = express(); config.defaultRateLimit = -1; redisConfig.db = 6; // Use different temp Redis instance for these tests redisConfig.keyPrefix = 'fhir-quota:'; }); afterEach(async () => { await deleteRedisKeys(getRedis(), redisConfig.keyPrefix); expect(await shutdownApp()).toBeUndefined(); }); test('Blocks request that would exceed limit', async () => { config.defaultFhirQuota = 20; await initApp(app, config); ({ accessToken } = await createTestProject({ withAccessToken: true })); const res = await request(app).get('/fhir/R4/Patient?_count=20').auth(accessToken, { type: 'bearer' }).send(); expect(res.status).toBe(200); expect(res.get('ratelimit')).toStrictEqual('"fhirInteractions";r=0;t=60'); const res2 = await request(app).get('/fhir/R4/Patient?_count=20').auth(accessToken, { type: 'bearer' }).send(); expect(res2.status).toBe(429); expect(res2.get('ratelimit')).toStrictEqual('"fhirInteractions";r=0;t=60'); }); test('Blocks single too-expensive request', async () => { config.defaultFhirQuota = 1; await initApp(app, config); ({ accessToken } = await createTestProject({ withAccessToken: true })); const res = await request(app) .post('/fhir/R4/Patient') .auth(accessToken, { type: 'bearer' }) .send({ resourceType: 'Patient' }); expect(res.status).toBe(429); }); test('Allows batch under limit', async () => { config.defaultFhirQuota = 1; await initApp(app, config); ({ accessToken } = await createTestProject({ withAccessToken: true })); const res = await request(app) .post('/fhir/R4/') .auth(accessToken, { type: 'bearer' }) .send({ resourceType: 'Bundle', type: 'batch', entry: [{ request: { method: 'GET', url: 'Patient' } }], } as Bundle); expect(res.status).toBe(200); }); test('Blocks oversized transaction bundle', async () => { config.defaultFhirQuota = 1; await initApp(app, config); ({ accessToken } = await createTestProject({ withAccessToken: true, project: { features: ['transaction-bundles'] }, })); const res = await request(app) .post('/fhir/R4/') .auth(accessToken, { type: 'bearer' }) .send({ resourceType: 'Bundle', type: 'transaction', entry: [{ request: { method: 'GET', url: 'Patient' } }, { request: { method: 'GET', url: 'Practitioner' } }], } satisfies Bundle); expect(res.status).toBe(429); }); test('Reports multiple, in-progress rate limits', async () => { config.defaultFhirQuota = 500; config.defaultRateLimit = 100; await initApp(app, config); ({ accessToken } = await createTestProject({ withAccessToken: true })); const res = await request(app).get('/fhir/R4/Patient?_count=20').auth(accessToken, { type: 'bearer' }).send(); expect(res.status).toBe(200); expect(res.get('ratelimit')).toStrictEqual('"requests";r=99;t=60, "fhirInteractions";r=480;t=60'); await sleep(1000); const res2 = await request(app).get('/fhir/R4/Patient?_count=20').auth(accessToken, { type: 'bearer' }).send(); expect(res2.status).toBe(200); expect(res2.get('ratelimit')).toStrictEqual('"requests";r=98;t=59, "fhirInteractions";r=460;t=59'); }); test('Respects Project setting override', async () => { config.defaultFhirQuota = 1; await initApp(app, config); ({ accessToken } = await createTestProject({ withAccessToken: true, project: { systemSetting: [{ name: 'userFhirQuota', valueInteger: 1000 }] }, })); const res = await request(app) .post('/fhir/R4/Patient') .auth(accessToken, { type: 'bearer' }) .send({ resourceType: 'Patient' }); expect(res.status).toBe(201); }); test('Respects ProjectMembership setting override', async () => { config.defaultFhirQuota = 1; await initApp(app, config); const { accessToken, repo, membership } = await createTestProject({ withAccessToken: true, withRepo: true, withClient: true, }); const userConfig = await repo.createResource<UserConfiguration>({ resourceType: 'UserConfiguration', option: [{ id: 'fhirQuota', valueInteger: 1000 }], }); await repo.updateResource<ProjectMembership>({ ...membership, userConfiguration: createReference(userConfig), }); const res = await request(app) .post('/fhir/R4/Patient') .auth(accessToken, { type: 'bearer' }) .send({ resourceType: 'Patient' }); expect(res.status).toBe(201); }); test('Respects Project level limit', async () => { config.defaultFhirQuota = 100; await initApp(app, config); const { accessToken, project } = await createTestProject({ withAccessToken: true, project: { systemSetting: [{ name: 'totalFhirQuota', valueInteger: 100 }], }, }); const email = `${randomUUID()}@example.com`; const password = randomUUID(); await inviteUser({ project, resourceType: 'Practitioner', firstName: 'A.', lastName: 'Zee', email, password }); const loginRes = await request(app).post('/auth/login').type('json').send({ email, password, scope: 'openid offline', codeChallenge: 'xyz', codeChallengeMethod: 'plain', }); expect(loginRes.status).toBe(200); const tokenRes = await request(app).post('/oauth2/token').type('form').send({ grant_type: 'authorization_code', code: loginRes.body.code, code_verifier: 'xyz', }); expect(tokenRes.status).toBe(200); expect(tokenRes.body.access_token).toBeDefined(); const otherToken = tokenRes.body.access_token; const res = await request(app) .post('/fhir/R4/Patient') .auth(accessToken, { type: 'bearer' }) .send({ resourceType: 'Patient' }); expect(res.status).toBe(201); const res2 = await request(app) .post('/fhir/R4/Patient') .auth(otherToken, { type: 'bearer' }) .send({ resourceType: 'Patient' }); expect(res2.status).toBe(429); }); });

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