import { EventEmitter } from 'node:events'; import type { IncomingMessage, ServerResponse } from 'http'; import { shell } from 'electron'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { handleAuthRoutes } from '@electron/api/routes/auth'; import { commitWorksSquareSession, flushWorksSquareSessionPersistence, getWorksSquareSessionSnapshot, initializeWorksSquareSession, resetWorksSquareSessionForTests, storeWorksSquareSession, } from '@electron/services/works-square-session'; import { resetManagedWorksSquareRuntimeForTests } from '@electron/services/works-square-runtime'; const providerServiceMock = vi.hoisted(() => ({ deleteAccountApiKey: vi.fn(), })); vi.mock('@electron/services/providers/provider-service', () => ({ getProviderService: () => providerServiceMock, })); function createResponse() { const chunks: string[] = []; const res = { statusCode: 0, setHeader: vi.fn(), end: vi.fn((chunk?: string) => { if (chunk) chunks.push(chunk); }), } as unknown as ServerResponse; return { res, get statusCode() { return res.statusCode; }, json: () => JSON.parse(chunks.join('')) as unknown, }; } function createRequest(method: string, body?: unknown): IncomingMessage { const req = new EventEmitter(); Object.assign(req, { method, headers: body === undefined ? {} : { 'content-type': 'application/json' }, [Symbol.asyncIterator]: async function* () { if (body !== undefined) { yield Buffer.from(JSON.stringify(body)); } }, }); return req as IncomingMessage; } describe('auth host api routes', () => { beforeEach(() => { vi.restoreAllMocks(); resetWorksSquareSessionForTests(); resetManagedWorksSquareRuntimeForTests(); providerServiceMock.deleteAccountApiKey.mockReset(); providerServiceMock.deleteAccountApiKey.mockResolvedValue(true); }); it('exchanges username and AES-encrypted password through the app SSO token endpoint', async () => { const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ access_token: 'access-token', refresh_token: 'refresh-token', token_type: 'Bearer', expires_in: 43200, username: 'zhangsan', user_id: '1', client_id: 'app', }), { status: 200 }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); const handled = await handleAuthRoutes( createRequest('POST', { username: 'zhangsan', password: 'passw0rd', code: 'a7k9', randomStr: '333e6825-760c-4c1a-8b56-eb9539b43dbd', scope: 'server', }), response.res, new URL('http://127.0.0.1:13210/api/auth/login'), {} as never, ); expect(handled).toBe(true); expect(response.statusCode).toBe(200); expect(response.json()).toEqual({ success: true, token: { access_token: 'access-token', token_type: 'Bearer', expires_in: 43200, username: 'zhangsan', user_id: '1', client_id: 'app', }, session: { accessToken: 'access-token', tokenType: 'Bearer', expiresAt: expect.any(Number), lastActiveAt: expect.any(Number), canRefresh: true, }, }); expect(fetchMock).toHaveBeenCalledOnce(); const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toBe('https://biz.nianxx.cn/auth/oauth2/token'); expect(init.method).toBe('POST'); expect(init.headers).toEqual({ Authorization: `Basic ${Buffer.from('app:app').toString('base64')}`, 'Content-Type': 'application/x-www-form-urlencoded', }); expect(String(init.body)).toBe( 'grant_type=password&username=zhangsan&password=ILsdQ7Wp2P8%3D&scope=server&code=a7k9&randomStr=333e6825-760c-4c1a-8b56-eb9539b43dbd', ); expect(String(init.body)).not.toContain('passw0rd'); }); it('allows explicit reauthorization to replace an unreadable persisted session', async () => { const persistence = { load: vi.fn().mockRejectedValue(new Error('credential cannot be decrypted')), save: vi.fn().mockResolvedValue(undefined), }; await initializeWorksSquareSession({ persistence }); const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ access_token: 'replacement-access-token', refresh_token: 'replacement-refresh-token', token_type: 'Bearer', expires_in: 3600, username: 'zhangsan', }), { status: 200 }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); await handleAuthRoutes( createRequest('POST', { username: 'zhangsan', password: 'passw0rd' }), response.res, new URL('http://127.0.0.1:13210/api/auth/login'), { opencodeManager: { stop: vi.fn().mockResolvedValue(undefined) }, } as never, ); expect(response.statusCode).toBe(200); expect(persistence.save).toHaveBeenNthCalledWith(1, null); expect(persistence.save).toHaveBeenLastCalledWith(expect.objectContaining({ accessToken: 'replacement-access-token', refreshToken: 'replacement-refresh-token', })); }); it('passes through SSO credential errors without exposing secrets', async () => { const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ code: 1, msg: 'Bad credentials', data: null }), { status: 401 }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); const handled = await handleAuthRoutes( createRequest('POST', { username: 'zhangsan', password: 'wrong', }), response.res, new URL('http://127.0.0.1:13210/api/auth/login'), {} as never, ); expect(handled).toBe(true); expect(response.statusCode).toBe(401); expect(response.json()).toEqual({ success: false, error: 'Bad credentials', }); expect(JSON.stringify(response.json())).not.toContain('app:app'); }); it('opens Works Square browser authorization and returns the approved desktop token', async () => { const fetchMock = vi.fn() .mockResolvedValueOnce( new Response(JSON.stringify({ request_id: 'desktop-request-id', device_secret: 'desktop-device-secret', authorize_url: 'https://square.nianxx.cn/#desktop-auth?request_id=desktop-request-id', poll_interval_seconds: 0, }), { status: 200 }), ) .mockResolvedValueOnce( new Response(JSON.stringify({ status: 'pending' }), { status: 200 }), ) .mockResolvedValueOnce( new Response(JSON.stringify({ status: 'approved', token: { access_token: 'desktop-access-token', refresh_token: 'desktop-refresh-token', token_type: 'Bearer', expires_in: 43200, username: 'student', user_id: '42', }, }), { status: 200 }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); const handled = await handleAuthRoutes( createRequest('POST'), response.res, new URL('http://127.0.0.1:13210/api/auth/browser/start'), {} as never, ); expect(handled).toBe(true); expect(response.statusCode).toBe(200); expect(response.json()).toEqual({ success: true, token: { access_token: 'desktop-access-token', token_type: 'Bearer', expires_in: 43200, username: 'student', user_id: '42', }, session: { accessToken: 'desktop-access-token', tokenType: 'Bearer', expiresAt: expect.any(Number), lastActiveAt: expect.any(Number), canRefresh: true, }, }); expect(shell.openExternal).toHaveBeenCalledWith( 'https://square.nianxx.cn/#desktop-auth?request_id=desktop-request-id', ); expect(fetchMock.mock.calls.map(([url]) => String(url))).toEqual([ 'https://square.nianxx.cn/api/auth/desktop/start', 'https://square.nianxx.cn/api/auth/desktop/token?request_id=desktop-request-id&device_secret=desktop-device-secret', 'https://square.nianxx.cn/api/auth/desktop/token?request_id=desktop-request-id&device_secret=desktop-device-secret', ]); }); it('stores the approved browser token bundle in the Main Works Square session cache', async () => { vi.setSystemTime(new Date('2026-07-06T08:00:00.000Z')); const fetchMock = vi.fn() .mockResolvedValueOnce( new Response(JSON.stringify({ request_id: 'desktop-request-id', device_secret: 'desktop-device-secret', authorize_url: 'https://square.nianxx.cn/#desktop-auth?request_id=desktop-request-id', poll_interval_seconds: 0, }), { status: 200 }), ) .mockResolvedValueOnce( new Response(JSON.stringify({ status: 'approved', token: { access_token: 'desktop-access-token', refresh_token: 'desktop-refresh-token', token_type: 'Bearer', expires_in: 43200, }, }), { status: 200 }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); await handleAuthRoutes( createRequest('POST'), response.res, new URL('http://127.0.0.1:13210/api/auth/browser/start'), {} as never, ); expect(response.statusCode).toBe(200); expect(getWorksSquareSessionSnapshot()).toMatchObject({ accessToken: 'desktop-access-token', tokenType: 'Bearer', expiresAt: Date.now() + 43_200_000, canRefresh: true, }); expect(getWorksSquareSessionSnapshot()).not.toHaveProperty('refreshToken'); }); it('accepts renderer session sync after app restart', async () => { vi.setSystemTime(new Date('2026-07-06T08:00:00.000Z')); const lastActiveAt = Date.now() - 24 * 60 * 60 * 1000; const response = createResponse(); const handled = await handleAuthRoutes( createRequest('POST', { accessToken: 'persisted-access-token', refreshToken: 'persisted-refresh-token', tokenType: 'Bearer', expiresAt: 1_783_000_000_000, lastActiveAt, }), response.res, new URL('http://127.0.0.1:13210/api/auth/session/sync'), {} as never, ); expect(handled).toBe(true); expect(response.statusCode).toBe(200); expect(response.json()).toEqual({ success: true, session: { accessToken: 'persisted-access-token', tokenType: 'Bearer', expiresAt: 1_783_000_000_000, lastActiveAt, canRefresh: true, }, }); expect(getWorksSquareSessionSnapshot()).toEqual({ accessToken: 'persisted-access-token', tokenType: 'Bearer', expiresAt: 1_783_000_000_000, lastActiveAt, canRefresh: true, }); }); it('does not overwrite recoverable Main credentials when restore is temporarily unavailable', async () => { const persistence = { load: vi.fn().mockRejectedValue(new Error('credential store locked')), save: vi.fn(), }; await initializeWorksSquareSession({ persistence }); const response = createResponse(); await handleAuthRoutes( createRequest('POST', { accessToken: 'renderer-access-without-refresh', refreshToken: null, tokenType: 'Bearer', expiresAt: Date.now() + 3600_000, lastActiveAt: Date.now(), }), response.res, new URL('http://127.0.0.1:13210/api/auth/session/sync'), {} as never, ); expect(response.statusCode).toBe(503); expect(persistence.load).toHaveBeenCalledTimes(2); expect(persistence.save).not.toHaveBeenCalled(); expect(getWorksSquareSessionSnapshot()).toBeNull(); }); it('retries Main restore before treating user activity as an expired session', async () => { const persistedSession = { accessToken: 'restored-access-token', refreshToken: 'restored-refresh-token', tokenType: 'Bearer', expiresAt: Date.now() + 3600_000, lastActiveAt: Date.now() - 60_000, }; const persistence = { load: vi.fn() .mockRejectedValueOnce(new Error('credential store locked')) .mockRejectedValueOnce(new Error('credential store still locked')) .mockResolvedValue(persistedSession), save: vi.fn().mockResolvedValue(undefined), }; await initializeWorksSquareSession({ persistence }); const unavailableResponse = createResponse(); await handleAuthRoutes( createRequest('POST'), unavailableResponse.res, new URL('http://127.0.0.1:13210/api/auth/session/sync'), {} as never, ); expect(unavailableResponse.statusCode).toBe(503); const activityResponse = createResponse(); await handleAuthRoutes( createRequest('POST'), activityResponse.res, new URL('http://127.0.0.1:13210/api/auth/session/activity'), {} as never, ); expect(activityResponse.statusCode).toBe(200); expect(activityResponse.json()).toMatchObject({ success: true, session: { accessToken: 'restored-access-token', canRefresh: true, }, }); expect(persistence.load).toHaveBeenCalledTimes(3); }); it('does not let a stale renderer session roll back the Main-owned refresh token', async () => { vi.setSystemTime(new Date('2026-07-06T08:00:00.000Z')); const lastActiveAt = Date.now() - 24 * 60 * 60 * 1000; storeWorksSquareSession({ accessToken: 'main-access-r1', refreshToken: 'main-refresh-r1', tokenType: 'Bearer', expiresAt: Date.now() + 3600_000, lastActiveAt, }); const syncResponse = createResponse(); await handleAuthRoutes( createRequest('POST', { accessToken: 'renderer-access-r0', refreshToken: 'renderer-refresh-r0', tokenType: 'Bearer', expiresAt: Date.now() + 7200_000, lastActiveAt: Date.now(), }), syncResponse.res, new URL('http://127.0.0.1:13210/api/auth/session/sync'), {} as never, ); expect(syncResponse.statusCode).toBe(200); expect(syncResponse.json()).toEqual({ success: true, session: { accessToken: 'main-access-r1', tokenType: 'Bearer', expiresAt: Date.now() + 3600_000, lastActiveAt, canRefresh: true, }, }); const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ access_token: 'main-access-r2', token_type: 'Bearer', expires_in: 3600, }), { status: 200 }), ); vi.stubGlobal('fetch', fetchMock); const refreshResponse = createResponse(); await handleAuthRoutes( createRequest('POST', { forceRefresh: true }), refreshResponse.res, new URL('http://127.0.0.1:13210/api/auth/session/refresh'), {} as never, ); expect(refreshResponse.statusCode).toBe(200); const [, refreshInit] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(String(refreshInit.body)).toBe( 'grant_type=refresh_token&refresh_token=main-refresh-r1', ); expect(String(refreshInit.body)).not.toContain('renderer-refresh-r0'); }); it('refreshes through the Main-owned session and returns the rotated token bundle', async () => { vi.setSystemTime(new Date('2026-07-06T08:00:00.000Z')); const lastActiveAt = Date.now() - 24 * 60 * 60 * 1000; const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ access_token: 'new-access-token', refresh_token: 'rotated-refresh-token', token_type: 'Bearer', expires_in: 3600, }), { status: 200 }), ); vi.stubGlobal('fetch', fetchMock); storeWorksSquareSession({ accessToken: 'expired-access-token', refreshToken: 'old-refresh-token', expiresAt: Date.now() - 1, lastActiveAt, }); const response = createResponse(); await handleAuthRoutes( createRequest('POST', { forceRefresh: false }), response.res, new URL('http://127.0.0.1:13210/api/auth/session/refresh'), {} as never, ); expect(response.statusCode).toBe(200); expect(response.json()).toEqual({ success: true, session: { accessToken: 'new-access-token', tokenType: 'Bearer', expiresAt: Date.now() + 3600_000, lastActiveAt, canRefresh: true, }, }); expect(fetchMock).toHaveBeenCalledOnce(); }); it('updates activity only through the explicit activity route', async () => { vi.setSystemTime(new Date('2026-07-06T08:00:00.000Z')); storeWorksSquareSession({ accessToken: 'access-token', refreshToken: 'refresh-token', expiresAt: Date.now() + 3600_000, lastActiveAt: Date.now() - 24 * 60 * 60 * 1000, }); const response = createResponse(); await handleAuthRoutes( createRequest('POST'), response.res, new URL('http://127.0.0.1:13210/api/auth/session/activity'), {} as never, ); expect(response.statusCode).toBe(200); expect(getWorksSquareSessionSnapshot()?.lastActiveAt).toBe(Date.now()); }); it('stops the runtime and clears the managed Works Square key on logout', async () => { const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ success: true }), { status: 200 }), ); vi.stubGlobal('fetch', fetchMock); const stop = vi.fn(async () => undefined); const closeEventSessions = vi.fn(async () => undefined); const response = createResponse(); const handled = await handleAuthRoutes( createRequest('POST', { accessToken: 'access-token' }), response.res, new URL('http://127.0.0.1:13210/api/auth/logout'), { opencodeManager: { stop }, imageWorkspace: { closeEventSessions }, } as never, ); expect(handled).toBe(true); expect(response.statusCode).toBe(200); expect(response.json()).toEqual({ success: true }); expect(stop).toHaveBeenCalledOnce(); expect(closeEventSessions).toHaveBeenCalledOnce(); expect(providerServiceMock.deleteAccountApiKey).toHaveBeenCalledWith('niancode-user-models'); }); it('does not report logout success when managed runtime cleanup fails', async () => { const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ success: true }), { status: 200 }), ); vi.stubGlobal('fetch', fetchMock); const stop = vi.fn().mockRejectedValue(new Error('runtime stop failed')); const response = createResponse(); await handleAuthRoutes( createRequest('POST', { accessToken: 'access-token' }), response.res, new URL('http://127.0.0.1:13210/api/auth/logout'), { opencodeManager: { stop }, } as never, ); expect(response.statusCode).toBe(500); expect(response.json()).toEqual({ success: false, error: 'Failed to clear local AI runtime state', }); expect(providerServiceMock.deleteAccountApiKey).toHaveBeenCalledWith('niancode-user-models'); }); it('does not report logout success when a remote event session cannot be closed', async () => { const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ success: true }), { status: 200 }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); await handleAuthRoutes( createRequest('POST', { accessToken: 'access-token' }), response.res, new URL('http://127.0.0.1:13210/api/auth/logout'), { opencodeManager: { stop: vi.fn().mockResolvedValue(undefined) }, imageWorkspace: { closeEventSessions: vi.fn().mockRejectedValue(new Error('remote close failed')), }, } as never, ); expect(response.statusCode).toBe(500); expect(fetchMock).toHaveBeenCalledOnce(); }); it('uses the Main session token for logout instead of a stale Renderer token', async () => { storeWorksSquareSession({ accessToken: 'main-current-access-token', refreshToken: 'main-current-refresh-token', expiresAt: Date.now() + 3600_000, lastActiveAt: Date.now(), }); const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ success: true }), { status: 200 }), ); vi.stubGlobal('fetch', fetchMock); const closeEventSessions = vi.fn(async () => undefined); const response = createResponse(); await handleAuthRoutes( createRequest('POST', { accessToken: 'renderer-stale-access-token' }), response.res, new URL('http://127.0.0.1:13210/api/auth/logout'), { opencodeManager: { stop: vi.fn().mockResolvedValue(undefined) }, imageWorkspace: { closeEventSessions }, } as never, ); expect(response.statusCode).toBe(200); expect(fetchMock).toHaveBeenCalledWith( 'https://biz.nianxx.cn/auth/token/logout', expect.objectContaining({ headers: { Authorization: 'Bearer main-current-access-token' }, }), ); expect(closeEventSessions).toHaveBeenCalledWith({ accessToken: 'main-current-access-token', tolerateRemoteFailure: false, }); }); it('blocks a new login while cleanup of the previous runtime still fails', async () => { const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ success: true }), { status: 200 }), ); vi.stubGlobal('fetch', fetchMock); const stop = vi.fn().mockRejectedValue(new Error('runtime stop failed')); const context = { opencodeManager: { stop } } as never; const logoutResponse = createResponse(); await handleAuthRoutes( createRequest('POST', { accessToken: 'old-access-token' }), logoutResponse.res, new URL('http://127.0.0.1:13210/api/auth/logout'), context, ); expect(logoutResponse.statusCode).toBe(500); fetchMock.mockClear(); const loginResponse = createResponse(); await handleAuthRoutes( createRequest('POST', { username: 'zhangsan', password: 'passw0rd' }), loginResponse.res, new URL('http://127.0.0.1:13210/api/auth/login'), context, ); expect(loginResponse.statusCode).toBe(503); expect(fetchMock).not.toHaveBeenCalled(); expect(stop).toHaveBeenCalledTimes(2); expect(getWorksSquareSessionSnapshot()).toBeNull(); }); it('does not report logout success when secure storage rejects API key deletion', async () => { providerServiceMock.deleteAccountApiKey.mockResolvedValueOnce(false); const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ success: true }), { status: 200 }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); await handleAuthRoutes( createRequest('POST', { accessToken: 'access-token' }), response.res, new URL('http://127.0.0.1:13210/api/auth/logout'), { opencodeManager: { stop: vi.fn().mockResolvedValue(undefined) }, } as never, ); expect(response.statusCode).toBe(500); expect(fetchMock).toHaveBeenCalledOnce(); }); it('still calls remote logout when clearing the persisted local session fails', async () => { const persistence = { load: vi.fn().mockResolvedValue(null), save: vi.fn() .mockResolvedValueOnce(undefined) .mockRejectedValueOnce(new Error('credential store locked')), }; await initializeWorksSquareSession({ persistence }); await commitWorksSquareSession({ accessToken: 'access-token', refreshToken: 'refresh-token', expiresAt: Date.now() + 3600_000, lastActiveAt: Date.now(), }); await flushWorksSquareSessionPersistence(); const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ success: true }), { status: 200 }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); await handleAuthRoutes( createRequest('POST', { accessToken: 'access-token' }), response.res, new URL('http://127.0.0.1:13210/api/auth/logout'), { opencodeManager: { stop: vi.fn().mockResolvedValue(undefined) }, } as never, ); expect(fetchMock).toHaveBeenCalledWith( 'https://biz.nianxx.cn/auth/token/logout', expect.objectContaining({ method: 'DELETE' }), ); expect(response.statusCode).toBe(500); }); });