feat: Wire authentication header system with auth stores

- Create AuthHeaderProvider that integrates with Firebase and API key stores
- Add core extension to register auth provider during preInit
- Implement automatic auth header injection for all HTTP requests
- Add comprehensive unit and integration tests
- Include examples showing migration from manual to automatic auth

This completes the header registration system by connecting it to the
actual authentication mechanisms in ComfyUI.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
bymyself
2025-08-17 10:38:40 -07:00
parent 18cc800bd3
commit d930e055f3
7 changed files with 735 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthHeaderProvider } from '@/providers/authHeaderProvider'
import { headerRegistry } from '@/services/headerRegistry'
// Mock the providers module
vi.mock('@/providers/authHeaderProvider', () => ({
AuthHeaderProvider: vi.fn()
}))
// Mock headerRegistry
vi.mock('@/services/headerRegistry', () => ({
headerRegistry: {
registerHeaderProvider: vi.fn()
}
}))
// Mock app
const mockApp = {
registerExtension: vi.fn()
}
vi.mock('@/scripts/app', () => ({
app: mockApp
}))
describe('authHeaders extension', () => {
beforeEach(() => {
vi.clearAllMocks()
// Reset module cache to ensure fresh imports
vi.resetModules()
})
it('should register extension with correct name', async () => {
// Import the extension (this will call app.registerExtension)
await import('@/extensions/core/authHeaders')
expect(mockApp.registerExtension).toHaveBeenCalledOnce()
const extensionConfig = mockApp.registerExtension.mock.calls[0][0]
expect(extensionConfig.name).toBe('Comfy.AuthHeaders')
})
it('should register auth header provider in preInit hook', async () => {
// Import the extension
await import('@/extensions/core/authHeaders')
const extensionConfig = mockApp.registerExtension.mock.calls[0][0]
expect(extensionConfig.preInit).toBeDefined()
// Call the preInit hook
await extensionConfig.preInit({})
// Verify AuthHeaderProvider was instantiated
expect(AuthHeaderProvider).toHaveBeenCalledOnce()
// Verify header provider was registered with high priority
expect(headerRegistry.registerHeaderProvider).toHaveBeenCalledWith(
expect.any(Object), // The AuthHeaderProvider instance
{ priority: 1000 }
)
})
it('should log initialization messages', async () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
// Import the extension
await import('@/extensions/core/authHeaders')
const extensionConfig = mockApp.registerExtension.mock.calls[0][0]
// Call the preInit hook
await extensionConfig.preInit({})
expect(consoleLogSpy).toHaveBeenCalledWith(
'[AuthHeaders] Registering authentication header provider'
)
expect(consoleLogSpy).toHaveBeenCalledWith(
'[AuthHeaders] Authentication headers will be automatically injected'
)
consoleLogSpy.mockRestore()
})
})

View File

