Files
supabase/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.test.tsx
Alaister Young 3811e75140 [MUL-1417] feat(studio): show SSL enforcement as always on for HA projects (#49524)
High Availability (Multigres) projects always enforce SSL, and the
management API now rejects any attempt to read or change the setting
(supabase/platform#37484). This makes the Database Settings toggle
reflect that instead of surfacing an error.

**Changed:**
- `SSLConfiguration`: skip the `ssl-enforcement` query for HA projects
(via `useHighAvailability`) and render the "Enforce SSL on incoming
connections" switch checked + disabled with the tooltip "SSL is always
enforced on High Availability projects". Non-HA projects are unchanged.
- `SSLEnforcementConfirmDialog`: add a controlled `open`/`onOpenChange`
mode. The switch now opens the dialog from its own `onCheckedChange`
rather than a wrapping `AlertDialogTrigger`, so a disabled switch can no
longer open the dialog by clicking the row wrapper beside it (this was
reachable for every disabled state, and for HA would have PUT into the
new 400 guardrail). The JIT section's existing trigger-with-children
usage is untouched.

**Added:**
- `SSLConfiguration.test.tsx` (MSW): HA → checked/disabled, tooltip, no
`ssl-enforcement` request, no dialog from switch/wrapper clicks; non-HA
→ reflects fetched config, switch opens the dialog and Cancel leaves it
unchanged.

## To test

On an HA project → Project Settings → Database → SSL configuration:
- Switch is on and disabled, hovering shows "SSL is always enforced on
High Availability projects"
- No request to `/v1/projects/{ref}/ssl-enforcement` fires, no spinner
sticks, no error toast
- Clicking the disabled switch or the empty area beside it does **not**
open the "brief downtime" dialog

On a non-HA project:
- Switch reflects the current config and the GET fires once
- Clicking the switch opens the confirm dialog with Enable/Disable SSL;
Cancel and Escape close it without changing the switch or sending a PUT
- Clicking beside the switch (not on it) does not open the dialog

Linear:
https://linear.app/supabase/issue/MUL-1417/database-settings-disable-ssl-enforcement-toggle

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- High Availability projects now show SSL as always enabled, with an
explanatory tooltip.
- SSL settings are protected from changes on High Availability projects.
- SSL confirmation dialogs now open and close reliably when changing
settings.
- Added accessible announcements for SSL configuration loading and
updates.

- **Bug Fixes**
- Improved SSL state handling for standard and High Availability
projects.
- Prevented unnecessary SSL enforcement checks for High Availability
projects.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-08-25 08:34:24 +00:00

173 lines
6.1 KiB
TypeScript

import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import type { components, paths } from 'api-types'
import { HttpResponse } from 'msw'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { SSLConfiguration } from './SSLConfiguration'
import { customRender } from '@/tests/lib/custom-render'
import { addAPIMock } from '@/tests/lib/msw'
type ProjectSettingsResponse = components['schemas']['ProjectSettingsResponse']
type SslEnforcementResponse = components['schemas']['SslEnforcementResponse']
type JitAccessConfigResponse =
paths['/v1/projects/{ref}/jit-access']['get']['responses'][200]['content']['application/json']
const { mockUseAsyncCheckPermissions, mockUseHighAvailability, mockUseSelectedProjectQuery } =
vi.hoisted(() => ({
mockUseAsyncCheckPermissions: vi.fn(),
mockUseHighAvailability: vi.fn(),
mockUseSelectedProjectQuery: vi.fn(),
}))
vi.mock('@/hooks/misc/useCheckPermissions', () => ({
useAsyncCheckPermissions: mockUseAsyncCheckPermissions,
}))
vi.mock('@/hooks/misc/useHighAvailability', () => ({
useHighAvailability: mockUseHighAvailability,
}))
vi.mock('@/hooks/misc/useSelectedProject', () => ({
useSelectedProjectQuery: mockUseSelectedProjectQuery,
}))
const HA_TOOLTIP = 'SSL is always enforced on High Availability projects'
function mockProjectSettings() {
addAPIMock({
method: 'get',
path: '/platform/projects/:ref/settings',
response: () =>
HttpResponse.json<ProjectSettingsResponse>({
app_config: {
db_schema: 'public',
endpoint: 'default.supabase.co',
storage_endpoint: 'storage.default.supabase.co',
},
cloud_provider: 'AWS',
db_dns_name: 'default.supabase.co',
db_host: 'default.supabase.co',
db_ip_addr_config: 'ipv4',
db_name: 'postgres',
db_port: 5432,
db_user: 'postgres',
inserted_at: '2025-02-16T22:24:42.115195',
name: 'default',
ref: 'default',
region: 'us-east-1',
ssl_enforced: true,
status: 'ACTIVE_HEALTHY',
}),
})
}
function mockJitDbAccess() {
addAPIMock({
method: 'get',
path: '/v1/projects/:ref/jit-access',
response: () =>
HttpResponse.json<JitAccessConfigResponse>({ state: 'disabled', appliedSuccessfully: true }),
})
}
/** Registers the SSL enforcement GET and returns a counter of how often it was hit. */
function mockSSLEnforcement(config: SslEnforcementResponse) {
const requests = { count: 0 }
addAPIMock({
method: 'get',
path: '/v1/projects/:ref/ssl-enforcement',
response: () => {
requests.count += 1
return HttpResponse.json<SslEnforcementResponse>(config)
},
})
return requests
}
describe('SSLConfiguration', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseSelectedProjectQuery.mockReturnValue({ data: { id: 1, ref: 'default' } })
mockUseAsyncCheckPermissions.mockReturnValue({ can: true })
mockProjectSettings()
mockJitDbAccess()
})
it('shows SSL enforcement as always on for High Availability projects', async () => {
mockUseHighAvailability.mockReturnValue({ isHighAvailability: true, isPending: false })
const sslEnforcementRequests = mockSSLEnforcement({
appliedSuccessfully: true,
currentConfig: { database: false },
})
customRender(<SSLConfiguration />)
const sslSwitch = await screen.findByRole('switch')
expect(sslSwitch).toBeChecked()
expect(sslSwitch).toBeDisabled()
// Nothing is loading for HA projects, so there is no announcement to make
expect(screen.getByRole('status')).toBeEmptyDOMElement()
// The tooltip trigger is the wrapper around the (disabled) switch
await userEvent.hover(sslSwitch.parentElement!)
expect((await screen.findAllByText(HA_TOOLTIP, {}, { timeout: 2000 })).length).toBeGreaterThan(
0
)
expect(sslEnforcementRequests.count).toBe(0)
})
it('does not open the confirm dialog around a disabled switch on High Availability projects', async () => {
mockUseHighAvailability.mockReturnValue({ isHighAvailability: true, isPending: false })
mockSSLEnforcement({ appliedSuccessfully: true, currentConfig: { database: false } })
customRender(<SSLConfiguration />)
const sslSwitch = await screen.findByRole('switch')
// Clicking the disabled switch and the row wrapper around it must not open the dialog
await userEvent.click(sslSwitch)
await userEvent.click(sslSwitch.parentElement!.parentElement!)
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
it('reflects the fetched SSL enforcement config for other projects', async () => {
mockUseHighAvailability.mockReturnValue({ isHighAvailability: false, isPending: false })
const sslEnforcementRequests = mockSSLEnforcement({
appliedSuccessfully: true,
currentConfig: { database: false },
})
customRender(<SSLConfiguration />)
const sslSwitch = await screen.findByRole('switch')
expect(sslSwitch).not.toBeChecked()
expect(sslSwitch).toBeEnabled()
expect(screen.getByRole('status')).toBeEmptyDOMElement()
expect(sslEnforcementRequests.count).toBe(1)
})
it('opens the confirm dialog from the switch and leaves it unchanged on cancel', async () => {
mockUseHighAvailability.mockReturnValue({ isHighAvailability: false, isPending: false })
mockSSLEnforcement({ appliedSuccessfully: true, currentConfig: { database: false } })
customRender(<SSLConfiguration />)
const sslSwitch = await screen.findByRole('switch')
await userEvent.click(sslSwitch)
const dialog = await screen.findByRole('alertdialog')
expect(dialog).toHaveTextContent('Updating SSL enforcement involves a brief downtime')
expect(screen.getByRole('button', { name: 'Enable SSL' })).toBeInTheDocument()
// Controlled switch must not flip while the dialog is open
expect(sslSwitch).not.toBeChecked()
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }))
await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument())
expect(sslSwitch).not.toBeChecked()
})
})