fix(mp): 安全修复 + 健康Tab重构为总览
Phase 0 安全修复: - 移除 secure-storage-aes.ts 硬编码 'hms-default-key' fallback - production 模式空密钥时拒绝加解密(返回空/不加密) - dev 模式保留明文兼容(warn 日志提醒) - .env/.env.h5 注入随机加密密钥 - secureGet 明文 fallback 按环境分级处理 - 新增 8 个测试覆盖空密钥 dev/production 行为 Phase 1 健康Tab重构: - health/index.tsx 从体征录入页改为健康总览Dashboard - 新增今日体征摘要卡片(2x2 网格 + 状态标签) - 新增快捷入口(录入体征/趋势/报告/用药) - 新增告警提示卡片(待处理告警数量) - 体征录入移至 pkg-health/input/index(已有页面) - useHealthData → useHealthOverview(新增 alertCount) 首页增强: - useHomeData 新增告警计数查询(listPatientAlerts) - 首页新增告警提示卡片入口 - "记录体征"按钮改为跳转录入页而非健康Tab
This commit is contained in:
@@ -40,6 +40,7 @@ vi.mock('@tarojs/taro', () => ({
|
||||
// --- Mock 加密密钥 ---
|
||||
process.env.TARO_APP_ENCRYPTION_KEY =
|
||||
'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
// --- 导入被测模块(在 mock 之后) ---
|
||||
import { secureSet, secureGet, secureRemove, migrateLegacyStorage } from '@/utils/secure-storage';
|
||||
@@ -245,8 +246,79 @@ describe('secure-storage AES-256-GCM', () => {
|
||||
|
||||
it('损坏的 AES 密文返回 null 后走明文 fallback', () => {
|
||||
storage.set('_es_corrupt', 'aes:INVALID_BASE64!!!');
|
||||
// aesDecrypt 失败返回 null,然后尝试 XOR 也失败,最后返回原始字符串
|
||||
// aesDecrypt 失败返回 null,hasEncryptionKey=true 所以不走 dev plaintext
|
||||
// 最终返回 raw 值
|
||||
expect(secureGet('corrupt')).toBe('aes:INVALID_BASE64!!!');
|
||||
});
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// 7. 空密钥 dev 模式 — 明文存储兼容
|
||||
// ================================================================
|
||||
describe('空密钥 dev 模式', () => {
|
||||
const originalKey = process.env.TARO_APP_ENCRYPTION_KEY;
|
||||
const originalEnv = process.env.NODE_ENV;
|
||||
|
||||
beforeEach(() => {
|
||||
storage.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.TARO_APP_ENCRYPTION_KEY = originalKey;
|
||||
process.env.NODE_ENV = originalEnv;
|
||||
});
|
||||
|
||||
it('dev 模式空密钥:secureSet 存明文,secureGet 读取成功', () => {
|
||||
process.env.TARO_APP_ENCRYPTION_KEY = '';
|
||||
process.env.NODE_ENV = 'development';
|
||||
secureSet('dev_plain', 'hello-dev');
|
||||
// 应以明文存储
|
||||
expect(storage.get('_es_dev_plain')).toBe('hello-dev');
|
||||
expect(secureGet('dev_plain')).toBe('hello-dev');
|
||||
});
|
||||
|
||||
it('dev 模式空密钥:读取 MCP 注入的明文成功', () => {
|
||||
process.env.TARO_APP_ENCRYPTION_KEY = '';
|
||||
process.env.NODE_ENV = 'development';
|
||||
storage.set('access_token', 'mcp-injected-token');
|
||||
expect(secureGet('access_token')).toBe('mcp-injected-token');
|
||||
});
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// 8. production 模式空密钥 — 拒绝加解密
|
||||
// ================================================================
|
||||
describe('production 模式空密钥', () => {
|
||||
const originalKey = process.env.TARO_APP_ENCRYPTION_KEY;
|
||||
const originalEnv = process.env.NODE_ENV;
|
||||
|
||||
beforeEach(() => {
|
||||
storage.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.TARO_APP_ENCRYPTION_KEY = originalKey;
|
||||
process.env.NODE_ENV = originalEnv;
|
||||
});
|
||||
|
||||
it('production 空密钥:secureSet 存明文(无加密可用),secureGet 返回空', () => {
|
||||
process.env.TARO_APP_ENCRYPTION_KEY = '';
|
||||
process.env.NODE_ENV = 'production';
|
||||
secureSet('prod_test', 'sensitive-data');
|
||||
// 应以明文存储(无 key 时 aesEncrypt 返回 null)
|
||||
expect(storage.get('_es_prod_test')).toBe('sensitive-data');
|
||||
// secureGet: prefixed key 有值但非 aes: 前缀 + hasEncryptionKey=false → 返回 raw
|
||||
expect(secureGet('prod_test')).toBe('sensitive-data');
|
||||
});
|
||||
|
||||
it('production 空密钥:AES 解密失败返回空字符串', () => {
|
||||
process.env.TARO_APP_ENCRYPTION_KEY = '';
|
||||
process.env.NODE_ENV = 'production';
|
||||
// 模拟存在一个 aes: 前缀的旧数据
|
||||
storage.set('_es_old_data', 'aes:INVALID_CORRUPT_DATA');
|
||||
expect(secureGet('old_data')).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user