mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
[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>
This commit is contained in:
1 parent
ed6217a169
commit
995f6f65c7
4 files changed
+470
-71
No files matched your search
@@ -0,0 +1,134 @@
|
||||
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'] }])
|
||||
})
|
||||
@@ -1,9 +1,10 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useParams } from 'common'
|
||||
import { Globe } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Badge, Card, CardContent, Skeleton } from 'ui'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import {
|
||||
PageSection,
|
||||
@@ -23,52 +24,56 @@ import { useBannedIPsQuery } from '@/data/banned-ips/banned-ips-query'
|
||||
import { useUserIPAddressQuery } from '@/data/misc/user-ip-address-query'
|
||||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||||
import { useHighAvailability } from '@/hooks/misc/useHighAvailability'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { useIsAwsK8sCloudProvider, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
|
||||
const HA_DISABLED_TITLE = 'Network bans unavailable on High Availability projects'
|
||||
const HA_DISABLED_DESCRIPTION =
|
||||
"We're working to bring network bans to High Availability projects. Contact support if this is blocking your work."
|
||||
const V3_DISABLED_TITLE = 'Network bans unavailable on v3 projects'
|
||||
|
||||
export const BannedIPs = () => {
|
||||
const { ref } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: project, error: projectError } = useSelectedProjectQuery()
|
||||
const { isHighAvailability } = useHighAvailability()
|
||||
const isAwsK8s = useIsAwsK8sCloudProvider()
|
||||
|
||||
const [selectedIPToUnban, setSelectedIPToUnban] = useState<string | null>(null) // Track the selected IP for unban
|
||||
const [selectedIPToUnban, setSelectedIPToUnban] = useState<string | null>(null)
|
||||
|
||||
const {
|
||||
isPending: isLoadingIPList,
|
||||
isFetching: isFetchingIPList,
|
||||
data: ipList,
|
||||
error: ipListError,
|
||||
} = useBannedIPsQuery({
|
||||
projectRef: ref,
|
||||
})
|
||||
} = useBannedIPsQuery({ projectRef: ref })
|
||||
|
||||
const { data: userIPAddress } = useUserIPAddressQuery()
|
||||
|
||||
const hasProjectError = !project && !!projectError
|
||||
const ipListLoading = isLoadingIPList || isFetchingIPList
|
||||
|
||||
const [showUnban, setShowUnban] = useState(false)
|
||||
const [confirmingIP, setConfirmingIP] = useState<string | null>(null) // Track the IP being confirmed for unban
|
||||
|
||||
const { can: canUnbanNetworks } = useAsyncCheckPermissions(PermissionAction.UPDATE, 'projects', {
|
||||
resource: {
|
||||
project_id: project?.id,
|
||||
},
|
||||
})
|
||||
|
||||
const isSectionDisabled = isHighAvailability || !canUnbanNetworks
|
||||
const sectionDisabledReason = isHighAvailability
|
||||
? HA_DISABLED_TITLE
|
||||
: 'You need additional permissions to unban networks'
|
||||
const isSectionDisabled = isHighAvailability || isAwsK8s || !canUnbanNetworks
|
||||
|
||||
if (isSectionDisabled && selectedIPToUnban !== null) {
|
||||
setSelectedIPToUnban(null)
|
||||
}
|
||||
|
||||
const sectionDisabledReason = useMemo(() => {
|
||||
if (isHighAvailability) return HA_DISABLED_TITLE
|
||||
if (isAwsK8s) return V3_DISABLED_TITLE
|
||||
return 'You need additional permissions to unban networks'
|
||||
}, [isHighAvailability, isAwsK8s])
|
||||
|
||||
const { mutate: unbanIPs, isPending: isUnbanning } = useBannedIPsDeleteMutation({
|
||||
onSuccess: () => {
|
||||
toast.success('IP address successfully unbanned')
|
||||
setSelectedIPToUnban(null) // Reset the selected IP for unban
|
||||
setShowUnban(false)
|
||||
setSelectedIPToUnban(null)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Failed to unban IP: ${error?.message}`)
|
||||
@@ -76,19 +81,13 @@ export const BannedIPs = () => {
|
||||
})
|
||||
|
||||
const onConfirmUnbanIP = () => {
|
||||
if (confirmingIP == null || !ref) return
|
||||
if (selectedIPToUnban === null || !ref || isSectionDisabled) return
|
||||
unbanIPs({
|
||||
projectRef: ref,
|
||||
ips: [confirmingIP], // Pass the IP as an array
|
||||
ips: [selectedIPToUnban], // Pass the IP as an array
|
||||
})
|
||||
}
|
||||
|
||||
const openConfirmationModal = (ip: string) => {
|
||||
setSelectedIPToUnban(ip) // Set the selected IP for unban
|
||||
setConfirmingIP(ip) // Set the IP being confirmed for unban
|
||||
setShowUnban(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection id="banned-ips">
|
||||
@@ -102,6 +101,13 @@ export const BannedIPs = () => {
|
||||
<DocsButton href={`${DOCS_URL}/reference/cli/supabase-network-bans`} />
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
{hasProjectError && (
|
||||
<AlertError
|
||||
error={projectError}
|
||||
subject="Failed to retrieve project details"
|
||||
projectRef={ref}
|
||||
/>
|
||||
)}
|
||||
{isHighAvailability && (
|
||||
<div className="mb-4">
|
||||
<HighAvailabilityDisabledSectionNotice
|
||||
@@ -110,47 +116,63 @@ export const BannedIPs = () => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isHighAvailability &&
|
||||
(ipListLoading ? (
|
||||
<Card>
|
||||
<CardContent className="space-y-4">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : ipListError ? (
|
||||
<AlertError error={ipListError} subject="Failed to retrieve banned IP addresses" />
|
||||
) : ipList.banned_ipv4_addresses.length > 0 ? (
|
||||
<Card>
|
||||
{ipList.banned_ipv4_addresses.map((ip) => (
|
||||
<CardContent key={ip} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-5">
|
||||
<Globe size={16} className="text-foreground-lighter" />
|
||||
<p className="text-sm font-mono">{ip}</p>
|
||||
{ip === userIPAddress && <Badge>Your IP address</Badge>}
|
||||
</div>
|
||||
<ButtonTooltip
|
||||
disabled={isSectionDisabled}
|
||||
onClick={() => openConfirmationModal(ip)}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: isSectionDisabled ? sectionDisabledReason : undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Unban IP
|
||||
</ButtonTooltip>
|
||||
{!isHighAvailability && isAwsK8s && (
|
||||
<Admonition
|
||||
type="default"
|
||||
title={V3_DISABLED_TITLE}
|
||||
description="Fail2Ban is not supported on v3 projects."
|
||||
/>
|
||||
)}
|
||||
{!hasProjectError && !isHighAvailability && !isAwsK8s && (
|
||||
<>
|
||||
{ipListError && (
|
||||
<AlertError
|
||||
error={ipListError}
|
||||
subject="Failed to retrieve banned IP addresses"
|
||||
projectRef={ref}
|
||||
/>
|
||||
)}
|
||||
{!ipListError && ipListLoading && (
|
||||
<Card>
|
||||
<CardContent className="space-y-4">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
</CardContent>
|
||||
))}
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="text-foreground text-sm">
|
||||
There are no banned IP addresses for your project
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
{!ipListError && !ipListLoading && ipList && (
|
||||
<Card>
|
||||
{ipList.banned_ipv4_addresses.length > 0 ? (
|
||||
ipList.banned_ipv4_addresses.map((ip) => (
|
||||
<CardContent key={ip} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-5">
|
||||
<Globe size={16} className="text-foreground-lighter" />
|
||||
<p className="text-sm font-mono">{ip}</p>
|
||||
{ip === userIPAddress && <Badge>Your IP address</Badge>}
|
||||
</div>
|
||||
<ButtonTooltip
|
||||
disabled={isSectionDisabled}
|
||||
onClick={() => setSelectedIPToUnban(ip)}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: isSectionDisabled ? sectionDisabledReason : undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Unban IP
|
||||
</ButtonTooltip>
|
||||
</CardContent>
|
||||
))
|
||||
) : (
|
||||
<CardContent className="text-foreground text-sm">
|
||||
There are no banned IP addresses for your project
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
|
||||
@@ -158,11 +180,11 @@ export const BannedIPs = () => {
|
||||
variant="destructive"
|
||||
size="medium"
|
||||
loading={isUnbanning}
|
||||
visible={showUnban}
|
||||
visible={selectedIPToUnban !== null && !isSectionDisabled}
|
||||
title="Confirm Unban IP"
|
||||
confirmLabel="Confirm Unban"
|
||||
confirmLabelLoading="Unbanning..."
|
||||
onCancel={() => setShowUnban(false)}
|
||||
onCancel={() => setSelectedIPToUnban(null)}
|
||||
onConfirm={onConfirmUnbanIP}
|
||||
alert={{
|
||||
title: 'This action cannot be undone',
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
import { act, waitFor } from '@testing-library/react'
|
||||
import { HttpResponse } from 'msw'
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { useBannedIPsQuery, type IPData } from './banned-ips-query'
|
||||
import { useAdvisorSignals } from '@/components/ui/AdvisorPanel/useAdvisorSignals'
|
||||
import { useProjectDetailQuery, type ProjectDetail } from '@/data/projects/project-detail-query'
|
||||
import { customRenderHook } from '@/tests/lib/custom-render'
|
||||
import { addAPIMock, type APIErrorBody } from '@/tests/lib/msw'
|
||||
|
||||
vi.mock('common', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('common')>()),
|
||||
IS_PLATFORM: true,
|
||||
}))
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
const BANNED_IPS: IPData = { banned_ipv4_addresses: ['203.0.113.10'] }
|
||||
|
||||
const mockBannedIPs = () => {
|
||||
const requests: string[] = []
|
||||
addAPIMock({
|
||||
method: 'post',
|
||||
path: '/v1/projects/:ref/network-bans/retrieve',
|
||||
response: ({ request }) => {
|
||||
requests.push(request.url)
|
||||
return HttpResponse.json<IPData>(BANNED_IPS)
|
||||
},
|
||||
})
|
||||
return requests
|
||||
}
|
||||
|
||||
describe('useBannedIPsQuery', () => {
|
||||
test.each([
|
||||
{ name: 'v3', cloud_provider: 'AWS_K8S', high_availability: false },
|
||||
{ name: 'HA', cloud_provider: 'AWS', high_availability: true },
|
||||
{ name: 'v3 HA', cloud_provider: 'AWS_K8S', high_availability: true },
|
||||
])('does not retrieve bans for $name with Settings and Advisor enabled', async (project) => {
|
||||
addAPIMock({
|
||||
method: 'get',
|
||||
path: '/platform/projects/:ref',
|
||||
response: () => HttpResponse.json<ProjectDetail>({ ...PROJECT, ...project }),
|
||||
})
|
||||
const requests = mockBannedIPs()
|
||||
const { result } = customRenderHook(() => ({
|
||||
project: useProjectDetailQuery({ ref: 'default' }),
|
||||
settings: useBannedIPsQuery({ projectRef: 'default' }),
|
||||
advisor: useAdvisorSignals({ projectRef: 'default' }),
|
||||
}))
|
||||
|
||||
await waitFor(() => expect(result.current.project.isSuccess).toBe(true))
|
||||
expect(result.current.settings.fetchStatus).toBe('idle')
|
||||
expect(result.current.advisor.data).toEqual([])
|
||||
expect(requests).toEqual([])
|
||||
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
try {
|
||||
await act(async () => {
|
||||
await expect(result.current.settings.refetch({ throwOnError: true })).rejects.toThrow()
|
||||
})
|
||||
expect(requests).toEqual([])
|
||||
} finally {
|
||||
consoleError.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test('shares banned IPs with Advisor on a supported project', async () => {
|
||||
addAPIMock({
|
||||
method: 'get',
|
||||
path: '/platform/projects/:ref',
|
||||
response: () => HttpResponse.json<ProjectDetail>(PROJECT),
|
||||
})
|
||||
const requests = mockBannedIPs()
|
||||
const { result } = customRenderHook(() => ({
|
||||
settings: useBannedIPsQuery({ projectRef: 'default' }),
|
||||
advisor: useAdvisorSignals({ projectRef: 'default' }),
|
||||
}))
|
||||
|
||||
await waitFor(() => expect(result.current.advisor.data).toHaveLength(1))
|
||||
expect(result.current.settings.data).toEqual(BANNED_IPS)
|
||||
expect(requests).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('waits for project details before retrieving bans', async () => {
|
||||
const { promise, resolve } = Promise.withResolvers<void>()
|
||||
let hasRequestedProject = false
|
||||
addAPIMock({
|
||||
method: 'get',
|
||||
path: '/platform/projects/:ref',
|
||||
response: async () => {
|
||||
hasRequestedProject = true
|
||||
await promise
|
||||
return HttpResponse.json<ProjectDetail>(PROJECT)
|
||||
},
|
||||
})
|
||||
const requests = mockBannedIPs()
|
||||
const { result } = customRenderHook(() => useBannedIPsQuery({ projectRef: 'default' }))
|
||||
|
||||
try {
|
||||
await waitFor(() => expect(hasRequestedProject).toBe(true))
|
||||
expect(result.current.fetchStatus).toBe('idle')
|
||||
expect(requests).toEqual([])
|
||||
} finally {
|
||||
resolve()
|
||||
}
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(requests).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('waits for project details to recover before retrieving bans', async () => {
|
||||
addAPIMock({
|
||||
method: 'get',
|
||||
path: '/platform/projects/:ref',
|
||||
response: () =>
|
||||
HttpResponse.json<APIErrorBody>({ message: 'Project unavailable' }, { status: 500 }),
|
||||
})
|
||||
const requests = mockBannedIPs()
|
||||
const { result } = customRenderHook(() => ({
|
||||
project: useProjectDetailQuery({ ref: 'default' }),
|
||||
bans: useBannedIPsQuery({ projectRef: 'default' }),
|
||||
}))
|
||||
|
||||
await waitFor(() => expect(result.current.project.isError).toBe(true))
|
||||
expect(result.current.bans.fetchStatus).toBe('idle')
|
||||
expect(requests).toEqual([])
|
||||
|
||||
addAPIMock({
|
||||
method: 'get',
|
||||
path: '/platform/projects/:ref',
|
||||
response: () => HttpResponse.json<ProjectDetail>(PROJECT),
|
||||
})
|
||||
await act(() => result.current.project.refetch())
|
||||
|
||||
await waitFor(() => expect(result.current.bans.isSuccess).toBe(true))
|
||||
expect(result.current.bans.data).toEqual(BANNED_IPS)
|
||||
expect(requests).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('keeps bans visible when project details fail to refresh with cached supported data', async () => {
|
||||
addAPIMock({
|
||||
method: 'get',
|
||||
path: '/platform/projects/:ref',
|
||||
response: () => HttpResponse.json<ProjectDetail>(PROJECT),
|
||||
})
|
||||
mockBannedIPs()
|
||||
const { result } = customRenderHook(() => ({
|
||||
project: useProjectDetailQuery({ ref: 'default' }),
|
||||
bans: useBannedIPsQuery({ projectRef: 'default' }),
|
||||
advisor: useAdvisorSignals({ projectRef: 'default' }),
|
||||
}))
|
||||
|
||||
await waitFor(() => expect(result.current.advisor.data).toHaveLength(1))
|
||||
addAPIMock({
|
||||
method: 'get',
|
||||
path: '/platform/projects/:ref',
|
||||
response: () =>
|
||||
HttpResponse.json<APIErrorBody>({ message: 'Project unavailable' }, { status: 500 }),
|
||||
})
|
||||
await act(async () => {
|
||||
const projectResult = await result.current.project.refetch()
|
||||
expect(projectResult.isError).toBe(true)
|
||||
})
|
||||
expect(result.current.bans.data).toEqual(BANNED_IPS)
|
||||
expect(result.current.advisor.data).toHaveLength(1)
|
||||
})
|
||||
|
||||
test.each([
|
||||
{ projectRef: 'default', enabled: false },
|
||||
{ projectRef: undefined, enabled: true },
|
||||
])('does not fetch with $projectRef and enabled=$enabled', ({ projectRef, enabled }) => {
|
||||
const { result } = customRenderHook(() => useBannedIPsQuery({ projectRef }, { enabled }))
|
||||
|
||||
expect(result.current.fetchStatus).toBe('idle')
|
||||
})
|
||||
|
||||
test('checks the requested project when navigating from supported to v3', async () => {
|
||||
addAPIMock({
|
||||
method: 'get',
|
||||
path: '/platform/projects/:ref',
|
||||
response: ({ params }) =>
|
||||
HttpResponse.json<ProjectDetail>({
|
||||
...PROJECT,
|
||||
ref: String(params.ref),
|
||||
cloud_provider: params.ref === 'v3' ? 'AWS_K8S' : 'AWS',
|
||||
}),
|
||||
})
|
||||
const requests = mockBannedIPs()
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
let projectRef = 'default'
|
||||
const { result, rerender } = customRenderHook(
|
||||
() => ({
|
||||
project: useProjectDetailQuery({ ref: projectRef }),
|
||||
bans: useBannedIPsQuery({ projectRef }),
|
||||
advisor: useAdvisorSignals({ projectRef }),
|
||||
}),
|
||||
{ queryClient }
|
||||
)
|
||||
|
||||
await waitFor(() => expect(result.current.bans.isSuccess).toBe(true))
|
||||
act(() => {
|
||||
projectRef = 'v3'
|
||||
rerender()
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.project.data?.ref).toBe('v3'))
|
||||
expect(result.current.bans.fetchStatus).toBe('idle')
|
||||
expect(result.current.advisor.data).toEqual([])
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(requests[0]).toContain('/projects/default/network-bans/retrieve')
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { skipToken, useQuery } from '@tanstack/react-query'
|
||||
import { IS_PLATFORM } from 'common'
|
||||
|
||||
import { BannedIPKeys } from './keys'
|
||||
import { handleError, post } from '@/data/fetchers'
|
||||
import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
|
||||
import { PROVIDERS } from '@/lib/constants'
|
||||
import type { ResponseError, UseCustomQueryOptions } from '@/types'
|
||||
|
||||
type BannedIPVariables = { projectRef?: string }
|
||||
@@ -25,13 +27,21 @@ export type IPError = ResponseError
|
||||
export const useBannedIPsQuery = <TData = IPData>(
|
||||
{ projectRef }: BannedIPVariables,
|
||||
{ enabled = true, ...options }: UseCustomQueryOptions<IPData, IPError, TData> = {}
|
||||
) =>
|
||||
useQuery<IPData, IPError, TData>({
|
||||
) => {
|
||||
const { data: project } = useProjectDetailQuery(
|
||||
{ ref: projectRef },
|
||||
{ enabled: enabled && IS_PLATFORM }
|
||||
)
|
||||
const isSupported =
|
||||
!!project && !project.high_availability && project.cloud_provider !== PROVIDERS.AWS_K8S.id
|
||||
|
||||
return useQuery<IPData, IPError, TData>({
|
||||
queryKey: BannedIPKeys.list(projectRef),
|
||||
queryFn: ({ signal }) => getBannedIPs({ projectRef }, signal),
|
||||
enabled: enabled && IS_PLATFORM && typeof projectRef !== 'undefined',
|
||||
queryFn: isSupported ? ({ signal }) => getBannedIPs({ projectRef }, signal) : skipToken,
|
||||
enabled: enabled && IS_PLATFORM && typeof projectRef !== 'undefined' && isSupported,
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 60_000,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user