Testing Validation and Access Control
In this lesson, you'll test validation logic and access control rules from the events Studio—functions with no external dependencies that are straightforward to test.
A "pure" function is predictable and isolated:
- Same inputs always produce same outputs - No randomness or hidden state
- No side effects - Doesn't modify external state, make API calls, or change files
- Easy to test - Pass inputs, verify outputs, done
Functions with no external dependencies are the easiest place to start testing because they require no mocks, no setup, and no teardown. The function is completely self-contained.
The Studio includes access control logic that determines who can edit certain fields. The slug field has a rule: anyone can set the initial slug, but only administrators can change it once set:
import type {CurrentUser} from 'sanity'
/** * Determines if the current user can edit a slug field * Only administrators can edit existing slugs */export function canEditSlug(user?: Omit<CurrentUser, 'role'> | null): boolean { return user?.roles.some((role) => role.name === 'administrator') ?? false}
This function is pure: given a user object, it returns whether they have admin privileges. No API calls, no state changes, no external dependencies.
To ensure this pure helper function matches our business logic, we should test the following scenarios:
- Admin user - Has administrator role (should return
true) - Regular user - Has non-admin role like editor (should return
false) - Multiple roles - User with both editor and admin roles (should return
true) - Null user - No user logged in (should return
false) - Empty roles - User exists but has no roles assigned (should return
false)
import {describe, it, expect} from 'vitest'import type {CurrentUser} from 'sanity'
import {canEditSlug} from './helpers'
describe('canEditSlug', () => { it('allows administrators to edit slugs', () => { const adminUser: Omit<CurrentUser, 'role'> = { id: 'admin-user', name: 'Admin User', email: 'admin@example.com', roles: [{name: 'administrator', title: 'Administrator'}], }
expect(canEditSlug(adminUser)).toBe(true) })
it('prevents non-admin users from editing slugs', () => { const regularUser: Omit<CurrentUser, 'role'> = { id: 'regular-user', name: 'Regular User', email: 'user@example.com', roles: [{name: 'editor', title: 'Editor'}], }
expect(canEditSlug(regularUser)).toBe(false) })
it('handles users with multiple roles', () => { const multiRoleUser: Omit<CurrentUser, 'role'> = { id: 'multirole-user', name: 'Multi-role User', email: 'multi@example.com', roles: [ {name: 'editor', title: 'Editor'}, {name: 'administrator', title: 'Administrator'}, ], }
expect(canEditSlug(multiRoleUser)).toBe(true) })
it('prevents access when user is `null`', () => { expect(canEditSlug(null)).toBe(false) })
it('prevents access when user has no roles', () => { const userWithoutRoles: Omit<CurrentUser, 'role'> = { id: 'no-roles', name: 'No Roles', email: 'noroles@example.com', roles: [], }
expect(canEditSlug(userWithoutRoles)).toBe(false) })})
Run the tests from the root with pnpm test. All tests should pass.
This approach ensures the permission check works correctly for all possible user states, protecting your content from unauthorized edits.
Another example might be the IANA tags that you might use to localize your content:
import type {StringRule} from 'sanity'
/** * IANA language tag pattern (BCP 47) * @see https://en.wikipedia.org/wiki/IETF_language_tag * * Supports formats like: * - en (2-letter language code) * - en-US (language + region) * - zh-Hant-TW (language + script + region) * - en-US-x-private (with private use extensions) */export const LANGUAGE_TAG_PATTERN = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|\d{3}))?(?:-[a-zA-Z0-9]{5,8}|-[0-9][a-zA-Z0-9]{3})*$/
/** * Validation function for IANA language tags (BCP 47 format) * * @example * ```ts * defineField({ * name: 'language', * type: 'string', * validation: validateLanguageTag * }) * ``` */export const validateLanguageTag = (rule: StringRule): StringRule => rule.regex(LANGUAGE_TAG_PATTERN, { name: 'IANA language tag', })
Our test suite for this validation might look something like:
import {describe, it, expect} from 'vitest'import {LANGUAGE_TAG_PATTERN} from './validation'
describe('LANGUAGE_TAG_PATTERN', () => { it('matches valid language tags (real-world examples)', () => { // Simple language codes (most common) expect(LANGUAGE_TAG_PATTERN.test('en')).toBe(true) // English expect(LANGUAGE_TAG_PATTERN.test('fr')).toBe(true) // French expect(LANGUAGE_TAG_PATTERN.test('ja')).toBe(true) // Japanese expect(LANGUAGE_TAG_PATTERN.test('nan')).toBe(true) // Min Nan Chinese (3-letter ISO 639-3)
// Language + Region (localization) expect(LANGUAGE_TAG_PATTERN.test('en-US')).toBe(true) // US English expect(LANGUAGE_TAG_PATTERN.test('en-GB')).toBe(true) // British English expect(LANGUAGE_TAG_PATTERN.test('fr-CA')).toBe(true) // Canadian French expect(LANGUAGE_TAG_PATTERN.test('es-419')).toBe(true) // Latin American Spanish (UN M.49 numeric code) })
it('rejects common mistakes', () => { // Case errors (most common mistake) expect(LANGUAGE_TAG_PATTERN.test('EN')).toBe(false) // Language must be lowercase expect(LANGUAGE_TAG_PATTERN.test('en-us')).toBe(false) // Region must be uppercase (en-US) expect(LANGUAGE_TAG_PATTERN.test('zh-hant')).toBe(false) // Script must be Title Case (zh-Hant) expect(LANGUAGE_TAG_PATTERN.test('zh-HANT')).toBe(false) // Script cannot be all caps
// Wrong separator expect(LANGUAGE_TAG_PATTERN.test('en_US')).toBe(false) // Must use hyphen, not underscore expect(LANGUAGE_TAG_PATTERN.test('en.US')).toBe(false) // Must use hyphen, not period
// Using full names instead of codes expect(LANGUAGE_TAG_PATTERN.test('english')).toBe(false) // Must use ISO code 'en', not full name expect(LANGUAGE_TAG_PATTERN.test('English')).toBe(false)
// Wrong region code length expect(LANGUAGE_TAG_PATTERN.test('en-USA')).toBe(false) // Region must be 2 letters, not 3 expect(LANGUAGE_TAG_PATTERN.test('en-U')).toBe(false) // Region cannot be 1 letter
// Confusing region with script expect(LANGUAGE_TAG_PATTERN.test('zh-CN')).toBe(true) // Valid but ambiguous - prefer zh-Hans expect(LANGUAGE_TAG_PATTERN.test('zh-TW')).toBe(true) // Valid but ambiguous - prefer zh-Hant })})
These functions exhibit a key trait: they're completely isolated. No API calls, no database queries, no React hooks. This makes them fast to test and easy to verify. Pure function tests require no special setup—no providers, no mocks, no configuration. This simplicity makes them an excellent starting point for your testing strategy.
More importantly though, these pure functions often form the foundation of critical business rules in your Sanity Studio. Validation functions ensure data integrity, formatting utilities maintain consistency, and helper functions encapsulate important domain logic. By thoroughly testing these functions, you're safeguarding the core rules that protect your content quality.
You've tested validation and access control logic—permission logic with edge cases like admin checks and null handling and IANA tag validation.
Next you'll test functions that need more context—validation rules that query Sanity's Content Lake to enforce business logic. You'll learn to mock Sanity client, create reusable fixtures, and build a testing harness for async validation logic.