import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { consumeWorksSquareStartupRuntimeCleanupRequired, flushWorksSquareSessionPersistence, getValidWorksSquareAccessToken, getWorksSquareSessionSnapshot, initializeWorksSquareSession, markWorksSquareSessionActive, resetWorksSquareSessionForTests, storeWorksSquareSession, subscribeWorksSquareSession, WORKS_SQUARE_SESSION_IDLE_TIMEOUT_MS, type WorksSquareSessionInput, type WorksSquareSessionPersistence, } from '@electron/services/works-square-session'; const DAY_MS = 24 * 60 * 60 * 1000; 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://biz.nianxx.cn/auth/oauth2/token', expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ Authorization: `Basic ${Buffer.from('app:app').toString('base64')}`, 'Content-Type': 'application/x-www-form-urlencoded', }), body: expect.any(URLSearchParams), }), ); const body = fetchImpl.mock.calls[0][1].body as URLSearchParams; expect(String(body)).toBe('grant_type=refresh_token&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('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 }); const body = secondFetch.mock.calls[0][1].body as URLSearchParams; expect(String(body)).toContain('refresh_token=rotated-refresh-token'); }); });