Files
supabase/apps/studio/components/interfaces/MCP/Secrets/useSecretRequest.test.ts
Gildas GarciaandAlaister Young 737b8595f2 Update API types (#50234)
## 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>
2026-09-11 12:17:49 +08:00

143 lines
4.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'))
})
})