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(/