UNPKG

@sanity/sdk

Version:
237 lines (219 loc) 8.13 kB
import {type SanityDocument} from '@sanity/types' import {type ExprNode} from 'groq-js' import {describe, expect, it} from 'vitest' import {createSanityInstance} from '../store/createSanityInstance' import {getDraftId, getPublishedId} from '../utils/ids' import {evaluateSync, parse} from './_synchronous-groq-js.mjs' import {type DocumentAction} from './actions' import {calculatePermissions, createGrantsLookup, type DatasetAcl, type Grant} from './permissions' import {type SyncTransactionState} from './reducers' const instance = createSanityInstance({projectId: 'p', dataset: 'd'}) afterAll(() => { instance.dispose() }) // Helper: Create a sample document that conforms to SanityDocument. const createDoc = (id: string, title: string, rev: string = 'initial'): SanityDocument => ({ _id: id, _type: 'article', _createdAt: '2025-01-01T00:00:00.000Z', _updatedAt: '2025-01-01T00:00:00.000Z', _rev: rev, title, }) // Simple "always allow" and "always deny" expressions const alwaysAllow = parse('true') const alwaysDeny = parse('false') // Helper to create a state object const createState = ( docStates: Record<string, {local: unknown}>, grants?: Record<Grant, ExprNode>, ): SyncTransactionState => ({ documentStates: docStates as SyncTransactionState['documentStates'], grants, queued: [], applied: [], }) describe('createGrantsLookup', () => { it('should create a lookup with expressions that evaluate to true', () => { const datasetAcl: DatasetAcl = [ {filter: '_id != null', permissions: ['read', 'update', 'create', 'history']}, ] const grants = createGrantsLookup(datasetAcl) const dummyDoc = {_id: 'doc1'} ;(['read', 'update', 'create', 'history'] as Grant[]).forEach((key) => { expect(grants[key]).toBeDefined() // Evaluate the expression for the dummy document. expect(evaluateSync(grants[key], {params: {document: dummyDoc}}).get()).toBe(true) }) }) }) describe('calculatePermissions', () => { const defaultGrants = { create: alwaysAllow, update: alwaysAllow, read: alwaysAllow, history: alwaysAllow, } it('should return allowed true when no errors occur', () => { // For a document.create action, the selector expects both published and draft keys. const state = createState( { [getPublishedId('doc1')]: {local: createDoc('doc1', 'Original Title')}, [getDraftId('doc1')]: {local: null}, }, defaultGrants, ) const actions: DocumentAction[] = [ {documentId: 'doc1', type: 'document.create', documentType: 'article'}, ] const result = calculatePermissions({instance, state}, actions) expect(result).toEqual({allowed: true}) }) it('should return undefined if documents are incomplete', () => { // Missing the draft key will cause documentsSelector to return undefined. const state = createState( { [getPublishedId('doc1')]: {local: createDoc('doc1', 'Title')}, // Missing getDraftId('doc1') }, defaultGrants, ) const actions: DocumentAction[] = [ {documentId: 'doc1', type: 'document.create', documentType: 'article'}, ] expect(calculatePermissions({instance, state}, actions)).toBeUndefined() }) it('should catch PermissionActionError from processActions and return allowed false with a reason', () => { // Deny the create grant so that document.create will fail. const deniedGrants = {...defaultGrants, create: alwaysDeny} const state = createState( { [getPublishedId('doc1')]: {local: createDoc('doc1', 'Title')}, [getDraftId('doc1')]: {local: null}, }, deniedGrants, ) const actions: DocumentAction[] = [ {documentId: 'doc1', type: 'document.create', documentType: 'article'}, ] const result = calculatePermissions({instance, state}, actions) expect(result).toBeDefined() expect(result?.allowed).toBe(false) expect(result?.reasons).toEqual( expect.arrayContaining([ expect.objectContaining({ message: expect.stringContaining( 'do not have permission to create a draft for document "doc1"', ), type: 'access', documentId: 'doc1', }), ]), ) }) it('should add a precondition error for a document.edit action with no patches when document is not found', () => { // Both published and draft documents are present as null. const state = createState( { [getPublishedId('doc1')]: {local: null}, [getDraftId('doc1')]: {local: null}, }, defaultGrants, ) const actions: DocumentAction[] = [ {documentId: 'doc1', documentType: 'book', type: 'document.edit'}, ] const result = calculatePermissions({instance, state}, actions) expect(result).toBeDefined() expect(result?.allowed).toBe(false) expect(result?.reasons).toEqual( expect.arrayContaining([ expect.objectContaining({ type: 'precondition', message: expect.stringContaining('could not be found'), documentId: 'doc1', }), ]), ) }) it('should add an access error for a document.edit action with no patches when update permission is denied', () => { const deniedGrants = {...defaultGrants, update: alwaysDeny} const state = createState( { [getPublishedId('doc1')]: {local: createDoc('doc1', 'Title')}, [getDraftId('doc1')]: {local: createDoc(getDraftId('doc1'), 'Draft Title')}, }, deniedGrants, ) const actions: DocumentAction[] = [ {documentId: 'doc1', documentType: 'book', type: 'document.edit'}, ] const result = calculatePermissions({instance, state}, actions) expect(result).toBeDefined() expect(result?.allowed).toBe(false) expect(result?.reasons).toEqual( expect.arrayContaining([ expect.objectContaining({ type: 'access', message: expect.stringContaining( 'You are not allowed to edit the document with ID "doc1"', ), documentId: 'doc1', }), ]), ) }) it('should return undefined if grants are not provided', () => { const state = createState({ [getPublishedId('doc1')]: {local: createDoc('doc1', 'Title')}, [getDraftId('doc1')]: {local: null}, }) const actions: DocumentAction[] = [ {documentId: 'doc1', type: 'document.create', documentType: 'article'}, ] expect(calculatePermissions({instance, state}, actions)).toBeUndefined() }) it('should catch ActionError from processActions and return a precondition error reason', () => { // For document.delete, if the published document is missing, processActions throws an ActionError. const state = createState( { [getPublishedId('doc1')]: {local: null}, [getDraftId('doc1')]: {local: null}, }, defaultGrants, ) const actions: DocumentAction[] = [ {documentId: 'doc1', documentType: 'book', type: 'document.delete'}, ] const result = calculatePermissions({instance, state}, actions) expect(result).toBeDefined() expect(result?.allowed).toBe(false) expect(result?.reasons).toEqual( expect.arrayContaining([ expect.objectContaining({ type: 'precondition', message: expect.stringContaining('The document you are trying to delete does not exist'), documentId: 'doc1', }), ]), ) }) it('should memoize the result for identical state and actions inputs', () => { const state = createState( { [getPublishedId('doc1')]: {local: createDoc('doc1', 'Title')}, [getDraftId('doc1')]: {local: null}, }, defaultGrants, ) const action: DocumentAction = { documentId: 'doc1', type: 'document.create', documentType: 'article', } // notice how the action is a copy const result1 = calculatePermissions({instance, state}, [{...action}]) const result2 = calculatePermissions({instance, state}, [{...action}]) expect(result1).toBe(result2) }) })