[feat] Integrate header registry with all HTTP clients

- Replace direct fetch() with fetchWithHeaders across entire codebase
- Replace axios.create() with createAxiosWithHeaders in all services
- Update tests to properly mock network client adapters
- Fix hoisting issues in test mocks for axios instances
- Ensure all network calls now support header injection

This completes Step 2: integrating the header registry infrastructure with all existing HTTP clients in the codebase.
This commit is contained in:
bymyself
2025-08-16 14:13:21 -07:00
parent d6695ea66e
commit d05153a0dc
19 changed files with 192 additions and 89 deletions

View File

@@ -2,6 +2,7 @@ import { flushPromises } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useTemplateWorkflows } from '@/composables/useTemplateWorkflows'
import { fetchWithHeaders } from '@/services/networkClientAdapter'
import { useWorkflowTemplatesStore } from '@/stores/workflowTemplatesStore'
// Mock the store
@@ -41,6 +42,11 @@ vi.mock('@/stores/dialogStore', () => ({
// Mock fetch
global.fetch = vi.fn()
// Mock fetchWithHeaders
vi.mock('@/services/networkClientAdapter', () => ({
fetchWithHeaders: vi.fn()
}))
describe('useTemplateWorkflows', () => {
let mockWorkflowTemplatesStore: any
@@ -100,6 +106,11 @@ describe('useTemplateWorkflows', () => {
vi.mocked(fetch).mockResolvedValue({
json: vi.fn().mockResolvedValue({ workflow: 'data' })
} as unknown as Response)
// Also mock fetchWithHeaders
vi.mocked(fetchWithHeaders).mockResolvedValue({
json: vi.fn().mockResolvedValue({ workflow: 'data' })
} as unknown as Response)
})
it('should load templates from store', async () => {
@@ -258,7 +269,9 @@ describe('useTemplateWorkflows', () => {
await flushPromises()
expect(result).toBe(true)
expect(fetch).toHaveBeenCalledWith('mock-file-url/templates/template1.json')
expect(vi.mocked(fetchWithHeaders)).toHaveBeenCalledWith(
'mock-file-url/templates/template1.json'
)
expect(loadingTemplateId.value).toBe(null) // Should reset after loading
})
@@ -273,7 +286,9 @@ describe('useTemplateWorkflows', () => {
await flushPromises()
expect(result).toBe(true)
expect(fetch).toHaveBeenCalledWith('mock-file-url/templates/template1.json')
expect(vi.mocked(fetchWithHeaders)).toHaveBeenCalledWith(
'mock-file-url/templates/template1.json'
)
})
it('should handle errors when loading templates', async () => {
@@ -282,8 +297,10 @@ describe('useTemplateWorkflows', () => {
// Set the store as loaded
mockWorkflowTemplatesStore.isLoaded = true
// Mock fetch to throw an error
vi.mocked(fetch).mockRejectedValueOnce(new Error('Failed to fetch'))
// Mock fetchWithHeaders to throw an error
vi.mocked(fetchWithHeaders).mockRejectedValueOnce(
new Error('Failed to fetch')
)
// Spy on console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})

View File

@@ -1,17 +1,35 @@
import axios from 'axios'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useRemoteWidget } from '@/composables/widgets/useRemoteWidget'
import { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
// Hoist the mock to avoid hoisting issues
const mockAxiosInstance = vi.hoisted(() => ({
get: vi.fn(),
interceptors: {
request: {
use: vi.fn()
},
response: {
use: vi.fn()
}
}
}))
vi.mock('axios', () => {
return {
default: {
get: vi.fn()
get: vi.fn(),
create: vi.fn(() => mockAxiosInstance)
}
}
})
// Mock networkClientAdapter to return the same axios instance
vi.mock('@/services/networkClientAdapter', () => ({
createAxiosWithHeaders: vi.fn(() => mockAxiosInstance)
}))
vi.mock('@/i18n', () => ({
i18n: {
global: {
@@ -63,12 +81,12 @@ const createMockOptions = (inputOverrides = {}) => ({
})
function mockAxiosResponse(data: unknown, status = 200) {
vi.mocked(axios.get).mockResolvedValueOnce({ data, status })
vi.mocked(mockAxiosInstance.get).mockResolvedValueOnce({ data, status })
}
function mockAxiosError(error: Error | string) {
const err = error instanceof Error ? error : new Error(error)
vi.mocked(axios.get).mockRejectedValueOnce(err)
vi.mocked(mockAxiosInstance.get).mockRejectedValueOnce(err)
}
function createHookWithData(data: unknown, inputOverrides = {}) {
@@ -96,7 +114,7 @@ describe('useRemoteWidget', () => {
beforeEach(() => {
vi.clearAllMocks()
// Reset mocks
vi.mocked(axios.get).mockReset()
vi.mocked(mockAxiosInstance.get).mockReset()
// Reset cache between tests
vi.spyOn(Map.prototype, 'get').mockClear()
vi.spyOn(Map.prototype, 'set').mockClear()
@@ -137,7 +155,7 @@ describe('useRemoteWidget', () => {
const mockData = ['optionA', 'optionB']
const { hook, result } = await setupHookWithResponse(mockData)
expect(result).toEqual(mockData)
expect(vi.mocked(axios.get)).toHaveBeenCalledWith(
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledWith(
hook.cacheKey.split(';')[0], // Get the route part from cache key
expect.any(Object)
)
@@ -216,7 +234,7 @@ describe('useRemoteWidget', () => {
await getResolvedValue(hook)
await getResolvedValue(hook)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
})
it('permanent widgets should re-fetch if refreshValue is called', async () => {
@@ -237,12 +255,12 @@ describe('useRemoteWidget', () => {
const hook = useRemoteWidget(createMockOptions())
await getResolvedValue(hook)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
vi.setSystemTime(Date.now() + FIRST_BACKOFF)
const secondData = await getResolvedValue(hook)
expect(secondData).toBe('Loading...')
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2)
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(2)
})
it('should treat empty refresh field as permanent', async () => {
@@ -251,7 +269,7 @@ describe('useRemoteWidget', () => {
await getResolvedValue(hook)
await getResolvedValue(hook)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
})
})
@@ -267,7 +285,7 @@ describe('useRemoteWidget', () => {
const newData = await getResolvedValue(hook)
expect(newData).toEqual(mockData2)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2)
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(2)
})
it('should not refresh when data is not stale', async () => {
@@ -278,7 +296,7 @@ describe('useRemoteWidget', () => {
vi.setSystemTime(Date.now() + 128)
await getResolvedValue(hook)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
})
it('should use backoff instead of refresh after error', async () => {
@@ -290,13 +308,13 @@ describe('useRemoteWidget', () => {
mockAxiosError('Network error')
vi.setSystemTime(Date.now() + refresh)
await getResolvedValue(hook)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2)
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(2)
mockAxiosResponse(['second success'])
vi.setSystemTime(Date.now() + FIRST_BACKOFF)
const thirdData = await getResolvedValue(hook)
expect(thirdData).toEqual(['second success'])
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(3)
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(3)
})
it('should use last valid value after error', async () => {
@@ -310,7 +328,7 @@ describe('useRemoteWidget', () => {
const secondData = await getResolvedValue(hook)
expect(secondData).toEqual(['a valid value'])
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2)
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(2)
})
})
@@ -332,15 +350,15 @@ describe('useRemoteWidget', () => {
expect(entry1?.error).toBeTruthy()
await getResolvedValue(hook)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
vi.setSystemTime(Date.now() + 500)
await getResolvedValue(hook)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1) // Still backing off
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1) // Still backing off
vi.setSystemTime(Date.now() + 3000)
await getResolvedValue(hook)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2)
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(2)
expect(entry1?.data).toBeDefined()
})
@@ -418,7 +436,9 @@ describe('useRemoteWidget', () => {
it('should prevent duplicate in-flight requests', async () => {
const promise = Promise.resolve({ data: ['non-duplicate'] })
vi.mocked(axios.get).mockImplementationOnce(() => promise as any)
vi.mocked(mockAxiosInstance.get).mockImplementationOnce(
() => promise as any
)
const hook = useRemoteWidget(createMockOptions())
const [result1, result2] = await Promise.all([
@@ -427,7 +447,7 @@ describe('useRemoteWidget', () => {
])
expect(result1).toBe(result2)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
})
})
@@ -446,7 +466,7 @@ describe('useRemoteWidget', () => {
expect(data1).toEqual(['shared data'])
expect(data2).toEqual(['shared data'])
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
expect(hook1.getCachedValue()).toBe(hook2.getCachedValue())
})
@@ -467,7 +487,7 @@ describe('useRemoteWidget', () => {
expect(data2).toBe(data1)
expect(data3).toBe(data1)
expect(data4).toBe(data1)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
expect(hook1.getCachedValue()).toBe(hook2.getCachedValue())
expect(hook2.getCachedValue()).toBe(hook3.getCachedValue())
expect(hook3.getCachedValue()).toBe(hook4.getCachedValue())
@@ -479,7 +499,9 @@ describe('useRemoteWidget', () => {
resolvePromise = resolve
})
vi.mocked(axios.get).mockImplementationOnce(() => delayedPromise as any)
vi.mocked(mockAxiosInstance.get).mockImplementationOnce(
() => delayedPromise as any
)
const hook = useRemoteWidget(createMockOptions())
hook.getValue()
@@ -500,7 +522,9 @@ describe('useRemoteWidget', () => {
resolvePromise = resolve
})
vi.mocked(axios.get).mockImplementationOnce(() => delayedPromise as any)
vi.mocked(mockAxiosInstance.get).mockImplementationOnce(
() => delayedPromise as any
)
let hook = useRemoteWidget(createMockOptions())
const fetchPromise = hook.getValue()

View File

@@ -27,6 +27,11 @@ vi.mock('axios', () => ({
}
}))
// Mock networkClientAdapter to return the same axios instance
vi.mock('@/services/networkClientAdapter', () => ({
createAxiosWithHeaders: vi.fn(() => mockAxiosInstance)
}))
vi.mock('@/stores/firebaseAuthStore', () => ({
useFirebaseAuthStore: vi.fn(() => mockFirebaseAuthStore)
}))

View File

@@ -15,6 +15,11 @@ vi.mock('axios', () => ({
}
}))
// Mock networkClientAdapter to return the same axios instance
vi.mock('@/services/networkClientAdapter', () => ({
createAxiosWithHeaders: vi.fn(() => mockAxiosInstance)
}))
describe('useReleaseService', () => {
let service: ReturnType<typeof useReleaseService>

View File

@@ -6,19 +6,27 @@ import * as vuefire from 'vuefire'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
// Mock axios before any imports that use it
vi.mock('axios', () => {
const mockAxiosInstance = {
get: vi.fn().mockResolvedValue({
data: { balance: { credits: 0 } },
status: 200
}),
post: vi.fn().mockResolvedValue({
data: { id: 'test-customer-id' },
status: 201
})
// Hoist the mock to avoid hoisting issues
const mockAxiosInstance = vi.hoisted(() => ({
get: vi.fn().mockResolvedValue({
data: { balance: { credits: 0 } },
status: 200
}),
post: vi.fn().mockResolvedValue({
data: { id: 'test-customer-id' },
status: 201
}),
interceptors: {
request: {
use: vi.fn()
},
response: {
use: vi.fn()
}
}
}))
vi.mock('axios', () => {
return {
default: {
create: vi.fn().mockReturnValue(mockAxiosInstance),
@@ -27,8 +35,12 @@ vi.mock('axios', () => {
}
})
// Mock networkClientAdapter
vi.mock('@/services/networkClientAdapter', () => ({
createAxiosWithHeaders: vi.fn(() => mockAxiosInstance)
}))
const mockedAxios = vi.mocked(axios)
const mockAxiosInstance = mockedAxios.create() as any
// Mock fetch
const mockFetch = vi.fn()

View File

@@ -3,6 +3,7 @@ import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { fetchWithHeaders } from '@/services/networkClientAdapter'
import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore'
vi.mock('@/scripts/api', () => ({
@@ -67,6 +68,11 @@ vi.mock('marked', () => ({
}
}))
// Mock fetchWithHeaders
vi.mock('@/services/networkClientAdapter', () => ({
fetchWithHeaders: vi.fn()
}))
describe('nodeHelpStore', () => {
// Define a mock node for testing
const mockCoreNode = {
@@ -91,10 +97,17 @@ describe('nodeHelpStore', () => {
const mockFetch = vi.fn()
global.fetch = mockFetch
// Helper to mock both fetch and fetchWithHeaders
const mockFetchResponse = (response: any) => {
mockFetch.mockResolvedValueOnce(response)
vi.mocked(fetchWithHeaders).mockResolvedValueOnce(response)
}
beforeEach(() => {
// Setup Pinia
setActivePinia(createPinia())
mockFetch.mockReset()
vi.mocked(fetchWithHeaders).mockReset()
})
it('should initialize with empty state', () => {
@@ -144,7 +157,7 @@ describe('nodeHelpStore', () => {
it('should render markdown content correctly', async () => {
const nodeHelpStore = useNodeHelpStore()
mockFetch.mockResolvedValueOnce({
mockFetchResponse({
ok: true,
text: async () => '# Test Help\nThis is test help content'
})
@@ -160,7 +173,7 @@ describe('nodeHelpStore', () => {
it('should handle fetch errors and fall back to description', async () => {
const nodeHelpStore = useNodeHelpStore()
mockFetch.mockResolvedValueOnce({
mockFetchResponse({
ok: false,
statusText: 'Not Found'
})
@@ -175,7 +188,7 @@ describe('nodeHelpStore', () => {
it('should include alt attribute for images', async () => {
const nodeHelpStore = useNodeHelpStore()
mockFetch.mockResolvedValueOnce({
mockFetchResponse({
ok: true,
text: async () => '![image](test.jpg)'
})
@@ -188,7 +201,7 @@ describe('nodeHelpStore', () => {
it('should prefix relative video src in custom nodes', async () => {
const nodeHelpStore = useNodeHelpStore()
mockFetch.mockResolvedValueOnce({
mockFetchResponse({
ok: true,
text: async () => '<video src="video.mp4"></video>'
})
@@ -203,7 +216,7 @@ describe('nodeHelpStore', () => {
it('should prefix relative video src for core nodes with node-specific base URL', async () => {
const nodeHelpStore = useNodeHelpStore()
mockFetch.mockResolvedValueOnce({
mockFetchResponse({
ok: true,
text: async () => '<video src="video.mp4"></video>'
})
@@ -218,7 +231,7 @@ describe('nodeHelpStore', () => {
it('should prefix relative source src in custom nodes', async () => {
const nodeHelpStore = useNodeHelpStore()
mockFetch.mockResolvedValueOnce({
mockFetchResponse({
ok: true,
text: async () =>
'<video><source src="video.mp4" type="video/mp4" /></video>'
@@ -234,7 +247,7 @@ describe('nodeHelpStore', () => {
it('should prefix relative source src for core nodes with node-specific base URL', async () => {
const nodeHelpStore = useNodeHelpStore()
mockFetch.mockResolvedValueOnce({
mockFetchResponse({
ok: true,
text: async () =>
'<video><source src="video.webm" type="video/webm" /></video>'
@@ -250,7 +263,9 @@ describe('nodeHelpStore', () => {
it('should handle loading state', async () => {
const nodeHelpStore = useNodeHelpStore()
mockFetch.mockImplementationOnce(() => new Promise(() => {})) // Never resolves
vi.mocked(fetchWithHeaders).mockImplementationOnce(
() => new Promise(() => {})
) // Never resolves
nodeHelpStore.openHelp(mockCoreNode as any)
await nextTick()
@@ -261,24 +276,24 @@ describe('nodeHelpStore', () => {
it('should try fallback URL for custom nodes', async () => {
const nodeHelpStore = useNodeHelpStore()
mockFetch
vi.mocked(fetchWithHeaders)
.mockResolvedValueOnce({
ok: false,
statusText: 'Not Found'
})
} as Response)
.mockResolvedValueOnce({
ok: true,
text: async () => '# Fallback content'
})
} as Response)
nodeHelpStore.openHelp(mockCustomNode as any)
await flushPromises()
expect(mockFetch).toHaveBeenCalledTimes(2)
expect(mockFetch).toHaveBeenCalledWith(
expect(vi.mocked(fetchWithHeaders)).toHaveBeenCalledTimes(2)
expect(vi.mocked(fetchWithHeaders)).toHaveBeenCalledWith(
'/extensions/test_module/docs/CustomNode/en.md'
)
expect(mockFetch).toHaveBeenCalledWith(
expect(vi.mocked(fetchWithHeaders)).toHaveBeenCalledWith(
'/extensions/test_module/docs/CustomNode.md'
)
})
@@ -286,7 +301,7 @@ describe('nodeHelpStore', () => {
it('should prefix relative img src in raw HTML for custom nodes', async () => {
const nodeHelpStore = useNodeHelpStore()
mockFetch.mockResolvedValueOnce({
mockFetchResponse({
ok: true,
text: async () => '# Test\n<img src="image.png" alt="Test image">'
})
@@ -302,7 +317,7 @@ describe('nodeHelpStore', () => {
it('should prefix relative img src in raw HTML for core nodes', async () => {
const nodeHelpStore = useNodeHelpStore()
mockFetch.mockResolvedValueOnce({
mockFetchResponse({
ok: true,
text: async () => '# Test\n<img src="image.png" alt="Test image">'
})
@@ -318,7 +333,7 @@ describe('nodeHelpStore', () => {
it('should not prefix absolute img src in raw HTML', async () => {
const nodeHelpStore = useNodeHelpStore()
mockFetch.mockResolvedValueOnce({
mockFetchResponse({
ok: true,
text: async () => '<img src="/absolute/image.png" alt="Absolute">'
})
@@ -334,7 +349,7 @@ describe('nodeHelpStore', () => {
it('should not prefix external img src in raw HTML', async () => {
const nodeHelpStore = useNodeHelpStore()
mockFetch.mockResolvedValueOnce({
mockFetchResponse({
ok: true,
text: async () =>
'<img src="https://example.com/image.png" alt="External">'
@@ -351,7 +366,7 @@ describe('nodeHelpStore', () => {
it('should handle various quote styles in media src attributes', async () => {
const nodeHelpStore = useNodeHelpStore()
mockFetch.mockResolvedValueOnce({
mockFetchResponse({
ok: true,
text: async () => `# Media Test