mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 03:09:50 +08:00
fix: restore security hardening compatibility and validation
Restore authorized rule reveal, explicit full HTTP capture and retention, video task business fields, and valid payment URLs. Add opt-in credential preservation for trusted recovery, fix frontend type contracts and async races, and eliminate PostgreSQL test fixture resource leaks. Document audit coverage and successful fmt and CI-scoped Clippy checks.
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp, type App } from 'vue'
|
||||
import type { ActionResultResponse } from '@/api/providerOps'
|
||||
import { useProviderBalance } from '../useProviderBalance'
|
||||
|
||||
const api = vi.hoisted(() => ({
|
||||
batchQueryBalance: vi.fn<() => Promise<Record<string, ActionResultResponse>>>(),
|
||||
getArchitectures: vi.fn().mockResolvedValue([]),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/providerOps', () => api)
|
||||
|
||||
let app: App | undefined
|
||||
let root: HTMLDivElement
|
||||
|
||||
function mountBalance() {
|
||||
let balance!: ReturnType<typeof useProviderBalance>
|
||||
root = document.createElement('div')
|
||||
app = createApp({
|
||||
setup() {
|
||||
balance = useProviderBalance()
|
||||
return () => null
|
||||
},
|
||||
})
|
||||
app.mount(root)
|
||||
return balance
|
||||
}
|
||||
|
||||
function result(status: ActionResultResponse['status'], available: number): ActionResultResponse {
|
||||
return {
|
||||
status,
|
||||
action_type: 'query_balance',
|
||||
data: { total_available: available, currency: 'USD', extra: {} },
|
||||
message: null,
|
||||
executed_at: '2026-09-07T00:00:00Z',
|
||||
response_time_ms: 0,
|
||||
cache_ttl_seconds: 0,
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
api.batchQueryBalance.mockReset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
app?.unmount()
|
||||
app = undefined
|
||||
root?.remove()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('provider balance refresh', () => {
|
||||
it('does not overwrite a newer refresh with an older pending retry', async () => {
|
||||
const balance = mountBalance()
|
||||
const providers = [{ id: 'provider-1', ops_configured: true }]
|
||||
let resolveRetry!: (value: Record<string, ActionResultResponse>) => void
|
||||
api.batchQueryBalance
|
||||
.mockResolvedValueOnce({ 'provider-1': result('pending', 0) })
|
||||
.mockImplementationOnce(() => new Promise(resolve => { resolveRetry = resolve }))
|
||||
.mockResolvedValueOnce({ 'provider-1': result('success', 20) })
|
||||
|
||||
await balance.loadBalances(providers)
|
||||
await vi.advanceTimersByTimeAsync(12_000)
|
||||
await balance.loadBalances(providers)
|
||||
resolveRetry({ 'provider-1': result('success', 10) })
|
||||
await Promise.resolve()
|
||||
|
||||
expect(balance.getProviderBalance('provider-1')).toEqual({ available: 20, currency: 'USD' })
|
||||
})
|
||||
|
||||
it('ignores a pending response after unmount', async () => {
|
||||
const balance = mountBalance()
|
||||
let resolveLoad!: (value: Record<string, ActionResultResponse>) => void
|
||||
api.batchQueryBalance.mockImplementationOnce(() => new Promise(resolve => { resolveLoad = resolve }))
|
||||
const loading = balance.loadBalances([{ id: 'provider-1', ops_configured: true }])
|
||||
app?.unmount()
|
||||
app = undefined
|
||||
resolveLoad({ 'provider-1': result('pending', 10) })
|
||||
await loading
|
||||
|
||||
expect(balance.balanceCache.value).toEqual({})
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
})
|
||||
|
||||
it('preserves zero balances and false check-in results', async () => {
|
||||
const balance = mountBalance()
|
||||
api.batchQueryBalance.mockResolvedValueOnce({
|
||||
'provider-1': {
|
||||
...result('success', 0),
|
||||
data: {
|
||||
total_available: 0,
|
||||
currency: 'USD',
|
||||
extra: { balance: 0, points: 0, checkin_success: false, checkin_message: 'try again' },
|
||||
},
|
||||
},
|
||||
})
|
||||
await balance.loadBalances([{ id: 'provider-1', ops_configured: true }])
|
||||
|
||||
expect(balance.getProviderBalanceBreakdown('provider-1')).toEqual({ balance: 0, points: 0, currency: 'USD' })
|
||||
expect(balance.getProviderCheckin('provider-1')).toEqual({ success: false, message: 'try again' })
|
||||
})
|
||||
})
|
||||
@@ -48,7 +48,7 @@ export function useProviderBalance() {
|
||||
const schemas: Record<string, CredentialsSchema> = {}
|
||||
for (const arch of archs) {
|
||||
if (arch.credentials_schema) {
|
||||
schemas[arch.architecture_id] = arch.credentials_schema as CredentialsSchema
|
||||
schemas[arch.architecture_id] = arch.credentials_schema
|
||||
}
|
||||
}
|
||||
architectureSchemas.value = schemas
|
||||
@@ -59,7 +59,7 @@ export function useProviderBalance() {
|
||||
}
|
||||
|
||||
// 异步加载余额数据(使用批量接口)
|
||||
async function loadBalances(providers: ProviderWithEndpointsSummary[], fullReload = true) {
|
||||
async function loadBalances(providers: Pick<ProviderWithEndpointsSummary, 'id' | 'ops_configured'>[], fullReload = true) {
|
||||
if (fullReload) {
|
||||
balanceCache.value = {}
|
||||
}
|
||||
@@ -108,6 +108,7 @@ export function useProviderBalance() {
|
||||
async function retryPendingBalances(providerIds: string[], loadVersion: number, retryCount: number) {
|
||||
try {
|
||||
const results = await batchQueryBalance(providerIds)
|
||||
if (loadVersion !== balanceLoadVersion) return
|
||||
const stillPending: string[] = []
|
||||
|
||||
for (const [providerId, result] of Object.entries(results)) {
|
||||
@@ -173,14 +174,16 @@ export function useProviderBalance() {
|
||||
return null
|
||||
}
|
||||
const data = result.data as Record<string, unknown>
|
||||
const extra = data.extra
|
||||
if (!extra || extra.balance === undefined || extra.points === undefined) {
|
||||
const extra = typeof data.extra === 'object' && data.extra !== null
|
||||
? data.extra as Record<string, unknown>
|
||||
: null
|
||||
if (!extra || typeof extra.balance !== 'number' || typeof extra.points !== 'number') {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
balance: extra.balance,
|
||||
points: extra.points,
|
||||
currency: data.currency || 'USD',
|
||||
currency: typeof data.currency === 'string' && data.currency ? data.currency : 'USD',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,13 +227,15 @@ export function useProviderBalance() {
|
||||
return null
|
||||
}
|
||||
const data = result.data as Record<string, unknown>
|
||||
const extra = data.extra
|
||||
if (!extra || extra.checkin_success === undefined) {
|
||||
const extra = typeof data.extra === 'object' && data.extra !== null
|
||||
? data.extra as Record<string, unknown>
|
||||
: null
|
||||
if (!extra || (extra.checkin_success !== null && typeof extra.checkin_success !== 'boolean')) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
success: extra.checkin_success,
|
||||
message: extra.checkin_message || '',
|
||||
message: typeof extra.checkin_message === 'string' ? extra.checkin_message : '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,13 +249,15 @@ export function useProviderBalance() {
|
||||
return null
|
||||
}
|
||||
const data = result.data as Record<string, unknown>
|
||||
const extra = data.extra
|
||||
const extra = typeof data.extra === 'object' && data.extra !== null
|
||||
? data.extra as Record<string, unknown>
|
||||
: null
|
||||
if (!extra || !extra.cookie_expired) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
expired: true,
|
||||
message: extra.cookie_expired_message || 'Cookie 已失效',
|
||||
message: typeof extra.cookie_expired_message === 'string' ? extra.cookie_expired_message : 'Cookie 已失效',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +303,9 @@ export function useProviderBalance() {
|
||||
}
|
||||
|
||||
const data = result.data as Record<string, unknown>
|
||||
const extra = data.extra
|
||||
const extra = typeof data.extra === 'object' && data.extra !== null
|
||||
? data.extra as Record<string, unknown>
|
||||
: null
|
||||
if (!extra) return []
|
||||
|
||||
// 从 schema 缓存中获取格式化配置
|
||||
@@ -319,6 +328,7 @@ export function useProviderBalance() {
|
||||
|
||||
// 组件卸载时清理
|
||||
function cleanup() {
|
||||
balanceLoadVersion++
|
||||
stopTick()
|
||||
pendingTimers.forEach(clearTimeout)
|
||||
pendingTimers.clear()
|
||||
|
||||
@@ -36,7 +36,10 @@ function providerModelsFetchResult(response: ProviderModelsQueryResponse): Fetch
|
||||
if (response.success && response.data?.models) {
|
||||
const partialWarning = response.data.warning ?? response.data.error
|
||||
return {
|
||||
models: response.data.models,
|
||||
models: response.data.models.map((model) => ({
|
||||
...model,
|
||||
api_formats: model.api_formats ?? (model.api_format ? [model.api_format] : []),
|
||||
})),
|
||||
warning: partialWarning ? parseUpstreamModelError(partialWarning) : undefined,
|
||||
fromCache: response.data.from_cache,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user