@@ -0,0 +1,234 @@
import axios from 'axios'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthHeaderProvider } from '@/providers/authHeaderProvider'
import { headerRegistry } from '@/services/headerRegistry'
import {
createAxiosWithHeaders,
fetchWithHeaders
} from '@/services/networkClientAdapter'
// Mock stores
const mockFirebaseAuthStore = {
getAuthHeader: vi.fn(),
getIdToken: vi.fn()
}
const mockApiKeyAuthStore = {
getAuthHeader: vi.fn()
}
vi.mock('@/stores/firebaseAuthStore', () => ({
useFirebaseAuthStore: () => mockFirebaseAuthStore
}))
vi.mock('@/stores/apiKeyAuthStore', () => ({
useApiKeyAuthStore: () => mockApiKeyAuthStore
}))
// Mock fetch
const mockFetch = vi.fn()
global.fetch = mockFetch
// Mock axios
vi.mock('axios')
const mockedAxios = axios as any
describe('Auth Header Integration', () => {
let authProviderRegistration: any
beforeEach(() => {
vi.clearAllMocks()
// Reset fetch mock
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ success: true })
})
// Reset axios mock
mockedAxios.create.mockReturnValue({
interceptors: {
request: {
use: vi.fn()
},
response: {
use: vi.fn()
}
},
defaults: {
headers: {
common: {},
get: {},
post: {},
put: {},
patch: {},
delete: {}
}
}
})
// Register auth header provider
authProviderRegistration = headerRegistry.registerHeaderProvider(
new AuthHeaderProvider(),
{ priority: 1000 }
)
})
afterEach(() => {
// Unregister the provider
authProviderRegistration.unregister()
vi.restoreAllMocks()
})
describe('fetchWithHeaders integration', () => {
it('should automatically add Firebase auth headers to fetch requests', async () => {
const mockAuthHeader = { Authorization: 'Bearer firebase-token-123' }
mockFirebaseAuthStore.getAuthHeader.mockResolvedValue(mockAuthHeader)
await fetchWithHeaders('https://api.example.com/data')
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/data',
expect.objectContaining({
headers: expect.any(Headers)
})
)
// Verify the auth header was added
const callArgs = mockFetch.mock.calls[0]
const headers = callArgs[1].headers as Headers
expect(headers.get('Authorization')).toBe('Bearer firebase-token-123')
})
it('should automatically add API key headers when Firebase is not available', async () => {
const mockApiKeyHeader = { 'X-API-KEY': 'test-api-key' }
mockFirebaseAuthStore.getAuthHeader.mockResolvedValue(mockApiKeyHeader)
await fetchWithHeaders('https://api.example.com/data')
const callArgs = mockFetch.mock.calls[0]
const headers = callArgs[1].headers as Headers
expect(headers.get('X-API-KEY')).toBe('test-api-key')
})
it('should merge auth headers with existing headers', async () => {
const mockAuthHeader = { Authorization: 'Bearer firebase-token-123' }
mockFirebaseAuthStore.getAuthHeader.mockResolvedValue(mockAuthHeader)
await fetchWithHeaders('https://api.example.com/data', {
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'custom-value'
}
})
const callArgs = mockFetch.mock.calls[0]
const headers = callArgs[1].headers as Headers
expect(headers.get('Authorization')).toBe('Bearer firebase-token-123')
expect(headers.get('Content-Type')).toBe('application/json')
expect(headers.get('X-Custom-Header')).toBe('custom-value')
})
it('should not add headers when no auth is available', async () => {
mockFirebaseAuthStore.getAuthHeader.mockResolvedValue(null)
await fetchWithHeaders('https://api.example.com/data')
const callArgs = mockFetch.mock.calls[0]
const headers = callArgs[1].headers as Headers
expect(headers.get('Authorization')).toBeNull()
expect(headers.get('X-API-KEY')).toBeNull()
})
})
describe('createAxiosWithHeaders integration', () => {
it('should setup interceptor to add auth headers', async () => {
const mockInstance = {
interceptors: {
request: {
use: vi.fn()
},
response: {
use: vi.fn()
}
},
defaults: {
headers: {
common: {},
get: {},
post: {},
put: {},
patch: {},
delete: {}
}
}
}
mockedAxios.create.mockReturnValue(mockInstance)
createAxiosWithHeaders({ baseURL: 'https://api.example.com' })
// Verify interceptor was registered
expect(mockInstance.interceptors.request.use).toHaveBeenCalledOnce()
// Get the interceptor function
const interceptorCall =
mockInstance.interceptors.request.use.mock.calls[0]
const requestInterceptor = interceptorCall[0]
// Test the interceptor
const mockAuthHeader = { Authorization: 'Bearer firebase-token-123' }
mockFirebaseAuthStore.getAuthHeader.mockResolvedValue(mockAuthHeader)
const config = {
url: '/test',
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
const modifiedConfig = await requestInterceptor(config)
expect(modifiedConfig.headers.Authorization).toBe(
'Bearer firebase-token-123'
)
expect(modifiedConfig.headers['Content-Type']).toBe('application/json')
})
})
describe('Multiple providers with priority', () => {
it('should apply headers in priority order', async () => {
// Register a second provider with higher priority
const customProvider = {
provideHeaders: vi.fn().mockResolvedValue({
'X-Custom': 'high-priority',
Authorization: 'Bearer custom-token' // This should override the auth provider
})
}
const customRegistration = headerRegistry.registerHeaderProvider(
customProvider,
{ priority: 2000 } // Higher priority than auth provider
)
// Auth provider returns different token
mockFirebaseAuthStore.getAuthHeader.mockResolvedValue({
Authorization: 'Bearer firebase-token'
})
await fetchWithHeaders('https://api.example.com/data')
const callArgs = mockFetch.mock.calls[0]
const headers = callArgs[1].headers as Headers
// Higher priority provider should win
expect(headers.get('Authorization')).toBe('Bearer custom-token')
expect(headers.get('X-Custom')).toBe('high-priority')
customRegistration.dispose()
})
})
})

