mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## Problem platform, v1 and v2 have been already completely migrated and introduced some changes. Some types have been renamed, some outputs and inputs updated. ## Solution - Update the API types - Fix the TS errors ## Update Taking this over to unblock #50134, which needs the new scoped token permission ids from the regenerated types. - Merged `master`. - Regenerated `api-v2.d.ts` from the production spec. The previous files came from a local API that exposed a webhook events endpoint production doesn't have yet. Production has since added standardized 400 error responses on the v2 organization endpoints. `api-v1.d.ts` and `platform.d.ts` already matched production. - Fixed `verify-production-types`. It formatted the regenerated files in a temp directory outside the repository, so Prettier fell back to its defaults and the comparison could never match the committed files. It now passes the repository config explicitly. `pnpm api:verify-types` passes on this branch. - Verified locally: `pnpm typecheck`, `pnpm api:verify-types`, Studio unit tests. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Preserved descriptions when saving, sharing, moving, or unsharing notebooks, reports, SQL snippets, and saved queries. * Improved handling of empty or null values across notebook descriptions, billing usage, pooler settings, and infrastructure fields. * Improved read-replica connection handling, including read-only connection strings. * Updated storage configuration and capability handling to match current settings. * **API and Compatibility** * Updated organization, project, storage, OAuth, billing, and infrastructure data handling to match current API responses. * OAuth app creation and updates now require scopes. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
143 lines
4.6 KiB
TypeScript
143 lines
4.6 KiB
TypeScript
import { act, waitFor } from '@testing-library/react'
|
||
import { delay, HttpResponse } from 'msw'
|
||
import { beforeEach, describe, expect, it } from 'vitest'
|
||
|
||
import type { SecretsParams } from './McpSecrets.params'
|
||
import { useSecretRequest } from './useSecretRequest'
|
||
import type { components } from '@/data/api'
|
||
import { customRenderHook } from '@/tests/lib/custom-render'
|
||
import { addAPIMock, type APIErrorBody } from '@/tests/lib/msw'
|
||
|
||
const REF_A = 'aaaaaaaaaaaaaaaaaaaa'
|
||
const NAME_A = 'OPENAI_API_KEY'
|
||
const NAME_B = 'RESEND_API_KEY'
|
||
|
||
const paramsFor = (ref: string, name: string): SecretsParams => ({
|
||
ref,
|
||
name,
|
||
dev: { state: undefined },
|
||
})
|
||
|
||
const WRITE_DELAY_MS = 120
|
||
|
||
const PROJECT: components['schemas']['ProjectDetailResponse_Output'] = {
|
||
cloud_provider: 'AWS',
|
||
db_host: 'db.example.supabase.co',
|
||
high_availability: false,
|
||
id: 1,
|
||
inserted_at: '2026-01-01T00:00:00Z',
|
||
integration_source: null,
|
||
is_branch_enabled: false,
|
||
is_physical_backups_enabled: false,
|
||
name: 'acme-production',
|
||
organization_id: 1,
|
||
ref: REF_A,
|
||
region: 'us-east-1',
|
||
restUrl: 'https://example.supabase.co/rest/v1/',
|
||
status: 'ACTIVE_HEALTHY',
|
||
subscription_id: 'sub_1',
|
||
updated_at: '2026-01-01T00:00:00Z',
|
||
connectionString: '',
|
||
}
|
||
|
||
let writtenNames: string[] = []
|
||
|
||
beforeEach(() => {
|
||
writtenNames = []
|
||
|
||
addAPIMock({ method: 'get', path: '/platform/projects/:ref', response: PROJECT })
|
||
addAPIMock({ method: 'get', path: '/v1/projects/:ref/secrets', response: [] })
|
||
|
||
addAPIMock({
|
||
method: 'post',
|
||
path: '/v1/projects/:ref/secrets',
|
||
response: async ({ request }) => {
|
||
const body = (await request.json()) as { name: string }[]
|
||
writtenNames.push(...body.map((secret) => secret.name))
|
||
await delay(WRITE_DELAY_MS)
|
||
return HttpResponse.json([])
|
||
},
|
||
})
|
||
})
|
||
|
||
describe('useSecretRequest', () => {
|
||
it('reports a stored outcome for the request that was actually written', async () => {
|
||
const { result } = customRenderHook(() => useSecretRequest(paramsFor(REF_A, NAME_A)))
|
||
|
||
await waitFor(() => expect(result.current.state.status).toBe('form'))
|
||
|
||
act(() => result.current.saveSecret('sk-value'))
|
||
|
||
await waitFor(() => expect(result.current.state.status).toBe('stored'))
|
||
expect(writtenNames).toEqual([NAME_A])
|
||
})
|
||
|
||
it('does not offer the form when the existing secrets cannot be read', async () => {
|
||
// Without the secrets list there is no way to know an overwrite is about to
|
||
// happen, so the form would be missing its warning.
|
||
addAPIMock({
|
||
method: 'get',
|
||
path: '/v1/projects/:ref/secrets',
|
||
response: () => HttpResponse.json<APIErrorBody>({ message: 'nope' }, { status: 500 }),
|
||
})
|
||
|
||
const { result } = customRenderHook(() => useSecretRequest(paramsFor(REF_A, NAME_A)))
|
||
|
||
await waitFor(() => expect(result.current.state.status).toBe('error'))
|
||
expect(writtenNames).toEqual([])
|
||
})
|
||
|
||
it('does not attribute request A’s settled write to request B', async () => {
|
||
let params = paramsFor(REF_A, NAME_A)
|
||
const { result, rerender } = customRenderHook(() => useSecretRequest(params))
|
||
|
||
await waitFor(() => expect(result.current.state.status).toBe('form'))
|
||
|
||
act(() => result.current.saveSecret('sk-belongs-to-a'))
|
||
params = paramsFor(REF_A, NAME_B)
|
||
rerender()
|
||
|
||
await waitFor(() => {
|
||
const { state } = result.current
|
||
expect(state.status === 'form' && state.request.keyName).toBe(NAME_B)
|
||
})
|
||
|
||
await new Promise((resolve) => setTimeout(resolve, WRITE_DELAY_MS * 3))
|
||
|
||
expect(result.current.state.status).toBe('form')
|
||
expect(writtenNames).toEqual([NAME_A])
|
||
})
|
||
|
||
it('does not carry a cancellation from request A onto request B', async () => {
|
||
let params = paramsFor(REF_A, NAME_A)
|
||
const { result, rerender } = customRenderHook(() => useSecretRequest(params))
|
||
|
||
await waitFor(() => expect(result.current.state.status).toBe('form'))
|
||
|
||
act(() => result.current.cancelRequest())
|
||
expect(result.current.state.status).toBe('cancelled')
|
||
|
||
params = paramsFor(REF_A, NAME_B)
|
||
rerender()
|
||
|
||
await waitFor(() => expect(result.current.state.status).toBe('form'))
|
||
})
|
||
|
||
it('still shows A’s outcome if the user navigates back to A', async () => {
|
||
let params = paramsFor(REF_A, NAME_A)
|
||
const { result, rerender } = customRenderHook(() => useSecretRequest(params))
|
||
|
||
await waitFor(() => expect(result.current.state.status).toBe('form'))
|
||
|
||
act(() => result.current.cancelRequest())
|
||
params = paramsFor(REF_A, NAME_B)
|
||
rerender()
|
||
await waitFor(() => expect(result.current.state.status).toBe('form'))
|
||
|
||
params = paramsFor(REF_A, NAME_A)
|
||
rerender()
|
||
|
||
await waitFor(() => expect(result.current.state.status).toBe('cancelled'))
|
||
})
|
||
})
|