diff --git a/apps/studio/components/interfaces/Realtime/RealtimeSettings.test.tsx b/apps/studio/components/interfaces/Realtime/RealtimeSettings.test.tsx index 67512a3806f..e817c0a0a0c 100644 --- a/apps/studio/components/interfaces/Realtime/RealtimeSettings.test.tsx +++ b/apps/studio/components/interfaces/Realtime/RealtimeSettings.test.tsx @@ -68,19 +68,28 @@ const REALTIME_CONFIG = { suspend: false, } as const satisfies RealtimeConfigurationData -const REALTIME_ENTITLEMENTS: Entitlement[] = ( - [ - ['realtime.max_concurrent_users', 50_000], - ['realtime.max_events_per_second', 50_000], - ['realtime.max_presence_events_per_second', 5_000], - ['realtime.max_payload_size_in_kb', 3_000], - ] satisfies [FeatureKey, number][] -).map(([key, value]) => ({ - config: { enabled: true, unit: '', unlimited: false, value }, - feature: { key, type: 'numeric' }, - hasAccess: true, - type: 'numeric', -})) +const buildRealtimeEntitlements = ( + overrides: Partial> = {} +): Entitlement[] => + ( + [ + ['realtime.max_concurrent_users', 50_000], + ['realtime.max_events_per_second', 50_000], + ['realtime.max_presence_events_per_second', 5_000], + ['realtime.max_payload_size_in_kb', 3_000], + ] satisfies [FeatureKey, number][] + ).map(([key, value]) => { + const override = overrides[key] + const unlimited = override === 'unlimited' + return { + config: { enabled: true, unit: '', unlimited, value: unlimited ? 0 : (override ?? value) }, + feature: { key, type: 'numeric' }, + hasAccess: true, + type: 'numeric', + } + }) + +const REALTIME_ENTITLEMENTS: Entitlement[] = buildRealtimeEntitlements() describe('RealtimeSettings', () => { beforeEach(() => { @@ -199,4 +208,65 @@ describe('RealtimeSettings', () => { expect(requests).toHaveLength(0) } }) + + test.each([ + { value: '300000', accepted: true }, + { value: '300001', accepted: false }, + ])( + '$accepted for max concurrent clients of $value when the entitlement is unlimited', + async ({ value, accepted }) => { + addAPIMock({ + method: 'get', + path: '/platform/organizations/:slug/entitlements', + response: { + entitlements: buildRealtimeEntitlements({ 'realtime.max_concurrent_users': 'unlimited' }), + }, + }) + + const requests: unknown[] = [] + addAPIMock({ + method: 'patch', + path: '/platform/projects/:ref/config/realtime', + response: async ({ request }) => { + requests.push(await request.json()) + return new HttpResponse(null, { status: 204 }) + }, + }) + + customRender() + + const maxConcurrentUsersInput = await screen.findByLabelText('Max concurrent clients') + await userEvent.clear(maxConcurrentUsersInput) + await userEvent.type(maxConcurrentUsersInput, value) + + fireEvent.click(screen.getByRole('button', { name: 'Save changes' })) + + if (accepted) { + const dialog = await screen.findByRole('dialog') + fireEvent.click(within(dialog).getByRole('button', { name: 'Save changes' })) + + await waitFor(() => expect(requests).toHaveLength(1)) + expect(requests[0]).toMatchObject({ max_concurrent_users: Number(value) }) + } else { + expect( + await screen.findByText('Cannot exceed 300,000 concurrent clients') + ).toBeInTheDocument() + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + expect(requests).toHaveLength(0) + } + } + ) + + test('rejects max concurrent clients above the entitlement when it is below the soft limit', async () => { + customRender() + + const maxConcurrentUsersInput = await screen.findByLabelText('Max concurrent clients') + await userEvent.clear(maxConcurrentUsersInput) + await userEvent.type(maxConcurrentUsersInput, '50001') + + fireEvent.click(screen.getByRole('button', { name: 'Save changes' })) + + expect(await screen.findByText('Cannot exceed 50,000 concurrent clients')).toBeInTheDocument() + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + }) }) diff --git a/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx b/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx index 6588bf7c533..8c45d457066 100644 --- a/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx +++ b/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx @@ -44,7 +44,7 @@ import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' const formId = 'realtime-configuration-form' const REALTIME_SOFT_LIMITS = { - max_concurrent_users: 50_000, + max_concurrent_users: 300_000, max_events_per_second: 50_000, max_presence_events_per_second: 5_000, max_payload_size_in_kb: 3_000,