diff --git a/frontend/src/pages/xray/outbounds/outbounds-tab-helpers.ts b/frontend/src/pages/xray/outbounds/outbounds-tab-helpers.ts index 108eea1c6..27bd0ba5a 100644 --- a/frontend/src/pages/xray/outbounds/outbounds-tab-helpers.ts +++ b/frontend/src/pages/xray/outbounds/outbounds-tab-helpers.ts @@ -25,27 +25,34 @@ export function originalOutboundIndex(rows: OutboundRow[], positionalIndex: numb export function outboundAddresses(o: OutboundRow): string[] { const settings = o.settings as Record | undefined; - switch (o.protocol) { - case Protocols.VMess: { + switch (true) { + case isOutboundProtocol(o, Protocols.VMess): { const serverObj = settings?.vnext as Array<{ address: string; port: number }> | undefined; return serverObj ? serverObj.map((s) => `${s.address}:${s.port}`) : []; } - case Protocols.VLESS: - return [`${settings?.address || ''}:${settings?.port || ''}`]; - case Protocols.HTTP: - case Protocols.Socks: - case Protocols.Shadowsocks: - case Protocols.Trojan: { + case isOutboundProtocol(o, Protocols.VLESS): + case isOutboundProtocol(o, Protocols.Hysteria): { + // A vless row carries either shape, and the probe reads both. + const vnext = settings?.vnext as Array<{ address?: string; port?: number }> | undefined; + const addr = vnext?.[0]?.address || (settings?.address as string | undefined); + const port = vnext?.[0]?.port || (settings?.port as string | number | undefined); + return addr || port ? [`${addr || ''}:${port || ''}`] : []; + } + case isOutboundProtocol(o, Protocols.HTTP): + case isOutboundProtocol(o, Protocols.Socks): + case isOutboundProtocol(o, Protocols.Shadowsocks): + case isOutboundProtocol(o, Protocols.Trojan): { const serverObj = settings?.servers as Array<{ address: string; port: number }> | undefined; return serverObj ? serverObj.map((s) => `${s.address}:${s.port}`) : []; } - case Protocols.DNS: { + case isOutboundProtocol(o, Protocols.DNS): { const addr = (settings?.rewriteAddress as string) || (settings?.address as string) || ''; const port = (settings?.rewritePort as string | number) || (settings?.port as string | number) || ''; return addr || port ? [`${addr}:${port}`] : []; } - case Protocols.Wireguard: + case isOutboundProtocol(o, Protocols.Wireguard): + case isOutboundProtocol(o, Protocols.AmneziaWG): return ((settings?.peers as Array<{ endpoint?: string }>) || []) .map((p) => p.endpoint || '') .filter(Boolean); diff --git a/frontend/src/pages/xray/outbounds/useOutboundColumns.tsx b/frontend/src/pages/xray/outbounds/useOutboundColumns.tsx index 64d0fbe5d..6c4fb2a97 100644 --- a/frontend/src/pages/xray/outbounds/useOutboundColumns.tsx +++ b/frontend/src/pages/xray/outbounds/useOutboundColumns.tsx @@ -18,7 +18,7 @@ import type { ColumnsType } from 'antd/es/table'; import { SizeFormatter } from '@/utils'; import { activateOnKey } from '@/utils/a11y'; -import { OutboundProtocols as Protocols } from '@/schemas/primitives'; +import { isOutboundProtocol, OutboundProtocols as Protocols } from '@/schemas/primitives'; import type { OutboundTestMode, OutboundTestState, @@ -169,8 +169,8 @@ export function useOutboundColumns({
{record.protocol} - {[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes( - record.protocol as never, + {[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].some( + (id) => isOutboundProtocol(record, id), ) && ( <> {record.streamSettings?.network} diff --git a/frontend/src/test/outbound-identity-tags-case.test.tsx b/frontend/src/test/outbound-identity-tags-case.test.tsx new file mode 100644 index 000000000..3926bb55a --- /dev/null +++ b/frontend/src/test/outbound-identity-tags-case.test.tsx @@ -0,0 +1,60 @@ +import { describe, it, expect, vi } from 'vitest'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +import OutboundsTab from '@/pages/xray/outbounds/OutboundsTab'; +import type { XraySettingsValue } from '@/hooks/useXraySetting'; + +import { renderWithProviders } from './test-utils'; + +// The core lowercases the id, so a "VMess" row is a vmess outbound: its stream +// tags must follow the same rule its address does. +function settingsWithCapitalisedProtocol(): XraySettingsValue { + return { + outbounds: [ + { + tag: 'proxy-a', + protocol: 'VMess', + settings: { vnext: [{ address: 'a.example.com', port: 443 }] }, + streamSettings: { network: 'ws', security: 'tls' }, + }, + ], + } as unknown as XraySettingsValue; +} + +function renderTab(settings: XraySettingsValue) { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + return renderWithProviders( + + + , + ); +} + +describe('OutboundsTab row for a case-variant protocol id', () => { + it('renders the stream tags and the address of a "VMess" row', () => { + renderTab(settingsWithCapitalisedProtocol()); + + const row = document.querySelector('.ant-table-tbody tr.ant-table-row'); + const tags = Array.from(row?.querySelectorAll('.protocol-line .ant-tag') ?? []).map( + (el) => el.textContent, + ); + expect(tags).toEqual(['VMess', 'ws', 'tls']); + expect(row?.textContent).toContain('a.example.com:443'); + }); +}); diff --git a/frontend/src/test/outbounds-addresses-case.test.ts b/frontend/src/test/outbounds-addresses-case.test.ts new file mode 100644 index 000000000..e79d828c2 --- /dev/null +++ b/frontend/src/test/outbounds-addresses-case.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest'; + +import { outboundAddresses } from '@/pages/xray/outbounds/outbounds-tab-helpers'; +import type { OutboundRow } from '@/pages/xray/outbounds/outbounds-tab-types'; + +// The core lowercases a protocol id before resolving the handler, so a row +// spelled "VMess" must still show the address its settings carry. +const row = (protocol: string, settings: Record): OutboundRow => ({ + key: 0, + tag: 'p', + protocol, + settings, +}); + +const vnext = { vnext: [{ address: 'a.example.com', port: 443 }] }; + +describe('outboundAddresses', () => { + it('reads a capitalised vmess id', () => { + expect(outboundAddresses(row('VMess', vnext))).toEqual(['a.example.com:443']); + }); + + it('reads a capitalised trojan id', () => { + expect( + outboundAddresses(row('Trojan', { servers: [{ address: 'b.example.com', port: 8443 }] })), + ).toEqual(['b.example.com:8443']); + }); + + it('reads a capitalised wireguard id', () => { + expect( + outboundAddresses(row('WireGuard', { peers: [{ endpoint: 'c.example.com:51820' }] })), + ).toEqual(['c.example.com:51820']); + }); + + it('reads a capitalised dns id', () => { + expect(outboundAddresses(row('DNS', { rewriteAddress: '1.1.1.1', rewritePort: 53 }))).toEqual([ + '1.1.1.1:53', + ]); + }); + + it('reads the flat server of a capitalised vless id', () => { + expect(outboundAddresses(row('VLESS', { address: 'd.example.com', port: 443 }))).toEqual([ + 'd.example.com:443', + ]); + }); + + it('leaves a canonical id unchanged', () => { + expect(outboundAddresses(row('vmess', vnext))).toEqual(['a.example.com:443']); + }); + + it('still returns nothing for a protocol that carries no address', () => { + expect(outboundAddresses(row('freedom', {}))).toEqual([]); + }); + + it('reads the vnext server of a vless row', () => { + expect(outboundAddresses(row('VLESS', vnext))).toEqual(['a.example.com:443']); + }); + + it('returns no bare separator for a vless row that carries no server', () => { + expect(outboundAddresses(row('VLESS', {}))).toEqual([]); + }); + + it('reads the flat server of a hysteria id', () => { + expect(outboundAddresses(row('hysteria', { address: 'e.example.com', port: 443 }))).toEqual([ + 'e.example.com:443', + ]); + }); + + it('reads the peer endpoint of an amneziawg id', () => { + expect( + outboundAddresses(row('amneziawg', { peers: [{ endpoint: 'f.example.com:51820' }] })), + ).toEqual(['f.example.com:51820']); + }); +});