View File

@@ -0,0 +1,144 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
ApiKeyHeaderProvider,
AuthHeaderProvider,
FirebaseAuthHeaderProvider
} from '@/providers/authHeaderProvider'
import type { HeaderProviderContext } from '@/types/headerTypes'
// Mock stores
const mockFirebaseAuthStore = {
getAuthHeader: vi.fn(),
getIdToken: vi.fn()
}
const mockApiKeyAuthStore = {
getAuthHeader: vi.fn()
}
vi.mock('@/stores/firebaseAuthStore', () => ({
useFirebaseAuthStore: () => mockFirebaseAuthStore
}))
vi.mock('@/stores/apiKeyAuthStore', () => ({
useApiKeyAuthStore: () => mockApiKeyAuthStore
}))
describe('authHeaderProvider', () => {
const mockContext: HeaderProviderContext = {
url: 'https://api.example.com/test',
method: 'GET'
}
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('AuthHeaderProvider', () => {
it('should provide Firebase auth header when available', async () => {
const provider = new AuthHeaderProvider()
const mockAuthHeader = { Authorization: 'Bearer firebase-token-123' }
mockFirebaseAuthStore.getAuthHeader.mockResolvedValue(mockAuthHeader)
const headers = await provider.provideHeaders(mockContext)
expect(headers).toEqual(mockAuthHeader)
expect(mockFirebaseAuthStore.getAuthHeader).toHaveBeenCalledOnce()
})
it('should provide API key header when Firebase auth is not available', async () => {
const provider = new AuthHeaderProvider()
const mockApiKeyHeader = { 'X-API-KEY': 'test-api-key' }
// Firebase returns null, but includes API key as fallback
mockFirebaseAuthStore.getAuthHeader.mockResolvedValue(mockApiKeyHeader)
const headers = await provider.provideHeaders(mockContext)
expect(headers).toEqual(mockApiKeyHeader)
})
it('should return empty object when no auth is available', async () => {
const provider = new AuthHeaderProvider()
mockFirebaseAuthStore.getAuthHeader.mockResolvedValue(null)
const headers = await provider.provideHeaders(mockContext)
expect(headers).toEqual({})
})
})
describe('ApiKeyHeaderProvider', () => {
it('should provide API key header when available', () => {
const provider = new ApiKeyHeaderProvider()
const mockApiKeyHeader = { 'X-API-KEY': 'test-api-key' }
mockApiKeyAuthStore.getAuthHeader.mockReturnValue(mockApiKeyHeader)
const headers = provider.provideHeaders(mockContext)
expect(headers).toEqual(mockApiKeyHeader)
expect(mockApiKeyAuthStore.getAuthHeader).toHaveBeenCalledOnce()
})
it('should return empty object when no API key is available', () => {
const provider = new ApiKeyHeaderProvider()
mockApiKeyAuthStore.getAuthHeader.mockReturnValue(null)
const headers = provider.provideHeaders(mockContext)
expect(headers).toEqual({})
})
})
describe('FirebaseAuthHeaderProvider', () => {
it('should provide Firebase auth header when available', async () => {
const provider = new FirebaseAuthHeaderProvider()
const mockToken = 'firebase-token-456'
mockFirebaseAuthStore.getIdToken.mockResolvedValue(mockToken)
const headers = await provider.provideHeaders(mockContext)
expect(headers).toEqual({
Authorization: `Bearer ${mockToken}`
})
expect(mockFirebaseAuthStore.getIdToken).toHaveBeenCalledOnce()
})
it('should return empty object when no Firebase token is available', async () => {
const provider = new FirebaseAuthHeaderProvider()
mockFirebaseAuthStore.getIdToken.mockResolvedValue(null)
const headers = await provider.provideHeaders(mockContext)
expect(headers).toEqual({})
})
it('should not fall back to API key', async () => {
const provider = new FirebaseAuthHeaderProvider()
// Firebase has no token
mockFirebaseAuthStore.getIdToken.mockResolvedValue(null)
// API key is available
mockApiKeyAuthStore.getAuthHeader.mockReturnValue({
'X-API-KEY': 'test-key'
})
const headers = await provider.provideHeaders(mockContext)
expect(headers).toEqual({})
// Should not call API key store
expect(mockApiKeyAuthStore.getAuthHeader).not.toHaveBeenCalled()
})
})
})