197 lines
6.0 KiB
React
197 lines
6.0 KiB
React
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
cleanup,
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
within,
|
|
} from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { Refine } from "@refinedev/core";
|
|
import { MemoryRouter } from "react-router";
|
|
import { NetworkExitList } from "./NetworkExitsPage";
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
const httpError = (message, status, body) =>
|
|
Object.assign(new Error(message), { status, body });
|
|
|
|
const networkExit = {
|
|
id: "exit-a",
|
|
protocol: "socks5",
|
|
host: "proxy.example",
|
|
port: 1080,
|
|
username: "proxy-user",
|
|
password: "plain-password",
|
|
health_status: "healthy",
|
|
observed_public_ip: "203.0.113.1",
|
|
};
|
|
|
|
function provider(exits = [], overrides = {}) {
|
|
return {
|
|
getList: vi.fn(({ resource }) =>
|
|
Promise.resolve(
|
|
resource === "network-exits"
|
|
? { data: exits, total: exits.length }
|
|
: { data: [], total: 0 },
|
|
),
|
|
),
|
|
create: vi.fn().mockResolvedValue({ data: networkExit }),
|
|
networkExitAction: vi.fn().mockResolvedValue(networkExit),
|
|
getOne: vi.fn(),
|
|
getMany: vi.fn(),
|
|
getManyReference: vi.fn(),
|
|
update: vi.fn(),
|
|
updateMany: vi.fn(),
|
|
delete: vi.fn(),
|
|
deleteMany: vi.fn(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
const renderExits = (dataProvider) =>
|
|
render(
|
|
<QueryClientProvider
|
|
client={
|
|
new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
|
}
|
|
>
|
|
<Refine dataProvider={dataProvider}>
|
|
<MemoryRouter>
|
|
<NetworkExitList />
|
|
</MemoryRouter>
|
|
</Refine>
|
|
</QueryClientProvider>,
|
|
);
|
|
|
|
describe("NetworkExitList", () => {
|
|
it("shows the ordered empty-state instruction", async () => {
|
|
renderExits(provider());
|
|
expect(await screen.findByText(/创建并检测健康后/)).toBeTruthy();
|
|
});
|
|
|
|
it("shows the exit ID as the primary identifier with stored credentials", async () => {
|
|
const dataProvider = provider([networkExit]);
|
|
renderExits(dataProvider);
|
|
|
|
const idLink = await screen.findByRole("link", { name: "exit-a" });
|
|
expect(idLink.getAttribute("href")).toBe("/network-exits/exit-a");
|
|
expect(screen.getByText("socks5://proxy.example:1080")).toBeTruthy();
|
|
expect((await screen.findAllByText(/proxy-user/)).length).toBeGreaterThan(
|
|
0,
|
|
);
|
|
expect(
|
|
(await screen.findAllByText(/plain-password/)).length,
|
|
).toBeGreaterThan(0);
|
|
fireEvent.click(screen.getAllByRole("button", { name: "检测" })[0]);
|
|
await waitFor(() =>
|
|
expect(dataProvider.networkExitAction).toHaveBeenCalledWith(
|
|
"exit-a",
|
|
"check",
|
|
),
|
|
);
|
|
});
|
|
|
|
it("creates with plain credentials and closes on success", async () => {
|
|
const dataProvider = provider();
|
|
renderExits(dataProvider);
|
|
|
|
fireEvent.click(
|
|
await screen.findByRole("button", { name: "创建网络出口" }),
|
|
);
|
|
const dialog = screen.getByRole("dialog");
|
|
fireEvent.change(within(dialog).getByRole("textbox", { name: "主机" }), {
|
|
target: { value: "proxy.example" },
|
|
});
|
|
fireEvent.change(within(dialog).getByRole("spinbutton", { name: "端口" }), {
|
|
target: { value: "1080" },
|
|
});
|
|
fireEvent.change(
|
|
within(dialog).getByRole("textbox", { name: "用户名(可选)" }),
|
|
{ target: { value: "proxy-user" } },
|
|
);
|
|
fireEvent.change(
|
|
within(dialog).getByRole("textbox", { name: "密码(可选)" }),
|
|
{ target: { value: "plain-password" } },
|
|
);
|
|
fireEvent.click(
|
|
within(dialog).getByRole("button", { name: "创建网络出口" }),
|
|
);
|
|
|
|
await waitFor(() =>
|
|
expect(dataProvider.create).toHaveBeenCalledWith({
|
|
resource: "network-exits",
|
|
variables: {
|
|
protocol: "socks5",
|
|
host: "proxy.example",
|
|
port: 1080,
|
|
username: "proxy-user",
|
|
password: "plain-password",
|
|
expected_public_ip: "",
|
|
expected_region: "",
|
|
},
|
|
}),
|
|
);
|
|
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
|
});
|
|
|
|
it("keeps the create modal open after a failure", async () => {
|
|
const dataProvider = provider([], {
|
|
create: vi.fn().mockRejectedValue(new Error("创建失败")),
|
|
});
|
|
renderExits(dataProvider);
|
|
|
|
fireEvent.click(
|
|
await screen.findByRole("button", { name: "创建网络出口" }),
|
|
);
|
|
const dialog = screen.getByRole("dialog");
|
|
fireEvent.change(within(dialog).getByRole("textbox", { name: "主机" }), {
|
|
target: { value: "proxy.example" },
|
|
});
|
|
fireEvent.change(within(dialog).getByRole("spinbutton", { name: "端口" }), {
|
|
target: { value: "1080" },
|
|
});
|
|
fireEvent.click(
|
|
within(dialog).getByRole("button", { name: "创建网络出口" }),
|
|
);
|
|
|
|
expect((await within(dialog).findByRole("alert")).textContent).toContain(
|
|
"创建失败",
|
|
);
|
|
expect(screen.getByRole("dialog")).toBeTruthy();
|
|
});
|
|
|
|
it("shows unknown bindings and retries when browsers return 502", async () => {
|
|
const dataProvider = provider([networkExit], {
|
|
getList: vi.fn(({ resource }) =>
|
|
resource === "browsers"
|
|
? Promise.reject(httpError("bad gateway", 502))
|
|
: Promise.resolve({ data: [networkExit], total: 1 }),
|
|
),
|
|
});
|
|
renderExits(dataProvider);
|
|
|
|
expect(await screen.findByText(/环境不可用/)).toBeTruthy();
|
|
expect(
|
|
(await screen.findAllByText(/绑定账号:状态未知|状态未知/)).length,
|
|
).toBeGreaterThan(0);
|
|
expect(screen.queryByText(/绑定账号:未绑定/)).toBeNull();
|
|
expect(screen.queryByText(/^未绑定$/)).toBeNull();
|
|
expect(screen.getAllByRole("button", { name: "检测" })[0].disabled).toBe(
|
|
false,
|
|
);
|
|
fireEvent.click(screen.getByRole("button", { name: "重试环境状态" }));
|
|
await waitFor(() =>
|
|
expect(
|
|
dataProvider.getList.mock.calls.filter(
|
|
([call]) => call.resource === "browsers",
|
|
).length,
|
|
).toBeGreaterThan(1),
|
|
);
|
|
});
|
|
});
|