From f20bd57df6f62f33bd80e1ac13a5a4a0fb91e039 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 21 Aug 2026 08:11:19 +0100 Subject: [PATCH] fix(desktop): fetch server config through the native HTTP client --- .../SetupWizard/ServerSelection.test.tsx | 115 ++++++++++++++++++ .../SetupWizard/ServerSelection.tsx | 3 + .../desktop/components/SetupWizard/index.tsx | 2 + 3 files changed, 120 insertions(+) create mode 100644 frontend/editor/src/desktop/components/SetupWizard/ServerSelection.test.tsx diff --git a/frontend/editor/src/desktop/components/SetupWizard/ServerSelection.test.tsx b/frontend/editor/src/desktop/components/SetupWizard/ServerSelection.test.tsx new file mode 100644 index 0000000000..d1f8bb157f --- /dev/null +++ b/frontend/editor/src/desktop/components/SetupWizard/ServerSelection.test.tsx @@ -0,0 +1,115 @@ +import { describe, expect, test, vi, beforeEach, afterEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MantineProvider } from "@mantine/core"; +import { allowConsole } from "@app/tests/failOnConsole"; + +const { pluginFetch, testConnection } = vi.hoisted(() => ({ + pluginFetch: vi.fn(), + testConnection: vi.fn(), +})); + +vi.mock("@tauri-apps/plugin-http", () => ({ fetch: pluginFetch })); + +vi.mock("@app/services/connectionModeService", () => ({ + connectionModeService: { testConnection }, +})); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (_key: string, fallback?: string) => fallback ?? _key, + }), +})); + +import { ServerSelection } from "@app/components/SetupWizard/ServerSelection"; + +const SERVER = "https://pdf.example.test"; + +function renderSelection(onSelect = vi.fn()) { + render( + + + , + ); + return onSelect; +} + +describe("ServerSelection server configuration fetch", () => { + beforeEach(() => { + pluginFetch.mockReset(); + testConnection.mockReset(); + testConnection.mockResolvedValue({ success: true }); + localStorage.clear(); + vi.spyOn(globalThis, "fetch"); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("reads the login config through the native HTTP client, not the webview", async () => { + pluginFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ loginMethod: "all", providerList: {} }), + }); + + const onSelect = renderSelection(); + const user = userEvent.setup(); + + await user.type(screen.getByLabelText("Server URL"), SERVER); + await user.click(screen.getByRole("button", { name: "Continue" })); + + await waitFor(() => expect(onSelect).toHaveBeenCalled()); + + expect(pluginFetch).toHaveBeenCalledWith( + `${SERVER}/api/v1/proprietary/ui-data/login`, + ); + // The webview's fetch is subject to the WebKit/CORS restrictions that made + // this fail instantly with "Load failed" against a working server. + expect(globalThis.fetch).not.toHaveBeenCalled(); + }); + + test("passes the detected login method and providers on", async () => { + pluginFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ + loginMethod: "oauth2", + providerList: { "/oauth2/authorization/google": "Google" }, + }), + }); + + const onSelect = renderSelection(); + const user = userEvent.setup(); + + await user.type(screen.getByLabelText("Server URL"), SERVER); + await user.click(screen.getByRole("button", { name: "Continue" })); + + await waitFor(() => expect(onSelect).toHaveBeenCalled()); + + expect(onSelect).toHaveBeenCalledWith({ + url: SERVER, + loginMethod: "oauth2", + enabledOAuthProviders: [ + { id: "google", path: "/oauth2/authorization/google", label: "Google" }, + ], + }); + }); + + test("treats a 404 login endpoint as security being disabled", async () => { + allowConsole.warn(/Login config request failed with status 404/); + pluginFetch.mockResolvedValue({ ok: false, status: 404 }); + + const onSelect = renderSelection(); + const user = userEvent.setup(); + + await user.type(screen.getByLabelText("Server URL"), SERVER); + await user.click(screen.getByRole("button", { name: "Continue" })); + + await waitFor(() => + expect(screen.getByText("Login Not Enabled")).toBeInTheDocument(), + ); + expect(onSelect).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/editor/src/desktop/components/SetupWizard/ServerSelection.tsx b/frontend/editor/src/desktop/components/SetupWizard/ServerSelection.tsx index cda4f2f4fc..19f14c42ca 100644 --- a/frontend/editor/src/desktop/components/SetupWizard/ServerSelection.tsx +++ b/frontend/editor/src/desktop/components/SetupWizard/ServerSelection.tsx @@ -1,4 +1,7 @@ import React, { useState } from "react"; +// Native HTTP client: the webview's fetch is CORS-restricted and fails instantly with +// "Load failed" against servers that are perfectly reachable. +import { fetch } from "@tauri-apps/plugin-http"; import { Stack, TextInput, Alert, Text } from "@mantine/core"; import { Button } from "@app/ui/Button"; import { useTranslation } from "react-i18next"; diff --git a/frontend/editor/src/desktop/components/SetupWizard/index.tsx b/frontend/editor/src/desktop/components/SetupWizard/index.tsx index b0779e6b78..91f84aca0c 100644 --- a/frontend/editor/src/desktop/components/SetupWizard/index.tsx +++ b/frontend/editor/src/desktop/components/SetupWizard/index.tsx @@ -1,5 +1,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +// Native HTTP client - see the note in ServerSelection.tsx. +import { fetch } from "@tauri-apps/plugin-http"; import { Stack, Text, Alert, Loader, Center } from "@mantine/core"; import { Button } from "@app/ui/Button"; import { DesktopAuthLayout } from "@app/components/SetupWizard/DesktopAuthLayout";