mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 23:59:08 +03:00
## Problem Closes #31221. The applications and shared UI packages still import the former `framer-motion` package instead of Motion's React entry point. ## Solution Replace runtime imports, type imports, and three test mocks with `motion/react` across the consuming workspaces. Replace the seven direct dependency declarations with `motion: ^11.18.2`. Preserve the existing animation engine version and all other dependency resolutions, including the asynchronous `domMax` feature loader. Historical blog examples remain as published; `framer-motion` remains a transitive implementation dependency of Motion. ## Review instructions 1. Run `pnpm install --frozen-lockfile` and check the seven dependency replacements in the lockfile. 2. Open the WWW Auth page and Docs homepage at desktop and mobile widths, with normal and reduced motion preferences. 3. Open the mobile navigation, expand Product on WWW, and close the menu using Escape and the close button. 4. Check the existing animation components, shared form validation, and Studio tests for unchanged behavior. Validation: - Studio: 731 test files, 8,334 passing tests and one expected failure, with `--maxWorkers=4`. - Shared UI, UI patterns, WWW, and UI library: 615 passing tests. - `pnpm run typecheck --concurrency=1`: all 16 repository tasks pass. - All six application source lint scripts and lint ratchets pass; generated Docs example copies are excluded from local lint. - Repository Prettier source check, lockfile formatting, and case hazard check pass. - WWW: 10 existing end-to-end checks across nine pages. - Docs: 5 existing guide link and accessibility checks across Database and Auth. - WWW and Docs mobile navigation pass at desktop/mobile sizes with normal/reduced motion; SQL-to-REST translation tabs exercise the asynchronous feature loader without browser errors. - Frozen-lockfile installation passes; all existing package and snapshot resolutions are unchanged. - Learn and UI library command palettes open, filter results, and close with Escape without browser errors. - Independent code review finds no actionable defects. - `npm run build -- --filter=www --filter=docs --filter=studio --filter=learn --filter=library --concurrency=1 --continue=always`: all five affected applications build successfully (9 tasks including prerequisites); asset uploads disabled for local validation. ## Checklist - [x] I have read [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) - [x] If I wrote a new docs topic or edited an existing topic, I used the `/write-the-docs` or `/edit-the-docs` skill, which applies the docs [style guide](https://github.com/supabase/supabase/tree/master/apps/docs/style-guide) (no docs topics changed) --------- Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
283 lines
9.2 KiB
TypeScript
283 lines
9.2 KiB
TypeScript
import { fireEvent, screen, waitFor, within } from '@testing-library/react'
|
|
import { mockAnimationsApi } from 'jsdom-testing-mocks'
|
|
import { HttpResponse } from 'msw'
|
|
import { describe, expect, test, vi } from 'vitest'
|
|
|
|
import { SessionsAuthSettingsForm } from './SessionsAuthSettingsForm'
|
|
import {
|
|
MAX_REFRESH_TOKEN_REUSE_INTERVAL_MESSAGE,
|
|
MAX_SESSIONS_INACTIVITY_TIMEOUT_MESSAGE,
|
|
MAX_SESSIONS_TIMEBOX_HOURS,
|
|
MAX_SESSIONS_TIMEBOX_MESSAGE,
|
|
} from './SessionsAuthSettingsForm.utils'
|
|
import { customRender } from '@/tests/lib/custom-render'
|
|
import { addAPIMock } from '@/tests/lib/msw'
|
|
|
|
// FormMessage animates via Motion, which relies on the Web Animations API.
|
|
mockAnimationsApi()
|
|
|
|
vi.mock('@/lib/constants', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('@/lib/constants')>()
|
|
return { ...actual, IS_PLATFORM: true }
|
|
})
|
|
|
|
vi.mock('@/hooks/misc/useCheckEntitlements', () => ({
|
|
useCheckEntitlements: () => ({ hasAccess: true, isLoading: false }),
|
|
}))
|
|
|
|
vi.mock('@/hooks/misc/useCheckPermissions', () => ({
|
|
useAsyncCheckPermissions: () => ({ can: true, isLoading: false, isSuccess: true }),
|
|
}))
|
|
|
|
function mockAuthConfig(overrides: Record<string, unknown> = {}) {
|
|
addAPIMock({
|
|
method: 'get',
|
|
path: '/platform/auth/:ref/config',
|
|
response: () =>
|
|
HttpResponse.json<any>({
|
|
JWT_EXP: 3600,
|
|
REFRESH_TOKEN_ROTATION_ENABLED: true,
|
|
SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: 10,
|
|
SESSIONS_TIMEBOX: 0,
|
|
SESSIONS_INACTIVITY_TIMEOUT: 0,
|
|
SESSIONS_SINGLE_PER_USER: false,
|
|
...overrides,
|
|
}),
|
|
})
|
|
}
|
|
|
|
function mockUpdateAuthConfig(onPatch?: (body: unknown) => void) {
|
|
addAPIMock({
|
|
method: 'patch',
|
|
path: '/platform/auth/:ref/config',
|
|
response: async ({ request }) => {
|
|
const body = await request.json()
|
|
onPatch?.(body)
|
|
return HttpResponse.json<any>({ ...(body as object) })
|
|
},
|
|
})
|
|
}
|
|
|
|
describe('SessionsAuthSettingsForm — Access Tokens', () => {
|
|
test('renders the access token expiry from the auth config', async () => {
|
|
mockAuthConfig({ JWT_EXP: 3600 })
|
|
|
|
customRender(<SessionsAuthSettingsForm />)
|
|
|
|
expect(await screen.findByText('Access Tokens')).toBeInTheDocument()
|
|
expect(await screen.findByDisplayValue('3600')).toBeInTheDocument()
|
|
expect(screen.getByText('Access token expiry time')).toBeInTheDocument()
|
|
})
|
|
|
|
test('saving a new expiry issues a PATCH with the JWT_EXP payload', async () => {
|
|
mockAuthConfig({ JWT_EXP: 3600 })
|
|
let patchBody: unknown
|
|
mockUpdateAuthConfig((body) => {
|
|
patchBody = body
|
|
})
|
|
|
|
customRender(<SessionsAuthSettingsForm />)
|
|
|
|
const input = await screen.findByDisplayValue('3600')
|
|
fireEvent.change(input, { target: { value: '7200' } })
|
|
|
|
const form = input.closest('form') as HTMLFormElement
|
|
const saveButton = within(form).getByRole('button', { name: 'Save changes' })
|
|
await waitFor(() => expect(saveButton).toBeEnabled())
|
|
fireEvent.click(saveButton)
|
|
|
|
await waitFor(() => expect(patchBody).toEqual({ JWT_EXP: 7200 }))
|
|
})
|
|
|
|
test('blocks submit when the expiry exceeds the maximum', async () => {
|
|
mockAuthConfig({ JWT_EXP: 3600 })
|
|
let patchCalled = false
|
|
mockUpdateAuthConfig(() => {
|
|
patchCalled = true
|
|
})
|
|
|
|
customRender(<SessionsAuthSettingsForm />)
|
|
|
|
const input = await screen.findByDisplayValue('3600')
|
|
fireEvent.change(input, { target: { value: '999999' } })
|
|
|
|
const form = input.closest('form') as HTMLFormElement
|
|
const saveButton = within(form).getByRole('button', { name: 'Save changes' })
|
|
await waitFor(() => expect(saveButton).toBeEnabled())
|
|
fireEvent.click(saveButton)
|
|
|
|
expect(await screen.findByText('Must be less than 604800')).toBeInTheDocument()
|
|
expect(patchCalled).toBe(false)
|
|
})
|
|
})
|
|
|
|
async function saveForm(control: HTMLElement) {
|
|
const form = control.closest('form') as HTMLFormElement
|
|
const saveButton = within(form).getByRole('button', { name: 'Save changes' })
|
|
await waitFor(() => expect(saveButton).toBeEnabled())
|
|
fireEvent.click(saveButton)
|
|
}
|
|
|
|
describe('SessionsAuthSettingsForm — User Sessions', () => {
|
|
test('blocks submit when the timebox exceeds the maximum', async () => {
|
|
mockAuthConfig({ SESSIONS_TIMEBOX: 0 })
|
|
let patchCalled = false
|
|
mockUpdateAuthConfig(() => {
|
|
patchCalled = true
|
|
})
|
|
|
|
customRender(<SessionsAuthSettingsForm />)
|
|
|
|
const input = await screen.findByLabelText('Time-box user sessions')
|
|
fireEvent.change(input, { target: { value: '9000' } })
|
|
await saveForm(input)
|
|
|
|
expect(await screen.findByText(MAX_SESSIONS_TIMEBOX_MESSAGE)).toBeInTheDocument()
|
|
expect(patchCalled).toBe(false)
|
|
})
|
|
|
|
test('accepts a timebox at the maximum', async () => {
|
|
mockAuthConfig({ SESSIONS_TIMEBOX: 0 })
|
|
let patchBody: unknown
|
|
mockUpdateAuthConfig((body) => {
|
|
patchBody = body
|
|
})
|
|
|
|
customRender(<SessionsAuthSettingsForm />)
|
|
|
|
const input = await screen.findByLabelText('Time-box user sessions')
|
|
fireEvent.change(input, { target: { value: String(MAX_SESSIONS_TIMEBOX_HOURS) } })
|
|
await saveForm(input)
|
|
|
|
await waitFor(() =>
|
|
expect(patchBody).toMatchObject({ SESSIONS_TIMEBOX: MAX_SESSIONS_TIMEBOX_HOURS })
|
|
)
|
|
})
|
|
|
|
test('saves an over-limit timebox that is already stored', async () => {
|
|
mockAuthConfig({ SESSIONS_TIMEBOX: 20000 })
|
|
let patchBody: unknown
|
|
mockUpdateAuthConfig((body) => {
|
|
patchBody = body
|
|
})
|
|
|
|
customRender(<SessionsAuthSettingsForm />)
|
|
|
|
// Save is gated on isDirty, so change an unrelated field in the same card
|
|
const singleSessionSwitch = await screen.findByLabelText('Enforce single session per user')
|
|
fireEvent.click(singleSessionSwitch)
|
|
await saveForm(singleSessionSwitch)
|
|
|
|
await waitFor(() =>
|
|
expect(patchBody).toMatchObject({ SESSIONS_TIMEBOX: 20000, SESSIONS_SINGLE_PER_USER: true })
|
|
)
|
|
})
|
|
|
|
test('blocks a reduction that is still above the maximum', async () => {
|
|
mockAuthConfig({ SESSIONS_TIMEBOX: 20000 })
|
|
let patchCalled = false
|
|
mockUpdateAuthConfig(() => {
|
|
patchCalled = true
|
|
})
|
|
|
|
customRender(<SessionsAuthSettingsForm />)
|
|
|
|
const input = await screen.findByLabelText('Time-box user sessions')
|
|
fireEvent.change(input, { target: { value: '15000' } })
|
|
await saveForm(input)
|
|
|
|
expect(await screen.findByText(MAX_SESSIONS_TIMEBOX_MESSAGE)).toBeInTheDocument()
|
|
expect(patchCalled).toBe(false)
|
|
})
|
|
|
|
test('saves a reduction into the allowed range', async () => {
|
|
mockAuthConfig({ SESSIONS_TIMEBOX: 20000 })
|
|
let patchBody: unknown
|
|
mockUpdateAuthConfig((body) => {
|
|
patchBody = body
|
|
})
|
|
|
|
customRender(<SessionsAuthSettingsForm />)
|
|
|
|
const input = await screen.findByLabelText('Time-box user sessions')
|
|
fireEvent.change(input, { target: { value: '5000' } })
|
|
await saveForm(input)
|
|
|
|
await waitFor(() => expect(patchBody).toMatchObject({ SESSIONS_TIMEBOX: 5000 }))
|
|
})
|
|
|
|
test('blocks submit when the inactivity timeout exceeds the maximum', async () => {
|
|
mockAuthConfig({ SESSIONS_INACTIVITY_TIMEOUT: 0 })
|
|
let patchCalled = false
|
|
mockUpdateAuthConfig(() => {
|
|
patchCalled = true
|
|
})
|
|
|
|
customRender(<SessionsAuthSettingsForm />)
|
|
|
|
const input = await screen.findByLabelText('Inactivity timeout')
|
|
fireEvent.change(input, { target: { value: '10000' } })
|
|
await saveForm(input)
|
|
|
|
expect(await screen.findByText(MAX_SESSIONS_INACTIVITY_TIMEOUT_MESSAGE)).toBeInTheDocument()
|
|
expect(patchCalled).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('SessionsAuthSettingsForm — Refresh Tokens', () => {
|
|
test('blocks submit when the reuse interval exceeds the maximum', async () => {
|
|
mockAuthConfig({ SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: 10 })
|
|
let patchCalled = false
|
|
mockUpdateAuthConfig(() => {
|
|
patchCalled = true
|
|
})
|
|
|
|
customRender(<SessionsAuthSettingsForm />)
|
|
|
|
const input = await screen.findByLabelText('Refresh token reuse interval')
|
|
fireEvent.change(input, { target: { value: '600' } })
|
|
await saveForm(input)
|
|
|
|
expect(await screen.findByText(MAX_REFRESH_TOKEN_REUSE_INTERVAL_MESSAGE)).toBeInTheDocument()
|
|
expect(patchCalled).toBe(false)
|
|
})
|
|
|
|
test('saves an over-limit reuse interval that is already stored', async () => {
|
|
mockAuthConfig({ SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: 600 })
|
|
let patchBody: unknown
|
|
mockUpdateAuthConfig((body) => {
|
|
patchBody = body
|
|
})
|
|
|
|
customRender(<SessionsAuthSettingsForm />)
|
|
|
|
const rotationSwitch = await screen.findByLabelText(
|
|
'Detect and revoke potentially compromised refresh tokens'
|
|
)
|
|
fireEvent.click(rotationSwitch)
|
|
await saveForm(rotationSwitch)
|
|
|
|
await waitFor(() =>
|
|
expect(patchBody).toMatchObject({ SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: 600 })
|
|
)
|
|
})
|
|
|
|
test('saves a reduction into the allowed range', async () => {
|
|
mockAuthConfig({ SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: 600 })
|
|
let patchBody: unknown
|
|
mockUpdateAuthConfig((body) => {
|
|
patchBody = body
|
|
})
|
|
|
|
customRender(<SessionsAuthSettingsForm />)
|
|
|
|
const input = await screen.findByLabelText('Refresh token reuse interval')
|
|
fireEvent.change(input, { target: { value: '10' } })
|
|
await saveForm(input)
|
|
|
|
await waitFor(() =>
|
|
expect(patchBody).toMatchObject({ SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: 10 })
|
|
)
|
|
})
|
|
})
|