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'); + }); +});