Created
March 17, 2026 11:52
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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