feat(desktop): 支持桌面端三十天免登录
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DESKTOP_SERVER_URL_KEY } from "./desktopServerConfig";
|
||||
import {
|
||||
getSessionToken,
|
||||
initializeSecureSessionStorage,
|
||||
resetSecureSessionStorageForTests,
|
||||
setSessionToken,
|
||||
} from "./secureSessionStorage";
|
||||
|
||||
const invokeMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("@tauri-apps/api/core", () => ({
|
||||
invoke: invokeMock,
|
||||
}));
|
||||
|
||||
const SERVER_ORIGIN = "https://ctms.example.com/";
|
||||
const DESKTOP_SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const encodeJson = (value: unknown): string => Buffer.from(JSON.stringify(value)).toString("base64url");
|
||||
|
||||
const createJwt = (expiresAtMs: number): string =>
|
||||
`${encodeJson({ alg: "none", typ: "JWT" })}.${encodeJson({ exp: Math.floor(expiresAtMs / 1000) })}.signature`;
|
||||
|
||||
const createStorage = (): Storage => {
|
||||
const data = new Map<string, string>();
|
||||
return {
|
||||
get length() {
|
||||
return data.size;
|
||||
},
|
||||
clear: () => data.clear(),
|
||||
getItem: (key) => data.get(key) ?? null,
|
||||
key: (index) => Array.from(data.keys())[index] ?? null,
|
||||
removeItem: (key) => data.delete(key),
|
||||
setItem: (key, value) => {
|
||||
data.set(key, String(value));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe("secure session storage", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-07-02T00:00:00.000Z"));
|
||||
resetSecureSessionStorageForTests();
|
||||
Object.defineProperty(window, "localStorage", { value: createStorage(), configurable: true });
|
||||
localStorage.clear();
|
||||
localStorage.setItem(DESKTOP_SERVER_URL_KEY, SERVER_ORIGIN);
|
||||
Object.defineProperty(window, "__TAURI_INTERNALS__", { value: {}, configurable: true });
|
||||
invokeMock.mockReset();
|
||||
invokeMock.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
resetSecureSessionStorageForTests();
|
||||
localStorage.clear();
|
||||
Reflect.deleteProperty(window, "__TAURI_INTERNALS__");
|
||||
});
|
||||
|
||||
it("stores desktop tokens as a 30 day secure session record", async () => {
|
||||
const token = createJwt(Date.now() + DESKTOP_SESSION_MAX_AGE_MS);
|
||||
|
||||
await setSessionToken(token);
|
||||
|
||||
expect(invokeMock).toHaveBeenCalledWith("credential_set", {
|
||||
serverOrigin: SERVER_ORIGIN,
|
||||
token: expect.any(String),
|
||||
});
|
||||
const stored = JSON.parse(invokeMock.mock.calls[0][1].token);
|
||||
expect(stored).toMatchObject({ version: 1, token });
|
||||
expect(stored.expiresAt - stored.storedAt).toBe(DESKTOP_SESSION_MAX_AGE_MS);
|
||||
});
|
||||
|
||||
it("restores a valid desktop secure session record on startup", async () => {
|
||||
const token = createJwt(Date.now() + DESKTOP_SESSION_MAX_AGE_MS);
|
||||
invokeMock.mockImplementation(async (command: string) => {
|
||||
if (command === "credential_get") {
|
||||
return JSON.stringify({
|
||||
version: 1,
|
||||
token,
|
||||
storedAt: Date.now(),
|
||||
expiresAt: Date.now() + DESKTOP_SESSION_MAX_AGE_MS,
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await initializeSecureSessionStorage();
|
||||
|
||||
expect(getSessionToken()).toBe(token);
|
||||
expect(invokeMock).toHaveBeenCalledWith("credential_get", { serverOrigin: SERVER_ORIGIN });
|
||||
});
|
||||
|
||||
it("deletes an expired desktop secure session record on startup", async () => {
|
||||
const token = createJwt(Date.now() + DESKTOP_SESSION_MAX_AGE_MS);
|
||||
invokeMock.mockImplementation(async (command: string) => {
|
||||
if (command === "credential_get") {
|
||||
return JSON.stringify({
|
||||
version: 1,
|
||||
token,
|
||||
storedAt: Date.now() - DESKTOP_SESSION_MAX_AGE_MS - 1_000,
|
||||
expiresAt: Date.now() - 1_000,
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await initializeSecureSessionStorage();
|
||||
|
||||
expect(getSessionToken()).toBeNull();
|
||||
expect(invokeMock).toHaveBeenCalledWith("credential_delete", { serverOrigin: SERVER_ORIGIN });
|
||||
});
|
||||
|
||||
it("rewrites a legacy raw desktop token into a secure session record", async () => {
|
||||
const token = createJwt(Date.now() + DESKTOP_SESSION_MAX_AGE_MS);
|
||||
invokeMock.mockImplementation(async (command: string) => {
|
||||
if (command === "credential_get") return token;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await initializeSecureSessionStorage();
|
||||
|
||||
expect(getSessionToken()).toBe(token);
|
||||
expect(invokeMock).toHaveBeenCalledWith("credential_set", {
|
||||
serverOrigin: SERVER_ORIGIN,
|
||||
token: expect.stringContaining(token),
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user