UNPKG

@sanity/sdk

Version:
604 lines (500 loc) 21.2 kB
import {type ClientConfig, createClient, type SanityClient} from '@sanity/client' import {type CurrentUser} from '@sanity/types' import {NEVER, type Subscription} from 'rxjs' import {afterEach, beforeEach, describe, it, vi} from 'vitest' import {createSanityInstance} from '../store/createSanityInstance' import {AuthStateType} from './authStateType' import { authStore, getAuthState, getCurrentUserState, getDashboardOrganizationId, getLoginUrlState, getTokenState, } from './authStore' import {handleAuthCallback} from './handleAuthCallback' import {checkForCookieAuth, getStudioTokenFromLocalStorage} from './studioModeAuth' import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser' import {subscribeToStorageEventsAndSetToken} from './subscribeToStorageEventsAndSetToken' import {getAuthCode, getTokenFromLocation, getTokenFromStorage} from './utils' vi.mock('./utils', async (importOriginal) => { const original = await importOriginal<typeof import('./utils')>() return { ...original, getAuthCode: vi.fn(), getTokenFromStorage: vi.fn(), getTokenFromLocation: vi.fn(), } }) vi.mock('./studioModeAuth', async (importOriginal) => { const original = await importOriginal<typeof import('./studioModeAuth')>() return { ...original, getStudioTokenFromLocalStorage: vi.fn(), checkForCookieAuth: vi.fn(), } }) vi.mock('./subscribeToStateAndFetchCurrentUser') vi.mock('./subscribeToStorageEventsAndSetToken') describe('authStore', () => { // Global beforeEach and afterEach for all tests beforeEach(() => { vi.resetAllMocks() vi.mocked(subscribeToStateAndFetchCurrentUser).mockImplementation(() => NEVER.subscribe()) vi.mocked(subscribeToStorageEventsAndSetToken).mockImplementation(() => NEVER.subscribe()) vi.mocked(getStudioTokenFromLocalStorage).mockReturnValue(null) vi.mocked(checkForCookieAuth).mockResolvedValue(false) vi.mocked(getTokenFromStorage).mockReturnValue(null) vi.mocked(getAuthCode).mockReturnValue(null) }) describe('getInitialState', () => { let instance: ReturnType<typeof createSanityInstance> const storageKey = '__sanity_auth_token' beforeEach(() => { vi.mocked(getTokenFromStorage).mockReturnValue(null) vi.mocked(getAuthCode).mockReturnValue(null) vi.mocked(getTokenFromLocation).mockReturnValue(null) }) afterEach(() => { instance?.dispose() }) it('sets initial options onto state', () => { const apiHost = 'test-api-host' const callbackUrl = '/login/callback' const providers = [ {name: 'test-provider', id: 'test', title: 'Test', url: 'https://example.com'}, ] const token = 'provided-token' const clientFactory = (config: ClientConfig) => createClient(config) const initialLocationHref = 'https://example.com' const storageArea = {} as Storage instance = createSanityInstance({ projectId: 'p', dataset: 'd', auth: { apiHost, callbackUrl, providers, token, clientFactory, initialLocationHref, storageArea, }, }) const {options, dashboardContext} = authStore.getInitialState(instance) expect(options.apiHost).toBe(apiHost) expect(options.callbackUrl).toBe(callbackUrl) expect(options.customProviders).toBe(providers) expect(options.providedToken).toBe(token) expect(options.clientFactory).toBe(clientFactory) expect(options.initialLocationHref).toBe(initialLocationHref) expect(options.storageKey).toBe(storageKey) expect(options.storageArea).toBe(storageArea) expect(dashboardContext).toStrictEqual({}) }) it('parses dashboardContext from initialLocationHref', () => { const context = {mode: 'test', env: 'staging', orgId: 'abc'} const initialLocationHref = `https://example.com/?_context=${encodeURIComponent(JSON.stringify(context))}` instance = createSanityInstance({ projectId: 'p', dataset: 'd', auth: {initialLocationHref}, }) const {dashboardContext, authState} = authStore.getInitialState(instance) expect(dashboardContext).toEqual(context) expect(authState.type).toBe(AuthStateType.LOGGED_OUT) }) it('parses dashboardContext and removes sid property', () => { const context = {mode: 'test', env: 'staging', orgId: 'abc', sid: 'ignore-me'} const expectedContext = {mode: 'test', env: 'staging', orgId: 'abc'} const initialLocationHref = `https://example.com/?_context=${encodeURIComponent(JSON.stringify(context))}` instance = createSanityInstance({ projectId: 'p', dataset: 'd', auth: {initialLocationHref}, }) const {dashboardContext, authState} = authStore.getInitialState(instance) expect(dashboardContext).toEqual(expectedContext) expect(authState.type).toBe(AuthStateType.LOGGED_OUT) }) it('handles invalid JSON in _context gracefully', () => { const initialLocationHref = `https://example.com/?_context=invalid-json` const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) instance = createSanityInstance({ projectId: 'p', dataset: 'd', auth: {initialLocationHref}, }) const {dashboardContext, authState} = authStore.getInitialState(instance) expect(dashboardContext).toStrictEqual({}) expect(authState.type).toBe(AuthStateType.LOGGED_OUT) expect(errorSpy).toHaveBeenCalledWith( 'Failed to parse dashboard context from initial location:', expect.any(Error), ) errorSpy.mockRestore() }) it('sets to logged in if provided token is present (even in dashboard)', () => { const token = 'provided-token' const context = {mode: 'dashboard'} const initialLocationHref = `https://example.com/?_context=${encodeURIComponent(JSON.stringify(context))}` instance = createSanityInstance({ projectId: 'p', dataset: 'd', auth: { token, initialLocationHref, }, }) const {authState, dashboardContext} = authStore.getInitialState(instance) expect(authState).toMatchObject({type: AuthStateType.LOGGED_IN, token}) expect(dashboardContext).toEqual(context) }) it('sets to logging in if `getAuthCode` returns a code (even in dashboard)', () => { const context = {orgId: 'org1'} const initialLocationHref = `https://example.com/?_context=${encodeURIComponent(JSON.stringify(context))}` instance = createSanityInstance({ projectId: 'p', dataset: 'd', auth: {initialLocationHref}, }) vi.mocked(getAuthCode).mockReturnValue('auth-code') const {authState, dashboardContext} = authStore.getInitialState(instance) expect(authState).toMatchObject({type: AuthStateType.LOGGING_IN}) expect(dashboardContext).toEqual(context) }) it('sets to logged in from storage token when NOT in dashboard', () => { const storageToken = 'storage-token' instance = createSanityInstance({ projectId: 'p', dataset: 'd', }) vi.mocked(getAuthCode).mockReturnValue(null) vi.mocked(getTokenFromStorage).mockReturnValue(storageToken) const {authState, dashboardContext} = authStore.getInitialState(instance) expect(authState).toMatchObject({type: AuthStateType.LOGGED_IN, token: storageToken}) expect(dashboardContext).toStrictEqual({}) }) it('sets to logged out (ignores storage token) when IN dashboard', () => { const storageToken = 'storage-token' const context = {mode: 'dashboard'} const initialLocationHref = `https://example.com/?_context=${encodeURIComponent(JSON.stringify(context))}` instance = createSanityInstance({ projectId: 'p', dataset: 'd', auth: {initialLocationHref}, }) vi.mocked(getAuthCode).mockReturnValue(null) vi.mocked(getTokenFromStorage).mockReturnValue(storageToken) const {authState, dashboardContext} = authStore.getInitialState(instance) expect(authState).toMatchObject({type: AuthStateType.LOGGED_OUT}) expect(dashboardContext).toEqual(context) }) it('sets the state to logged out when no token, code, or context', () => { instance = createSanityInstance({ projectId: 'p', dataset: 'd', }) vi.mocked(getAuthCode).mockReturnValue(null) vi.mocked(getTokenFromStorage).mockReturnValue(null) const {authState, dashboardContext} = authStore.getInitialState(instance) expect(authState).toMatchObject({type: AuthStateType.LOGGED_OUT}) expect(dashboardContext).toStrictEqual({}) }) it('sets to logged in using studio token when studio mode is enabled and token exists', () => { const studioToken = 'studio-token' const projectId = 'studio-project' const mockStorage = { getItem: vi.fn(), setItem: vi.fn(), removeItem: vi.fn(), } as unknown as Storage // Mock storage vi.mocked(getStudioTokenFromLocalStorage).mockReturnValue(studioToken) instance = createSanityInstance({ projectId, dataset: 'd', studioMode: {enabled: true}, auth: {storageArea: mockStorage}, // Provide mock storage }) const {authState, options} = authStore.getInitialState(instance) expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage, projectId) expect(authState).toMatchObject({type: AuthStateType.LOGGED_IN, token: studioToken}) expect(options.authMethod).toBe('localstorage') }) it('checks for cookie auth when studio mode is enabled and no studio token exists', async () => { vi.useFakeTimers() const projectId = 'studio-project' const mockStorage = { getItem: vi.fn(), setItem: vi.fn(), removeItem: vi.fn(), } as unknown as Storage // Mock storage vi.mocked(getStudioTokenFromLocalStorage).mockReturnValue(null) // Mock cookie check to return true asynchronously vi.mocked(checkForCookieAuth).mockResolvedValue(true) instance = createSanityInstance({ projectId, dataset: 'd', studioMode: {enabled: true}, auth: {storageArea: mockStorage}, // Provide mock storage }) // Initial state might be logged out before the async check completes const {authState: initialAuthState} = authStore.getInitialState(instance) expect(initialAuthState.type).toBe(AuthStateType.LOGGED_OUT) // Or potentially logging in depending on other factors expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage, projectId) expect(checkForCookieAuth).toHaveBeenCalledWith(projectId, expect.any(Function)) // Wait for the promise in getInitialState to resolve await vi.runAllTimersAsync() vi.useRealTimers() }) it('falls back to default auth (storage token) when studio mode is disabled', () => { const storageToken = 'regular-storage-token' vi.mocked(getTokenFromStorage).mockReturnValue(storageToken) instance = createSanityInstance({ projectId: 'p', dataset: 'd', }) const {authState, options} = authStore.getInitialState(instance) expect(getStudioTokenFromLocalStorage).not.toHaveBeenCalled() expect(checkForCookieAuth).not.toHaveBeenCalled() expect(getTokenFromStorage).toHaveBeenCalled() expect(authState).toMatchObject({type: AuthStateType.LOGGED_IN, token: storageToken}) expect(options.authMethod).toBe('localstorage') }) it('sets to logging in if getTokenFromLocation returns a token', () => { const initialLocationHref = 'https://example.com/#token=hash-token' instance = createSanityInstance({ projectId: 'p', dataset: 'd', auth: {initialLocationHref}, }) vi.mocked(getAuthCode).mockReturnValue(null) vi.mocked(getTokenFromLocation).mockReturnValue('hash-token') const {authState} = authStore.getInitialState(instance) expect(authState).toMatchObject({ type: AuthStateType.LOGGING_IN, isExchangingToken: false, }) expect(getTokenFromLocation).toHaveBeenCalledWith(initialLocationHref) }) }) describe('initialize', () => { let mockLocalStorage: Storage let instance: ReturnType<typeof createSanityInstance> let stateUnsubscribe: ReturnType<typeof vi.fn> let storageEventsUnsubscribe: ReturnType<typeof vi.fn> beforeEach(() => { // Create fresh mock localStorage for each test mockLocalStorage = { getItem: vi.fn(), setItem: vi.fn(), removeItem: vi.fn(), clear: vi.fn(), key: vi.fn(), length: 0, // Define getter to match real Storage objects get constructor() { return Storage }, } as unknown as Storage stateUnsubscribe = vi.fn() storageEventsUnsubscribe = vi.fn() vi.mocked(subscribeToStateAndFetchCurrentUser).mockReturnValue({ unsubscribe: stateUnsubscribe, } as unknown as Subscription) vi.mocked(subscribeToStorageEventsAndSetToken).mockReturnValue({ unsubscribe: storageEventsUnsubscribe, } as unknown as Subscription) }) afterEach(() => { instance?.dispose() }) it('subscribes to state and storage events and unsubscribes on dispose', () => { instance = createSanityInstance({ projectId: 'p', dataset: 'd', auth: {storageArea: mockLocalStorage}, }) Object.defineProperty(mockLocalStorage, 'constructor', { get: () => Storage, }) expect(subscribeToStateAndFetchCurrentUser).not.toHaveBeenCalled() expect(subscribeToStorageEventsAndSetToken).not.toHaveBeenCalled() // call a bound action to lazily create the store getAuthState(instance) expect(subscribeToStateAndFetchCurrentUser).toHaveBeenCalled() expect(subscribeToStorageEventsAndSetToken).toHaveBeenCalled() instance.dispose() expect(stateUnsubscribe).toHaveBeenCalled() expect(storageEventsUnsubscribe).toHaveBeenCalled() }) it('does not subscribe to storage events when not using storage area', () => { instance = createSanityInstance({ projectId: 'p', dataset: 'd', auth: {storageArea: undefined}, }) getAuthState(instance) expect(subscribeToStateAndFetchCurrentUser).toHaveBeenCalled() expect(subscribeToStorageEventsAndSetToken).not.toHaveBeenCalled() instance.dispose() expect(stateUnsubscribe).toHaveBeenCalled() expect(storageEventsUnsubscribe).not.toHaveBeenCalled() }) }) describe('getCurrentUserState', () => { let instance: ReturnType<typeof createSanityInstance> let currentUser: CurrentUser beforeEach(() => { currentUser = {id: 'example-user'} as CurrentUser }) afterEach(() => { instance?.dispose() }) it('returns the current user if logged in and current user is non-null', () => { vi.mocked(subscribeToStateAndFetchCurrentUser).mockImplementation(({state}) => { state.set('setCurrentUser', { authState: { type: AuthStateType.LOGGED_IN, token: 'token', currentUser, }, }) return NEVER.subscribe() }) instance = createSanityInstance({projectId: 'p', dataset: 'd'}) const {getCurrent} = getCurrentUserState(instance) // pureness check expect(getCurrent()).toBe(getCurrent()) }) it('returns null otherwise', () => { instance = createSanityInstance({projectId: 'p', dataset: 'd'}) const {getCurrent} = getCurrentUserState(instance) expect(getCurrent()).toBe(null) }) }) describe('getTokenState', () => { let instance: ReturnType<typeof createSanityInstance> afterEach(() => { instance?.dispose() }) it('returns the token if logged in', () => { const token = 'hard-coded-token' instance = createSanityInstance({ projectId: 'p', dataset: 'd', auth: {token}, }) const tokenState = getTokenState(instance) expect(tokenState.getCurrent()).toBe(token) // pureness check expect(tokenState.getCurrent()).toBe(tokenState.getCurrent()) }) it('returns null otherwise', () => { instance = createSanityInstance({projectId: 'p', dataset: 'd'}) const tokenState = getTokenState(instance) expect(tokenState.getCurrent()).toBe(null) // pureness check expect(tokenState.getCurrent()).toBe(tokenState.getCurrent()) }) }) describe('getLoginUrlsState', () => { let instance: ReturnType<typeof createSanityInstance> afterEach(() => { instance?.dispose() }) it('returns the default login url', () => { instance = createSanityInstance({projectId: 'p', dataset: 'd'}) const loginUrlState = getLoginUrlState(instance) expect(loginUrlState.getCurrent()).toBe( 'https://www.sanity.io/login?origin=http%3A%2F%2Flocalhost%2F&type=stampedToken&withSid=true', ) }) }) describe('getAuthState', () => { let instance: ReturnType<typeof createSanityInstance> afterEach(() => { instance?.dispose() }) it('returns the current state in `authState`', () => { instance = createSanityInstance({ projectId: 'p', dataset: 'd', auth: {token: 'hard-coded-token'}, }) const {getCurrent} = getAuthState(instance) expect(getCurrent()).toEqual({ currentUser: null, token: 'hard-coded-token', type: 'logged-in', }) // pureness check expect(getCurrent()).toBe(getCurrent()) }) }) describe('getDashboardOrganizationId', () => { let instance: ReturnType<typeof createSanityInstance> beforeEach(() => { vi.mocked(getAuthCode).mockReturnValue('test-auth-code') }) afterEach(() => { instance?.dispose() }) it('returns the organization id if present in initial context', () => { const context = {orgId: 'initial-org-id'} const initialLocationHref = `https://example.com/?_context=${encodeURIComponent(JSON.stringify(context))}` instance = createSanityInstance({ projectId: 'p', dataset: 'd', auth: {initialLocationHref}, }) // No callback needed, check initial state const organizationId = getDashboardOrganizationId(instance) expect(organizationId.getCurrent()).toBe('initial-org-id') }) it('returns the organization id from callback context if handling callback', async () => { const initialContext = {orgId: 'initial-org-id'} const initialLocationHref = `https://example.com/?_context=${encodeURIComponent(JSON.stringify(initialContext))}` const callbackContext = {orgId: 'callback-org-id', mode: 'test'} // Context from callback URL const mockClient = {request: vi.fn().mockResolvedValue({token: 'test-token', label: 'tes'})} const authCode = 'test-auth-code' instance = createSanityInstance({ projectId: 'p', dataset: 'd', auth: { clientFactory: () => mockClient as unknown as SanityClient, initialLocationHref, // Set initial context }, }) // Mock getAuthCode to return a code for the callback URL vi.mocked(getAuthCode).mockReturnValue(authCode) // Create a callback URL with the different _context and the sid const callbackUrl = `https://example.com/login/callback?sid=${authCode}&_context=${encodeURIComponent(JSON.stringify(callbackContext))}` // Ensure initial state has initial orgId const initialOrgId = getDashboardOrganizationId(instance) expect(initialOrgId.getCurrent()).toBe('initial-org-id') // Call handleCallback with the callback URL await handleAuthCallback(instance, callbackUrl) // Use await as handleAuthCallback is async // Wait for the state update to be reflected in the selector await vi.waitUntil( () => getDashboardOrganizationId(instance).getCurrent() === 'callback-org-id', ) // Add a microtask yield just in case await new Promise((resolve) => setTimeout(resolve, 0)) // Check that the orgId from the callback context is now set const finalOrgId = getDashboardOrganizationId(instance) expect(finalOrgId.getCurrent()).toBe('callback-org-id') }) it('returns undefined orgId if not present', () => { const context = {mode: 'test'} // No orgId const initialLocationHref = `https://example.com/?_context=${encodeURIComponent(JSON.stringify(context))}` instance = createSanityInstance({ projectId: 'p', dataset: 'd', auth: {initialLocationHref}, }) const organizationId = getDashboardOrganizationId(instance) expect(organizationId.getCurrent()).toBeUndefined() }) }) })