mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 09:59:03 +08:00
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>
173 lines
6.1 KiB
TypeScript
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()
|
|
})
|
|
})
|