fix(xray): sync routing rules when outbound tag is renamed (#5006)

* chore: ignore local .cursor directory

* fix(xray): sync routing rules when outbound tag is renamed

Renaming an outbound in the Outbounds tab only updated the outbound list, leaving routing rules pointing at the old tag. Propagate tag changes to routing rules, balancer selectors, and sockopt dialerProxy references, matching the behavior already used for balancer and WARP/Nord renames.

* test: mock HttpUtil to fix unhandled vitest rejections

* test(frontend): mock axios globally to prevent flaky network errors on CI

* test(frontend): fix eslint any errors in component test setup

---------

Co-authored-by: Rqzbeh <rqzbeh@users.noreply.github.com>
This commit is contained in:
nima1024m
2026-06-08 22:00:41 +03:30
committed by GitHub
parent 1c74b995c3
commit e8171ab4f7
6 changed files with 121 additions and 6 deletions
@@ -0,0 +1,61 @@
import { describe, it, expect } from 'vitest';
import type { XraySettingsValue } from '@/hooks/useXraySetting';
import { propagateOutboundTagRename } from '@/pages/xray/basics/helpers';
function baseTemplate(): XraySettingsValue {
return {
outbounds: [
{ tag: 'To-External-Proxy', protocol: 'vless' },
{ tag: 'direct', protocol: 'freedom' },
],
routing: {
rules: [
{
type: 'field',
inboundTag: ['iran-in'],
outboundTag: 'To-External-Proxy',
},
],
balancers: [
{
tag: 'lb-1',
selector: ['To-External-Proxy', 'direct'],
fallbackTag: 'To-External-Proxy',
},
],
},
} as XraySettingsValue;
}
describe('propagateOutboundTagRename', () => {
it('updates routing rule outboundTag when outbound is renamed', () => {
const t = baseTemplate();
propagateOutboundTagRename(t, 'To-External-Proxy', 'external-vps');
expect(t.routing?.rules?.[0]?.outboundTag).toBe('external-vps');
});
it('updates balancer selector and fallbackTag', () => {
const t = baseTemplate();
propagateOutboundTagRename(t, 'To-External-Proxy', 'external-vps');
expect(t.routing?.balancers?.[0]?.selector).toEqual(['external-vps', 'direct']);
expect(t.routing?.balancers?.[0]?.fallbackTag).toBe('external-vps');
});
it('updates sockopt dialerProxy references in other outbounds', () => {
const t = baseTemplate();
(t.outbounds![1] as { streamSettings?: { sockopt?: { dialerProxy?: string } } }).streamSettings = {
sockopt: { dialerProxy: 'To-External-Proxy' },
};
propagateOutboundTagRename(t, 'To-External-Proxy', 'external-vps');
const dialerProxy = (t.outbounds![1] as { streamSettings?: { sockopt?: { dialerProxy?: string } } })
.streamSettings?.sockopt?.dialerProxy;
expect(dialerProxy).toBe('external-vps');
});
it('is a no-op when old and new tags are equal', () => {
const t = baseTemplate();
propagateOutboundTagRename(t, 'To-External-Proxy', 'To-External-Proxy');
expect(t.routing?.rules?.[0]?.outboundTag).toBe('To-External-Proxy');
});
});
+16
View File
@@ -74,3 +74,19 @@ afterEach(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
}
});
import { HttpUtil } from '@/utils';
vi.mock('axios', () => {
return {
default: {
get: vi.fn().mockResolvedValue({ data: { success: true, obj: {} } }),
post: vi.fn().mockResolvedValue({ data: { success: true, obj: {} } }),
}
};
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.spyOn(HttpUtil, 'post').mockResolvedValue({ success: true, obj: {} } as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.spyOn(HttpUtil, 'get').mockResolvedValue({ success: true, obj: {} } as any);