Files
crawler-plugin/frontend-vue/tests/secret-store.test.ts
T
huangzd1997 82a782550e feat(密钥): 用户 API 密钥服务端化——V115 按账号绑定存储 + 后台密钥管理页 + 桌面端全站拦截与配置引导
- 新增 usersecret 模块:外观专利/货源查询密钥从本地 localStorage 迁移至 biz_user_api_secret(AES 加密、按 uid 绑定)
- 后台「密钥管理」页:脱敏展示、立即检测、清空;每日 04:30 分布式锁定时巡检
- 桌面端:密钥设置面板走服务端、未配齐引导 /setup-secrets、代理余量展示
- 删除专利汇令牌全链路与密钥保留时长选择器
2026-09-13 10:03:59 +08:00

133 lines
4.3 KiB
TypeScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { http } from '../src/shared/api/http.ts'
import {
clearApiSecretCache,
getStoredApiSecret,
getStoredApiSecretSnapshot,
loadApiSecrets,
saveApiSecret,
} from '../src/shared/utils/api-secret-store.ts'
type RequestConfig = { url?: string; method?: string; data?: unknown }
type MockTest = Parameters<typeof test>[1] extends (t: infer T) => unknown ? T : never
function createStorage() {
const store = new Map<string, string>()
return {
getItem: (key: string) => (store.has(key) ? (store.get(key) as string) : null),
setItem: (key: string, value: string) => {
store.set(key, String(value))
},
removeItem: (key: string) => {
store.delete(key)
},
}
}
function setupWindow() {
const localStorage = createStorage()
const sessionStorage = createStorage()
// 密钥按登录用户隔离:uid 必须存在(与真实前端 localStorage.uid 一致)
localStorage.setItem('uid', '42')
;(globalThis as Record<string, unknown>).window = {
localStorage,
sessionStorage,
location: { origin: 'http://localhost' },
}
return { localStorage, sessionStorage }
}
function mockRequest(t: MockTest, impl: (config: RequestConfig) => Promise<unknown>) {
t.mock.method(http, 'request', impl as never)
}
const okResponse = (data: unknown) => Promise.resolve({ data: { success: true, message: 'ok', data } })
function serverItem(moduleKey: string, overrides: Record<string, unknown> = {}) {
return {
moduleKey,
moduleLabel: moduleKey === 'appearance-patent' ? '外观专利密钥' : '货源查询密钥',
masked: '',
exists: false,
checkStatus: 'unknown',
checkCode: '',
checkMessage: '',
checkLatencyMs: null,
checkedAt: null,
updatedAt: null,
...overrides,
}
}
function bundleResponse(items: unknown[]) {
return okResponse({
items,
requiredModules: ['appearance-patent', 'similar-asin'],
complete: false,
})
}
const LEGACY_MIRROR_KEY = 'brand:api-secret:42:appearance-patent'
test('test_secret_store_reads_legacy_value_before_load', () => {
const { localStorage } = setupWindow()
clearApiSecretCache()
localStorage.setItem(LEGACY_MIRROR_KEY, JSON.stringify({ value: 'legacy-key-1', retention: 'forever' }))
assert.equal(getStoredApiSecret('appearance-patent'), 'legacy-key-1')
})
test('test_secret_store_load_failure_returns_unknown_and_keeps_cache', async (t) => {
const { localStorage } = setupWindow()
clearApiSecretCache()
localStorage.setItem(LEGACY_MIRROR_KEY, JSON.stringify({ value: 'legacy-key-2' }))
mockRequest(t, () => Promise.reject(new Error('服务不可用')))
const state = await loadApiSecrets({ force: true })
assert.equal(state, 'unknown')
assert.equal(getStoredApiSecret('appearance-patent'), 'legacy-key-2')
})
test('test_secret_store_migrates_legacy_value_once_and_clears_local', async (t) => {
const { localStorage } = setupWindow()
clearApiSecretCache()
localStorage.setItem(LEGACY_MIRROR_KEY, JSON.stringify({ value: 'legacy-key-3' }))
const calls: string[] = []
mockRequest(t, (config) => {
calls.push(config.url || '')
if ((config.url || '').includes('/migrate')) {
return okResponse({ migrated: 1 })
}
return bundleResponse([serverItem('appearance-patent'), serverItem('similar-asin')])
})
const state = await loadApiSecrets({ force: true })
assert.equal(state, 'incomplete')
assert.ok(calls.some((url) => url.includes('/api/user-secrets/migrate')), '应调用迁移接口')
assert.equal(localStorage.getItem(LEGACY_MIRROR_KEY), null, '迁移后应清理旧记录')
})
test('test_secret_store_save_keeps_plain_value_locally', async (t) => {
setupWindow()
clearApiSecretCache()
mockRequest(t, () =>
okResponse(serverItem('appearance-patent', { masked: 'sk-a****1234', exists: true })),
)
await saveApiSecret('appearance-patent', 'sk-abc123456')
assert.equal(getStoredApiSecret('appearance-patent'), 'sk-abc123456')
const snapshot = getStoredApiSecretSnapshot('appearance-patent')
assert.equal(snapshot.masked, 'sk-a****1234')
assert.equal(snapshot.exists, true)
})
test('test_secret_store_save_rejects_empty_value', async () => {
setupWindow()
clearApiSecretCache()
await assert.rejects(() => saveApiSecret('similar-asin', ' '), /密钥不能为空/)
})