mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-01 14:46:36 +08:00
[优化] go 引用调整
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ThemeProvider } from '@/components/providers/theme-provider';
|
||||
import { DashboardOverview } from '@/features/dashboard/components/dashboard-overview';
|
||||
|
||||
vi.mock('echarts-for-react', () => ({
|
||||
default: () => <div data-testid="echarts-mock" />,
|
||||
}));
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('DashboardOverview', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function stubMatchMedia() {
|
||||
vi.stubGlobal(
|
||||
'matchMedia',
|
||||
vi.fn().mockImplementation(() => ({
|
||||
matches: false,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
function renderDashboardOverview() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<DashboardOverview />
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
it('renders dashboard summary cards', async () => {
|
||||
stubMatchMedia();
|
||||
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn((input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
|
||||
if (url.includes('/dashboard/overview')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: {
|
||||
generated_at: '2026-03-14T08:00:00Z',
|
||||
summary: {
|
||||
total_nodes: 2,
|
||||
online_nodes: 2,
|
||||
offline_nodes: 0,
|
||||
pending_nodes: 0,
|
||||
unhealthy_nodes: 1,
|
||||
},
|
||||
traffic: {
|
||||
request_count: 900,
|
||||
unique_visitors: 200,
|
||||
error_count: 36,
|
||||
estimated_qps: 15,
|
||||
reported_nodes: 2,
|
||||
},
|
||||
capacity: {
|
||||
average_cpu_usage_percent: 68.5,
|
||||
average_memory_usage_percent: 71.8,
|
||||
high_cpu_nodes: 1,
|
||||
high_memory_nodes: 1,
|
||||
high_storage_nodes: 1,
|
||||
},
|
||||
distributions: {
|
||||
source_countries: [
|
||||
['CN', 440],
|
||||
['US', 320],
|
||||
['SG', 140],
|
||||
],
|
||||
status_codes: [
|
||||
['200', 820],
|
||||
['502', 24],
|
||||
['500', 12],
|
||||
],
|
||||
top_domains: [
|
||||
['app.example.com', 580],
|
||||
['api.example.com', 220],
|
||||
],
|
||||
},
|
||||
trends: {
|
||||
traffic_24h: Array.from({ length: 24 }, (_, index) => [
|
||||
`2026-03-13T${String(index).padStart(2, '0')}:00:00Z`,
|
||||
index * 10,
|
||||
index,
|
||||
index * 3,
|
||||
]),
|
||||
capacity_24h: Array.from({ length: 24 }, (_, index) => [
|
||||
`2026-03-13T${String(index).padStart(2, '0')}:00:00Z`,
|
||||
index,
|
||||
index + 10,
|
||||
2,
|
||||
]),
|
||||
network_24h: Array.from({ length: 24 }, (_, index) => [
|
||||
`2026-03-13T${String(index).padStart(2, '0')}:00:00Z`,
|
||||
index * 100,
|
||||
index * 120,
|
||||
index * 140,
|
||||
index * 160,
|
||||
2,
|
||||
]),
|
||||
disk_io_24h: Array.from({ length: 24 }, (_, index) => [
|
||||
`2026-03-13T${String(index).padStart(2, '0')}:00:00Z`,
|
||||
index * 50,
|
||||
index * 70,
|
||||
2,
|
||||
]),
|
||||
},
|
||||
nodes: [
|
||||
[
|
||||
1,
|
||||
'node-a',
|
||||
'edge-a',
|
||||
'Shanghai',
|
||||
31.2304,
|
||||
121.4737,
|
||||
'online',
|
||||
'healthy',
|
||||
'20260314-001',
|
||||
'2026-03-14T08:00:00Z',
|
||||
0,
|
||||
45,
|
||||
50,
|
||||
60,
|
||||
600,
|
||||
6,
|
||||
120,
|
||||
],
|
||||
[
|
||||
2,
|
||||
'node-b',
|
||||
'edge-b',
|
||||
'San Francisco',
|
||||
37.7749,
|
||||
-122.4194,
|
||||
'online',
|
||||
'unhealthy',
|
||||
'20260313-001',
|
||||
'2026-03-14T08:00:00Z',
|
||||
2,
|
||||
92,
|
||||
88,
|
||||
95,
|
||||
300,
|
||||
30,
|
||||
80,
|
||||
],
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unhandled fetch: ${url}`));
|
||||
}),
|
||||
);
|
||||
|
||||
renderDashboardOverview();
|
||||
|
||||
expect(await screen.findByText('全球态势板')).toBeInTheDocument();
|
||||
expect(await screen.findByText('24 小时请求趋势')).toBeInTheDocument();
|
||||
expect(await screen.findByText('来源分布')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Top Domain')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Top 节点榜单')).toBeInTheDocument();
|
||||
expect(await screen.findByText('节点健康列表')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders empty state when dashboard arrays are returned as null', async () => {
|
||||
stubMatchMedia();
|
||||
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn((input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
|
||||
if (url.includes('/dashboard/overview')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: {
|
||||
generated_at: '2026-03-14T08:00:00Z',
|
||||
summary: {
|
||||
total_nodes: 0,
|
||||
online_nodes: 0,
|
||||
offline_nodes: 0,
|
||||
pending_nodes: 0,
|
||||
unhealthy_nodes: 0,
|
||||
},
|
||||
traffic: {
|
||||
request_count: 0,
|
||||
unique_visitors: 0,
|
||||
error_count: 0,
|
||||
estimated_qps: 0,
|
||||
reported_nodes: 0,
|
||||
},
|
||||
capacity: {
|
||||
average_cpu_usage_percent: 0,
|
||||
average_memory_usage_percent: 0,
|
||||
high_cpu_nodes: 0,
|
||||
high_memory_nodes: 0,
|
||||
high_storage_nodes: 0,
|
||||
},
|
||||
distributions: {
|
||||
source_countries: null,
|
||||
status_codes: null,
|
||||
top_domains: null,
|
||||
},
|
||||
trends: {
|
||||
traffic_24h: null,
|
||||
capacity_24h: null,
|
||||
network_24h: null,
|
||||
disk_io_24h: null,
|
||||
},
|
||||
nodes: null,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unhandled fetch: ${url}`));
|
||||
}),
|
||||
);
|
||||
|
||||
renderDashboardOverview();
|
||||
|
||||
expect(await screen.findByText('暂无节点')).toBeInTheDocument();
|
||||
expect(await screen.findByText('暂无来源分布数据')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
||||
import {render, waitFor} from '@testing-library/react';
|
||||
import {type ReactNode, StrictMode} from 'react';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
import {GitHubOAuthCallback} from '@/features/auth/components/github-oauth-callback';
|
||||
|
||||
const replaceMock = vi.fn();
|
||||
const setUserMock = vi.fn();
|
||||
const exchangeGitHubCodeMock = vi.fn();
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
replace: replaceMock,
|
||||
}),
|
||||
useSearchParams: () => new URLSearchParams('code=github-code-123'),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/providers/auth-provider', () => ({
|
||||
useAuth: () => ({
|
||||
setUser: setUserMock,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/auth/api/auth', () => ({
|
||||
exchangeGitHubCode: (code: string) => exchangeGitHubCodeMock(code),
|
||||
}));
|
||||
|
||||
function createQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
mutations: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function renderWithProviders(ui: ReactNode) {
|
||||
const queryClient = createQueryClient();
|
||||
|
||||
return render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('GitHubOAuthCallback', () => {
|
||||
beforeEach(() => {
|
||||
replaceMock.mockReset();
|
||||
setUserMock.mockReset();
|
||||
exchangeGitHubCodeMock.mockReset();
|
||||
exchangeGitHubCodeMock.mockResolvedValue({
|
||||
id: 1,
|
||||
username: 'github-user',
|
||||
role: 1,
|
||||
status: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('exchanges the same GitHub code only once', async () => {
|
||||
const view = renderWithProviders(<GitHubOAuthCallback />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(exchangeGitHubCodeMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
view.rerender(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={createQueryClient()}>
|
||||
<GitHubOAuthCallback />
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(replaceMock).toHaveBeenCalledWith('/');
|
||||
});
|
||||
|
||||
expect(exchangeGitHubCodeMock).toHaveBeenCalledTimes(1);
|
||||
expect(exchangeGitHubCodeMock).toHaveBeenCalledWith('github-code-123');
|
||||
expect(setUserMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
import {calculateNiceAxisMax, formatBytes, formatBytesPerSecond, formatPercent,} from '@/lib/utils/metrics';
|
||||
|
||||
describe('metrics utils', () => {
|
||||
it('formats byte values with readable units', () => {
|
||||
expect(formatBytes(0)).toBe('0 B');
|
||||
expect(formatBytes(1536)).toBe('1.5 KB');
|
||||
expect(formatBytes(183 * 1024 * 1024)).toBe('183 MB');
|
||||
expect(formatBytes(14.7 * 1024 * 1024)).toBe('14.7 MB');
|
||||
});
|
||||
|
||||
it('formats byte rates with per-second units', () => {
|
||||
expect(formatBytesPerSecond(0, 3600)).toBe('0 B/s');
|
||||
expect(formatBytesPerSecond(1630858.9, 3600)).toBe('453 B/s');
|
||||
expect(formatBytesPerSecond(22620490956.8, 3600)).toBe('6 MB/s');
|
||||
});
|
||||
|
||||
it('formats percent values without long decimals', () => {
|
||||
expect(formatPercent(58.36809884639982)).toBe('58.4%');
|
||||
});
|
||||
|
||||
it('rounds chart max values to friendly axis steps', () => {
|
||||
expect(calculateNiceAxisMax([0, 58.36809884639982])).toBe(80);
|
||||
expect(calculateNiceAxisMax([0, 1630858.9])).toBe(2000000);
|
||||
expect(calculateNiceAxisMax([0, 22620490956.8])).toBe(25000000000);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
import {getCurrentNavigationItem, isPathActive} from '@/lib/utils/navigation';
|
||||
|
||||
describe('navigation utils', () => {
|
||||
it('marks root path as active only for home', () => {
|
||||
expect(isPathActive('/', '/')).toBe(true);
|
||||
expect(isPathActive('/node', '/')).toBe(false);
|
||||
});
|
||||
|
||||
it('resolves current navigation item for nested paths', () => {
|
||||
expect(getCurrentNavigationItem('/node/abc')?.label).toBe('节点');
|
||||
expect(getCurrentNavigationItem('/website')?.label).toBe('网站');
|
||||
expect(getCurrentNavigationItem('/performance')?.label).toBe('性能');
|
||||
expect(getCurrentNavigationItem('/setting')?.label).toBe('设置');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
import type {NodeItem} from '@/features/nodes/types';
|
||||
import {isNodeAbnormal} from '@/features/nodes/utils';
|
||||
|
||||
function buildNode(overrides: Partial<NodeItem> = {}): NodeItem {
|
||||
return {
|
||||
id: 1,
|
||||
node_id: 'node-1',
|
||||
node_type: 'edge_node',
|
||||
name: 'node-1',
|
||||
ip: '127.0.0.1',
|
||||
ip_manual_override: false,
|
||||
relay_bind_port: 7000,
|
||||
relay_vhost_http_port: 8080,
|
||||
relay_client_access_addr: '',
|
||||
relay_agent_access_addr: '',
|
||||
relay_client_proxy_url: '',
|
||||
relay_auth_token: '',
|
||||
relay_status: 'healthy',
|
||||
relay_web_server_enabled: false,
|
||||
relay_frps_connections: 0,
|
||||
relay_frps_proxy_count: 0,
|
||||
geo_name: '',
|
||||
geo_latitude: null,
|
||||
geo_longitude: null,
|
||||
geo_manual_override: false,
|
||||
access_token: '',
|
||||
auto_update_enabled: false,
|
||||
update_requested: false,
|
||||
update_channel: 'stable',
|
||||
update_tag: '',
|
||||
restart_openresty_requested: false,
|
||||
version: '1.0.0',
|
||||
ext_version: '',
|
||||
openresty_status: 'healthy',
|
||||
openresty_message: '',
|
||||
status: 'online',
|
||||
current_version: '20260601-001',
|
||||
last_seen_at: '',
|
||||
last_error: '',
|
||||
latest_apply_result: '',
|
||||
latest_apply_message: '',
|
||||
latest_apply_checksum: '',
|
||||
latest_main_config_checksum: '',
|
||||
latest_route_config_checksum: '',
|
||||
latest_support_file_count: 0,
|
||||
latest_apply_at: null,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('isNodeAbnormal', () => {
|
||||
it('treats offline nodes as abnormal', () => {
|
||||
expect(
|
||||
isNodeAbnormal(buildNode({ status: 'offline' }), '20260601-001'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('treats unhealthy edge nodes as abnormal', () => {
|
||||
expect(
|
||||
isNodeAbnormal(
|
||||
buildNode({ node_type: 'edge_node', openresty_status: 'unhealthy' }),
|
||||
'20260601-001',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('treats unhealthy relay nodes as abnormal', () => {
|
||||
expect(
|
||||
isNodeAbnormal(
|
||||
buildNode({ node_type: 'tunnel_relay', relay_status: 'unhealthy' }),
|
||||
'20260601-001',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('treats nodes behind the active version as abnormal', () => {
|
||||
expect(
|
||||
isNodeAbnormal(
|
||||
buildNode({
|
||||
node_type: 'tunnel_client',
|
||||
current_version: '20260531-001',
|
||||
}),
|
||||
'20260601-001',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores version lag when there is no active version', () => {
|
||||
expect(
|
||||
isNodeAbnormal(
|
||||
buildNode({
|
||||
node_type: 'tunnel_client',
|
||||
current_version: '20260531-001',
|
||||
}),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps healthy synced nodes out of the abnormal filter', () => {
|
||||
expect(isNodeAbnormal(buildNode(), '20260601-001')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,444 @@
|
||||
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
||||
import {render, screen, waitFor, within} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type {ReactNode} from 'react';
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
import {ThemeProvider} from '@/components/providers/theme-provider';
|
||||
import {ProxyRouteConfigPage} from '@/features/proxy-routes/components/proxy-route-config-page';
|
||||
import {ProxyRoutesPage} from '@/features/proxy-routes/components/proxy-routes-page';
|
||||
|
||||
const pushMock = vi.fn();
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: pushMock,
|
||||
}),
|
||||
}));
|
||||
|
||||
function stubMatchMedia() {
|
||||
vi.stubGlobal(
|
||||
'matchMedia',
|
||||
vi.fn().mockImplementation(() => ({
|
||||
matches: false,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
function buildRoute(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: 9,
|
||||
site_name: 'marketing-site',
|
||||
domain: 'app.example.com',
|
||||
domains: ['app.example.com', 'www.example.com'],
|
||||
primary_domain: 'app.example.com',
|
||||
domain_count: 2,
|
||||
origin_id: null,
|
||||
origin_url: 'https://origin-a.internal:443',
|
||||
origin_host: '',
|
||||
upstreams: JSON.stringify([
|
||||
'https://origin-a.internal:443',
|
||||
'https://origin-b.internal:443',
|
||||
]),
|
||||
upstream_list: [
|
||||
'https://origin-a.internal:443',
|
||||
'https://origin-b.internal:443',
|
||||
],
|
||||
enabled: true,
|
||||
enable_https: true,
|
||||
cert_id: 1,
|
||||
cert_ids: [1],
|
||||
domain_cert_ids: [1, 0],
|
||||
redirect_http: true,
|
||||
limit_conn_per_server: 120,
|
||||
limit_conn_per_ip: 12,
|
||||
limit_rate: '512k',
|
||||
cache_enabled: true,
|
||||
cache_policy: 'path_prefix',
|
||||
cache_rules: JSON.stringify(['/assets']),
|
||||
cache_rule_list: ['/assets'],
|
||||
custom_headers: JSON.stringify([{ key: 'X-Site', value: 'marketing' }]),
|
||||
custom_header_list: [{ key: 'X-Site', value: 'marketing' }],
|
||||
remark: 'Marketing website',
|
||||
created_at: '2026-03-20T08:00:00Z',
|
||||
updated_at: '2026-03-21T08:00:00Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function buildDiff(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
active_version: '20260330-001',
|
||||
added_sites: [],
|
||||
removed_sites: [],
|
||||
modified_sites: [],
|
||||
added_domains: [],
|
||||
removed_domains: [],
|
||||
modified_domains: [],
|
||||
main_config_changed: false,
|
||||
changed_option_keys: [],
|
||||
changed_option_details: [],
|
||||
current_website_count: 1,
|
||||
active_website_count: 1,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function renderWithProviders(ui: ReactNode) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
mutations: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>{ui}</ThemeProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('Proxy route website pages', () => {
|
||||
beforeEach(() => {
|
||||
pushMock.mockReset();
|
||||
stubMatchMedia();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('renders website list summary with config entry', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn((input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
|
||||
if (url.includes('/proxy-routes/')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: [buildRoute()],
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url.includes('/config-versions/diff')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: buildDiff({
|
||||
modified_sites: ['marketing-site'],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unhandled fetch: ${url}`));
|
||||
}),
|
||||
);
|
||||
|
||||
renderWithProviders(<ProxyRoutesPage />);
|
||||
|
||||
expect(await screen.findByText('marketing-site')).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/app\.example\.com/).length).toBeGreaterThan(0);
|
||||
expect(screen.getByRole('link')).toHaveAttribute(
|
||||
'href',
|
||||
'/proxy-route/detail?id=9§ion=domains',
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a website and navigates to config page', async () => {
|
||||
const routes: Array<Record<string, unknown>> = [];
|
||||
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
const method = init?.method?.toUpperCase() ?? 'GET';
|
||||
|
||||
if (url.includes('/proxy-routes/') && method === 'POST') {
|
||||
const payload = JSON.parse(String(init?.body));
|
||||
const created = buildRoute({
|
||||
id: 21,
|
||||
site_name: payload.site_name,
|
||||
domain: payload.domain,
|
||||
domains: payload.domains,
|
||||
primary_domain: payload.domain,
|
||||
domain_count: payload.domains.length,
|
||||
origin_url: payload.origin_url,
|
||||
upstreams: JSON.stringify([
|
||||
payload.origin_url,
|
||||
...payload.upstreams,
|
||||
]),
|
||||
upstream_list: [payload.origin_url, ...payload.upstreams],
|
||||
enabled: payload.enabled,
|
||||
enable_https: payload.enable_https,
|
||||
cert_id: payload.cert_id,
|
||||
cert_ids: payload.cert_ids ?? [],
|
||||
domain_cert_ids: payload.domain_cert_ids ?? [],
|
||||
redirect_http: payload.redirect_http,
|
||||
limit_conn_per_server: 0,
|
||||
limit_conn_per_ip: 0,
|
||||
limit_rate: '',
|
||||
cache_enabled: false,
|
||||
cache_policy: 'url',
|
||||
cache_rules: '[]',
|
||||
cache_rule_list: [],
|
||||
custom_headers: '[]',
|
||||
custom_header_list: [],
|
||||
remark: payload.remark,
|
||||
});
|
||||
routes.splice(0, routes.length, created);
|
||||
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: created,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url.includes('/proxy-routes/')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: routes,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url.includes('/config-versions/diff')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: buildDiff(),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url.includes('/managed-domains/')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: [
|
||||
{ id: 1, domain: '*.example.com', cert_id: 1, enabled: true },
|
||||
],
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url.includes('/tls-certificates/')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: [{ id: 1, name: 'example-cert', not_after: null }],
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unhandled fetch: ${url}`));
|
||||
}),
|
||||
);
|
||||
|
||||
renderWithProviders(<ProxyRoutesPage />);
|
||||
|
||||
const user = userEvent.setup();
|
||||
const pageButtons = await screen.findAllByRole('button');
|
||||
await user.click(pageButtons[1]);
|
||||
|
||||
const dialog = await screen.findByRole('dialog');
|
||||
expect(dialog).toBeInTheDocument();
|
||||
|
||||
await user.type(
|
||||
within(dialog).getByPlaceholderText('marketing-site'),
|
||||
'launch-site',
|
||||
);
|
||||
|
||||
const primaryDomainInput = within(dialog).getByLabelText('域名 1');
|
||||
await user.type(primaryDomainInput, 'app.exam');
|
||||
await user.click(
|
||||
await within(dialog).findByRole('button', { name: 'app.example.com' }),
|
||||
);
|
||||
|
||||
await user.click(within(dialog).getByLabelText('新增域名输入框'));
|
||||
await user.type(within(dialog).getByLabelText('域名 2'), 'www.example.com');
|
||||
|
||||
await user.type(
|
||||
within(dialog).getByLabelText('上游地址'),
|
||||
'https://origin-a.internal:443{enter}https://origin-b.internal:443',
|
||||
);
|
||||
|
||||
const submitButton = document.querySelector(
|
||||
'button[form="create-website-form"]',
|
||||
) as HTMLButtonElement | null;
|
||||
expect(submitButton).toBeInstanceOf(HTMLButtonElement);
|
||||
if (!submitButton) {
|
||||
throw new Error('missing create submit button');
|
||||
}
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(pushMock).toHaveBeenCalledWith(
|
||||
'/proxy-route/detail?id=21§ion=domains',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('saves domain settings from config page by section', async () => {
|
||||
const updateRequests: Array<Record<string, unknown>> = [];
|
||||
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
const method = init?.method?.toUpperCase() ?? 'GET';
|
||||
|
||||
if (url.includes('/proxy-routes/9/update') && method === 'POST') {
|
||||
const payload = JSON.parse(String(init?.body)) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
updateRequests.push(payload);
|
||||
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: buildRoute({
|
||||
site_name: payload.site_name,
|
||||
domain: (payload.domains as string[])[0],
|
||||
domains: payload.domains,
|
||||
primary_domain: (payload.domains as string[])[0],
|
||||
domain_count: (payload.domains as string[]).length,
|
||||
enabled: payload.enabled,
|
||||
enable_https: payload.enable_https,
|
||||
cert_id: payload.cert_id,
|
||||
cert_ids: payload.cert_ids,
|
||||
domain_cert_ids: payload.domain_cert_ids,
|
||||
redirect_http: payload.redirect_http,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url.includes('/proxy-routes/9')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: buildRoute(),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url.includes('/tls-certificates/')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: [{ id: 1, name: 'example-cert', not_after: null }],
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url.includes('/managed-domains/')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: [
|
||||
{ id: 1, domain: '*.example.com', cert_id: 1, enabled: true },
|
||||
],
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unhandled fetch: ${url}`));
|
||||
}),
|
||||
);
|
||||
|
||||
renderWithProviders(
|
||||
<ProxyRouteConfigPage routeId="9" initialSection="domains" />,
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
expect(await screen.findByText('marketing-site')).toBeInTheDocument();
|
||||
|
||||
const siteNameInput = screen.getByPlaceholderText('marketing-site');
|
||||
await user.clear(siteNameInput);
|
||||
await user.type(siteNameInput, 'brand-site');
|
||||
|
||||
const primaryDomainInput = screen.getByLabelText('域名 1');
|
||||
await user.clear(primaryDomainInput);
|
||||
await user.type(primaryDomainInput, 'brand.example.com');
|
||||
|
||||
const secondaryDomainInput = screen.getByLabelText('域名 2');
|
||||
await user.clear(secondaryDomainInput);
|
||||
await user.type(secondaryDomainInput, 'www.brand.example.com');
|
||||
|
||||
await user.selectOptions(screen.getByLabelText('证书 1'), '1');
|
||||
await user.selectOptions(screen.getByLabelText('证书 2'), '');
|
||||
|
||||
const saveButton = document.querySelector(
|
||||
'button[form="proxy-route-domains-form"]',
|
||||
) as HTMLButtonElement | null;
|
||||
expect(saveButton).toBeInstanceOf(HTMLButtonElement);
|
||||
if (!saveButton) {
|
||||
throw new Error('missing domain save button');
|
||||
}
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateRequests).toHaveLength(1);
|
||||
});
|
||||
|
||||
expect(updateRequests[0]).toMatchObject({
|
||||
site_name: 'brand-site',
|
||||
domain: 'brand.example.com',
|
||||
domains: ['brand.example.com', 'www.brand.example.com'],
|
||||
enabled: true,
|
||||
enable_https: true,
|
||||
cert_id: 1,
|
||||
cert_ids: [1],
|
||||
domain_cert_ids: [1, 0],
|
||||
redirect_http: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { isThemeMode, resolveTheme } from '@/lib/theme/theme';
|
||||
|
||||
describe('theme utils', () => {
|
||||
it('validates theme mode values', () => {
|
||||
expect(isThemeMode('light')).toBe(true);
|
||||
expect(isThemeMode('dark')).toBe(true);
|
||||
expect(isThemeMode('system')).toBe(true);
|
||||
expect(isThemeMode('custom')).toBe(false);
|
||||
});
|
||||
|
||||
it('resolves system mode using system preference', () => {
|
||||
expect(resolveTheme('system', true)).toBe('dark');
|
||||
expect(resolveTheme('system', false)).toBe('light');
|
||||
expect(resolveTheme('dark', false)).toBe('dark');
|
||||
expect(resolveTheme('light', true)).toBe('light');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,386 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { ReactNode } from 'react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ThemeProvider } from '@/components/providers/theme-provider';
|
||||
import { WAFIPGroupsPage } from '@/features/waf/components/ip-groups-page';
|
||||
import { WAFPage } from '@/features/waf/components/waf-page';
|
||||
|
||||
const pushMock = vi.fn();
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: pushMock,
|
||||
}),
|
||||
}));
|
||||
|
||||
function renderWithProviders(ui: ReactNode) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>{ui}</ThemeProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
function stubMatchMedia() {
|
||||
vi.stubGlobal(
|
||||
'matchMedia',
|
||||
vi.fn().mockImplementation(() => ({
|
||||
matches: false,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
function buildIPGroup(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: 3,
|
||||
name: 'edge blacklist',
|
||||
type: 'manual',
|
||||
enabled: true,
|
||||
ip_list: ['203.0.113.10'],
|
||||
auto_config: {},
|
||||
subscription_url: '',
|
||||
subscription_format: 'text',
|
||||
subscription_mapping_rule: '',
|
||||
sync_interval_minutes: 1440,
|
||||
last_sync_status: '',
|
||||
last_sync_message: '',
|
||||
remark: '',
|
||||
referenced_by_rule_count: 0,
|
||||
created_at: '2026-06-01T00:00:00Z',
|
||||
updated_at: '2026-06-01T00:00:00Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRuleGroup(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: 1,
|
||||
name: '全局规则组',
|
||||
enabled: true,
|
||||
is_global: true,
|
||||
block_status_code: 418,
|
||||
block_response_body: '',
|
||||
ip_whitelist: [],
|
||||
ip_blacklist: [],
|
||||
ip_whitelist_group_ids: [],
|
||||
ip_blacklist_group_ids: [],
|
||||
country_whitelist: [],
|
||||
country_blacklist: [],
|
||||
region_whitelist: [],
|
||||
region_blacklist: [],
|
||||
pow_enabled: false,
|
||||
pow_config: {
|
||||
difficulty: 4,
|
||||
algorithm: 'fast',
|
||||
session_ttl: 600,
|
||||
challenge_ttl: 300,
|
||||
whitelist: {
|
||||
ips: [],
|
||||
ip_cidrs: [],
|
||||
paths: [],
|
||||
path_regexes: [],
|
||||
user_agents: [],
|
||||
},
|
||||
blacklist: {
|
||||
ips: [],
|
||||
ip_cidrs: [],
|
||||
paths: [],
|
||||
path_regexes: [],
|
||||
user_agents: [],
|
||||
},
|
||||
},
|
||||
remark: '',
|
||||
applied_site_ids: [],
|
||||
applied_site_count: 0,
|
||||
created_at: '2026-06-01T00:00:00Z',
|
||||
updated_at: '2026-06-01T00:00:00Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('WAF IP groups', () => {
|
||||
beforeEach(() => {
|
||||
pushMock.mockReset();
|
||||
stubMatchMedia();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('renders empty state and saves a manual IP group', async () => {
|
||||
let groups: Array<Record<string, unknown>> = [];
|
||||
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
const method = init?.method?.toUpperCase() ?? 'GET';
|
||||
|
||||
if (url.includes('/waf/ip-groups') && method === 'POST') {
|
||||
const payload = JSON.parse(String(init?.body));
|
||||
const created = buildIPGroup({
|
||||
id: 7,
|
||||
name: payload.name,
|
||||
ip_list: payload.ip_list,
|
||||
});
|
||||
groups = [created];
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({ success: true, message: '', data: created }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url.includes('/waf/ip-groups')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({ success: true, message: '', data: groups }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unhandled fetch: ${url}`));
|
||||
}),
|
||||
);
|
||||
|
||||
renderWithProviders(<WAFIPGroupsPage />);
|
||||
|
||||
expect(await screen.findByText('暂无 IP 组')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole('button', { name: /新建 IP 组/ }));
|
||||
await userEvent.clear(screen.getByLabelText('IP 组名称'));
|
||||
await userEvent.type(screen.getByLabelText('IP 组名称'), 'blocked edge');
|
||||
await userEvent.type(
|
||||
screen.getByPlaceholderText(/203\.0\.113\.10/),
|
||||
'203.0.113.10',
|
||||
);
|
||||
await userEvent.click(screen.getByRole('button', { name: /保存 IP 组/ }));
|
||||
|
||||
expect(await screen.findByText('IP 组已保存。')).toBeInTheDocument();
|
||||
await waitFor(() => expect(groups).toHaveLength(1));
|
||||
});
|
||||
|
||||
it('adds automatic Expr preset rules', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn((input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
|
||||
if (url.includes('/waf/ip-groups')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({ success: true, message: '', data: [] }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unhandled fetch: ${url}`));
|
||||
}),
|
||||
);
|
||||
|
||||
renderWithProviders(<WAFIPGroupsPage />);
|
||||
|
||||
await screen.findByText('暂无 IP 组');
|
||||
await userEvent.click(screen.getByRole('button', { name: /新建 IP 组/ }));
|
||||
await userEvent.selectOptions(screen.getByLabelText('类型'), 'automatic');
|
||||
await userEvent.click(screen.getByText('单 IP 404 高频扫描'));
|
||||
await userEvent.click(screen.getByText('单 IP 直连访问异常'));
|
||||
|
||||
const textarea = screen.getByLabelText(/自动配置 JSON/);
|
||||
const value = (textarea as HTMLTextAreaElement).value;
|
||||
expect(value).toContain('request_count > 100 && StatusRatio(404) >= 0.8');
|
||||
expect(value).toContain('ip_host_count > 50 && ip_host_ratio > 0.5');
|
||||
});
|
||||
|
||||
it('tests automatic Expr rules before saving', async () => {
|
||||
const testMock = vi.fn();
|
||||
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
const method = init?.method?.toUpperCase() ?? 'GET';
|
||||
|
||||
if (url.includes('/waf/ip-groups/test') && method === 'POST') {
|
||||
testMock(JSON.parse(String(init?.body)));
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: {
|
||||
matched_ips: ['203.0.113.10', '203.0.113.11'],
|
||||
matched_count: 2,
|
||||
lookback_minutes: 60,
|
||||
rule_count: 1,
|
||||
tested_at: '2026-06-01T00:00:00Z',
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url.includes('/waf/ip-groups')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({ success: true, message: '', data: [] }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unhandled fetch: ${url}`));
|
||||
}),
|
||||
);
|
||||
|
||||
renderWithProviders(<WAFIPGroupsPage />);
|
||||
|
||||
await screen.findByText('暂无 IP 组');
|
||||
await userEvent.click(screen.getByRole('button', { name: /新建 IP 组/ }));
|
||||
await userEvent.selectOptions(screen.getByLabelText('类型'), 'automatic');
|
||||
await userEvent.click(screen.getByText('单 IP 404 高频扫描'));
|
||||
await userEvent.click(screen.getByRole('button', { name: /测试规则/ }));
|
||||
|
||||
expect(await screen.findByText('命中 2 个 IP。')).toBeInTheDocument();
|
||||
expect(screen.getByText(/203\.0\.113\.10/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/203\.0\.113\.11/)).toBeInTheDocument();
|
||||
expect(testMock).toHaveBeenCalledWith({
|
||||
auto_config: expect.objectContaining({
|
||||
lookback_minutes: 60,
|
||||
rules: [
|
||||
expect.objectContaining({
|
||||
expr: 'request_count > 100 && StatusRatio(404) >= 0.8',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('opens IP group management from WAF page and references an IP group', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
const method = init?.method?.toUpperCase() ?? 'GET';
|
||||
|
||||
if (url.includes('/waf/rule-groups/1/update') && method === 'POST') {
|
||||
const payload = JSON.parse(String(init?.body));
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: buildRuleGroup({
|
||||
ip_blacklist_group_ids: payload.ip_blacklist_group_ids,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url.includes('/waf/rule-groups')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: [buildRuleGroup()],
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url.includes('/waf/ip-groups')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: [buildIPGroup()],
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url.includes('/proxy-routes/')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({ success: true, message: '', data: [] }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unhandled fetch: ${url}`));
|
||||
}),
|
||||
);
|
||||
|
||||
renderWithProviders(<WAFPage />);
|
||||
|
||||
await userEvent.click(
|
||||
await screen.findByRole('button', { name: /管理 IP 组/ }),
|
||||
);
|
||||
expect(pushMock).toHaveBeenCalledWith('/waf/ip-groups');
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /黑白名单/ }));
|
||||
await userEvent.click(screen.getByRole('button', { name: /添加/ }));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'IP 组' }));
|
||||
await userEvent.click(await screen.findByLabelText(/edge blacklist/));
|
||||
const addButtons = screen.getAllByRole('button', { name: '添加' });
|
||||
await userEvent.click(addButtons[addButtons.length - 1]);
|
||||
|
||||
expect(await screen.findByText(/IP组: edge blacklist/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('defaults new rule entries to blacklist', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn((input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
|
||||
if (url.includes('/waf/rule-groups')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '',
|
||||
data: [buildRuleGroup()],
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url.includes('/waf/ip-groups') || url.includes('/proxy-routes/')) {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({ success: true, message: '', data: [] }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unhandled fetch: ${url}`));
|
||||
}),
|
||||
);
|
||||
|
||||
renderWithProviders(<WAFPage />);
|
||||
|
||||
await userEvent.click(
|
||||
await screen.findByRole('button', { name: /黑白名单/ }),
|
||||
);
|
||||
await userEvent.click(screen.getByRole('button', { name: /添加/ }));
|
||||
|
||||
expect(
|
||||
await screen.findByRole('heading', { name: '添加黑名单规则' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user