fix(inbounds): forwarding echo lost in DBInbound — add the missing field

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.
This commit is contained in:
Muse
2026-10-21 20:59:36 +00:00
parent 4da8d58307
commit d867b15f4a
2 changed files with 81 additions and 0 deletions
+3
View File
@@ -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';
@@ -0,0 +1,78 @@
/// <reference types="vite/client" />
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');
});
});