mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 18:22:40 +00:00
feat: Add header registry infrastructure
- Create TypeScript interfaces for header providers - Implement header registry with priority-based ordering - Add network client adapters that integrate with the registry - Add comprehensive unit tests Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
253
tests-ui/tests/services/headerRegistry.test.ts
Normal file
253
tests-ui/tests/services/headerRegistry.test.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { headerRegistry } from '@/services/headerRegistry'
|
||||
import type {
|
||||
HeaderProviderContext,
|
||||
IHeaderProvider
|
||||
} from '@/types/headerTypes'
|
||||
|
||||
describe('headerRegistry', () => {
|
||||
beforeEach(() => {
|
||||
headerRegistry.clear()
|
||||
})
|
||||
|
||||
describe('registerHeaderProvider', () => {
|
||||
it('should register a header provider', () => {
|
||||
const provider: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({ 'X-Test': 'value' })
|
||||
}
|
||||
|
||||
const registration = headerRegistry.registerHeaderProvider(provider)
|
||||
|
||||
expect(registration).toBeDefined()
|
||||
expect(registration.id).toMatch(/^header-provider-\d+$/)
|
||||
expect(headerRegistry.providerCount).toBe(1)
|
||||
})
|
||||
|
||||
it('should return a disposable registration', () => {
|
||||
const provider: IHeaderProvider = {
|
||||
provideHeaders: vi.fn()
|
||||
}
|
||||
|
||||
const registration = headerRegistry.registerHeaderProvider(provider)
|
||||
expect(headerRegistry.providerCount).toBe(1)
|
||||
|
||||
registration.dispose()
|
||||
expect(headerRegistry.providerCount).toBe(0)
|
||||
})
|
||||
|
||||
it('should insert providers in priority order', async () => {
|
||||
const provider1: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({ 'X-Priority': 'low' })
|
||||
}
|
||||
const provider2: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({ 'X-Priority': 'high' })
|
||||
}
|
||||
const provider3: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({ 'X-Priority': 'medium' })
|
||||
}
|
||||
|
||||
headerRegistry.registerHeaderProvider(provider1, { priority: 1 })
|
||||
headerRegistry.registerHeaderProvider(provider2, { priority: 10 })
|
||||
headerRegistry.registerHeaderProvider(provider3, { priority: 5 })
|
||||
|
||||
const context: HeaderProviderContext = {
|
||||
url: 'https://api.example.com',
|
||||
method: 'GET'
|
||||
}
|
||||
|
||||
const headers = await headerRegistry.getHeaders(context)
|
||||
|
||||
// Higher priority provider should override
|
||||
expect(headers['X-Priority']).toBe('high')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getHeaders', () => {
|
||||
it('should combine headers from all providers', async () => {
|
||||
const provider1: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({
|
||||
'X-Header-1': 'value1',
|
||||
'X-Common': 'provider1'
|
||||
})
|
||||
}
|
||||
const provider2: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({
|
||||
'X-Header-2': 'value2',
|
||||
'X-Common': 'provider2'
|
||||
})
|
||||
}
|
||||
|
||||
headerRegistry.registerHeaderProvider(provider1, { priority: 1 })
|
||||
headerRegistry.registerHeaderProvider(provider2, { priority: 2 })
|
||||
|
||||
const context: HeaderProviderContext = {
|
||||
url: 'https://api.example.com',
|
||||
method: 'GET'
|
||||
}
|
||||
|
||||
const headers = await headerRegistry.getHeaders(context)
|
||||
|
||||
expect(headers).toEqual({
|
||||
'X-Header-1': 'value1',
|
||||
'X-Header-2': 'value2',
|
||||
'X-Common': 'provider2' // Higher priority wins
|
||||
})
|
||||
})
|
||||
|
||||
it('should resolve function header values', async () => {
|
||||
const provider: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({
|
||||
'X-Static': 'static',
|
||||
'X-Dynamic': () => 'dynamic',
|
||||
'X-Async': async () => 'async-value'
|
||||
})
|
||||
}
|
||||
|
||||
headerRegistry.registerHeaderProvider(provider)
|
||||
|
||||
const context: HeaderProviderContext = {
|
||||
url: 'https://api.example.com',
|
||||
method: 'GET'
|
||||
}
|
||||
|
||||
const headers = await headerRegistry.getHeaders(context)
|
||||
|
||||
expect(headers).toEqual({
|
||||
'X-Static': 'static',
|
||||
'X-Dynamic': 'dynamic',
|
||||
'X-Async': 'async-value'
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle async providers', async () => {
|
||||
const provider: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockImplementation(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
return { 'X-Async': 'resolved' }
|
||||
})
|
||||
}
|
||||
|
||||
headerRegistry.registerHeaderProvider(provider)
|
||||
|
||||
const context: HeaderProviderContext = {
|
||||
url: 'https://api.example.com',
|
||||
method: 'GET'
|
||||
}
|
||||
|
||||
const headers = await headerRegistry.getHeaders(context)
|
||||
|
||||
expect(headers).toEqual({ 'X-Async': 'resolved' })
|
||||
})
|
||||
|
||||
it('should apply filters when provided', async () => {
|
||||
const provider1: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({ 'X-Api': 'api-header' })
|
||||
}
|
||||
const provider2: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({ 'X-Other': 'other-header' })
|
||||
}
|
||||
|
||||
// Only apply to API URLs
|
||||
headerRegistry.registerHeaderProvider(provider1, {
|
||||
filter: (ctx) => ctx.url.includes('/api/')
|
||||
})
|
||||
|
||||
// Apply to all URLs
|
||||
headerRegistry.registerHeaderProvider(provider2)
|
||||
|
||||
const apiContext: HeaderProviderContext = {
|
||||
url: 'https://example.com/api/users',
|
||||
method: 'GET'
|
||||
}
|
||||
|
||||
const otherContext: HeaderProviderContext = {
|
||||
url: 'https://example.com/assets/image.png',
|
||||
method: 'GET'
|
||||
}
|
||||
|
||||
const apiHeaders = await headerRegistry.getHeaders(apiContext)
|
||||
const otherHeaders = await headerRegistry.getHeaders(otherContext)
|
||||
|
||||
expect(apiHeaders).toEqual({
|
||||
'X-Api': 'api-header',
|
||||
'X-Other': 'other-header'
|
||||
})
|
||||
|
||||
expect(otherHeaders).toEqual({
|
||||
'X-Other': 'other-header'
|
||||
})
|
||||
})
|
||||
|
||||
it('should continue with other providers if one fails', async () => {
|
||||
const provider1: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockRejectedValue(new Error('Provider error'))
|
||||
}
|
||||
const provider2: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({ 'X-Header': 'value' })
|
||||
}
|
||||
|
||||
headerRegistry.registerHeaderProvider(provider1)
|
||||
headerRegistry.registerHeaderProvider(provider2)
|
||||
|
||||
const context: HeaderProviderContext = {
|
||||
url: 'https://api.example.com',
|
||||
method: 'GET'
|
||||
}
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const headers = await headerRegistry.getHeaders(context)
|
||||
|
||||
expect(headers).toEqual({ 'X-Header': 'value' })
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Error getting headers from provider'),
|
||||
expect.any(Error)
|
||||
)
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('clear', () => {
|
||||
it('should remove all providers', () => {
|
||||
const provider1: IHeaderProvider = {
|
||||
provideHeaders: vi.fn()
|
||||
}
|
||||
const provider2: IHeaderProvider = {
|
||||
provideHeaders: vi.fn()
|
||||
}
|
||||
|
||||
headerRegistry.registerHeaderProvider(provider1)
|
||||
headerRegistry.registerHeaderProvider(provider2)
|
||||
|
||||
expect(headerRegistry.providerCount).toBe(2)
|
||||
|
||||
headerRegistry.clear()
|
||||
|
||||
expect(headerRegistry.providerCount).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('providerCount', () => {
|
||||
it('should return the correct count of providers', () => {
|
||||
expect(headerRegistry.providerCount).toBe(0)
|
||||
|
||||
const provider: IHeaderProvider = {
|
||||
provideHeaders: vi.fn()
|
||||
}
|
||||
|
||||
const reg1 = headerRegistry.registerHeaderProvider(provider)
|
||||
expect(headerRegistry.providerCount).toBe(1)
|
||||
|
||||
const reg2 = headerRegistry.registerHeaderProvider(provider)
|
||||
expect(headerRegistry.providerCount).toBe(2)
|
||||
|
||||
reg1.dispose()
|
||||
expect(headerRegistry.providerCount).toBe(1)
|
||||
|
||||
reg2.dispose()
|
||||
expect(headerRegistry.providerCount).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
289
tests-ui/tests/services/networkClientAdapter.test.ts
Normal file
289
tests-ui/tests/services/networkClientAdapter.test.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import axios from 'axios'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { headerRegistry } from '@/services/headerRegistry'
|
||||
import {
|
||||
createAxiosWithHeaders,
|
||||
fetchWithHeaders
|
||||
} from '@/services/networkClientAdapter'
|
||||
import type { IHeaderProvider } from '@/types/headerTypes'
|
||||
|
||||
// Mock axios
|
||||
vi.mock('axios')
|
||||
|
||||
// Mock fetch globally
|
||||
const mockFetch = vi.fn()
|
||||
vi.stubGlobal('fetch', mockFetch)
|
||||
|
||||
describe('networkClientAdapter', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
headerRegistry.clear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('createAxiosWithHeaders', () => {
|
||||
it('should create an axios instance with header injection', async () => {
|
||||
// Setup mock axios instance
|
||||
const mockInterceptors = {
|
||||
request: {
|
||||
use: vi.fn()
|
||||
},
|
||||
response: {
|
||||
use: vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
const mockAxiosInstance = {
|
||||
interceptors: mockInterceptors,
|
||||
get: vi.fn(),
|
||||
post: vi.fn()
|
||||
}
|
||||
|
||||
vi.mocked(axios.create).mockReturnValue(mockAxiosInstance as any)
|
||||
|
||||
// Create instance
|
||||
createAxiosWithHeaders({ baseURL: 'https://api.example.com' })
|
||||
|
||||
// Verify axios.create was called with config
|
||||
expect(axios.create).toHaveBeenCalledWith({
|
||||
baseURL: 'https://api.example.com'
|
||||
})
|
||||
|
||||
// Verify interceptor was added
|
||||
expect(mockInterceptors.request.use).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
|
||||
it('should inject headers from registry on request', async () => {
|
||||
// Setup header provider
|
||||
const provider: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({
|
||||
'X-Custom-Header': 'custom-value'
|
||||
})
|
||||
}
|
||||
headerRegistry.registerHeaderProvider(provider)
|
||||
|
||||
// Setup mock axios
|
||||
const mockInterceptors = {
|
||||
request: {
|
||||
use: vi.fn()
|
||||
},
|
||||
response: {
|
||||
use: vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
const mockAxiosInstance = {
|
||||
interceptors: mockInterceptors
|
||||
}
|
||||
|
||||
vi.mocked(axios.create).mockReturnValue(mockAxiosInstance as any)
|
||||
|
||||
// Create instance
|
||||
createAxiosWithHeaders()
|
||||
|
||||
// Get the interceptor function
|
||||
const [interceptorFn] = mockInterceptors.request.use.mock.calls[0]
|
||||
|
||||
// Test the interceptor
|
||||
const config = {
|
||||
url: '/api/test',
|
||||
method: 'POST',
|
||||
data: { foo: 'bar' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await interceptorFn(config)
|
||||
|
||||
// Verify provider was called with correct context
|
||||
expect(provider.provideHeaders).toHaveBeenCalledWith({
|
||||
url: '/api/test',
|
||||
method: 'POST',
|
||||
body: { foo: 'bar' },
|
||||
config
|
||||
})
|
||||
|
||||
// Verify headers were merged
|
||||
expect(result.headers).toEqual({
|
||||
'Content-Type': 'application/json',
|
||||
'X-Custom-Header': 'custom-value'
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle interceptor errors', async () => {
|
||||
// Setup mock axios
|
||||
const mockInterceptors = {
|
||||
request: {
|
||||
use: vi.fn()
|
||||
},
|
||||
response: {
|
||||
use: vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
const mockAxiosInstance = {
|
||||
interceptors: mockInterceptors
|
||||
}
|
||||
|
||||
vi.mocked(axios.create).mockReturnValue(mockAxiosInstance as any)
|
||||
|
||||
// Create instance
|
||||
createAxiosWithHeaders()
|
||||
|
||||
// Get the error handler
|
||||
const [, errorHandler] = mockInterceptors.request.use.mock.calls[0]
|
||||
|
||||
// Test error handling
|
||||
const error = new Error('Request error')
|
||||
await expect(errorHandler(error)).rejects.toThrow('Request error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchWithHeaders', () => {
|
||||
it('should inject headers from registry into fetch requests', async () => {
|
||||
// Setup header provider
|
||||
const provider: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({
|
||||
'X-Api-Key': 'test-key',
|
||||
'X-Request-ID': '12345'
|
||||
})
|
||||
}
|
||||
headerRegistry.registerHeaderProvider(provider)
|
||||
|
||||
// Setup fetch mock
|
||||
mockFetch.mockResolvedValue(new Response('OK'))
|
||||
|
||||
// Make request
|
||||
await fetchWithHeaders('https://api.example.com/data', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// Verify provider was called
|
||||
expect(provider.provideHeaders).toHaveBeenCalledWith({
|
||||
url: 'https://api.example.com/data',
|
||||
method: 'GET',
|
||||
body: undefined
|
||||
})
|
||||
|
||||
// Verify fetch was called with merged headers
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/data',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
headers: expect.any(Headers)
|
||||
})
|
||||
)
|
||||
|
||||
// Check the headers
|
||||
const [, init] = mockFetch.mock.calls[0]
|
||||
const headers = init.headers as Headers
|
||||
expect(headers.get('Accept')).toBe('application/json')
|
||||
expect(headers.get('X-Api-Key')).toBe('test-key')
|
||||
expect(headers.get('X-Request-ID')).toBe('12345')
|
||||
})
|
||||
|
||||
it('should handle URL objects', async () => {
|
||||
const provider: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({})
|
||||
}
|
||||
headerRegistry.registerHeaderProvider(provider)
|
||||
|
||||
mockFetch.mockResolvedValue(new Response('OK'))
|
||||
|
||||
const url = new URL('https://api.example.com/test')
|
||||
await fetchWithHeaders(url)
|
||||
|
||||
expect(provider.provideHeaders).toHaveBeenCalledWith({
|
||||
url: 'https://api.example.com/test',
|
||||
method: 'GET',
|
||||
body: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle Request objects', async () => {
|
||||
const provider: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({
|
||||
'X-Custom': 'value'
|
||||
})
|
||||
}
|
||||
headerRegistry.registerHeaderProvider(provider)
|
||||
|
||||
mockFetch.mockResolvedValue(new Response('OK'))
|
||||
|
||||
const request = new Request('https://api.example.com/test', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ data: 'test' })
|
||||
})
|
||||
|
||||
await fetchWithHeaders(request)
|
||||
|
||||
expect(provider.provideHeaders).toHaveBeenCalledWith({
|
||||
url: 'https://api.example.com/test',
|
||||
method: 'POST',
|
||||
body: undefined // init.body is undefined when using Request object
|
||||
})
|
||||
|
||||
// Verify headers were added
|
||||
const [, init] = mockFetch.mock.calls[0]
|
||||
const headers = init.headers as Headers
|
||||
expect(headers.get('X-Custom')).toBe('value')
|
||||
})
|
||||
|
||||
it('should convert header values to strings', async () => {
|
||||
const provider: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({
|
||||
'X-Number': 123,
|
||||
'X-Boolean': true,
|
||||
'X-String': 'test'
|
||||
})
|
||||
}
|
||||
headerRegistry.registerHeaderProvider(provider)
|
||||
|
||||
mockFetch.mockResolvedValue(new Response('OK'))
|
||||
|
||||
await fetchWithHeaders('https://api.example.com')
|
||||
|
||||
const [, init] = mockFetch.mock.calls[0]
|
||||
const headers = init.headers as Headers
|
||||
expect(headers.get('X-Number')).toBe('123')
|
||||
expect(headers.get('X-Boolean')).toBe('true')
|
||||
expect(headers.get('X-String')).toBe('test')
|
||||
})
|
||||
|
||||
it('should preserve existing headers and let registry override', async () => {
|
||||
const provider: IHeaderProvider = {
|
||||
provideHeaders: vi.fn().mockReturnValue({
|
||||
'X-Override': 'new-value',
|
||||
'X-New': 'added'
|
||||
})
|
||||
}
|
||||
headerRegistry.registerHeaderProvider(provider)
|
||||
|
||||
mockFetch.mockResolvedValue(new Response('OK'))
|
||||
|
||||
await fetchWithHeaders('https://api.example.com', {
|
||||
headers: {
|
||||
'X-Override': 'old-value',
|
||||
'X-Existing': 'keep-me'
|
||||
}
|
||||
})
|
||||
|
||||
const [, init] = mockFetch.mock.calls[0]
|
||||
const headers = init.headers as Headers
|
||||
expect(headers.get('X-Override')).toBe('new-value') // Registry wins
|
||||
expect(headers.get('X-Existing')).toBe('keep-me')
|
||||
expect(headers.get('X-New')).toBe('added')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user