CoursesTesting Sanity StudioTesting Validation and Access Control
Testing Sanity Studio

Testing Validation and Access Control

Start with the simplest testing scenario: functions with no external dependencies or side effects. Test access control logic that determines who can edit fields, validation functions that enforce business rules, and utility functions that transform data. These isolated functions are straightforward to test and often contain critical business logic that protects content quality across your organization.
Log in to mark your progress for each Lesson and Task

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
}
Learn more about Sanity's role-based access control in Users, roles and using roles

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:

  1. Admin user - Has administrator role (should return true)
  2. Regular user - Has non-admin role like editor (should return false)
  3. Multiple roles - User with both editor and admin roles (should return true)
  4. Null user - No user logged in (should return false)
  5. 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',
})
This example uses TSDoc-style comments to annotate and provide at-a-glance, inline documentation when hovering over definitions.

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.

Mark lesson as complete
You have 1 uncompleted task in this lesson
0 of 1