import type { IncomingMessage, ServerResponse } from 'node:http'; import { Readable } from 'node:stream'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createLearningRouteHandler } from '@electron/api/routes/learning'; 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 json() { return JSON.parse(chunks.join('')) as Record; }, }; } describe('Learning Main route boundary', () => { const fetchImpl = vi.fn(); const getAccessToken = vi.fn(); beforeEach(() => { fetchImpl.mockReset(); getAccessToken.mockReset(); }); it('does not claim unrelated routes', async () => { const handler = createLearningRouteHandler({ fetchImpl, getAccessToken }); const response = createResponse(); await expect(handler( { method: 'GET' } as IncomingMessage, response.res, new URL('http://127.0.0.1/api/works/projects'), {} as never, )).resolves.toBe(false); expect(fetchImpl).not.toHaveBeenCalled(); }); it('forwards catalog reads with a Main-owned access token', async () => { getAccessToken.mockResolvedValue('works-token'); fetchImpl.mockResolvedValue(new Response(JSON.stringify([]), { status: 200, headers: { 'Content-Type': 'application/json' }, })); const handler = createLearningRouteHandler({ fetchImpl, getAccessToken, apiBaseUrl: 'https://square.example/', }); const response = createResponse(); await expect(handler( { method: 'GET' } as IncomingMessage, response.res, new URL('http://127.0.0.1/api/works/learning/courses?limit=24&unknown=no'), {} as never, )).resolves.toBe(true); expect(fetchImpl).toHaveBeenCalledWith( 'https://square.example/api/learning/courses?limit=24', expect.objectContaining({ method: 'GET', headers: { Accept: 'application/json', Authorization: 'Bearer works-token', }, }), ); expect(response.json).toEqual({ success: true, data: [] }); }); it('refreshes once after an upstream 401', async () => { getAccessToken.mockResolvedValueOnce('expired-token').mockResolvedValueOnce('new-token'); fetchImpl .mockResolvedValueOnce(new Response(JSON.stringify({ detail: 'expired' }), { status: 401 })) .mockResolvedValueOnce(new Response(JSON.stringify([]), { status: 200 })); const handler = createLearningRouteHandler({ fetchImpl, getAccessToken, apiBaseUrl: 'https://square.example' }); const response = createResponse(); await handler( { method: 'GET' } as IncomingMessage, response.res, new URL('http://127.0.0.1/api/works/learning/courses'), {} as never, ); expect(getAccessToken).toHaveBeenNthCalledWith(2, { fetchImpl, forceRefresh: true }); expect(fetchImpl).toHaveBeenCalledTimes(2); expect(response.json).toMatchObject({ success: true }); }); it('returns a stable auth error without contacting the service', async () => { getAccessToken.mockResolvedValue(null); const handler = createLearningRouteHandler({ fetchImpl, getAccessToken }); const response = createResponse(); await handler( { method: 'GET' } as IncomingMessage, response.res, new URL('http://127.0.0.1/api/works/learning/courses'), {} as never, ); expect(response.res.statusCode).toBe(401); expect(response.json).toMatchObject({ code: 'LEARNING_AUTH_REQUIRED' }); expect(fetchImpl).not.toHaveBeenCalled(); }); it('forwards single-course generation without exposing the token to Renderer', async () => { getAccessToken.mockResolvedValue('works-token'); fetchImpl.mockResolvedValue(new Response(JSON.stringify({ jobId: 'job-1', status: 'queued', done: false, }), { status: 202, headers: { 'Content-Type': 'application/json' } })); const handler = createLearningRouteHandler({ fetchImpl, getAccessToken, apiBaseUrl: 'https://square.example', }); const options = { requirement: '互动讲解二次函数', enableWebSearch: false, enableImageGeneration: true, enableVideoGeneration: false, enableTTS: true, interactiveMode: true, taskEngineMode: false, }; const request = Readable.from([JSON.stringify(options)]) as IncomingMessage; request.method = 'POST'; const response = createResponse(); await handler( request, response.res, new URL('http://127.0.0.1/api/works/learning/generations'), {} as never, ); expect(fetchImpl).toHaveBeenCalledWith( 'https://square.example/api/learning/generations', expect.objectContaining({ method: 'POST', body: JSON.stringify(options), headers: expect.objectContaining({ Authorization: 'Bearer works-token' }), }), ); expect(response.res.statusCode).toBe(202); }); it.each(['cancel', 'resume'])('forwards the generation %s control operation without a body', async (operation) => { getAccessToken.mockResolvedValue('works-token'); fetchImpl.mockResolvedValue(new Response(JSON.stringify({ jobId: 'job-1', status: operation === 'cancel' ? 'cancelled' : 'queued', done: operation === 'cancel', }), { status: 200 })); const handler = createLearningRouteHandler({ fetchImpl, getAccessToken, apiBaseUrl: 'https://square.example' }); const response = createResponse(); await handler( { method: 'POST' } as IncomingMessage, response.res, new URL(`http://127.0.0.1/api/works/learning/generations/job-1/${operation}`), {} as never, ); expect(fetchImpl).toHaveBeenCalledWith( `https://square.example/api/learning/generations/job-1/${operation}`, expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ Authorization: 'Bearer works-token' }) }), ); expect(fetchImpl.mock.calls[0][1]).not.toHaveProperty('body'); }); });