mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
fix: feat: add Zod schema validation for server capabilities JSON response (#9826)
This commit is contained in:
@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
import { refreshRemoteConfig } from './refreshRemoteConfig'
|
||||
import { remoteConfig } from './remoteConfig'
|
||||
import { remoteConfig, remoteConfigState } from './remoteConfig'
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
@@ -16,7 +16,7 @@ vi.stubGlobal('fetch', vi.fn())
|
||||
describe('refreshRemoteConfig', () => {
|
||||
const mockConfig = { feature1: true, feature2: 'value' }
|
||||
|
||||
function mockSuccessResponse(config = mockConfig) {
|
||||
function mockSuccessResponse(config: Record<string, unknown> = mockConfig) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => config
|
||||
@@ -123,4 +123,67 @@ describe('refreshRemoteConfig', () => {
|
||||
expect(window.__CONFIG__).toEqual(existingConfig)
|
||||
})
|
||||
})
|
||||
|
||||
describe('schema validation', () => {
|
||||
it('accepts a valid remote config response', async () => {
|
||||
const validConfig = {
|
||||
team_workspaces_enabled: true,
|
||||
subscription_required: false,
|
||||
max_upload_size: 1024
|
||||
}
|
||||
vi.mocked(api.fetchApi).mockResolvedValue(
|
||||
mockSuccessResponse(validConfig)
|
||||
)
|
||||
|
||||
await refreshRemoteConfig()
|
||||
|
||||
expect(remoteConfig.value).toEqual(validConfig)
|
||||
expect(window.__CONFIG__).toEqual(validConfig)
|
||||
expect(remoteConfigState.value).toBe('authenticated')
|
||||
})
|
||||
|
||||
it('rejects response with invalid type for boolean flag', async () => {
|
||||
const invalidConfig = {
|
||||
team_workspaces_enabled: 'not-a-boolean'
|
||||
}
|
||||
vi.mocked(api.fetchApi).mockResolvedValue(
|
||||
mockSuccessResponse(invalidConfig)
|
||||
)
|
||||
|
||||
await refreshRemoteConfig()
|
||||
|
||||
expect(remoteConfig.value).toEqual({})
|
||||
expect(window.__CONFIG__).toEqual({})
|
||||
expect(remoteConfigState.value).toBe('error')
|
||||
})
|
||||
|
||||
it('rejects response with invalid type for number field', async () => {
|
||||
const invalidConfig = {
|
||||
max_upload_size: 'not-a-number'
|
||||
}
|
||||
vi.mocked(api.fetchApi).mockResolvedValue(
|
||||
mockSuccessResponse(invalidConfig)
|
||||
)
|
||||
|
||||
await refreshRemoteConfig()
|
||||
|
||||
expect(remoteConfig.value).toEqual({})
|
||||
expect(window.__CONFIG__).toEqual({})
|
||||
expect(remoteConfigState.value).toBe('error')
|
||||
})
|
||||
|
||||
it('preserves unknown keys via passthrough', async () => {
|
||||
const configWithExtra = {
|
||||
team_workspaces_enabled: true,
|
||||
some_future_flag: 'new-value'
|
||||
}
|
||||
vi.mocked(api.fetchApi).mockResolvedValue(
|
||||
mockSuccessResponse(configWithExtra)
|
||||
)
|
||||
|
||||
await refreshRemoteConfig()
|
||||
|
||||
expect(remoteConfig.value).toEqual(configWithExtra)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { fromZodError } from 'zod-validation-error'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
import { remoteConfig, remoteConfigState } from './remoteConfig'
|
||||
import { remoteConfigSchema } from './remoteConfigSchema'
|
||||
import type { RemoteConfig } from './types'
|
||||
|
||||
interface RefreshRemoteConfigOptions {
|
||||
/**
|
||||
@@ -30,7 +34,21 @@ export async function refreshRemoteConfig(
|
||||
: await fetch('/api/features', { cache: 'no-store' })
|
||||
|
||||
if (response.ok) {
|
||||
const config = await response.json()
|
||||
const json = await response.json()
|
||||
const result = remoteConfigSchema.safeParse(json)
|
||||
|
||||
if (!result.success) {
|
||||
console.warn(
|
||||
'Invalid remote config response:',
|
||||
fromZodError(result.error).message
|
||||
)
|
||||
window.__CONFIG__ = {}
|
||||
remoteConfig.value = {}
|
||||
remoteConfigState.value = 'error'
|
||||
return
|
||||
}
|
||||
|
||||
const config = result.data as RemoteConfig
|
||||
window.__CONFIG__ = config
|
||||
remoteConfig.value = config
|
||||
remoteConfigState.value = useAuth ? 'authenticated' : 'anonymous'
|
||||
|
||||
48
src/platform/remoteConfig/remoteConfigSchema.ts
Normal file
48
src/platform/remoteConfig/remoteConfigSchema.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const zServerHealthAlert = z.object({
|
||||
message: z.string(),
|
||||
tooltip: z.string().optional(),
|
||||
severity: z.enum(['info', 'warning', 'error']).optional(),
|
||||
badge: z.string().optional()
|
||||
})
|
||||
|
||||
const zFirebaseRuntimeConfig = z.object({
|
||||
apiKey: z.string(),
|
||||
authDomain: z.string(),
|
||||
databaseURL: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
storageBucket: z.string(),
|
||||
messagingSenderId: z.string(),
|
||||
appId: z.string(),
|
||||
measurementId: z.string().optional()
|
||||
})
|
||||
|
||||
export const remoteConfigSchema = z.object({
|
||||
gtm_container_id: z.string().optional(),
|
||||
ga_measurement_id: z.string().optional(),
|
||||
mixpanel_token: z.string().optional(),
|
||||
posthog_project_token: z.string().optional(),
|
||||
posthog_api_host: z.string().optional(),
|
||||
posthog_config: z.record(z.unknown()).optional(),
|
||||
subscription_required: z.boolean().optional(),
|
||||
server_health_alert: zServerHealthAlert.optional(),
|
||||
max_upload_size: z.number().optional(),
|
||||
comfy_api_base_url: z.string().optional(),
|
||||
comfy_platform_base_url: z.string().optional(),
|
||||
firebase_config: zFirebaseRuntimeConfig.optional(),
|
||||
telemetry_disabled_events: z.array(z.string()).optional(),
|
||||
model_upload_button_enabled: z.boolean().optional(),
|
||||
asset_rename_enabled: z.boolean().optional(),
|
||||
private_models_enabled: z.boolean().optional(),
|
||||
onboarding_survey_enabled: z.boolean().optional(),
|
||||
linear_toggle_enabled: z.boolean().optional(),
|
||||
team_workspaces_enabled: z.boolean().optional(),
|
||||
user_secrets_enabled: z.boolean().optional(),
|
||||
node_library_essentials_enabled: z.boolean().optional(),
|
||||
free_tier_credits: z.number().optional(),
|
||||
new_free_tier_subscriptions: z.boolean().optional(),
|
||||
workflow_sharing_enabled: z.boolean().optional(),
|
||||
comfyhub_upload_enabled: z.boolean().optional(),
|
||||
comfyhub_profile_gate_enabled: z.boolean().optional()
|
||||
}).passthrough()
|
||||
Reference in New Issue
Block a user