Skip to content

Instantly share code, notes, and snippets.

@aaronmcadam
Created March 17, 2026 11:52
Show Gist options
  • Select an option

  • Save aaronmcadam/f5dc969896f73e74a0df6f257d2dfec8 to your computer and use it in GitHub Desktop.

Select an option

Save aaronmcadam/f5dc969896f73e74a0df6f257d2dfec8 to your computer and use it in GitHub Desktop.
A test for a <Protect> component, similar to Clerk's <Show> but as an RSC.
/**
* Unit tests for the <Protect> server component.
*
* RSC testing is still evolving — there's no official vitest/RTL support for
* async server components yet. We call the component function directly and
* render the result with renderToString in a node environment.
*
* @see https://aurorascharff.no/posts/running-tests-with-rtl-and-vitest-on-internationalized-react-server-components-in-nextjs-app-router/
*/
import { test, expect, vi } from 'vitest'
import { createElement, type ReactNode } from 'react'
import { renderToString } from 'react-dom/server'
import { Protect } from './protect'
vi.mock('./session', () => ({
verifySession: vi.fn(),
}))
const { verifySession } = await import('./session')
function mockSession(permissions: string[]) {
vi.mocked(verifySession).mockResolvedValue({
token: 'fake-token',
permissions: new Set(permissions),
})
}
function mockNoSession() {
vi.mocked(verifySession).mockResolvedValue(null)
}
function span(text: string) {
return createElement('span', null, text)
}
async function renderProtect(props: {
permission?: string
when?: (has: (params: { permission: string }) => boolean) => boolean
fallback?: ReactNode
children: ReactNode
}) {
const element = await Protect(props)
return renderToString(createElement(() => element))
}
test('renders children when user has permission', async () => {
mockSession(['roles.read'])
const html = await renderProtect({
permission: 'roles.read',
children: span('Content'),
})
expect(html).toContain('Content')
})
test('renders fallback when user lacks permission', async () => {
mockSession(['user.list'])
const html = await renderProtect({
permission: 'roles.read',
fallback: span('Denied'),
children: span('Content'),
})
expect(html).toContain('Denied')
expect(html).not.toContain('Content')
})
test('hides children when user lacks permission and no fallback', async () => {
mockSession(['user.list'])
const html = await renderProtect({
permission: 'roles.read',
children: span('Content'),
})
expect(html).not.toContain('Content')
})
test('renders children when compound check passes', async () => {
mockSession(['roles.update'])
const html = await renderProtect({
when: (has) => has({ permission: 'roles.read' }) || has({ permission: 'roles.update' }),
children: span('Content'),
})
expect(html).toContain('Content')
})
test('renders fallback when compound check fails', async () => {
mockSession(['user.list'])
const html = await renderProtect({
when: (has) => has({ permission: 'roles.read' }) && has({ permission: 'roles.update' }),
fallback: span('Denied'),
children: span('Content'),
})
expect(html).toContain('Denied')
expect(html).not.toContain('Content')
})
test('renders fallback when no session exists', async () => {
mockNoSession()
const html = await renderProtect({
permission: 'roles.read',
fallback: span('Denied'),
children: span('Content'),
})
expect(html).toContain('Denied')
expect(html).not.toContain('Content')
})
test('renders children when no permission or when prop is provided', async () => {
mockSession([])
const html = await renderProtect({
children: span('Always visible'),
})
expect(html).toContain('Always visible')
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment