// @vitest-environment node /** * 客户端服务器选择的边界。 * * 正式包只跟随构建渠道(`VITE_AGC_PLATFORM_CHANNEL`),自定义地址只在本地 Vite debug 可用; * 归一化必须拒绝把凭据、路径、查询、哈希或明文远程地址带进登录请求——这些值随后会作为 * origin 交给 Rust 与后端,渲染层这一层只负责即时反馈与内存投影。 */ import { beforeEach, expect, test, vi } from 'vitest'; const { invoke, resolveTauriInvoke } = vi.hoisted(() => { const invokeMock = vi.fn(); return { invoke: invokeMock, resolveTauriInvoke: vi.fn(() => invokeMock), }; }); vi.mock('../src/app/tauri', () => ({ resolveTauriInvoke })); async function loadClientHttp() { return import('../src/services/clientHttp'); } beforeEach(() => { vi.resetModules(); vi.unstubAllEnvs(); invoke.mockReset(); resolveTauriInvoke.mockReturnValue(invoke); }); test('非法服务器地址按原因逐条拒绝', async () => { const { normalizeClientServerBaseUrl } = await loadClientHttp(); expect(() => normalizeClientServerBaseUrl('dev.genarrative.world')).toThrow( '服务器地址无效', ); expect(() => normalizeClientServerBaseUrl('ftp://example.com')).toThrow( '服务器地址必须是纯 HTTP(S) 地址', ); expect(() => normalizeClientServerBaseUrl('https://user:pass@example.com'), ).toThrow('服务器地址必须是纯 HTTP(S) 地址'); expect(() => normalizeClientServerBaseUrl('https://example.com/api')).toThrow( '服务器地址必须是纯 HTTP(S) 地址', ); expect(() => normalizeClientServerBaseUrl('https://example.com/?token=1'), ).toThrow('服务器地址必须是纯 HTTP(S) 地址'); expect(() => normalizeClientServerBaseUrl('https://example.com/#frag'), ).toThrow('服务器地址必须是纯 HTTP(S) 地址'); expect(() => normalizeClientServerBaseUrl('http://example.com')).toThrow( '非本机服务器必须使用 HTTPS', ); }); test('合法地址去掉尾部斜杠并保留回环端口', async () => { const { normalizeClientServerBaseUrl } = await loadClientHttp(); expect(normalizeClientServerBaseUrl('https://dev.genarrative.world///')).toBe( 'https://dev.genarrative.world', ); expect(normalizeClientServerBaseUrl(' http://127.0.0.1:10001/ ')).toBe( 'http://127.0.0.1:10001', ); expect(normalizeClientServerBaseUrl('http://localhost:8080')).toBe( 'http://localhost:8080', ); }); test('预设映射到渠道地址,自定义走归一化', async () => { const mod = await loadClientHttp(); expect( mod.getClientServerBaseUrl({ preset: 'release', customBaseUrl: '' }), ).toBe(mod.AGC_RELEASE_API_BASE_URL); expect(mod.getClientServerBaseUrl({ preset: 'dev', customBaseUrl: '' })).toBe( mod.AGC_DEVELOPMENT_API_BASE_URL, ); expect( mod.getClientServerBaseUrl({ preset: 'custom', customBaseUrl: 'https://staging.example.com/', }), ).toBe('https://staging.example.com'); expect(mod.isClientServerPreset('custom')).toBe(true); expect(mod.isClientServerPreset('staging')).toBe(false); }); test('开发态切换服务器会落盘并在内存中生效', async () => { vi.stubEnv('DEV', true); const mod = await loadClientHttp(); expect(mod.isClientServerSelectionEnabled()).toBe(true); expect( mod.setClientServerSelection({ preset: 'custom', customBaseUrl: ' https://staging.example.com/ ', }), ).toEqual({ preset: 'custom', customBaseUrl: 'https://staging.example.com' }); await Promise.resolve(); expect(invoke).toHaveBeenCalledWith('set_client_server_selection', { preset: 'custom', customBaseUrl: 'https://staging.example.com', }); expect(mod.getClientServerBaseUrl()).toBe('https://staging.example.com'); }); test('开发态从 Rust 偏好恢复服务器选择', async () => { vi.stubEnv('DEV', true); const mod = await loadClientHttp(); invoke.mockResolvedValue({ clientServerSelection: { preset: 'custom', customBaseUrl: 'https://restored.example.com', }, }); await expect(mod.hydrateClientServerSelection()).resolves.toEqual({ preset: 'custom', customBaseUrl: 'https://restored.example.com', }); expect(invoke).toHaveBeenCalledWith('read_workspace_preferences'); expect(mod.getClientServerSelection()).toEqual({ preset: 'custom', customBaseUrl: 'https://restored.example.com', }); }); // 「打包态不开放服务器切换」这条规则这里测不了:`import.meta.env.DEV` 在测试运行下恒为 // true,`vi.stubEnv('DEV', false)` 改不动它(Vite/Vitest 把它按构建期常量处理),因此 // `isClientServerSelectionEnabled()` 与 `getClientServerSelection()` 的打包分支只能在真实 // 发布构建里核对。本文件覆盖的是地址归一化、预设映射、开发态落盘与偏好恢复。