实现客户端登录七天滑动续期
需求:解决短效访问令牌到期后客户端一小时掉登录的问题。 实现:由 Electron Main 加密管理并轮换刷新凭据,按真实用户活动续期,七天闲置后清理会话,并补齐并发、迁移和终态回归测试。
This commit is contained in:
100
tests/unit/works-square-session-persistence.test.ts
Normal file
100
tests/unit/works-square-session-persistence.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const fakeStore = vi.hoisted(() => ({
|
||||
options: null as Record<string, unknown> | null,
|
||||
values: new Map<string, unknown>(),
|
||||
constructorError: null as Error | null,
|
||||
}));
|
||||
|
||||
vi.mock('electron-store', () => ({
|
||||
default: class FakeElectronStore {
|
||||
constructor(options: Record<string, unknown>) {
|
||||
fakeStore.options = options;
|
||||
if (fakeStore.constructorError && options.clearInvalidConfig !== true) {
|
||||
throw fakeStore.constructorError;
|
||||
}
|
||||
}
|
||||
|
||||
get(key: string) {
|
||||
return fakeStore.values.get(key);
|
||||
}
|
||||
|
||||
set(key: string, value: unknown) {
|
||||
fakeStore.values.set(key, value);
|
||||
}
|
||||
|
||||
delete(key: string) {
|
||||
fakeStore.values.delete(key);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Works Square encrypted session persistence', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
fakeStore.options = null;
|
||||
fakeStore.values.clear();
|
||||
fakeStore.constructorError = null;
|
||||
});
|
||||
|
||||
it('stores one encrypted Main-only record and restores it', async () => {
|
||||
const firstModule = await import('@electron/services/works-square-session');
|
||||
await firstModule.initializeWorksSquareSession();
|
||||
firstModule.storeWorksSquareSession({
|
||||
accessToken: 'plain-access-token',
|
||||
refreshToken: 'plain-refresh-token',
|
||||
expiresAt: Date.now() + 3600_000,
|
||||
lastActiveAt: Date.now(),
|
||||
});
|
||||
await firstModule.flushWorksSquareSessionPersistence();
|
||||
|
||||
expect(fakeStore.options).toMatchObject({
|
||||
name: 'works-square-session',
|
||||
configFileMode: 0o600,
|
||||
});
|
||||
const serializedStore = JSON.stringify(Object.fromEntries(fakeStore.values));
|
||||
expect(serializedStore).not.toContain('plain-access-token');
|
||||
expect(serializedStore).not.toContain('plain-refresh-token');
|
||||
|
||||
vi.resetModules();
|
||||
const restoredModule = await import('@electron/services/works-square-session');
|
||||
await expect(restoredModule.initializeWorksSquareSession()).resolves.toMatchObject({
|
||||
accessToken: 'plain-access-token',
|
||||
canRefresh: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves a temporarily unreadable record until explicit reauthorization discards it', async () => {
|
||||
const record = {
|
||||
version: 1,
|
||||
authBase: 'https://biz.nianxx.cn/auth/',
|
||||
ciphertext: Buffer.from('encrypted:not-json', 'utf8').toString('base64'),
|
||||
};
|
||||
fakeStore.values.set('record', record);
|
||||
|
||||
const sessionModule = await import('@electron/services/works-square-session');
|
||||
await expect(sessionModule.initializeWorksSquareSession()).resolves.toBeNull();
|
||||
|
||||
expect(fakeStore.values.get('record')).toEqual(record);
|
||||
expect(sessionModule.getWorksSquareSessionRestoreStatus()).toBe('unavailable');
|
||||
|
||||
await expect(sessionModule.discardUnrestorableWorksSquareSession()).resolves.toBe(true);
|
||||
expect(fakeStore.values.has('record')).toBe(false);
|
||||
expect(sessionModule.getWorksSquareSessionRestoreStatus()).toBe('ready');
|
||||
});
|
||||
|
||||
it('discards malformed store config without aborting Main startup', async () => {
|
||||
fakeStore.constructorError = new SyntaxError('Unexpected token in JSON');
|
||||
|
||||
const sessionModule = await import('@electron/services/works-square-session');
|
||||
await expect(sessionModule.initializeWorksSquareSession()).resolves.toBeNull();
|
||||
|
||||
expect(fakeStore.options).toMatchObject({
|
||||
name: 'works-square-session',
|
||||
configFileMode: 0o600,
|
||||
clearInvalidConfig: true,
|
||||
});
|
||||
expect(sessionModule.getWorksSquareSessionRestoreStatus()).toBe('ready');
|
||||
expect(sessionModule.consumeWorksSquareStartupRuntimeCleanupRequired()).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user