From d867b15f4ade6530ec39fc6f72d87f1b38e42e74 Mon Sep 17 00:00:00 2001 From: Muse Date: Wed, 21 Oct 2026 20:59:36 +0000 Subject: [PATCH] =?UTF-8?q?fix(inbounds):=20forwarding=20echo=20lost=20in?= =?UTF-8?q?=20DBInbound=20=E2=80=94=20add=20the=20missing=20field?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The edit modal seeds from the list row wrapped in DBInbound, whose cloneProps only copies keys the instance already owns. DBInbound had no forwarding field, so the stored ForwardingSpec JSON was silently dropped on every row wrap and rawInboundToFormValues fell back to direct — saved forwarding compiled and worked, but never echoed back into the form. Backend/API carried the field correctly all along. Regression test covers managed/manual/direct through the exact row -> DBInbound -> form-values chain. --- frontend/src/models/dbinbound.ts | 3 + .../src/test/inbound-forwarding-echo.test.ts | 78 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 frontend/src/test/inbound-forwarding-echo.test.ts diff --git a/frontend/src/models/dbinbound.ts b/frontend/src/models/dbinbound.ts index fd18970..296b9e2 100644 --- a/frontend/src/models/dbinbound.ts +++ b/frontend/src/models/dbinbound.ts @@ -39,6 +39,7 @@ export type DBInboundInit = Partial<{ streamSettings: RawJsonField; tag: string; sniffing: RawJsonField; + forwarding: string; clientStats: ClientStats[]; nodeId: number | null; shareAddrStrategy: string; @@ -88,6 +89,7 @@ export class DBInbound { streamSettings: RawJsonField; tag: string; sniffing: RawJsonField; + forwarding: string; clientStats: ClientStats[]; nodeId: number | null; shareAddrStrategy: string; @@ -119,6 +121,7 @@ export class DBInbound { this.streamSettings = ''; this.tag = ''; this.sniffing = ''; + this.forwarding = ''; this.clientStats = []; this.nodeId = null; this.shareAddrStrategy = 'node'; diff --git a/frontend/src/test/inbound-forwarding-echo.test.ts b/frontend/src/test/inbound-forwarding-echo.test.ts new file mode 100644 index 0000000..b3dc4f2 --- /dev/null +++ b/frontend/src/test/inbound-forwarding-echo.test.ts @@ -0,0 +1,78 @@ +/// +import { describe, expect, it } from 'vitest'; + +import { rawInboundToFormValues, type RawInboundRow } from '@/lib/xray/inbound-form-adapter'; +import { DBInbound, type DBInboundInit } from '@/models/dbinbound'; + +// Regression (P9.1): an inbound whose forwarding was saved must echo it +// back when the edit modal is seeded from the list row. The list page +// wraps API rows in DBInbound before handing them to the modal, so the +// row -> DBInbound -> form-values path is the exact chain that broke: +// DBInbound had no `forwarding` field and cloneProps only copies keys +// the instance already owns, silently dropping the stored spec. + +const MANAGED = JSON.stringify({ + mode: 'forward', + landing: 'managed', + targetInboundId: 42, + address: '', + port: 0, + protocol: 'socks5', + username: '', + password: '', +}); + +const MANUAL = JSON.stringify({ + mode: 'forward', + landing: 'manual', + targetInboundId: 0, + address: '10.240.64.2', + port: 47814, + protocol: 'socks5', + username: 'ngs_user', + password: 's3cret', +}); + +function apiRow(forwarding: string): RawInboundRow & DBInboundInit { + return { + id: 7, + port: 40159, + listen: '', + protocol: 'vless', + remark: 'PPFL1-V4', + enable: true, + expiryTime: 0, + settings: JSON.stringify({ clients: [], decryption: 'none', fallbacks: [] }), + streamSettings: JSON.stringify({ network: 'tcp', security: 'reality', tcpSettings: {} }), + sniffing: JSON.stringify({ enabled: false }), + forwarding, + } as RawInboundRow & DBInboundInit; +} + +describe('forwarding echo through DBInbound (edit modal seeding)', () => { + it('managed forwarding survives row -> DBInbound -> form values', () => { + const db = new DBInbound(apiRow(MANAGED)); + const values = rawInboundToFormValues(db); + expect(values.forwarding.mode).toBe('forward'); + expect(values.forwarding.landing).toBe('managed'); + expect(values.forwarding.targetInboundId).toBe(42); + }); + + it('manual forwarding survives with endpoint and credentials', () => { + const db = new DBInbound(apiRow(MANUAL)); + const values = rawInboundToFormValues(db); + expect(values.forwarding.mode).toBe('forward'); + expect(values.forwarding.landing).toBe('manual'); + expect(values.forwarding.address).toBe('10.240.64.2'); + expect(values.forwarding.port).toBe(47814); + expect(values.forwarding.protocol).toBe('socks5'); + expect(values.forwarding.username).toBe('ngs_user'); + expect(values.forwarding.password).toBe('s3cret'); + }); + + it('empty forwarding stays direct', () => { + const db = new DBInbound(apiRow('')); + const values = rawInboundToFormValues(db); + expect(values.forwarding.mode).toBe('direct'); + }); +});