Files
supabase/apps/studio/hooks/misc/__tests__/useUserProjectRegions.test.ts
Charis d22702e907 refactor(studio): extract user project regions + emergency override hooks (#51033)
## Summary
- Extracts `useUserProjectRegions` (org + project region aggregation,
fail-open on fetch errors) and `useEmergencyIncidentOverride` (the
`ongoingIncident` flag / env var check) out of
`useStatusPageBannerVisibility`, so the upcoming incident.io status-page
banner can reuse both without duplicating the org/project fan-out logic.
- No behavior change for the legacy banner except one intentional fix:
`StatusPageBanner.utils.ts` compared the incident's affected regions
(lowercased) against the user's regions (not normalized), so a
mixed-case region on either side could silently fail to match. Both
sides now go through the same `normalizeRegion` helper.
- Part of the Linear FE-4057 stack (PR 5a of 6). Base branch is PR 4
(`charis/fe-4057-pr4-project-creation-status-admonition`), not master.

Linear: FE-4057

## Test plan
- [x] `pnpm --filter studio run typecheck`
- [x] `pnpm --filter studio run lint:ratchet`
- [x] `pnpm knip --workspace apps/studio`
- [x] `pnpm test:prettier`
- [x] `pnpm --filter studio exec vitest run` on the touched test files
and the `hooks/misc/` and `components/layouts/AppLayout/` directories
(all passing, no regressions)

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved status banner targeting by matching incidents against
normalized regions across the user’s projects.
* Updated banner visibility checks to handle incomplete project or
region data, including when project information fails to load.
* Improved loading behavior so the banner can be evaluated based on
user-region data rather than waiting for separate organization and
project requests.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-29 17:49:06 -04:00

150 lines
4.6 KiB
TypeScript

import { waitFor } from '@testing-library/react'
import { platformComponents as components } from 'api-types'
import { HttpResponse } from 'msw'
import { describe, expect, it } from 'vitest'
import { useUserProjectRegions } from '../useUserProjectRegions'
import { createMockOrganizationResponse } from '@/tests/helpers'
import { customRenderHook } from '@/tests/lib/custom-render'
import { addAPIMock, type APIErrorBody } from '@/tests/lib/msw'
import { createMockProfileContext } from '@/tests/lib/profile-helpers'
type OrganizationResponse = components['schemas']['OrganizationResponse_Output']
type OrganizationProjectsResponse = components['schemas']['OrganizationProjectsResponse_Output']
type OrgProject = OrganizationProjectsResponse['projects'][number]
type OrgDatabase = OrgProject['databases'][number]
function mockProject(
ref: string,
databases: Array<{ region: string; type: 'PRIMARY' | 'READ_REPLICA' }>
): OrgProject {
return {
cloud_provider: 'AWS',
databases: databases.map(
({ region, type }): OrgDatabase => ({
cloud_provider: 'AWS',
identifier: ref,
region,
status: 'ACTIVE_HEALTHY',
type,
})
),
inserted_at: new Date().toISOString(),
integration_source: null,
is_branch: false,
name: ref,
ref,
region: databases[0]?.region ?? 'us-east-1',
status: 'ACTIVE_HEALTHY',
}
}
describe('useUserProjectRegions', () => {
it('resolves normalized regions across orgs, primaries, and read replicas', async () => {
addAPIMock({
method: 'get',
path: '/platform/organizations',
response: () =>
HttpResponse.json<OrganizationResponse[]>([
createMockOrganizationResponse({ slug: 'org-a' }),
createMockOrganizationResponse({ slug: 'org-b' }),
]),
})
addAPIMock({
method: 'get',
path: '/platform/organizations/:slug/projects',
response: ({ params }) => {
if (params.slug === 'org-a') {
return HttpResponse.json<OrganizationProjectsResponse>({
pagination: { count: 1, limit: 100, offset: 0 },
projects: [
mockProject('project-a', [
{ region: 'US-East-1', type: 'PRIMARY' },
{ region: 'eu-west-1', type: 'READ_REPLICA' },
]),
],
})
}
return HttpResponse.json<OrganizationProjectsResponse>({
pagination: { count: 1, limit: 100, offset: 0 },
projects: [mockProject('project-b', [{ region: 'ap-southeast-1', type: 'PRIMARY' }])],
})
},
})
const { result } = customRenderHook(() => useUserProjectRegions({ enabled: true }), {
profileContext: createMockProfileContext(),
})
await waitFor(() =>
expect(result.current).toEqual({
status: 'resolved',
context: {
hasProjects: true,
regions: new Set(['us-east-1', 'eu-west-1', 'ap-southeast-1']),
isComplete: true,
},
})
)
})
it('marks the context incomplete when an org has more projects than the page returned', async () => {
addAPIMock({
method: 'get',
path: '/platform/organizations',
response: () =>
HttpResponse.json<OrganizationResponse[]>([
createMockOrganizationResponse({ slug: 'org-a' }),
]),
})
addAPIMock({
method: 'get',
path: '/platform/organizations/:slug/projects',
response: () =>
HttpResponse.json<OrganizationProjectsResponse>({
pagination: { count: 150, limit: 100, offset: 0 },
projects: [mockProject('project-a', [{ region: 'us-east-1', type: 'PRIMARY' }])],
}),
})
const { result } = customRenderHook(() => useUserProjectRegions({ enabled: true }), {
profileContext: createMockProfileContext(),
})
await waitFor(() =>
expect(result.current).toEqual({
status: 'resolved',
context: {
hasProjects: true,
regions: new Set(['us-east-1']),
isComplete: false,
},
})
)
})
it('fails open when the organizations query errors', async () => {
addAPIMock({
method: 'get',
path: '/platform/organizations',
response: () =>
HttpResponse.json<APIErrorBody>({ message: 'Boom from the backend' }, { status: 500 }),
})
const { result } = customRenderHook(() => useUserProjectRegions({ enabled: true }), {
profileContext: createMockProfileContext(),
})
await waitFor(() =>
expect(result.current).toEqual({
status: 'resolved',
context: {
hasProjects: true,
regions: new Set(),
isComplete: false,
},
})
)
})
})