mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 13:19:09 +03:00
## Context PR here mainly breaks up the files under `ConnectSheet` to separate the functional logic so that we can write unit tests. No behavior changes intended beyond the bug fixes ## Changes involved - **Test organization:** moved all root-level `ConnectSheet` test files into `ConnectSheet/__tests__/` for consistency with other parts of the codebase that use this convention. - **Bug fix:** read replica label had a stray `}` / missing `)`, rendering as e.g. `Read Replica (us-east-1 - abc123})` instead of `Read Replica (us-east-1 - abc123)`. - **`ConnectSheet.tsx`:** extracted the "hydrate sheet state on open" `useEffect` logic (mode/field/URL param resolution from URL vs. localStorage) into a new `ConnectSheet.utils.ts`, with unit tests - **`useConnectServerEnv.ts`:** fixed two race conditions in the secret reveal/hide flow: - `toggle()` and `getValue()` could each fire a separate reveal request if triggered close together — now deduped to share one in-flight request. - `getValue()` could hide a secret that had just been explicitly revealed by a concurrent `toggle()`, due to reading a stale closure value — now reads the live state via `useLatest`. - Also stopped swallowing the original error on reveal failure (now attached via `cause`). - Added tests for the above, plus the 10s auto-hide timer (previously untested). - **`ConnectStepsSection.tsx`:** extracted `resolveContentPath` and the three inline "show notice" booleans (IPv4 addon, session pooler, self-hosted MCP) into `ConnectStepsSection.utils.ts`, matching the existing pattern for the Data API notice. Added unit tests for all of them. ## To test - [ ] Just a basic smoke test of the Connect sheet should do <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved connect setup hydration so saved preferences and URL values are applied more consistently when opening the sheet, including automatic URL backfilling where needed. * Refreshed connection guidance notices (IPv4 add-on, session pooler, and self-hosted MCP) with more consistent logic. * **Bug Fixes** * Fixed secret reveal behavior to keep concurrent reveal actions in sync, handle failures more safely, and ensure auto-hide works reliably. * Corrected the read-replica option label formatting. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
165 lines
5.0 KiB
TypeScript
165 lines
5.0 KiB
TypeScript
import { act, renderHook } from '@testing-library/react'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { useConnectServerEnv } from '../useConnectServerEnv'
|
|
import { useRevealedSecret } from '@/components/interfaces/APIKeys/useRevealedSecret'
|
|
import { useAPIKeys } from '@/data/api-keys/api-keys-query'
|
|
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
|
|
vi.mock('@/hooks/misc/useCheckPermissions', () => ({
|
|
useAsyncCheckPermissions: vi.fn(),
|
|
}))
|
|
vi.mock('@/data/config/project-endpoint-query', () => ({
|
|
useProjectApiUrl: vi.fn(),
|
|
}))
|
|
vi.mock('@/data/api-keys/api-keys-query', () => ({
|
|
useAPIKeys: vi.fn(),
|
|
}))
|
|
vi.mock('@/components/interfaces/APIKeys/useRevealedSecret', () => ({
|
|
useRevealedSecret: vi.fn(),
|
|
}))
|
|
|
|
const SECRET_KEY = { id: 'secret-id', api_key: 'sb_secret_abcdefghijklmnopqrstuv' }
|
|
const SECRET_FULL = 'sb_secret_abcdefghijklmnopqrstuv_full'
|
|
|
|
function deferred<T>() {
|
|
let resolve!: (value: T) => void
|
|
let reject!: (reason?: unknown) => void
|
|
const promise = new Promise<T>((res, rej) => {
|
|
resolve = res
|
|
reject = rej
|
|
})
|
|
return { promise, resolve, reject }
|
|
}
|
|
|
|
describe('useConnectServerEnv secret reveal', () => {
|
|
const clearMock = vi.fn()
|
|
let revealMock: ReturnType<typeof vi.fn>
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(useAsyncCheckPermissions).mockReturnValue({
|
|
can: true,
|
|
isLoading: false,
|
|
} as any)
|
|
vi.mocked(useProjectApiUrl).mockReturnValue({
|
|
data: 'https://test.supabase.co',
|
|
isPending: false,
|
|
} as any)
|
|
vi.mocked(useAPIKeys).mockReturnValue({
|
|
data: { secretKey: SECRET_KEY, publishableKey: { api_key: 'sb_publishable_x' } },
|
|
isLoading: false,
|
|
} as any)
|
|
|
|
clearMock.mockClear()
|
|
revealMock = vi.fn()
|
|
vi.mocked(useRevealedSecret).mockReturnValue({
|
|
data: undefined,
|
|
isLoading: false,
|
|
reveal: revealMock,
|
|
clear: clearMock,
|
|
} as any)
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('shares one in-flight reveal request between a concurrent toggle() and getValue()', async () => {
|
|
const { promise, resolve } = deferred<string>()
|
|
revealMock.mockReturnValue(promise)
|
|
|
|
const { result } = renderHook(() => useConnectServerEnv())
|
|
|
|
// A caller invokes getValue() first (e.g. "Copy secret") while isRevealed
|
|
// is still false...
|
|
let getValuePromise!: Promise<string>
|
|
act(() => {
|
|
getValuePromise = result.current.secret.getValue()
|
|
})
|
|
|
|
// ...then, before that reveal resolves, the user clicks "Reveal".
|
|
let togglePromise!: Promise<void>
|
|
act(() => {
|
|
togglePromise = result.current.secret.toggle()
|
|
})
|
|
|
|
// Only one underlying request should have been made.
|
|
expect(revealMock).toHaveBeenCalledTimes(1)
|
|
|
|
await act(async () => {
|
|
resolve(SECRET_FULL)
|
|
await Promise.all([getValuePromise, togglePromise])
|
|
})
|
|
|
|
expect(await getValuePromise).toBe(SECRET_FULL)
|
|
// getValue()'s closure was created while isRevealed was still false, but
|
|
// toggle() flipped it true before the reveal resolved. It must check the
|
|
// live state, not hide the secret the user just explicitly revealed.
|
|
expect(clearMock).not.toHaveBeenCalled()
|
|
expect(result.current.secret.isRevealed).toBe(true)
|
|
})
|
|
|
|
it('hides the secret again after a bare getValue() peek (no concurrent toggle)', async () => {
|
|
const { promise, resolve } = deferred<string>()
|
|
revealMock.mockReturnValue(promise)
|
|
|
|
const { result } = renderHook(() => useConnectServerEnv())
|
|
|
|
let getValuePromise!: Promise<string>
|
|
act(() => {
|
|
getValuePromise = result.current.secret.getValue()
|
|
})
|
|
|
|
await act(async () => {
|
|
resolve(SECRET_FULL)
|
|
await getValuePromise
|
|
})
|
|
|
|
expect(await getValuePromise).toBe(SECRET_FULL)
|
|
expect(clearMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('rejects toggle() with the original error attached as `cause`, and resets isRevealed', async () => {
|
|
const originalError = new Error('network down')
|
|
revealMock.mockRejectedValue(originalError)
|
|
|
|
const { result } = renderHook(() => useConnectServerEnv())
|
|
|
|
await expect(
|
|
act(async () => {
|
|
await result.current.secret.toggle()
|
|
})
|
|
).rejects.toMatchObject({
|
|
message: 'Failed to reveal secret API key',
|
|
cause: originalError,
|
|
})
|
|
|
|
expect(result.current.secret.isRevealed).toBe(false)
|
|
})
|
|
|
|
it('auto-hides the secret after the reveal window elapses', async () => {
|
|
vi.useFakeTimers()
|
|
vi.mocked(useRevealedSecret).mockReturnValue({
|
|
data: SECRET_FULL,
|
|
isLoading: false,
|
|
reveal: revealMock.mockResolvedValue(SECRET_FULL),
|
|
clear: clearMock,
|
|
} as any)
|
|
|
|
const { result } = renderHook(() => useConnectServerEnv())
|
|
|
|
await act(async () => {
|
|
await result.current.secret.toggle()
|
|
})
|
|
expect(result.current.secret.isRevealed).toBe(true)
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(10_000)
|
|
})
|
|
|
|
expect(result.current.secret.isRevealed).toBe(false)
|
|
expect(clearMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|