import { render, screen, waitFor } from "@testing-library/react"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import userEvent from "@testing-library/user-event"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { MemoryRouter, Routes, Route } from "react-router"; import LaunchRoute from "#/routes/launch"; // Mock the hooks const mockMutateAsync = vi.fn(); const mockNavigate = vi.fn(); vi.mock("#/hooks/mutation/use-create-conversation", () => ({ useCreateConversation: () => ({ mutateAsync: mockMutateAsync, isPending: false, }), })); vi.mock("react-router", async () => { const actual = await vi.importActual("react-router"); return { ...actual, useNavigate: () => mockNavigate, }; }); vi.mock("#/hooks/query/use-is-authed", () => ({ useIsAuthed: () => ({ data: true }), })); vi.mock("#/hooks/query/use-config", () => ({ useConfig: () => ({ data: { APP_MODE: "saas" } }), })); function renderLaunchRoute(searchParams: string) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, }, }); return render( } /> , ); } describe("LaunchRoute", () => { beforeEach(() => { vi.clearAllMocks(); mockMutateAsync.mockResolvedValue({ conversation_id: "test-conv-123" }); }); afterEach(() => { vi.resetAllMocks(); }); describe("Query Parameter Parsing", () => { it("should parse valid base64 encoded plugins", async () => { // Single plugin with parameters const plugins = [ { source: "github:owner/repo", ref: "main", parameters: { apiKey: "test-key", maxRetries: 3 }, }, ]; const encoded = btoa(JSON.stringify(plugins)); renderLaunchRoute(`?plugins=${encoded}`); expect(screen.getByTestId("plugin-launch-modal")).toBeInTheDocument(); expect(screen.getByText("owner/repo")).toBeInTheDocument(); }); it("should parse multiple plugins from base64", async () => { const plugins = [ { source: "github:owner/repo1", parameters: { key: "value1" } }, { source: "github:owner/repo2" }, ]; const encoded = btoa(JSON.stringify(plugins)); renderLaunchRoute(`?plugins=${encoded}`); expect(screen.getByTestId("plugin-launch-modal")).toBeInTheDocument(); // Plugin names appear multiple times, use getAllByText expect(screen.getAllByText("owner/repo1").length).toBeGreaterThan(0); expect(screen.getAllByText("owner/repo2").length).toBeGreaterThan(0); }); it("should show error for invalid base64 encoding", () => { renderLaunchRoute("?plugins=not-valid-base64!!!"); expect(screen.getByTestId("launch-error")).toBeInTheDocument(); expect(screen.getByText("LAUNCH$ERROR_INVALID_FORMAT")).toBeInTheDocument(); }); it("should show error for invalid JSON in decoded base64", () => { const invalidJson = btoa("not valid json"); renderLaunchRoute(`?plugins=${invalidJson}`); expect(screen.getByTestId("launch-error")).toBeInTheDocument(); expect(screen.getByText("LAUNCH$ERROR_INVALID_FORMAT")).toBeInTheDocument(); }); it("should show error when decoded plugins is not an array", () => { const notArray = btoa(JSON.stringify({ source: "github:owner/repo" })); renderLaunchRoute(`?plugins=${notArray}`); expect(screen.getByTestId("launch-error")).toBeInTheDocument(); expect(screen.getByText("LAUNCH$ERROR_INVALID_FORMAT")).toBeInTheDocument(); }); it("should show error when plugin is missing source", () => { const missingSource = btoa(JSON.stringify([{ ref: "main" }])); renderLaunchRoute(`?plugins=${missingSource}`); expect(screen.getByTestId("launch-error")).toBeInTheDocument(); expect(screen.getByText("LAUNCH$ERROR_INVALID_FORMAT")).toBeInTheDocument(); }); it("should parse simple params format (plugin_source)", () => { renderLaunchRoute("?plugin_source=github:owner/simple-repo"); expect(screen.getByTestId("plugin-launch-modal")).toBeInTheDocument(); // Plugin name appears multiple times, use getAllByText expect(screen.getAllByText("owner/simple-repo").length).toBeGreaterThan(0); }); it("should parse simple params with ref", () => { renderLaunchRoute( "?plugin_source=github:owner/repo&plugin_ref=v1.0.0", ); expect(screen.getByTestId("plugin-launch-modal")).toBeInTheDocument(); // Plugin name appears multiple times, use getAllByText expect(screen.getAllByText("owner/repo").length).toBeGreaterThan(0); }); it("should show error when no plugins specified", () => { renderLaunchRoute(""); expect(screen.getByTestId("launch-error")).toBeInTheDocument(); expect(screen.getByText("LAUNCH$ERROR_NO_PLUGINS")).toBeInTheDocument(); }); }); describe("Message Sanitization", () => { it("should display sanitized message", () => { const plugins = [{ source: "github:owner/repo" }]; const encoded = btoa(JSON.stringify(plugins)); const message = "Hello, this is a safe message"; renderLaunchRoute(`?plugins=${encoded}&message=${encodeURIComponent(message)}`); expect(screen.getByText("Hello, this is a safe message")).toBeInTheDocument(); }); it("should remove script tags from message (XSS prevention)", () => { const plugins = [{ source: "github:owner/repo" }]; const encoded = btoa(JSON.stringify(plugins)); const maliciousMessage = 'Safe text'; renderLaunchRoute( `?plugins=${encoded}&message=${encodeURIComponent(maliciousMessage)}`, ); // Script tags should be stripped (text content preserved but safe as plain text) expect(screen.queryByText(/