Files
eSIM-Tools/tests/modules/secure-storage.test.js
Abner b5ec8eb18a 🐛 fix: 修复受限环境下 secure-storage 触发 sessionStorage SecurityError
- 新增 safeGetStorage 探测能力探针,捕获 sessionStorage/localStorage 读取与写入探测时抛出的 SecurityError,返回可用的 Storage 或 null
- 将顶层敏感数据迁移逻辑外层包裹 try-catch,避免 iframe/sandbox/隐私设置等受限上下文导致模块初始化阶段未捕获异常中断
- 在 migrateFromLocalStorage 中基于 safeGetStorage 动态获取 localStorage,localStorage 不可用时直接跳过迁移而非抛错
- 补充受限环境降级单元测试:模拟 sessionStorage/localStorage 属性读取被拒、以及 sessionStorage 写入失败时确保降级到 fallbackStorage 且不抛异常
2026-05-08 18:33:59 +08:00

209 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* SecureStorage 模块单元测试
* 覆盖安全存储、TTL 过期、降级存储等核心功能
*/
import secureStorage from '../../src/js/modules/secure-storage';
describe('SecureStorage', () => {
let store;
let realStore;
beforeEach(() => {
store = secureStorage;
// 创建真实的内存存储来模拟 sessionStorage 行为
realStore = {};
// 用带实际存储行为的 mock 替换 sessionStorage
const mockStorage = {
getItem: jest.fn((key) => realStore[key] || null),
setItem: jest.fn((key, value) => { realStore[key] = value; }),
removeItem: jest.fn((key) => { delete realStore[key]; }),
clear: jest.fn(() => { Object.keys(realStore).forEach(k => delete realStore[k]); }),
};
store.storage = mockStorage;
store.fallbackStorage.clear();
});
describe('setItem() / getItem()', () => {
it('应该存储和读取数据', () => {
store.setItem('key1', 'value1');
expect(store.storage.setItem).toHaveBeenCalled();
expect(store.getItem('key1')).toBe('value1');
});
it('应该存储对象类型数据', () => {
const obj = { a: 1, b: 'test' };
store.setItem('obj', obj);
expect(store.getItem('obj')).toEqual(obj);
});
it('应该在 sessionStorage 不可用时降级到内存存储', () => {
store.storage = null;
store.setItem('key', 'value');
expect(store.fallbackStorage.has('key')).toBe(true);
expect(store.getItem('key')).toBe('value');
});
it('应该在 JSON.stringify 失败时降级到内存存储', () => {
const circular = {};
circular.self = circular;
store.setItem('circular', circular);
expect(store.fallbackStorage.has('circular')).toBe(true);
});
});
describe('TTL 过期机制', () => {
it('应该在数据过期后返回 null', () => {
const expiredData = {
value: 'old',
expires: Date.now() - 1000,
timestamp: Date.now() - 10000
};
realStore['expired'] = JSON.stringify(expiredData);
const result = store.getItem('expired');
expect(result).toBeNull();
});
it('应该在数据未过期时返回值', () => {
const validData = {
value: 'fresh',
expires: Date.now() + 60000,
timestamp: Date.now()
};
realStore['fresh'] = JSON.stringify(validData);
expect(store.getItem('fresh')).toBe('fresh');
});
it('应该使用自定义 TTL', () => {
store.setItem('short', 'ttl', 1000);
expect(store.storage.setItem).toHaveBeenCalled();
});
});
describe('removeItem()', () => {
it('应该从 sessionStorage 移除数据', () => {
store.removeItem('key');
expect(store.storage.removeItem).toHaveBeenCalledWith('key');
});
it('应该从 fallbackStorage 移除数据', () => {
store.storage = null;
store.setItem('key', 'value');
store.removeItem('key');
expect(store.fallbackStorage.has('key')).toBe(false);
});
});
describe('clear()', () => {
it('应该清除所有数据', () => {
store.clear();
expect(store.storage.clear).toHaveBeenCalled();
expect(store.fallbackStorage.size).toBe(0);
});
});
describe('has()', () => {
it('应该返回 true 如果数据存在', () => {
const validData = {
value: 'exists',
expires: Date.now() + 60000,
timestamp: Date.now()
};
realStore['exists'] = JSON.stringify(validData);
expect(store.has('exists')).toBe(true);
});
it('应该返回 false 如果数据不存在', () => {
expect(store.has('nope')).toBe(false);
});
});
describe('getItem() 异常处理', () => {
it('应该在 JSON.parse 失败时返回 null', () => {
realStore['bad'] = 'invalid json{{{';
expect(store.getItem('bad')).toBeNull();
});
});
describe('受限环境降级', () => {
let originalSessionStorage;
let originalLocalStorage;
beforeEach(() => {
originalSessionStorage = Object.getOwnPropertyDescriptor(window, 'sessionStorage');
originalLocalStorage = Object.getOwnPropertyDescriptor(window, 'localStorage');
});
afterEach(() => {
if (originalSessionStorage) {
Object.defineProperty(window, 'sessionStorage', originalSessionStorage);
} else {
delete window.sessionStorage;
}
if (originalLocalStorage) {
Object.defineProperty(window, 'localStorage', originalLocalStorage);
} else {
delete window.localStorage;
}
});
it('应该在 sessionStorage 访问被拒时降级到内存存储', () => {
// 模拟 SecurityError属性读取抛异常
Object.defineProperty(window, 'sessionStorage', {
get() {
throw new DOMException('Access is denied', 'SecurityError');
},
configurable: true
});
// 直接构造新实例,构造函数内部调用 safeGetStorage 会捕获异常
const SecureStorageClass = store.constructor;
const restricted = new SecureStorageClass();
expect(restricted.storage).toBeNull();
restricted.setItem('key', 'value');
expect(restricted.getItem('key')).toBe('value');
expect(restricted.fallbackStorage.has('key')).toBe(true);
});
it('应该在 localStorage 访问被拒时跳过迁移', () => {
Object.defineProperty(window, 'localStorage', {
get() {
throw new DOMException('Access is denied', 'SecurityError');
},
configurable: true
});
// migrateFromLocalStorage 内部调用 safeGetStorage 会捕获异常
expect(() => store.migrateFromLocalStorage('any_key')).not.toThrow();
});
it('应该在 sessionStorage 写入失败时降级到内存', () => {
// 模拟 sessionStorage.setItem 拒绝写入
store.storage = {
getItem: jest.fn(() => null),
setItem: jest.fn(() => {
throw new DOMException('Quota exceeded', 'QuotaExceededError');
}),
removeItem: jest.fn()
};
// 迁移数据localStorage 读取成功,但 sessionStorage 写入失败
const local = {
getItem: jest.fn(() => 'old_value'),
setItem: jest.fn(),
removeItem: jest.fn()
};
Object.defineProperty(window, 'localStorage', {
get: () => local,
configurable: true
});
store.migrateFromLocalStorage('test_key');
// 数据应降级到 fallbackStorage
expect(store.fallbackStorage.has('test_key')).toBe(true);
});
});
});