| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- import type { ReactNode } from 'react';
- import { renderHook, waitFor, act } from '@testing-library/react';
- import { QueryClientProvider } from '@tanstack/react-query';
- import { afterEach, describe, expect, it, vi } from 'vitest';
- import { sameSpeedMap, useClients } from '@/hooks/useClients';
- import { makeTestQueryClient } from '@/test/test-utils';
- import { HttpUtil, Msg } from '@/utils';
- import type { ClientsSummary } from '@/schemas/client';
- afterEach(() => {
- vi.restoreAllMocks();
- });
- describe('websocket payload identity preservation', () => {
- const speed = (up: number, down: number) => ({ up, down });
- it('treats an unchanged speed map as unchanged', () => {
- const a = { 'a@x': speed(1, 2), 'b@x': speed(3, 4) };
- expect(sameSpeedMap(a, { 'a@x': speed(1, 2), 'b@x': speed(3, 4) })).toBe(true);
- expect(sameSpeedMap(a, { 'a@x': speed(1, 2) })).toBe(false);
- expect(sameSpeedMap(a, { 'a@x': speed(1, 2), 'b@x': speed(3, 5) })).toBe(false);
- expect(sameSpeedMap(a, { 'a@x': speed(1, 2), 'c@x': speed(3, 4) })).toBe(false);
- expect(sameSpeedMap({}, {})).toBe(true);
- });
- });
- describe('client summary always reflects the server, never a client_stats recompute (#6116)', () => {
- const serverSummary: ClientsSummary = {
- total: 3,
- active: 3,
- onlineCount: 0,
- depletedCount: 0,
- expiringCount: 0,
- deactiveCount: 0,
- online: [],
- depleted: [],
- expiring: [],
- deactive: [],
- };
- const pagedResponse = {
- items: [],
- total: 3,
- filtered: 3,
- page: 1,
- pageSize: 25,
- groups: [],
- summary: serverSummary,
- };
- function mockPanel() {
- vi.spyOn(HttpUtil, 'get').mockImplementation(async (url: string) => {
- if (url.includes('/clients/list/paged')) return new Msg(true, '', pagedResponse);
- if (url.includes('/inbounds/options')) return new Msg(true, '', []);
- return new Msg(true, '', null);
- });
- vi.spyOn(HttpUtil, 'post').mockImplementation(async (url: string) => {
- if (url.includes('/setting/defaultSettings')) return new Msg(true, '', { pageSize: 25 });
- if (url.includes('/clients/onlines')) return new Msg(true, '', []);
- return new Msg(true, '', null);
- });
- }
- function wrapperFor() {
- const queryClient = makeTestQueryClient();
- return ({ children }: { children: ReactNode }) => (
- <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
- );
- }
- async function loadedHook() {
- mockPanel();
- const { result } = renderHook(() => useClients(), { wrapper: wrapperFor() });
- await waitFor(() => expect(result.current.settingsReady).toBe(true));
- act(() => {
- result.current.setQuery({ page: 1, pageSize: 25, sort: 'createdAt', order: 'ascend' });
- });
- await waitFor(() => expect(result.current.fetched).toBe(true));
- expect(result.current.summary).toEqual(serverSummary);
- return result;
- }
- it('stays pinned to the server summary across a client_stats push carrying an orphan row with no matching gap', async () => {
- const result = await loadedHook();
- act(() => {
- result.current.applyClientStatsEvent({
- snapshot: true,
- clients: [
- { email: 'a@x', enable: true, up: 0, down: 0, total: 0, expiryTime: 0 },
- { email: 'b@x', enable: true, up: 0, down: 0, total: 0, expiryTime: 0 },
- { email: 'c@x', enable: true, up: 0, down: 0, total: 0, expiryTime: 0 },
- { email: 'ghost@x', enable: false, up: 0, down: 0, total: 1, expiryTime: 1 },
- ],
- });
- });
- expect(result.current.summary).toEqual(serverSummary);
- });
- it('stays pinned to the server summary across a client_stats push where an orphan and a gap net out to the server total', async () => {
- const result = await loadedHook();
- act(() => {
- result.current.applyClientStatsEvent({
- snapshot: true,
- clients: [
- { email: 'a@x', enable: true, up: 0, down: 0, total: 0, expiryTime: 0 },
- { email: 'b@x', enable: true, up: 0, down: 0, total: 0, expiryTime: 0 },
- { email: 'ghost@x', enable: false, up: 0, down: 0, total: 1, expiryTime: 1 },
- ],
- });
- });
- expect(result.current.summary).toEqual(serverSummary);
- });
- it('combines independently sampled TUIC and Xray speeds and replaces each source snapshot', async () => {
- const result = await loadedHook();
- act(() => {
- result.current.applyTrafficEvent({
- clientTraffics: [{ email: '[email protected]', up: 100, down: 150 }],
- });
- });
- expect(result.current.clientSpeed['[email protected]']).toEqual({ up: 20, down: 30 });
- act(() => {
- result.current.applyTrafficEvent({
- clientTrafficSource: 'tuic',
- clientTrafficIntervalMs: 10_000,
- clientTraffics: [
- { email: '[email protected]', up: 300, down: 100 },
- { email: '[email protected]', up: 100, down: 100 },
- ],
- });
- });
- expect(result.current.clientSpeed['[email protected]']).toEqual({ up: 60, down: 50 });
- act(() => {
- result.current.applyTrafficEvent({
- clientTraffics: [{ email: '[email protected]', up: 50, down: 25 }],
- });
- });
- expect(result.current.clientSpeed['[email protected]']).toEqual({ up: 50, down: 25 });
- act(() => {
- result.current.applyTrafficEvent({
- clientTrafficSource: 'tuic',
- clientTrafficIntervalMs: 10_000,
- clientTraffics: [],
- });
- });
- expect(result.current.clientSpeed['[email protected]']).toEqual({ up: 10, down: 5 });
- });
- it('expires stale per-source speeds without clearing the other source', async () => {
- const result = await loadedHook();
- vi.useFakeTimers();
- try {
- act(() => {
- result.current.applyTrafficEvent({
- clientTrafficSource: 'xray',
- clientTrafficIntervalMs: 1_000,
- clientTraffics: [{ email: '[email protected]', up: 100, down: 200 }],
- });
- result.current.applyTrafficEvent({
- clientTrafficSource: 'tuic',
- clientTrafficIntervalMs: 2_000,
- clientTraffics: [{ email: '[email protected]', up: 300, down: 400 }],
- });
- });
- expect(result.current.clientSpeed['[email protected]']).toEqual({ up: 250, down: 400 });
- act(() => vi.advanceTimersByTime(2_000));
- expect(result.current.clientSpeed['[email protected]']).toEqual({ up: 150, down: 200 });
- act(() => vi.advanceTimersByTime(2_000));
- expect(result.current.clientSpeed).toEqual({});
- } finally {
- vi.useRealTimers();
- }
- });
- });
|