UNPKG

@sanity/sdk

Version:
313 lines (266 loc) 11.6 kB
import {createClient, type SanityClient} from '@sanity/client' import {Subject} from 'rxjs' import {beforeEach, describe, expect, it, vi} from 'vitest' import {getAuthMethodState, getTokenState} from '../auth/authStore' import {canvasSource, datasetSource, mediaLibrarySource} from '../config/sanityConfig' import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance' import {getClient, getClientState} from './clientStore' // Mock dependencies vi.mock('@sanity/client') vi.mock('../auth/authStore') let instance: SanityInstance let authMethod$: Subject<'cookie' | 'localstorage' | undefined> beforeEach(() => { vi.resetAllMocks() // Initialize Subjects ONCE per test run before mocks use them authMethod$ = new Subject<'cookie' | 'localstorage' | undefined>() vi.mocked(getTokenState).mockReturnValue({ getCurrent: vi.fn().mockReturnValue('initial-token'), subscribe: vi.fn(), observable: new Subject(), }) vi.mocked(getAuthMethodState).mockReturnValue({ // Mock initial state value if needed by other parts of setup getCurrent: vi.fn().mockReturnValue(undefined), subscribe: vi.fn(), observable: authMethod$, // Consistently return the module-scope Subject }) vi.mocked(createClient).mockImplementation( (clientConfig) => ({config: () => clientConfig}) as SanityClient, ) instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'}) }) afterEach(() => { instance.dispose() }) describe('clientStore', () => { describe('getClient', () => { it('should create a client with default configuration', () => { const client = getClient(instance, {apiVersion: '2024-11-12'}) const defaultConfiguration = { useCdn: false, ignoreBrowserTokenWarning: true, allowReconfigure: false, requestTagPrefix: 'sanity.sdk', projectId: 'test-project', dataset: 'test-dataset', token: 'initial-token', } expect(vi.mocked(createClient)).toHaveBeenCalledWith({ ...defaultConfiguration, apiVersion: '2024-11-12', }) expect(client.config()).toEqual({ ...defaultConfiguration, apiVersion: '2024-11-12', }) }) it('should throw when using disallowed configuration keys', () => { expect(() => getClient(instance, { apiVersion: '2024-11-12', // @ts-expect-error Testing invalid key illegalKey: 'foo', }), ).toThrowError(/unsupported properties: illegalKey/) }) it('should throw a helpful error when called without options', () => { expect(() => // @ts-expect-error Testing missing options getClient(instance, undefined), ).toThrowError(/requires a configuration object with at least an "apiVersion" property/) }) it('should throw a helpful error when called with null options', () => { expect(() => // @ts-expect-error Testing null options getClient(instance, null), ).toThrowError(/requires a configuration object with at least an "apiVersion" property/) }) it('should reuse clients with identical configurations', () => { const options = {apiVersion: '2024-11-12', useCdn: true} const client1 = getClient(instance, options) const client2 = getClient(instance, options) expect(client1).toBe(client2) expect(vi.mocked(createClient)).toHaveBeenCalledTimes(1) }) it('should create new clients when configuration changes', () => { const client1 = getClient(instance, {apiVersion: '2024-11-12'}) const client2 = getClient(instance, {apiVersion: '2023-08-01'}) expect(client1).not.toBe(client2) expect(vi.mocked(createClient)).toHaveBeenCalledTimes(2) }) }) describe('token handling', () => { it('should reset clients when token changes', () => { // Initial client with first token const tokenState = getTokenState(instance) vi.mocked(tokenState.getCurrent).mockReturnValue('first-token') const client1 = getClient(instance, {apiVersion: '2024-11-12'}) // Simulate token change vi.mocked(tokenState.getCurrent).mockReturnValue('new-token') const token$ = tokenState.observable as Subject<string> token$.next('new-token') // New client should be created with new token const client2 = getClient(instance, {apiVersion: '2024-11-12'}) expect(client1).not.toBe(client2) expect(vi.mocked(createClient)).toHaveBeenCalledWith( expect.objectContaining({ token: 'new-token', }), ) }) }) describe('getClientState', () => { it('should provide a state source that emits client changes', async () => { // Get initial client state with a specific configuration const state = getClientState(instance, {apiVersion: '2024-11-12'}) // Get initial client const initialClient = state.getCurrent() expect(initialClient).toBeDefined() // Setup a spy to track emissions from the observable const nextSpy = vi.fn() const subscription = state.observable.subscribe(nextSpy) // Should have emitted once initially expect(nextSpy).toHaveBeenCalledTimes(1) expect(nextSpy).toHaveBeenCalledWith(initialClient) // Simulate token change const tokenState = getTokenState(instance) vi.mocked(tokenState.getCurrent).mockReturnValue('updated-token') const token$ = tokenState.observable as Subject<string> token$.next('updated-token') // Should emit a new client instance expect(nextSpy).toHaveBeenCalledTimes(2) // The new client should be different from the initial one const updatedClient = nextSpy.mock.calls[1][0] expect(updatedClient).not.toBe(initialClient) // The updated client should have the new token expect(updatedClient.config()).toEqual( expect.objectContaining({ token: 'updated-token', }), ) // Clean up subscription subscription.unsubscribe() }) }) describe('source handling', () => { it('should create client when source is provided', () => { const source = datasetSource('source-project', 'source-dataset') const client = getClient(instance, {apiVersion: '2024-11-12', source}) expect(vi.mocked(createClient)).toHaveBeenCalledWith( expect.objectContaining({ apiVersion: '2024-11-12', source: expect.objectContaining({ __sanity_internal_sourceId: { projectId: 'source-project', dataset: 'source-dataset', }, }), }), ) // Client should be projectless - no projectId/dataset in config expect(client.config()).not.toHaveProperty('projectId') expect(client.config()).not.toHaveProperty('dataset') expect(client.config()).toEqual( expect.objectContaining({ source: expect.objectContaining({ __sanity_internal_sourceId: { projectId: 'source-project', dataset: 'source-dataset', }, }), }), ) }) it('should create resource when source has array sourceId and be projectless', () => { const source = mediaLibrarySource('media-lib-123') const client = getClient(instance, {apiVersion: '2024-11-12', source}) expect(vi.mocked(createClient)).toHaveBeenCalledWith( expect.objectContaining({ '~experimental_resource': {type: 'media-library', id: 'media-lib-123'}, 'apiVersion': '2024-11-12', }), ) // Client should be projectless - no projectId/dataset in config expect(client.config()).not.toHaveProperty('projectId') expect(client.config()).not.toHaveProperty('dataset') expect(client.config()).toEqual( expect.objectContaining({ '~experimental_resource': {type: 'media-library', id: 'media-lib-123'}, }), ) }) it('should create resource when canvas source is provided and be projectless', () => { const source = canvasSource('canvas-123') const client = getClient(instance, {apiVersion: '2024-11-12', source}) expect(vi.mocked(createClient)).toHaveBeenCalledWith( expect.objectContaining({ '~experimental_resource': {type: 'canvas', id: 'canvas-123'}, 'apiVersion': '2024-11-12', }), ) // Client should be projectless - no projectId/dataset in config expect(client.config()).not.toHaveProperty('projectId') expect(client.config()).not.toHaveProperty('dataset') expect(client.config()).toEqual( expect.objectContaining({ '~experimental_resource': {type: 'canvas', id: 'canvas-123'}, }), ) }) it('should create projectless client when source is provided, ignoring instance config', () => { const source = datasetSource('source-project', 'source-dataset') const client = getClient(instance, {apiVersion: '2024-11-12', source}) // Client should be projectless - source takes precedence, instance config is ignored expect(client.config()).not.toHaveProperty('projectId') expect(client.config()).not.toHaveProperty('dataset') expect(client.config()).toEqual( expect.objectContaining({ source: expect.objectContaining({ __sanity_internal_sourceId: { projectId: 'source-project', dataset: 'source-dataset', }, }), }), ) }) it('should warn when both source and explicit projectId/dataset are provided', () => { const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) const source = datasetSource('source-project', 'source-dataset') const client = getClient(instance, { apiVersion: '2024-11-12', source, projectId: 'explicit-project', dataset: 'explicit-dataset', }) expect(consoleSpy).toHaveBeenCalledWith( 'Both source and explicit projectId/dataset are provided. The source will be used and projectId/dataset will be ignored.', ) // Client should still be projectless despite explicit projectId/dataset expect(client.config()).not.toHaveProperty('projectId') expect(client.config()).not.toHaveProperty('dataset') consoleSpy.mockRestore() }) it('should create different clients for different sources', () => { const source1 = datasetSource('project-1', 'dataset-1') const source2 = datasetSource('project-2', 'dataset-2') const source3 = mediaLibrarySource('media-lib-1') const client1 = getClient(instance, {apiVersion: '2024-11-12', source: source1}) const client2 = getClient(instance, {apiVersion: '2024-11-12', source: source2}) const client3 = getClient(instance, {apiVersion: '2024-11-12', source: source3}) expect(client1).not.toBe(client2) expect(client2).not.toBe(client3) expect(client1).not.toBe(client3) expect(vi.mocked(createClient)).toHaveBeenCalledTimes(3) }) it('should reuse clients with identical source configurations', () => { const source = datasetSource('same-project', 'same-dataset') const options = {apiVersion: '2024-11-12', source} const client1 = getClient(instance, options) const client2 = getClient(instance, options) expect(client1).toBe(client2) expect(vi.mocked(createClient)).toHaveBeenCalledTimes(1) }) }) })