import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { consumeWorksSquareStartupRuntimeCleanupRequired, clearWorksSquareSession, flushWorksSquareSessionPersistence, getValidWorksSquareAccessToken, getWorksSquareAccountBinding, getWorksSquareSessionSnapshot, initializeWorksSquareSession, markWorksSquareSessionActive, resetWorksSquareSessionForTests, storeWorksSquareSession, storeWorksSquareSessionFromTokenPayload, subscribeWorksSquareSession, WORKS_SQUARE_SESSION_IDLE_TIMEOUT_MS, type WorksSquareSessionInput, type WorksSquareSessionPersistence, } from '@electron/services/works-square-session'; const DAY_MS = 24 * 60 * 60 * 1000; function jwt(claims: Record): string { return `header.${Buffer.from(JSON.stringify(claims)).toString('base64url')}.signature`; } describe('works-square-session service', () => { beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-07-06T08:00:00.000Z')); resetWorksSquareSessionForTests(); }); afterEach(() => { vi.useRealTimers(); resetWorksSquareSessionForTests(); }); it('returns the cached access token while it is outside the refresh skew', async () => { storeWorksSquareSession({ accessToken: 'access-token', refreshToken: 'refresh-token', tokenType: 'Bearer', expiresAt: Date.now() + 120_000, }); await expect(getValidWorksSquareAccessToken()).resolves.toBe('access-token'); expect(getWorksSquareSessionSnapshot()).toMatchObject({ accessToken: 'access-token', tokenType: 'Bearer', expiresAt: Date.now() + 120_000, canRefresh: true, }); }); it('refreshes the access token when the cached session is near expiry', async () => { const fetchImpl = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ access_token: 'new-access-token', refresh_token: 'new-refresh-token', token_type: 'Bearer', expires_in: 43200, }), { status: 200 }), ); storeWorksSquareSession({ accessToken: 'old-access-token', refreshToken: 'old-refresh-token', tokenType: 'Bearer', expiresAt: Date.now() + 10_000, }); await expect(getValidWorksSquareAccessToken({ fetchImpl })).resolves.toBe('new-access-token'); expect(fetchImpl).toHaveBeenCalledWith( 'https://square.nianxx.cn/api/auth/refresh', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: 'old-refresh-token' }), }), ); expect(getWorksSquareSessionSnapshot()).toMatchObject({ accessToken: 'new-access-token', tokenType: 'Bearer', expiresAt: Date.now() + 43_200_000, canRefresh: true, }); }); it('returns null and clears the cached access token when refresh fails with 401', async () => { const fetchImpl = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ msg: 'invalid refresh token' }), { status: 401 }), ); storeWorksSquareSession({ accessToken: 'old-access-token', refreshToken: 'old-refresh-token', tokenType: 'Bearer', expiresAt: Date.now() + 10_000, }); await expect(getValidWorksSquareAccessToken({ fetchImpl })).resolves.toBeNull(); expect(getWorksSquareSessionSnapshot()).toBeNull(); }); it('keeps the refresh token after a temporary OAuth failure', async () => { const fetchImpl = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ msg: 'temporarily unavailable' }), { status: 503 }), ); storeWorksSquareSession({ accessToken: 'old-access-token', refreshToken: 'refresh-token-for-retry', expiresAt: Date.now() + 10_000, }); await expect(getValidWorksSquareAccessToken({ fetchImpl })).resolves.toBeNull(); expect(getWorksSquareSessionSnapshot()).toMatchObject({ accessToken: 'old-access-token', canRefresh: true, }); }); it('keeps renewing when the user is active every day', async () => { const startedAt = Date.now(); let refreshIndex = 0; const fetchImpl = vi.fn().mockImplementation(async () => { refreshIndex += 1; return new Response(JSON.stringify({ access_token: `access-token-${refreshIndex}`, refresh_token: `refresh-token-${refreshIndex}`, token_type: 'Bearer', expires_in: 3600, }), { status: 200 }); }); storeWorksSquareSession({ accessToken: 'initial-access-token', refreshToken: 'initial-refresh-token', tokenType: 'Bearer', expiresAt: startedAt + 3600_000, lastActiveAt: startedAt, }); for (let day = 1; day <= 8; day += 1) { const nowMs = startedAt + day * DAY_MS; vi.setSystemTime(nowMs); await expect(markWorksSquareSessionActive(nowMs)).resolves.toMatchObject({ lastActiveAt: nowMs, }); await expect(getValidWorksSquareAccessToken({ fetchImpl, nowMs, })).resolves.toBe(`access-token-${day}`); expect(getWorksSquareSessionSnapshot()?.lastActiveAt).toBe(nowMs); } expect(fetchImpl).toHaveBeenCalledTimes(8); expect(getWorksSquareSessionSnapshot()?.canRefresh).toBe(true); }); it('requires authorization after seven days without user activity', async () => { const lastActiveAt = Date.now(); const fetchImpl = vi.fn(); storeWorksSquareSession({ accessToken: 'access-token', refreshToken: 'refresh-token', expiresAt: lastActiveAt + 3600_000, lastActiveAt, }); const nowMs = lastActiveAt + WORKS_SQUARE_SESSION_IDLE_TIMEOUT_MS; vi.setSystemTime(nowMs); await expect(getValidWorksSquareAccessToken({ fetchImpl, nowMs, })).resolves.toBeNull(); expect(fetchImpl).not.toHaveBeenCalled(); expect(getWorksSquareSessionSnapshot()).toBeNull(); }); it('marks an idle session clear as terminal for Main runtime cleanup', async () => { const listener = vi.fn(); subscribeWorksSquareSession(listener); const lastActiveAt = Date.now(); storeWorksSquareSession({ accessToken: 'access-token', refreshToken: 'refresh-token', expiresAt: lastActiveAt + 3600_000, lastActiveAt, }); listener.mockClear(); await getValidWorksSquareAccessToken({ nowMs: lastActiveAt + WORKS_SQUARE_SESSION_IDLE_TIMEOUT_MS, }); expect(listener).toHaveBeenCalledWith(null, 'terminal', expect.objectContaining({ accessToken: 'access-token', })); }); it('requests one startup runtime cleanup when a persisted session is idle', async () => { const persistence: WorksSquareSessionPersistence = { load: vi.fn().mockResolvedValue({ accessToken: 'stale-access-token', refreshToken: 'stale-refresh-token', expiresAt: Date.now() - 1, lastActiveAt: Date.now() - WORKS_SQUARE_SESSION_IDLE_TIMEOUT_MS, }), save: vi.fn().mockResolvedValue(undefined), }; await initializeWorksSquareSession({ persistence }); expect(consumeWorksSquareStartupRuntimeCleanupRequired()).toBe(true); expect(consumeWorksSquareStartupRuntimeCleanupRequired()).toBe(false); }); it('does not write an unnecessary clear when no persisted Main session exists', async () => { const persistence: WorksSquareSessionPersistence = { load: vi.fn().mockResolvedValue(null), save: vi.fn().mockRejectedValue(new Error('credential store locked')), }; await expect(initializeWorksSquareSession({ persistence })).resolves.toBeNull(); expect(persistence.save).not.toHaveBeenCalled(); }); it('does not count a background refresh as user activity', async () => { const lastActiveAt = Date.now() - DAY_MS; const fetchImpl = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ access_token: 'new-access-token', refresh_token: 'new-refresh-token', token_type: 'Bearer', expires_in: 3600, }), { status: 200 }), ); storeWorksSquareSession({ accessToken: 'old-access-token', refreshToken: 'old-refresh-token', expiresAt: Date.now() + 10_000, lastActiveAt, }); await expect(getValidWorksSquareAccessToken({ fetchImpl })).resolves.toBe('new-access-token'); expect(getWorksSquareSessionSnapshot()).toMatchObject({ canRefresh: true, lastActiveAt, }); }); it('coalesces concurrent refreshes into one OAuth request', async () => { let resolveRefresh!: (response: Response) => void; const fetchImpl = vi.fn().mockImplementationOnce(() => new Promise((resolve) => { resolveRefresh = resolve; })); storeWorksSquareSession({ accessToken: 'old-access-token', refreshToken: 'old-refresh-token', expiresAt: Date.now() + 10_000, }); const first = getValidWorksSquareAccessToken({ fetchImpl }); const second = getValidWorksSquareAccessToken({ fetchImpl }); expect(fetchImpl).toHaveBeenCalledOnce(); resolveRefresh(new Response(JSON.stringify({ access_token: 'new-access-token', refresh_token: 'new-refresh-token', token_type: 'Bearer', expires_in: 3600, }), { status: 200 })); await expect(Promise.all([first, second])).resolves.toEqual([ 'new-access-token', 'new-access-token', ]); expect(fetchImpl).toHaveBeenCalledOnce(); }); it('expires a stuck shared refresh and permits the next refresh attempt', async () => { let requestSignal: AbortSignal | null = null; const fetchImpl = vi.fn((_input, init) => { requestSignal = init?.signal ?? null; return new Promise(() => undefined); }); storeWorksSquareSession({ accessToken: 'old-access-token', refreshToken: 'old-refresh-token', expiresAt: Date.now() + 10_000, }); const first = getValidWorksSquareAccessToken({ fetchImpl, requestTimeoutMs: 25 }); const second = getValidWorksSquareAccessToken({ fetchImpl, requestTimeoutMs: 25 }); const outcomes = Promise.allSettled([first, second]); await vi.advanceTimersByTimeAsync(25); await expect(Promise.race([ outcomes, Promise.resolve('pending'), ])).resolves.toEqual([ expect.objectContaining({ status: 'rejected', reason: expect.objectContaining({ name: 'RequestDeadlineExceededError' }), }), expect.objectContaining({ status: 'rejected', reason: expect.objectContaining({ name: 'RequestDeadlineExceededError' }), }), ]); expect(requestSignal?.aborted).toBe(true); expect(fetchImpl).toHaveBeenCalledOnce(); fetchImpl.mockResolvedValueOnce(new Response(JSON.stringify({ access_token: 'new-access-token', refresh_token: 'new-refresh-token', token_type: 'Bearer', expires_in: 3600, }), { status: 200 })); await expect(getValidWorksSquareAccessToken({ fetchImpl, requestTimeoutMs: 25, })).resolves.toBe('new-access-token'); expect(fetchImpl).toHaveBeenCalledTimes(2); }); it('still allows refresh one millisecond before the seven-day boundary', async () => { const lastActiveAt = Date.now(); const nowMs = lastActiveAt + WORKS_SQUARE_SESSION_IDLE_TIMEOUT_MS - 1; const fetchImpl = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ access_token: 'renewed-access-token', refresh_token: 'renewed-refresh-token', token_type: 'Bearer', expires_in: 3600, }), { status: 200 }), ); storeWorksSquareSession({ accessToken: 'expired-access-token', refreshToken: 'refresh-token', expiresAt: lastActiveAt + 3600_000, lastActiveAt, }); vi.setSystemTime(nowMs); await expect(getValidWorksSquareAccessToken({ fetchImpl, nowMs })).resolves.toBe( 'renewed-access-token', ); expect(fetchImpl).toHaveBeenCalledOnce(); }); it('does not let a stale refresh failure clear a newer login', async () => { let resolveOldRefresh!: (response: Response) => void; const fetchImpl = vi.fn().mockImplementationOnce(() => new Promise((resolve) => { resolveOldRefresh = resolve; })); storeWorksSquareSession({ accessToken: 'old-access-token', refreshToken: 'old-refresh-token', expiresAt: Date.now() + 10_000, }); const oldRefresh = getValidWorksSquareAccessToken({ fetchImpl }); storeWorksSquareSession({ accessToken: 'new-login-access-token', refreshToken: 'new-login-refresh-token', expiresAt: Date.now() + 3600_000, }); resolveOldRefresh(new Response(JSON.stringify({ error: 'invalid_grant' }), { status: 401 })); await expect(oldRefresh).resolves.toBeNull(); expect(getWorksSquareSessionSnapshot()).toMatchObject({ accessToken: 'new-login-access-token', canRefresh: true, }); }); it('does not return a newer login token to an older in-flight refresh', async () => { let resolveOldRefresh!: (response: Response) => void; const fetchImpl = vi.fn().mockImplementationOnce(() => new Promise((resolve) => { resolveOldRefresh = resolve; })); storeWorksSquareSession({ accessToken: 'old-access-token', refreshToken: 'old-refresh-token', expiresAt: Date.now() + 10_000, }); const oldRefresh = getValidWorksSquareAccessToken({ fetchImpl }); storeWorksSquareSession({ accessToken: 'new-login-access-token', refreshToken: 'new-login-refresh-token', expiresAt: Date.now() + 3600_000, }); resolveOldRefresh(new Response(JSON.stringify({ access_token: 'rotated-old-access-token', refresh_token: 'rotated-old-refresh-token', expires_in: 3600, }), { status: 200 })); await expect(oldRefresh).resolves.toBeNull(); expect(getWorksSquareSessionSnapshot()).toMatchObject({ accessToken: 'new-login-access-token', canRefresh: true, }); }); it('persists a rotated refresh token before returning and restores it after restart', async () => { let persisted: WorksSquareSessionInput | null = null; const persistence: WorksSquareSessionPersistence = { load: vi.fn(async () => persisted), save: vi.fn(async (session) => { persisted = session ? { ...session } : null; }), }; await initializeWorksSquareSession({ persistence }); storeWorksSquareSession({ accessToken: 'old-access-token', refreshToken: 'old-refresh-token', expiresAt: Date.now() + 10_000, }); await flushWorksSquareSessionPersistence(); const firstFetch = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ access_token: 'rotated-access-token', refresh_token: 'rotated-refresh-token', token_type: 'Bearer', expires_in: 3600, }), { status: 200 }), ); await expect(getValidWorksSquareAccessToken({ fetchImpl: firstFetch })).resolves.toBe( 'rotated-access-token', ); expect(persisted).toMatchObject({ refreshToken: 'rotated-refresh-token' }); resetWorksSquareSessionForTests(); await initializeWorksSquareSession({ persistence }); const secondFetch = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ access_token: 'after-restart-access-token', refresh_token: 'after-restart-refresh-token', token_type: 'Bearer', expires_in: 3600, }), { status: 200 }), ); await getValidWorksSquareAccessToken({ fetchImpl: secondFetch, forceRefresh: true }); expect(secondFetch.mock.calls[0][1].body).toBe(JSON.stringify({ refresh_token: 'rotated-refresh-token', })); }); it('uses user_id ahead of username for a stable opaque account partition', () => { storeWorksSquareSessionFromTokenPayload({ access_token: 'access-a', user_id: 'raw-user-123', username: 'first@example.com', expires_in: 3600, }); const first = getWorksSquareAccountBinding(); storeWorksSquareSessionFromTokenPayload({ access_token: 'access-b', user_id: 'raw-user-123', username: 'renamed@example.com', expires_in: 3600, }); const second = getWorksSquareAccountBinding(); expect(first?.accountKey).toMatch(/^[0-9a-f]{64}$/); expect(second?.accountKey).toBe(first?.accountKey); expect(second?.accountKey).not.toContain('raw-user-123'); expect(second?.epoch).toBe(first?.epoch); }); it('retains the account partition across refresh, clears it on logout, and changes it on account switch', async () => { storeWorksSquareSessionFromTokenPayload({ access_token: 'old-a', refresh_token: 'refresh-a', user_id: 'account-a', expires_in: 1, }); const accountA = getWorksSquareAccountBinding(); const fetchImpl = vi.fn().mockResolvedValue(new Response(JSON.stringify({ access_token: 'refreshed-a', refresh_token: 'refresh-a-2', user_id: 'unexpected-account-b', expires_in: 3600, }), { status: 200 })); await getValidWorksSquareAccessToken({ fetchImpl, forceRefresh: true }); expect(getWorksSquareAccountBinding()).toEqual(accountA); clearWorksSquareSession(); expect(getWorksSquareAccountBinding()).toBeNull(); storeWorksSquareSessionFromTokenPayload({ access_token: 'access-b', user_id: 'account-b', expires_in: 3600, }); expect(getWorksSquareAccountBinding()?.accountKey).not.toBe(accountA?.accountKey); expect(getWorksSquareAccountBinding()?.epoch).toBeGreaterThan(accountA?.epoch ?? 0); }); it('migrates a legacy persisted Renderer session from safe JWT identity claims', async () => { const persistence: WorksSquareSessionPersistence = { load: vi.fn().mockResolvedValue({ accessToken: jwt({ user_id: 'legacy-user', username: 'legacy@example.com' }), refreshToken: 'legacy-refresh', expiresAt: Date.now() + 3600_000, lastActiveAt: Date.now(), }), save: vi.fn().mockResolvedValue(undefined), }; await initializeWorksSquareSession({ persistence }); const binding = getWorksSquareAccountBinding(); expect(binding?.accountKey).toMatch(/^[0-9a-f]{64}$/); expect(binding?.accountKey).not.toContain('legacy-user'); expect(persistence.save).toHaveBeenCalledWith(expect.objectContaining({ accountPartitionKey: binding?.accountKey, })); }); });