mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +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:
133
src/examples/headerProviderExample.ts
Normal file
133
src/examples/headerProviderExample.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* Example of how extensions can register header providers
|
||||||
|
* This file demonstrates the header registration API for extension developers
|
||||||
|
*/
|
||||||
|
import { headerRegistry } from '@/services/headerRegistry'
|
||||||
|
import type {
|
||||||
|
HeaderMap,
|
||||||
|
HeaderProviderContext,
|
||||||
|
IHeaderProvider
|
||||||
|
} from '@/types/headerTypes'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example 1: Simple static header provider
|
||||||
|
*/
|
||||||
|
class StaticHeaderProvider implements IHeaderProvider {
|
||||||
|
provideHeaders(_context: HeaderProviderContext): HeaderMap {
|
||||||
|
return {
|
||||||
|
'X-Extension-Name': 'my-extension',
|
||||||
|
'X-Extension-Version': '1.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example 2: Dynamic header provider that adds headers based on the request
|
||||||
|
*/
|
||||||
|
class DynamicHeaderProvider implements IHeaderProvider {
|
||||||
|
async provideHeaders(context: HeaderProviderContext): Promise<HeaderMap> {
|
||||||
|
const headers: HeaderMap = {}
|
||||||
|
|
||||||
|
// Add different headers based on the URL
|
||||||
|
if (context.url.includes('/api/')) {
|
||||||
|
headers['X-API-Version'] = 'v2'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add headers based on request method
|
||||||
|
if (context.method === 'POST' || context.method === 'PUT') {
|
||||||
|
headers['X-Request-ID'] = () => crypto.randomUUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timestamp header
|
||||||
|
headers['X-Timestamp'] = () => new Date().toISOString()
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example 3: Auth token provider
|
||||||
|
*/
|
||||||
|
class AuthTokenProvider implements IHeaderProvider {
|
||||||
|
private getToken(): string | null {
|
||||||
|
// This could retrieve a token from storage, state, etc.
|
||||||
|
return localStorage.getItem('auth-token')
|
||||||
|
}
|
||||||
|
|
||||||
|
provideHeaders(_context: HeaderProviderContext): HeaderMap {
|
||||||
|
const token = this.getToken()
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
return {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example of how to register providers in an extension
|
||||||
|
*/
|
||||||
|
export function setupHeaderProviders() {
|
||||||
|
// Register a simple static provider
|
||||||
|
const staticRegistration = headerRegistry.registerHeaderProvider(
|
||||||
|
new StaticHeaderProvider()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register a dynamic provider with higher priority
|
||||||
|
const dynamicRegistration = headerRegistry.registerHeaderProvider(
|
||||||
|
new DynamicHeaderProvider(),
|
||||||
|
{ priority: 10 }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register an auth provider that only applies to API endpoints
|
||||||
|
const authRegistration = headerRegistry.registerHeaderProvider(
|
||||||
|
new AuthTokenProvider(),
|
||||||
|
{
|
||||||
|
priority: 20, // Higher priority to override other auth headers
|
||||||
|
filter: (context) => context.url.includes('/api/')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return cleanup function for when extension is unloaded
|
||||||
|
return () => {
|
||||||
|
staticRegistration.dispose()
|
||||||
|
dynamicRegistration.dispose()
|
||||||
|
authRegistration.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example of a provider that integrates with a cloud service
|
||||||
|
*/
|
||||||
|
export class CloudServiceHeaderProvider implements IHeaderProvider {
|
||||||
|
constructor(
|
||||||
|
private apiKey: string,
|
||||||
|
private workspaceId: string
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async provideHeaders(context: HeaderProviderContext): Promise<HeaderMap> {
|
||||||
|
// Only add headers for requests to the cloud service
|
||||||
|
if (!context.url.includes('cloud.comfyui.com')) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'X-API-Key': this.apiKey,
|
||||||
|
'X-Workspace-ID': this.workspaceId,
|
||||||
|
'X-Client-Version': '1.0.0',
|
||||||
|
'X-Session-ID': async () => {
|
||||||
|
// Could fetch or generate session ID asynchronously
|
||||||
|
const sessionId = await this.getOrCreateSessionId()
|
||||||
|
return sessionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOrCreateSessionId(): Promise<string> {
|
||||||
|
// Simulate async session creation
|
||||||
|
return 'session-' + Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/services/headerRegistry.ts
Normal file
129
src/services/headerRegistry.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import type {
|
||||||
|
HeaderMap,
|
||||||
|
HeaderProviderContext,
|
||||||
|
HeaderProviderOptions,
|
||||||
|
HeaderValue,
|
||||||
|
IHeaderProvider,
|
||||||
|
IHeaderProviderRegistration
|
||||||
|
} from '@/types/headerTypes'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal registration entry
|
||||||
|
*/
|
||||||
|
interface HeaderProviderEntry {
|
||||||
|
id: string
|
||||||
|
provider: IHeaderProvider
|
||||||
|
options: HeaderProviderOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry for HTTP header providers
|
||||||
|
* Follows VSCode extension patterns for registration and lifecycle
|
||||||
|
*/
|
||||||
|
class HeaderRegistry {
|
||||||
|
private providers: HeaderProviderEntry[] = []
|
||||||
|
private nextId = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a header provider
|
||||||
|
* @param provider - The header provider implementation
|
||||||
|
* @param options - Registration options
|
||||||
|
* @returns Registration handle for disposal
|
||||||
|
*/
|
||||||
|
registerHeaderProvider(
|
||||||
|
provider: IHeaderProvider,
|
||||||
|
options: HeaderProviderOptions = {}
|
||||||
|
): IHeaderProviderRegistration {
|
||||||
|
const id = `header-provider-${this.nextId++}`
|
||||||
|
|
||||||
|
const entry: HeaderProviderEntry = {
|
||||||
|
id,
|
||||||
|
provider,
|
||||||
|
options: {
|
||||||
|
priority: options.priority ?? 0,
|
||||||
|
filter: options.filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert provider in priority order (higher priority = later in array)
|
||||||
|
const insertIndex = this.providers.findIndex(
|
||||||
|
(p) => (p.options.priority ?? 0) > (entry.options.priority ?? 0)
|
||||||
|
)
|
||||||
|
if (insertIndex === -1) {
|
||||||
|
this.providers.push(entry)
|
||||||
|
} else {
|
||||||
|
this.providers.splice(insertIndex, 0, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return disposable handle
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
dispose: () => {
|
||||||
|
const index = this.providers.findIndex((p) => p.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.providers.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all headers for a request by combining all registered providers
|
||||||
|
* @param context - Request context
|
||||||
|
* @returns Combined headers from all providers
|
||||||
|
*/
|
||||||
|
async getHeaders(context: HeaderProviderContext): Promise<HeaderMap> {
|
||||||
|
const result: HeaderMap = {}
|
||||||
|
|
||||||
|
// Process providers in order (lower priority first, so higher priority can override)
|
||||||
|
for (const entry of this.providers) {
|
||||||
|
// Check filter if provided
|
||||||
|
if (entry.options.filter && !entry.options.filter(context)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers = await entry.provider.provideHeaders(context)
|
||||||
|
|
||||||
|
// Merge headers, resolving any function values
|
||||||
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
|
result[key] = await this.resolveHeaderValue(value)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error getting headers from provider ${entry.id}:`, error)
|
||||||
|
// Continue with other providers even if one fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a header value, handling functions
|
||||||
|
*/
|
||||||
|
private async resolveHeaderValue(
|
||||||
|
value: HeaderValue
|
||||||
|
): Promise<string | number | boolean> {
|
||||||
|
if (typeof value === 'function') {
|
||||||
|
return await value()
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all registered providers
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
this.providers = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the count of registered providers
|
||||||
|
*/
|
||||||
|
get providerCount(): number {
|
||||||
|
return this.providers.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const headerRegistry = new HeaderRegistry()
|
||||||
87
src/services/networkClientAdapter.ts
Normal file
87
src/services/networkClientAdapter.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import { headerRegistry } from '@/services/headerRegistry'
|
||||||
|
import type { HeaderProviderContext } from '@/types/headerTypes'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an axios instance with automatic header injection from the registry
|
||||||
|
* @param config - Base axios configuration
|
||||||
|
* @returns Axios instance with header injection
|
||||||
|
*/
|
||||||
|
export function createAxiosWithHeaders(
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): AxiosInstance {
|
||||||
|
const instance = axios.create(config)
|
||||||
|
|
||||||
|
// Add request interceptor to inject headers
|
||||||
|
instance.interceptors.request.use(
|
||||||
|
async (requestConfig) => {
|
||||||
|
// Build context for header providers
|
||||||
|
const context: HeaderProviderContext = {
|
||||||
|
url: requestConfig.url || '',
|
||||||
|
method: requestConfig.method || 'GET',
|
||||||
|
body: requestConfig.data,
|
||||||
|
config: requestConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get headers from registry
|
||||||
|
const registryHeaders = await headerRegistry.getHeaders(context)
|
||||||
|
|
||||||
|
// Merge with existing headers (registry headers take precedence)
|
||||||
|
for (const [key, value] of Object.entries(registryHeaders)) {
|
||||||
|
requestConfig.headers[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestConfig
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the native fetch API with header injection from the registry
|
||||||
|
* @param input - Request URL or Request object
|
||||||
|
* @param init - Request initialization options
|
||||||
|
* @returns Promise resolving to Response
|
||||||
|
*/
|
||||||
|
export async function fetchWithHeaders(
|
||||||
|
input: RequestInfo | URL,
|
||||||
|
init?: RequestInit
|
||||||
|
): Promise<Response> {
|
||||||
|
// Extract URL and method for context
|
||||||
|
const url =
|
||||||
|
typeof input === 'string'
|
||||||
|
? input
|
||||||
|
: input instanceof URL
|
||||||
|
? input.toString()
|
||||||
|
: input.url
|
||||||
|
const method =
|
||||||
|
init?.method || (input instanceof Request ? input.method : 'GET')
|
||||||
|
|
||||||
|
// Build context for header providers
|
||||||
|
const context: HeaderProviderContext = {
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
body: init?.body
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get headers from registry
|
||||||
|
const registryHeaders = await headerRegistry.getHeaders(context)
|
||||||
|
|
||||||
|
// Convert registry headers to Headers object format
|
||||||
|
const headers = new Headers(init?.headers)
|
||||||
|
for (const [key, value] of Object.entries(registryHeaders)) {
|
||||||
|
headers.set(key, String(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform fetch with merged headers
|
||||||
|
return fetch(input, {
|
||||||
|
...init,
|
||||||
|
headers
|
||||||
|
})
|
||||||
|
}
|
||||||
61
src/types/headerTypes.ts
Normal file
61
src/types/headerTypes.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import type { AxiosRequestConfig } from 'axios'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header value can be a string, number, boolean, or a function that returns one of these
|
||||||
|
*/
|
||||||
|
export type HeaderValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| (() => string | number | boolean | Promise<string | number | boolean>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header provider interface for extensions to implement
|
||||||
|
*/
|
||||||
|
export interface IHeaderProvider {
|
||||||
|
/**
|
||||||
|
* Provides headers for HTTP requests
|
||||||
|
* @param context - Request context containing URL and method
|
||||||
|
* @returns Headers to be added to the request
|
||||||
|
*/
|
||||||
|
provideHeaders(context: HeaderProviderContext): HeaderMap | Promise<HeaderMap>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context passed to header providers
|
||||||
|
*/
|
||||||
|
export interface HeaderProviderContext {
|
||||||
|
/** The URL being requested */
|
||||||
|
url: string
|
||||||
|
/** HTTP method */
|
||||||
|
method: string
|
||||||
|
/** Optional request body */
|
||||||
|
body?: any
|
||||||
|
/** Original request config if available */
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of header names to values
|
||||||
|
*/
|
||||||
|
export type HeaderMap = Record<string, HeaderValue>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration handle returned when registering a header provider
|
||||||
|
*/
|
||||||
|
export interface IHeaderProviderRegistration {
|
||||||
|
/** Unique ID for this registration */
|
||||||
|
id: string
|
||||||
|
/** Disposes of this registration */
|
||||||
|
dispose(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for registering a header provider
|
||||||
|
*/
|
||||||
|
export interface HeaderProviderOptions {
|
||||||
|
/** Priority for this provider (higher = runs later, can override earlier providers) */
|
||||||
|
priority?: number
|
||||||
|
/** Optional filter to limit which requests this provider applies to */
|
||||||
|
filter?: (context: HeaderProviderContext) => boolean
|
||||||
|
}
|
||||||
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