Files
Alaister YoungandAlaister Young 995f6f65c7 [FE-4483] fix(studio): disable network bans for v3 projects (#50997)
Disables network bans for v3 (`AWS_K8S`) projects and shows a specific
unsupported notice. The shared banned-IP query waits for project details
and skips unsupported projects, covering both Database Settings and
Advisor for v3 and High Availability projects.

The hook returns the standard query result and uses `skipToken` to
prevent unsupported requests, including manual refetches. Database
Settings handles project-detail errors at the call site. Open unban
confirmations are cleared when the section becomes disabled, and
submission checks eligibility.

Addresses
[FE-4483](https://linear.app/supabase/issue/FE-4483/disable-network-bans-for-v3-aws-k8s-projects).

## To test

- Open Database Settings on a v3 project. Check that Network bans shows
the v3 notice, hides the IP list and unban controls, and makes no
network-bans retrieval request on initial load or reload, including
while Advisor is mounted.
- Check that an HA project still shows its existing notice and makes no
network-bans retrieval request on initial load or reload.
- Navigate from a supported project to a v3 or HA project and check that
no banned-IP request is sent for the unsupported project and no
banned-IP signals from the previous project appear in Advisor.
- If project details fail without cached data, check that Network bans
shows an error after retries finish and does not retrieve bans. A
successful retry should restore normal behavior.
- Open an unban confirmation on a supported project, then navigate to a
v3 or HA project. Check that the dialog closes without sending an unban
request and stays closed when returning. A newly opened confirmation
should still work.
- On a supported project, check the empty state and banned IP list.
Confirm that users with permission can unban an IP and users without
permission see a disabled button with the permissions tooltip.

Validation: 17 focused tests passed, covering automatic and manual
request suppression, project-detail error display and recovery, and
navigation between supported and unsupported projects. Changed-file
ESLint, Prettier, and full Studio typecheck (without the incremental
cache) passed. Earlier local browser checks on `9912c6c` confirmed no
retrieval requests for an HA project on AWS_K8S across reloads and
Advisor, and a successful empty state on a supported project. The local
failed-project case redirected to the organization after retries, so the
inline error remains verified by the component test only. The latest
preview, standalone v3 notice, populated bans/unban, and no-permission
tooltip still need browser verification.

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

* **Bug Fixes**
* Banned IP settings now show an unsupported-project notice for AWS
Kubernetes projects and hide ban lists and unban actions for AWS
Kubernetes and High Availability projects.
* Banned IP data loads only after project details are available and only
for supported projects; unsupported projects do not display cached ban
data.
  * Project-detail errors are shown separately from ban-list errors.
* Unban confirmations close when a project becomes unsupported or an
unban succeeds. Unbanning is unavailable when you lack update permission
or the project is unsupported.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-09-29 17:51:14 +00:00

135 lines
4.8 KiB
TypeScript

import { QueryClient } from '@tanstack/react-query'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { mockAnimationsApi } from 'jsdom-testing-mocks'
import { HttpResponse } from 'msw'
import { beforeEach, expect, test, vi } from 'vitest'
import { BannedIPs } from './BannedIPs'
import type { deleteBannedIPs } from '@/data/banned-ips/banned-ips-delete-mutations'
import type { IPData } from '@/data/banned-ips/banned-ips-query'
import type { ProjectDetail } from '@/data/projects/project-detail-query'
import { customRender } from '@/tests/lib/custom-render'
import { addAPIMock, type APIErrorBody } from '@/tests/lib/msw'
mockAnimationsApi()
const routeParams = vi.hoisted(() => ({ ref: 'default' }))
beforeEach(() => {
routeParams.ref = 'default'
})
vi.mock('common', async (importOriginal) => ({
...(await importOriginal<typeof import('common')>()),
IS_PLATFORM: true,
useParams: () => routeParams,
}))
vi.mock('@/lib/constants', async (importOriginal) => ({
...(await importOriginal<typeof import('@/lib/constants')>()),
IS_PLATFORM: true,
}))
vi.mock('@/hooks/misc/useCheckPermissions', () => ({
useAsyncCheckPermissions: () => ({ can: true }),
}))
vi.mock('@/lib/telemetry/track', () => ({ useTrack: () => vi.fn() }))
test('shows a project-details error instead of leaving Network bans loading', async () => {
addAPIMock({
method: 'get',
path: '/platform/projects/:ref',
response: () =>
HttpResponse.json<APIErrorBody>({ message: 'Project unavailable' }, { status: 500 }),
})
customRender(<BannedIPs />)
expect(await screen.findByText('Failed to retrieve project details')).toBeVisible()
expect(screen.getByText('Error: Project unavailable')).toBeVisible()
expect(screen.getByRole('link', { name: 'Contact support' })).toHaveAttribute(
'href',
expect.stringContaining('projectRef=default')
)
expect(screen.queryByRole('button', { name: 'Unban IP' })).not.toBeInTheDocument()
expect(
screen.queryByText('There are no banned IP addresses for your project')
).not.toBeInTheDocument()
})
const PROJECT: ProjectDetail = {
cloud_provider: 'AWS',
connectionString: 'postgresql://postgres:password@db.default.supabase.co:5432/postgres',
db_host: 'db.default.supabase.co',
dbVersion: 'supabase-postgres-15.1.0',
high_availability: false,
id: 1,
infra_compute_size: 'micro',
inserted_at: '2026-01-01T00:00:00.000Z',
integration_source: null,
is_branch_enabled: false,
is_physical_backups_enabled: false,
name: 'Test project',
organization_id: 1,
ref: 'default',
region: 'us-east-1',
restUrl: 'https://default.supabase.co',
status: 'ACTIVE_HEALTHY',
subscription_id: 'subscription-1',
updated_at: '2026-01-01T00:00:00.000Z',
}
test.each([
{ name: 'v3', cloud_provider: 'AWS_K8S', high_availability: false },
{ name: 'HA', cloud_provider: 'AWS', high_availability: true },
])('clears an open unban confirmation when navigating to $name', async (unsupported) => {
let bannedIPs: IPData = { banned_ipv4_addresses: ['203.0.113.10'] }
const unbanRequests: unknown[] = []
addAPIMock({
method: 'get',
path: '/platform/projects/:ref',
response: ({ params }) =>
HttpResponse.json<ProjectDetail>(
params.ref === 'default' ? PROJECT : { ...PROJECT, ...unsupported, ref: String(params.ref) }
),
})
addAPIMock({
method: 'post',
path: '/v1/projects/:ref/network-bans/retrieve',
response: () => HttpResponse.json<IPData>(bannedIPs),
})
addAPIMock({
method: 'delete',
path: '/v1/projects/:ref/network-bans',
response: async ({ request }) => {
unbanRequests.push(await request.json())
bannedIPs = { banned_ipv4_addresses: [] }
return HttpResponse.json<Awaited<ReturnType<typeof deleteBannedIPs>>>(null)
},
})
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
const { rerender } = customRender(<BannedIPs />, { queryClient })
fireEvent.click(await screen.findByRole('button', { name: 'Unban IP' }))
expect(await screen.findByRole('dialog', { name: 'Confirm Unban IP' })).toBeVisible()
routeParams.ref = 'unsupported'
rerender(<BannedIPs />)
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
expect(screen.queryByRole('button', { name: 'Unban IP' })).not.toBeInTheDocument()
expect(unbanRequests).toEqual([])
routeParams.ref = 'default'
rerender(<BannedIPs />)
const unbanButton = await screen.findByRole('button', { name: 'Unban IP' })
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
expect(unbanRequests).toEqual([])
fireEvent.click(unbanButton)
fireEvent.click(await screen.findByRole('button', { name: 'Confirm Unban' }))
expect(await screen.findByText('There are no banned IP addresses for your project')).toBeVisible()
expect(unbanRequests).toEqual([{ ipv4_addresses: ['203.0.113.10'] }])
})