feat(gateways): 网关列表增加连通性与健康状态探测展示
- GET /api/gateways 并发探测 /healthz(连通性)与 /v1/browsers(健康),失败原因随列表返回 - 前端新增连通性/健康两列,失败原因悬浮提示 - 覆盖健康、令牌被拒、离线三类探测路径的单元测试
This commit is contained in:
+144
-52
@@ -1,68 +1,160 @@
|
||||
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 { GatewayList } from './GatewaysPage'
|
||||
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 { GatewayList } from "./GatewaysPage";
|
||||
|
||||
afterEach(() => { cleanup(); vi.restoreAllMocks() })
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const gateways = [{ id: 'gw-1', name: 'gw-1', endpoint: 'http://docker-gateway:8081', token: 'unit-test-gateway-token' }]
|
||||
const gateways = [
|
||||
{
|
||||
id: "gw-1",
|
||||
name: "gw-1",
|
||||
endpoint: "http://docker-gateway:8081",
|
||||
token: "unit-test-gateway-token",
|
||||
connectivity: "online",
|
||||
health: "healthy",
|
||||
},
|
||||
{
|
||||
id: "gw-2",
|
||||
name: "gw-2",
|
||||
endpoint: "http://gw2:8081",
|
||||
token: "offline-token-123456",
|
||||
connectivity: "offline",
|
||||
connectivity_reason: "connection refused",
|
||||
health: "unhealthy",
|
||||
health_reason: "gateway unreachable",
|
||||
},
|
||||
];
|
||||
|
||||
function provider(overrides = {}) {
|
||||
return {
|
||||
getList: vi.fn(() => Promise.resolve({ data: gateways, total: gateways.length })),
|
||||
create: vi.fn().mockResolvedValue({ data: { name: 'gw-2', endpoint: 'http://gw2:8081', token: 'generated-token-abcdef' } }),
|
||||
deleteOne: vi.fn().mockResolvedValue({ data: { id: 'gw-1' } }),
|
||||
getOne: vi.fn(), getMany: vi.fn(), getManyReference: vi.fn(), update: vi.fn(), updateMany: vi.fn(), deleteMany: vi.fn(), browserAction: vi.fn(),
|
||||
getList: vi.fn(() =>
|
||||
Promise.resolve({ data: gateways, total: gateways.length }),
|
||||
),
|
||||
create: vi
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
name: "gw-2",
|
||||
endpoint: "http://gw2:8081",
|
||||
token: "generated-token-abcdef",
|
||||
},
|
||||
}),
|
||||
deleteOne: vi.fn().mockResolvedValue({ data: { id: "gw-1" } }),
|
||||
getOne: vi.fn(),
|
||||
getMany: vi.fn(),
|
||||
getManyReference: vi.fn(),
|
||||
update: vi.fn(),
|
||||
updateMany: vi.fn(),
|
||||
deleteMany: vi.fn(),
|
||||
browserAction: vi.fn(),
|
||||
...overrides,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const renderGateways = dataProvider => render(
|
||||
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
|
||||
<Refine dataProvider={dataProvider}>
|
||||
<MemoryRouter><GatewayList /></MemoryRouter>
|
||||
</Refine>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
const renderGateways = (dataProvider) =>
|
||||
render(
|
||||
<QueryClientProvider
|
||||
client={
|
||||
new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
}
|
||||
>
|
||||
<Refine dataProvider={dataProvider}>
|
||||
<MemoryRouter>
|
||||
<GatewayList />
|
||||
</MemoryRouter>
|
||||
</Refine>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
describe('GatewayList', () => {
|
||||
it('registers a gateway and reveals the generated token', async () => {
|
||||
const dataProvider = provider()
|
||||
renderGateways(dataProvider)
|
||||
await screen.findAllByText('gw-1')
|
||||
describe("GatewayList", () => {
|
||||
it("registers a gateway and reveals the generated token", async () => {
|
||||
const dataProvider = provider();
|
||||
renderGateways(dataProvider);
|
||||
await screen.findAllByText("gw-1");
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '注册网关' }))
|
||||
const dialog = screen.getByRole('dialog')
|
||||
fireEvent.change(within(dialog).getByRole('textbox', { name: '名称' }), { target: { value: 'gw-2' } })
|
||||
fireEvent.change(within(dialog).getByRole('textbox', { name: 'Endpoint' }), { target: { value: 'http://gw2:8081' } })
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '注册网关' }))
|
||||
fireEvent.click(screen.getByRole("button", { name: "注册网关" }));
|
||||
const dialog = screen.getByRole("dialog");
|
||||
fireEvent.change(within(dialog).getByRole("textbox", { name: "名称" }), {
|
||||
target: { value: "gw-2" },
|
||||
});
|
||||
fireEvent.change(
|
||||
within(dialog).getByRole("textbox", { name: "Endpoint" }),
|
||||
{ target: { value: "http://gw2:8081" } },
|
||||
);
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "注册网关" }));
|
||||
|
||||
await waitFor(() => expect(dataProvider.create).toHaveBeenCalledWith({ resource: 'gateways', variables: { name: 'gw-2', endpoint: 'http://gw2:8081', token: '' } }))
|
||||
expect((await screen.findByRole('alert')).textContent).toContain('generated-token-abcdef')
|
||||
})
|
||||
await waitFor(() =>
|
||||
expect(dataProvider.create).toHaveBeenCalledWith({
|
||||
resource: "gateways",
|
||||
variables: { name: "gw-2", endpoint: "http://gw2:8081", token: "" },
|
||||
}),
|
||||
);
|
||||
expect((await screen.findByRole("alert")).textContent).toContain(
|
||||
"generated-token-abcdef",
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects an invalid endpoint before submit', async () => {
|
||||
const dataProvider = provider()
|
||||
renderGateways(dataProvider)
|
||||
await screen.findAllByText('gw-1')
|
||||
it("rejects an invalid endpoint before submit", async () => {
|
||||
const dataProvider = provider();
|
||||
renderGateways(dataProvider);
|
||||
await screen.findAllByText("gw-1");
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '注册网关' }))
|
||||
const dialog = screen.getByRole('dialog')
|
||||
fireEvent.change(within(dialog).getByRole('textbox', { name: '名称' }), { target: { value: 'gw-2' } })
|
||||
fireEvent.change(within(dialog).getByRole('textbox', { name: 'Endpoint' }), { target: { value: 'ftp://gw2:8081' } })
|
||||
expect(within(dialog).getByRole('button', { name: '注册网关' }).disabled).toBe(true)
|
||||
expect(dataProvider.create).not.toHaveBeenCalled()
|
||||
})
|
||||
fireEvent.click(screen.getByRole("button", { name: "注册网关" }));
|
||||
const dialog = screen.getByRole("dialog");
|
||||
fireEvent.change(within(dialog).getByRole("textbox", { name: "名称" }), {
|
||||
target: { value: "gw-2" },
|
||||
});
|
||||
fireEvent.change(
|
||||
within(dialog).getByRole("textbox", { name: "Endpoint" }),
|
||||
{ target: { value: "ftp://gw2:8081" } },
|
||||
);
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: "注册网关" }).disabled,
|
||||
).toBe(true);
|
||||
expect(dataProvider.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('deletes a gateway after confirm', async () => {
|
||||
const dataProvider = provider()
|
||||
renderGateways(dataProvider)
|
||||
it("deletes a gateway after confirm", async () => {
|
||||
const dataProvider = provider();
|
||||
renderGateways(dataProvider);
|
||||
|
||||
fireEvent.click(await screen.findByLabelText('删除 gw-1'))
|
||||
fireEvent.click(await screen.findByRole('button', { name: '确认删除' }))
|
||||
fireEvent.click(await screen.findByLabelText("删除 gw-1"));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "确认删除" }));
|
||||
|
||||
await waitFor(() => expect(dataProvider.deleteOne).toHaveBeenCalledWith({ resource: 'gateways', id: 'gw-1' }))
|
||||
})
|
||||
})
|
||||
await waitFor(() =>
|
||||
expect(dataProvider.deleteOne).toHaveBeenCalledWith({
|
||||
resource: "gateways",
|
||||
id: "gw-1",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows connectivity and health with reasons for failing gateways", async () => {
|
||||
renderGateways(provider());
|
||||
|
||||
const online = await screen.findByText("在线");
|
||||
expect((await screen.findAllByText("健康")).length).toBeGreaterThan(0);
|
||||
const offline = screen.getByText("离线");
|
||||
const unhealthy = screen.getByText("异常");
|
||||
expect(offline.closest("[title]")?.getAttribute("title")).toBe(
|
||||
"connection refused",
|
||||
);
|
||||
expect(unhealthy.closest("[title]")?.getAttribute("title")).toBe(
|
||||
"gateway unreachable",
|
||||
);
|
||||
expect(online.closest("[title]")).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user