mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 13:49:09 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Initial build of the URL mode interstitial. Has complete mock data to be able to view different states (this will be stripped out in the end). A starting point for us to use as an intercept. This is linked with @barryroodt ticket in the MCP project. https://github.com/supabase/mcp/pull/412 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added an authenticated MCP Secrets page for securely storing API secrets requested by AI tools. - Added provider-aware guidance for OpenAI, Anthropic, Resend, and Stripe keys, including validation warnings and dashboard links. - Added clear success, cancellation, expiration, error, and wrong-account states with account switching. - Added loading placeholders, secret visibility controls, overwrite warnings, and accessibility announcements. - **Bug Fixes** - Improved interstitial animations to respect reduced-motion preferences. - Preserved return destinations and related parameters during sign-in flows. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
158 lines
4.8 KiB
TypeScript
158 lines
4.8 KiB
TypeScript
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
import type { SecretRequest } from './McpSecrets.types'
|
|
import { McpSecretsCard } from './McpSecretsCard'
|
|
import { customRender } from '@/tests/lib/custom-render'
|
|
|
|
const request: SecretRequest = {
|
|
tool: 'create_edge_function_secret',
|
|
ref: 'aaaaaaaaaaaaaaaaaaaa',
|
|
project: 'acme-production',
|
|
account: 'ops@example.com',
|
|
keyName: 'OPENAI_API_KEY',
|
|
}
|
|
|
|
const noop = () => {}
|
|
|
|
const renderCard = (state: Parameters<typeof McpSecretsCard>[0]['state']) =>
|
|
customRender(
|
|
<McpSecretsCard
|
|
state={state}
|
|
isSaving={false}
|
|
onSave={noop}
|
|
onCancel={noop}
|
|
onSwitchAccount={noop}
|
|
/>
|
|
)
|
|
|
|
const secretField = () => screen.getByLabelText<HTMLInputElement>('Secret value')
|
|
|
|
describe('McpSecretsCard', () => {
|
|
it('does not carry a typed secret across a change of request', async () => {
|
|
const user = userEvent.setup()
|
|
const { rerender } = renderCard({ status: 'form', request })
|
|
|
|
await user.type(secretField(), 'sk-for-the-first-request')
|
|
await user.click(screen.getByRole('button', { name: 'Show secret value' }))
|
|
expect(secretField().value).toBe('sk-for-the-first-request')
|
|
expect(secretField().type).toBe('text')
|
|
|
|
rerender(
|
|
<McpSecretsCard
|
|
state={{ status: 'form', request: { ...request, keyName: 'RESEND_API_KEY' } }}
|
|
isSaving={false}
|
|
onSave={noop}
|
|
onCancel={noop}
|
|
onSwitchAccount={noop}
|
|
/>
|
|
)
|
|
|
|
await waitFor(() => expect(screen.getByDisplayValue('RESEND_API_KEY')).toBeInTheDocument())
|
|
expect(secretField().value).toBe('')
|
|
expect(secretField().type).toBe('password')
|
|
})
|
|
|
|
it('resets when only the project changes, since a ref can move under one name', async () => {
|
|
const user = userEvent.setup()
|
|
const { rerender } = renderCard({ status: 'form', request })
|
|
|
|
await user.type(secretField(), 'sk-for-project-a')
|
|
|
|
rerender(
|
|
<McpSecretsCard
|
|
state={{
|
|
status: 'form',
|
|
request: { ...request, ref: 'bbbbbbbbbbbbbbbbbbbb', project: 'acme-production' },
|
|
}}
|
|
isSaving={false}
|
|
onSave={noop}
|
|
onCancel={noop}
|
|
onSwitchAccount={noop}
|
|
/>
|
|
)
|
|
|
|
await waitFor(() => expect(secretField().value).toBe(''))
|
|
})
|
|
|
|
it('keeps what the user typed when the request is unchanged', async () => {
|
|
const user = userEvent.setup()
|
|
const { rerender } = renderCard({ status: 'form', request })
|
|
|
|
await user.type(secretField(), 'sk-still-being-typed')
|
|
|
|
rerender(
|
|
<McpSecretsCard
|
|
state={{
|
|
status: 'form',
|
|
request: { ...request, existingSecret: { updatedAt: new Date().toISOString() } },
|
|
}}
|
|
isSaving={false}
|
|
onSave={noop}
|
|
onCancel={noop}
|
|
onSwitchAccount={noop}
|
|
/>
|
|
)
|
|
|
|
expect(await screen.findByText(/already exists/)).toBeInTheDocument()
|
|
expect(secretField().value).toBe('sk-still-being-typed')
|
|
})
|
|
|
|
it('keeps the prefix-warning live region mounted before there is a warning to announce', async () => {
|
|
const user = userEvent.setup()
|
|
renderCard({
|
|
status: 'form',
|
|
request: { ...request, providerHint: { name: 'OpenAI', prefix: 'sk-' } },
|
|
})
|
|
|
|
const liveRegion = screen.getByRole('status')
|
|
expect(liveRegion).toHaveAttribute('aria-live', 'polite')
|
|
expect(liveRegion).toBeEmptyDOMElement()
|
|
|
|
await user.type(secretField(), 'not-a-real-prefix')
|
|
|
|
await waitFor(() => expect(liveRegion).toHaveTextContent(/usually start with sk-/))
|
|
expect(screen.getByRole('status')).toBe(liveRegion)
|
|
})
|
|
|
|
it('blocks an empty submit and explains why, without calling onSave', async () => {
|
|
const onSave = vi.fn()
|
|
customRender(
|
|
<McpSecretsCard
|
|
state={{ status: 'form', request }}
|
|
isSaving={false}
|
|
onSave={onSave}
|
|
onCancel={noop}
|
|
onSwitchAccount={noop}
|
|
/>
|
|
)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
|
|
|
|
expect(await screen.findByText('Enter the key value')).toBeInTheDocument()
|
|
expect(secretField()).toHaveAttribute('aria-invalid', 'true')
|
|
expect(secretField().getAttribute('aria-describedby')).toContain('form-item-message')
|
|
expect(onSave).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('submits the value exactly as typed, with no trimming', async () => {
|
|
const onSave = vi.fn()
|
|
const user = userEvent.setup()
|
|
customRender(
|
|
<McpSecretsCard
|
|
state={{ status: 'form', request }}
|
|
isSaving={false}
|
|
onSave={onSave}
|
|
onCancel={noop}
|
|
onSwitchAccount={noop}
|
|
/>
|
|
)
|
|
|
|
await user.type(secretField(), ' sk-padded ')
|
|
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
|
|
|
|
await waitFor(() => expect(onSave).toHaveBeenCalledWith(' sk-padded '))
|
|
})
|
|
})
|