82a782550e
- 新增 usersecret 模块:外观专利/货源查询密钥从本地 localStorage 迁移至 biz_user_api_secret(AES 加密、按 uid 绑定) - 后台「密钥管理」页:脱敏展示、立即检测、清空;每日 04:30 分布式锁定时巡检 - 桌面端:密钥设置面板走服务端、未配齐引导 /setup-secrets、代理余量展示 - 删除专利汇令牌全链路与密钥保留时长选择器
133 lines
4.3 KiB
TypeScript
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', ' '), /密钥不能为空/)
|
|
})
|