diff --git a/src/api/fetcher.ts b/src/api/fetcher.ts index a6f490e..72fb17c 100644 --- a/src/api/fetcher.ts +++ b/src/api/fetcher.ts @@ -27,6 +27,11 @@ export interface FetcherOptions { * Optional fetch implementation override (useful for testing or custom fetch implementations). */ fetch?: typeof fetch + /** + * Optional base URL override. Defaults to 'https://api.figma.com'. + * Useful for testing with mocks or proxies. + */ + baseUrl?: string } /** @@ -81,6 +86,7 @@ export async function fetcher( signal: providedSignal, timeout, fetch: customFetch = fetch, + baseUrl = FIGMA_API_BASE_URL, } = options ?? {} // Create timeout signal if timeout is provided and no signal is provided @@ -102,7 +108,7 @@ export async function fetcher( const requestUrl = url.startsWith('http://') || url.startsWith('https://') ? url - : `${FIGMA_API_BASE_URL}${url.startsWith('/') ? '' : '/'}${url}` + : `${baseUrl}${url.startsWith('/') ? '' : '/'}${url}` const response = await customFetch(requestUrl, { method: 'GET', diff --git a/src/api/mutator.ts b/src/api/mutator.ts index a81f57e..e0a07e2 100644 --- a/src/api/mutator.ts +++ b/src/api/mutator.ts @@ -24,6 +24,11 @@ export interface MutatorOptions { * Optional fetch implementation override (useful for testing or custom fetch implementations). */ fetch?: typeof fetch + /** + * Optional base URL override. Defaults to 'https://api.figma.com'. + * Useful for testing with mocks or proxies. + */ + baseUrl?: string } /** @@ -81,6 +86,7 @@ export async function mutator( signal: providedSignal, timeout, fetch: customFetch = fetch, + baseUrl = FIGMA_API_BASE_URL, } = options ?? {} // Create timeout signal if timeout is provided and no signal is provided @@ -121,7 +127,7 @@ export async function mutator( const requestUrl = url.startsWith('http://') || url.startsWith('https://') ? url - : `${FIGMA_API_BASE_URL}${url.startsWith('/') ? '' : '/'}${url}` + : `${baseUrl}${url.startsWith('/') ? '' : '/'}${url}` const response = await customFetch(requestUrl, init) diff --git a/tests/api/fetcher.test.ts b/tests/api/fetcher.test.ts index 43b596d..a64074f 100644 --- a/tests/api/fetcher.test.ts +++ b/tests/api/fetcher.test.ts @@ -418,4 +418,56 @@ describe('fetcher', () => { expect(result).toEqual({ data: 'custom' }) }) }) + + describe('baseUrl override', () => { + it('should use custom baseUrl when provided', async () => { + const customFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: 'custom' }), + }) + + await fetcher('/v1/files/abc/variables/local', DUMMY_TOKEN, { + fetch: customFetch as typeof fetch, + baseUrl: 'https://proxy.example.com', + }) + + expect(customFetch).toHaveBeenCalledWith( + 'https://proxy.example.com/v1/files/abc/variables/local', + expect.any(Object) + ) + }) + + it('should use default baseUrl when not provided', async () => { + const customFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: 'custom' }), + }) + + await fetcher('/v1/files/abc/variables/local', DUMMY_TOKEN, { + fetch: customFetch as typeof fetch, + }) + + expect(customFetch).toHaveBeenCalledWith( + 'https://api.figma.com/v1/files/abc/variables/local', + expect.any(Object) + ) + }) + + it('should ignore baseUrl when URL is already absolute', async () => { + const customFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: 'custom' }), + }) + + await fetcher('https://other.api.com/endpoint', DUMMY_TOKEN, { + fetch: customFetch as typeof fetch, + baseUrl: 'https://proxy.example.com', + }) + + expect(customFetch).toHaveBeenCalledWith( + 'https://other.api.com/endpoint', + expect.any(Object) + ) + }) + }) }) diff --git a/tests/api/mutator.test.ts b/tests/api/mutator.test.ts index 41ceb6f..61e0eaf 100644 --- a/tests/api/mutator.test.ts +++ b/tests/api/mutator.test.ts @@ -485,4 +485,62 @@ describe('mutator', () => { expect(result).toEqual({ id: '123' }) }) }) + + describe('baseUrl override', () => { + it('should use custom baseUrl when provided', async () => { + const customFetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + body: 'not-null', + json: () => Promise.resolve({ id: '123' }), + }) + + await mutator('/v1/files/abc/variables', token, 'CREATE', body, { + fetch: customFetch as typeof fetch, + baseUrl: 'https://proxy.example.com', + }) + + expect(customFetch).toHaveBeenCalledWith( + 'https://proxy.example.com/v1/files/abc/variables', + expect.any(Object) + ) + }) + + it('should use default baseUrl when not provided', async () => { + const customFetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + body: 'not-null', + json: () => Promise.resolve({ id: '123' }), + }) + + await mutator('/v1/files/abc/variables', token, 'CREATE', body, { + fetch: customFetch as typeof fetch, + }) + + expect(customFetch).toHaveBeenCalledWith( + 'https://api.figma.com/v1/files/abc/variables', + expect.any(Object) + ) + }) + + it('should ignore baseUrl when URL is already absolute', async () => { + const customFetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + body: 'not-null', + json: () => Promise.resolve({ id: '123' }), + }) + + await mutator('https://other.api.com/endpoint', token, 'CREATE', body, { + fetch: customFetch as typeof fetch, + baseUrl: 'https://proxy.example.com', + }) + + expect(customFetch).toHaveBeenCalledWith( + 'https://other.api.com/endpoint', + expect.any(Object) + ) + }) + }) })