mirror of
https://github.com/2dust/v2rayN.git
synced 2026-04-21 06:55:46 +00:00
Add more test (#9162)
* Add test Add more test and fmt test * Update to xunit.v3
This commit is contained in:
parent
67592d1922
commit
d67321eed0
9 changed files with 1542 additions and 228 deletions
|
|
@ -9,11 +9,12 @@
|
||||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
|
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
|
||||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.13" />
|
<PackageVersion Include="Avalonia.Desktop" Version="11.3.13" />
|
||||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.13" />
|
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.13" />
|
||||||
|
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
|
||||||
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" />
|
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" />
|
||||||
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.12" />
|
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.12" />
|
||||||
<PackageVersion Include="CliWrap" Version="3.10.1" />
|
<PackageVersion Include="CliWrap" Version="3.10.1" />
|
||||||
<PackageVersion Include="Downloader" Version="5.1.0" />
|
<PackageVersion Include="Downloader" Version="5.1.0" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
|
||||||
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
||||||
<PackageVersion Include="MaterialDesignThemes" Version="5.3.1" />
|
<PackageVersion Include="MaterialDesignThemes" Version="5.3.1" />
|
||||||
<PackageVersion Include="QRCoder" Version="1.8.0" />
|
<PackageVersion Include="QRCoder" Version="1.8.0" />
|
||||||
|
|
@ -27,8 +28,8 @@
|
||||||
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
|
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
|
||||||
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
||||||
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
||||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||||
|
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
||||||
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
|
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
|
||||||
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.14" />
|
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.14" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
using AwesomeAssertions;
|
||||||
|
using ServiceLib.Enums;
|
||||||
|
using ServiceLib.Handler.Builder;
|
||||||
|
using ServiceLib.Helper;
|
||||||
|
using ServiceLib.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ServiceLib.Tests.CoreConfig.Context;
|
||||||
|
|
||||||
|
public class CoreConfigContextBuilderTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveNodeAsync_DirectCycleDependency_ShouldFailWithCycleError()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig();
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var groupAId = NewId("group-a");
|
||||||
|
var groupBId = NewId("group-b");
|
||||||
|
var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId]);
|
||||||
|
var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupAId]);
|
||||||
|
|
||||||
|
await UpsertProfilesAsync(groupA, groupB);
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray);
|
||||||
|
context.AllProxiesMap.Clear();
|
||||||
|
|
||||||
|
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
|
||||||
|
|
||||||
|
validatorResult.Success.Should().BeFalse();
|
||||||
|
validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
|
||||||
|
context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
|
||||||
|
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveNodeAsync_IndirectCycleDependency_ShouldFailWithCycleError()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig();
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var groupAId = NewId("group-a");
|
||||||
|
var groupBId = NewId("group-b");
|
||||||
|
var groupCId = NewId("group-c");
|
||||||
|
var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId]);
|
||||||
|
var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupCId]);
|
||||||
|
var groupC = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupCId, "group-c", [groupAId]);
|
||||||
|
|
||||||
|
await UpsertProfilesAsync(groupA, groupB, groupC);
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray);
|
||||||
|
context.AllProxiesMap.Clear();
|
||||||
|
|
||||||
|
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
|
||||||
|
|
||||||
|
validatorResult.Success.Should().BeFalse();
|
||||||
|
validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
|
||||||
|
context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
|
||||||
|
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
|
||||||
|
context.AllProxiesMap.Should().NotContainKey(groupC.IndexId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveNodeAsync_CycleWithValidBranch_ShouldSkipCycleAndKeepValidChild()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig();
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var groupAId = NewId("group-a");
|
||||||
|
var groupBId = NewId("group-b");
|
||||||
|
var leafId = NewId("leaf");
|
||||||
|
var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId, leafId]);
|
||||||
|
var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupAId]);
|
||||||
|
var leaf = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, leafId, "leaf");
|
||||||
|
|
||||||
|
await UpsertProfilesAsync(groupA, groupB, leaf);
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray);
|
||||||
|
context.AllProxiesMap.Clear();
|
||||||
|
|
||||||
|
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
|
||||||
|
|
||||||
|
validatorResult.Success.Should().BeTrue();
|
||||||
|
validatorResult.Errors.Should().BeEmpty();
|
||||||
|
validatorResult.Warnings.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
|
||||||
|
|
||||||
|
context.AllProxiesMap.Should().ContainKey(leaf.IndexId);
|
||||||
|
context.AllProxiesMap.Should().ContainKey(groupA.IndexId);
|
||||||
|
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
|
||||||
|
groupA.GetProtocolExtra().ChildItems.Should().Be(leaf.IndexId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NewId(string prefix)
|
||||||
|
{
|
||||||
|
return $"{prefix}-{Guid.NewGuid():N}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ContainsCycleDependencyMessage(string message)
|
||||||
|
{
|
||||||
|
return message.Contains("cycle dependency", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| message.Contains("循环依赖", StringComparison.Ordinal)
|
||||||
|
|| message.Contains("循環依賴", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task UpsertProfilesAsync(params ProfileItem[] profiles)
|
||||||
|
{
|
||||||
|
SQLiteHelper.Instance.CreateTable<ProfileItem>();
|
||||||
|
foreach (var profile in profiles)
|
||||||
|
{
|
||||||
|
await SQLiteHelper.Instance.ReplaceAsync(profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
197
v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs
Normal file
197
v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
using ServiceLib.Enums;
|
||||||
|
using ServiceLib.Manager;
|
||||||
|
using ServiceLib.Models;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace ServiceLib.Tests.CoreConfig;
|
||||||
|
|
||||||
|
internal static class CoreConfigTestFactory
|
||||||
|
{
|
||||||
|
public static void BindAppManagerConfig(Config config)
|
||||||
|
{
|
||||||
|
var field = typeof(AppManager).GetField("_config", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
field?.SetValue(AppManager.Instance, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Config CreateConfig(ECoreType vmessCoreType = ECoreType.Xray)
|
||||||
|
{
|
||||||
|
return new Config
|
||||||
|
{
|
||||||
|
CoreBasicItem = new CoreBasicItem { Loglevel = "warning", MuxEnabled = false },
|
||||||
|
TunModeItem = new TunModeItem { EnableTun = false, IcmpRouting = "default" },
|
||||||
|
KcpItem = new KcpItem(),
|
||||||
|
GrpcItem = new GrpcItem(),
|
||||||
|
RoutingBasicItem =
|
||||||
|
new RoutingBasicItem
|
||||||
|
{
|
||||||
|
DomainStrategy = Global.AsIs,
|
||||||
|
DomainStrategy4Singbox = string.Empty,
|
||||||
|
RoutingIndexId = string.Empty,
|
||||||
|
},
|
||||||
|
GuiItem = new GUIItem { EnableStatistics = false, DisplayRealTimeSpeed = false, EnableLog = false },
|
||||||
|
MsgUIItem = new MsgUIItem(),
|
||||||
|
UiItem =
|
||||||
|
new UIItem
|
||||||
|
{
|
||||||
|
CurrentLanguage = "en", CurrentFontFamily = "sans", MainColumnItem = [], WindowSizeItem = []
|
||||||
|
},
|
||||||
|
ConstItem = new ConstItem(),
|
||||||
|
SpeedTestItem = new SpeedTestItem
|
||||||
|
{
|
||||||
|
SpeedPingTestUrl = Global.SpeedPingTestUrls.First(),
|
||||||
|
SpeedTestUrl = Global.SpeedTestUrls.First(),
|
||||||
|
SpeedTestTimeout = 10,
|
||||||
|
MixedConcurrencyCount = 1,
|
||||||
|
IPAPIUrl = string.Empty,
|
||||||
|
},
|
||||||
|
Mux4RayItem = new Mux4RayItem { Concurrency = 8, XudpConcurrency = 16, XudpProxyUDP443 = "reject" },
|
||||||
|
Mux4SboxItem = new Mux4SboxItem { Protocol = Global.SingboxMuxs.First(), MaxConnections = 8 },
|
||||||
|
HysteriaItem = new HysteriaItem { UpMbps = 100, DownMbps = 100 },
|
||||||
|
ClashUIItem = new ClashUIItem { ConnectionsColumnItem = [] },
|
||||||
|
SystemProxyItem =
|
||||||
|
new SystemProxyItem
|
||||||
|
{
|
||||||
|
SystemProxyExceptions = string.Empty, SystemProxyAdvancedProtocol = string.Empty
|
||||||
|
},
|
||||||
|
WebDavItem = new WebDavItem(),
|
||||||
|
CheckUpdateItem = new CheckUpdateItem(),
|
||||||
|
Fragment4RayItem = new Fragment4RayItem { Packets = "tlshello", Length = "100-200", Interval = "10-20" },
|
||||||
|
Inbound =
|
||||||
|
[
|
||||||
|
new InItem
|
||||||
|
{
|
||||||
|
Protocol = nameof(EInboundProtocol.socks),
|
||||||
|
LocalPort = 10808,
|
||||||
|
UdpEnabled = true,
|
||||||
|
SniffingEnabled = true,
|
||||||
|
RouteOnly = false,
|
||||||
|
DestOverride = ["http", "tls"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
GlobalHotkeys = [],
|
||||||
|
CoreTypeItem =
|
||||||
|
[
|
||||||
|
new CoreTypeItem { ConfigType = EConfigType.VMess, CoreType = vmessCoreType }
|
||||||
|
],
|
||||||
|
SimpleDNSItem = new SimpleDNSItem
|
||||||
|
{
|
||||||
|
BootstrapDNS = Global.DomainPureIPDNSAddress.FirstOrDefault(),
|
||||||
|
ServeStale = false,
|
||||||
|
ParallelQuery = false,
|
||||||
|
Strategy4Freedom = Global.AsIs,
|
||||||
|
Strategy4Proxy = Global.AsIs,
|
||||||
|
},
|
||||||
|
IndexId = string.Empty,
|
||||||
|
SubIndexId = string.Empty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProfileItem CreateVmessNode(ECoreType coreType, string indexId = "node-1", string remarks = "demo")
|
||||||
|
{
|
||||||
|
var node = new ProfileItem
|
||||||
|
{
|
||||||
|
IndexId = indexId,
|
||||||
|
ConfigType = EConfigType.VMess,
|
||||||
|
CoreType = coreType,
|
||||||
|
Remarks = remarks,
|
||||||
|
Address = "example.com",
|
||||||
|
Port = 443,
|
||||||
|
Password = Guid.NewGuid().ToString(),
|
||||||
|
Network = nameof(ETransport.raw),
|
||||||
|
StreamSecurity = string.Empty,
|
||||||
|
Subid = string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
node.SetProtocolExtra(node.GetProtocolExtra() with { AlterId = "0", VmessSecurity = Global.DefaultSecurity, });
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProfileItem CreateSocksNode(ECoreType coreType, string indexId = "node-socks-1",
|
||||||
|
string remarks = "demo-socks")
|
||||||
|
{
|
||||||
|
return new ProfileItem
|
||||||
|
{
|
||||||
|
IndexId = indexId,
|
||||||
|
ConfigType = EConfigType.SOCKS,
|
||||||
|
CoreType = coreType,
|
||||||
|
Remarks = remarks,
|
||||||
|
Address = "127.0.0.1",
|
||||||
|
Port = 1080,
|
||||||
|
Password = "pass",
|
||||||
|
Username = "user",
|
||||||
|
Network = nameof(ETransport.raw),
|
||||||
|
StreamSecurity = string.Empty,
|
||||||
|
Subid = string.Empty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProfileItem CreatePolicyGroupNode(ECoreType coreType, string indexId, string remarks,
|
||||||
|
IEnumerable<string> childIndexIds)
|
||||||
|
{
|
||||||
|
var node = new ProfileItem
|
||||||
|
{
|
||||||
|
IndexId = indexId, ConfigType = EConfigType.PolicyGroup, CoreType = coreType, Remarks = remarks,
|
||||||
|
};
|
||||||
|
node.SetProtocolExtra(node.GetProtocolExtra() with
|
||||||
|
{
|
||||||
|
GroupType = nameof(EConfigType.PolicyGroup), ChildItems = string.Join(",", childIndexIds),
|
||||||
|
});
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProfileItem CreateProxyChainNode(ECoreType coreType, string indexId, string remarks,
|
||||||
|
IEnumerable<string> childIndexIds)
|
||||||
|
{
|
||||||
|
var node = new ProfileItem
|
||||||
|
{
|
||||||
|
IndexId = indexId, ConfigType = EConfigType.ProxyChain, CoreType = coreType, Remarks = remarks,
|
||||||
|
};
|
||||||
|
node.SetProtocolExtra(node.GetProtocolExtra() with
|
||||||
|
{
|
||||||
|
GroupType = nameof(EConfigType.ProxyChain), ChildItems = string.Join(",", childIndexIds),
|
||||||
|
});
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CoreConfigContext CreateContext(Config config, ProfileItem node, ECoreType runCoreType)
|
||||||
|
{
|
||||||
|
return new CoreConfigContext
|
||||||
|
{
|
||||||
|
Node = node,
|
||||||
|
RunCoreType = runCoreType,
|
||||||
|
AppConfig = config,
|
||||||
|
RoutingItem = new RoutingItem
|
||||||
|
{
|
||||||
|
Id = "r1",
|
||||||
|
Remarks = "default",
|
||||||
|
RuleSet = "[]",
|
||||||
|
DomainStrategy = Global.AsIs,
|
||||||
|
DomainStrategy4Singbox = string.Empty,
|
||||||
|
},
|
||||||
|
RawDnsItem = null,
|
||||||
|
SimpleDnsItem = config.SimpleDNSItem,
|
||||||
|
AllProxiesMap = new Dictionary<string, ProfileItem> { [node.IndexId] = node },
|
||||||
|
FullConfigTemplate = null,
|
||||||
|
IsTunEnabled = false,
|
||||||
|
ProtectDomainList = [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Config CreateConfigWithDirectExpectedIPs(ECoreType coreType,
|
||||||
|
string directExpectedIPs = "192.168.0.0/16,geoip:cn")
|
||||||
|
{
|
||||||
|
var config = CreateConfig(coreType);
|
||||||
|
config.SimpleDNSItem.DirectExpectedIPs = directExpectedIPs;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Config CreateConfigWithBootstrapDNS(ECoreType coreType, string bootstrapDns = "8.8.8.8")
|
||||||
|
{
|
||||||
|
var config = CreateConfig(coreType);
|
||||||
|
config.SimpleDNSItem.BootstrapDNS = bootstrapDns;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,511 @@
|
||||||
|
using AwesomeAssertions;
|
||||||
|
using ServiceLib.Common;
|
||||||
|
using ServiceLib.Enums;
|
||||||
|
using ServiceLib.Models;
|
||||||
|
using ServiceLib.Services.CoreConfig;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ServiceLib.Tests.CoreConfig.Singbox;
|
||||||
|
|
||||||
|
public class CoreConfigSingboxServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box);
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box);
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
result.Data.Should().NotBeNull();
|
||||||
|
|
||||||
|
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString());
|
||||||
|
singboxConfig.Should().NotBeNull();
|
||||||
|
singboxConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks");
|
||||||
|
singboxConfig.inbounds.Should().Contain(i => i.type == nameof(EInboundProtocol.mixed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildSelector()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n1", "node-1");
|
||||||
|
var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n2", "node-2");
|
||||||
|
var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.sing_box, "g1", "group",
|
||||||
|
[n1.IndexId, n2.IndexId]);
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, group, ECoreType.sing_box);
|
||||||
|
context.AllProxiesMap[n1.IndexId] = n1;
|
||||||
|
context.AllProxiesMap[n2.IndexId] = n2;
|
||||||
|
context.AllProxiesMap[group.IndexId] = group;
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_ProxyChain_ShouldBuildDetourChain()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n1", "node-1");
|
||||||
|
var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n2", "node-2");
|
||||||
|
var chain = CoreConfigTestFactory.CreateProxyChainNode(ECoreType.sing_box, "c1", "chain",
|
||||||
|
[n1.IndexId, n2.IndexId]);
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, chain, ECoreType.sing_box);
|
||||||
|
context.AllProxiesMap[n1.IndexId] = n1;
|
||||||
|
context.AllProxiesMap[n2.IndexId] = n2;
|
||||||
|
context.AllProxiesMap[chain.IndexId] = chain;
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks");
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
|
||||||
|
cfg.outbounds.Should().Contain(o =>
|
||||||
|
o.tag == Global.ProxyTag &&
|
||||||
|
(o.detour ?? string.Empty).StartsWith("chain-proxy-1-", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n1", "node-1");
|
||||||
|
var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n2", "node-2");
|
||||||
|
var n3 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n3", "node-3");
|
||||||
|
var chain = CoreConfigTestFactory.CreateProxyChainNode(ECoreType.sing_box, "c1", "chain",
|
||||||
|
[n1.IndexId, n2.IndexId]);
|
||||||
|
var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.sing_box, "g1", "group",
|
||||||
|
[chain.IndexId, n3.IndexId]);
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, group, ECoreType.sing_box);
|
||||||
|
context.AllProxiesMap[n1.IndexId] = n1;
|
||||||
|
context.AllProxiesMap[n2.IndexId] = n2;
|
||||||
|
context.AllProxiesMap[n3.IndexId] = n3;
|
||||||
|
context.AllProxiesMap[chain.IndexId] = chain;
|
||||||
|
context.AllProxiesMap[group.IndexId] = group;
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n1", "node-1");
|
||||||
|
var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n2", "node-2");
|
||||||
|
var n3 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n3", "node-3");
|
||||||
|
var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.sing_box, "g1", "group",
|
||||||
|
[n1.IndexId, n2.IndexId]);
|
||||||
|
var chain = CoreConfigTestFactory.CreateProxyChainNode(ECoreType.sing_box, "c1", "chain",
|
||||||
|
[group.IndexId, n3.IndexId]);
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, chain, ECoreType.sing_box);
|
||||||
|
context.AllProxiesMap[n1.IndexId] = n1;
|
||||||
|
context.AllProxiesMap[n2.IndexId] = n2;
|
||||||
|
context.AllProxiesMap[n3.IndexId] = n3;
|
||||||
|
context.AllProxiesMap[group.IndexId] = group;
|
||||||
|
context.AllProxiesMap[chain.IndexId] = chain;
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal));
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
var proxyCloneCount = cfg.outbounds.Count(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal));
|
||||||
|
proxyCloneCount.Should().Be(2);
|
||||||
|
|
||||||
|
var allCloneDetoursPointToGroupBranches = cfg.outbounds
|
||||||
|
.Where(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal))
|
||||||
|
.All(o => (o.detour ?? string.Empty).StartsWith("chain-proxy-1-group-", StringComparison.Ordinal));
|
||||||
|
allCloneDetoursPointToGroupBranches.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
|
||||||
|
{
|
||||||
|
RoutingItem = new RoutingItem
|
||||||
|
{
|
||||||
|
Id = "r-split-1",
|
||||||
|
Remarks = "split-direct-block",
|
||||||
|
RuleSet = JsonUtils.Serialize(new List<RulesItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
RuleType = ERuleType.Routing,
|
||||||
|
OutboundTag = Global.DirectTag,
|
||||||
|
Domain = ["full:direct.example.com"],
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
RuleType = ERuleType.Routing,
|
||||||
|
OutboundTag = Global.BlockTag,
|
||||||
|
Domain = ["full:block.example.com"],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DomainStrategy = Global.AsIs,
|
||||||
|
DomainStrategy4Singbox = string.Empty,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
var hasDirectRule = cfg.route.rules.Any(r =>
|
||||||
|
r.domain != null
|
||||||
|
&& r.domain.Contains("direct.example.com")
|
||||||
|
&& r.outbound == Global.DirectTag);
|
||||||
|
hasDirectRule.Should().BeTrue();
|
||||||
|
|
||||||
|
var hasBlockRule = cfg.route.rules.Any(r =>
|
||||||
|
r.domain != null
|
||||||
|
&& r.domain.Contains("block.example.com")
|
||||||
|
&& r.action == "reject");
|
||||||
|
hasBlockRule.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main");
|
||||||
|
var routeNode = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-route", "route-node");
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
|
||||||
|
{
|
||||||
|
RoutingItem = new RoutingItem
|
||||||
|
{
|
||||||
|
Id = "r-split-2",
|
||||||
|
Remarks = "split-remark",
|
||||||
|
RuleSet = JsonUtils.Serialize(new List<RulesItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
RuleType = ERuleType.Routing,
|
||||||
|
OutboundTag = routeNode.Remarks,
|
||||||
|
Domain = ["full:route.example.com"],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DomainStrategy = Global.AsIs,
|
||||||
|
DomainStrategy4Singbox = string.Empty,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
context.AllProxiesMap[$"remark:{routeNode.Remarks}"] = routeNode;
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
var expectedPrefix = $"{routeNode.IndexId}-{Global.ProxyTag}-{routeNode.Remarks}";
|
||||||
|
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal));
|
||||||
|
|
||||||
|
var hasRouteRule = cfg.route.rules.Any(r =>
|
||||||
|
r.domain != null
|
||||||
|
&& r.domain.Contains("route.example.com")
|
||||||
|
&& (r.outbound ?? string.Empty).StartsWith(expectedPrefix, StringComparison.Ordinal));
|
||||||
|
hasRouteRule.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyGeoipAndCidrToDirectDnsRule()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(
|
||||||
|
ECoreType.sing_box,
|
||||||
|
"192.168.0.0/16,geoip:cn");
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
|
||||||
|
{
|
||||||
|
RoutingItem = new RoutingItem
|
||||||
|
{
|
||||||
|
Id = "r-dns-direct-expected",
|
||||||
|
Remarks = "dns-direct-expected",
|
||||||
|
RuleSet = JsonUtils.Serialize(new List<RulesItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
RuleType = ERuleType.DNS,
|
||||||
|
OutboundTag = Global.DirectTag,
|
||||||
|
Domain = ["geosite:cn"],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DomainStrategy = Global.AsIs,
|
||||||
|
DomainStrategy4Singbox = string.Empty,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
var hasExpectedRule = cfg.dns.rules?.Any(r =>
|
||||||
|
r.server == Global.SingboxDirectDNSTag
|
||||||
|
&& r.ip_cidr?.Contains("192.168.0.0/16") == true
|
||||||
|
&& r.rule_set?.Contains("geosite-cn") == true
|
||||||
|
&& r.rule_set?.Contains("geoip-cn") == true) ?? false;
|
||||||
|
|
||||||
|
hasExpectedRule.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_BootstrapDNS_ShouldConfigurePureIPResolver()
|
||||||
|
{
|
||||||
|
var bootstrapDns = "8.8.8.8";
|
||||||
|
var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.sing_box, bootstrapDns);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box);
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
config.SimpleDNSItem.BootstrapDNS.Should().Be(bootstrapDns);
|
||||||
|
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
var bootstrapServer = cfg.dns.servers?.FirstOrDefault(s => s.tag == Global.SingboxLocalDNSTag);
|
||||||
|
bootstrapServer.Should().NotBeNull();
|
||||||
|
(bootstrapServer?.server ?? string.Empty).Should().Contain(bootstrapDns);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectFinalDns()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
config.SimpleDNSItem.DirectDNS = "1.1.1.1";
|
||||||
|
config.SimpleDNSItem.RemoteDNS = "9.9.9.9";
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
|
||||||
|
{
|
||||||
|
RoutingItem = new RoutingItem
|
||||||
|
{
|
||||||
|
Id = "r-direct-final",
|
||||||
|
Remarks = "direct-final",
|
||||||
|
RuleSet = JsonUtils.Serialize(new List<RulesItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
RuleType = ERuleType.Routing,
|
||||||
|
OutboundTag = Global.DirectTag,
|
||||||
|
Ip = ["0.0.0.0/0"],
|
||||||
|
Port = "0-65535",
|
||||||
|
Network = "tcp,udp",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DomainStrategy = Global.AsIs,
|
||||||
|
DomainStrategy4Singbox = string.Empty,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
cfg.dns.final.Should().Be(Global.SingboxDirectDNSTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedRule()
|
||||||
|
{
|
||||||
|
var config =
|
||||||
|
CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.sing_box, "192.168.0.0/16,geoip:cn");
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
|
||||||
|
{
|
||||||
|
RoutingItem = new RoutingItem
|
||||||
|
{
|
||||||
|
Id = "r-dns-direct-unmatched",
|
||||||
|
Remarks = "dns-direct-unmatched",
|
||||||
|
RuleSet = JsonUtils.Serialize(new List<RulesItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
RuleType = ERuleType.DNS,
|
||||||
|
OutboundTag = Global.DirectTag,
|
||||||
|
Domain = ["geosite:us"],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DomainStrategy = Global.AsIs,
|
||||||
|
DomainStrategy4Singbox = string.Empty,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
var hasExpectedRule = cfg.dns.rules?.Any(r =>
|
||||||
|
r.server == Global.SingboxDirectDNSTag
|
||||||
|
&& r.ip_cidr?.Contains("192.168.0.0/16") == true
|
||||||
|
&& r.rule_set?.Contains("geoip-cn") == true) ?? false;
|
||||||
|
hasExpectedRule.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("geosite:cn", "geosite-cn")]
|
||||||
|
[InlineData("geosite:geolocation-cn", "geosite-geolocation-cn")]
|
||||||
|
[InlineData("geosite:tld-cn", "geosite-tld-cn")]
|
||||||
|
public void GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedRule(string domainTag,
|
||||||
|
string expectedRuleSetTag)
|
||||||
|
{
|
||||||
|
var config =
|
||||||
|
CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.sing_box, "192.168.0.0/16,geoip:cn");
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
|
||||||
|
{
|
||||||
|
RoutingItem = new RoutingItem
|
||||||
|
{
|
||||||
|
Id = "r-dns-direct-variant",
|
||||||
|
Remarks = "dns-direct-variant",
|
||||||
|
RuleSet = JsonUtils.Serialize(new List<RulesItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Enabled = true, RuleType = ERuleType.DNS, OutboundTag = Global.DirectTag, Domain = [domainTag],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DomainStrategy = Global.AsIs,
|
||||||
|
DomainStrategy4Singbox = string.Empty,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
var hasExpectedRule = cfg.dns.rules?.Any(r =>
|
||||||
|
r.server == Global.SingboxDirectDNSTag
|
||||||
|
&& r.ip_cidr?.Contains("192.168.0.0/16") == true
|
||||||
|
&& r.rule_set?.Contains(expectedRuleSetTag) == true
|
||||||
|
&& r.rule_set?.Contains("geoip-cn") == true) ?? false;
|
||||||
|
hasExpectedRule.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_Hosts_ShouldPopulateHostsServerAndDomainResolver()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1";
|
||||||
|
config.SimpleDNSItem.DirectDNS = "https://resolver.example/dns-query";
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box);
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
var hostsServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxHostsDNSTag);
|
||||||
|
hostsServer.Should().NotBeNull();
|
||||||
|
hostsServer!.predefined.Should().ContainKey("resolver.example");
|
||||||
|
hostsServer.predefined!["resolver.example"].Should().Contain("1.1.1.1");
|
||||||
|
|
||||||
|
var directServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxDirectDNSTag);
|
||||||
|
directServer.Should().NotBeNull();
|
||||||
|
directServer!.domain_resolver.Should().Be(Global.SingboxHostsDNSTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsAndInjectLocalResolver()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main");
|
||||||
|
var rawDns = new Dns4Sbox
|
||||||
|
{
|
||||||
|
servers =
|
||||||
|
[
|
||||||
|
new Server4Sbox { tag = "remote", address = "8.8.8.8", detour = Global.ProxyTag, }
|
||||||
|
],
|
||||||
|
rules = [],
|
||||||
|
};
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
|
||||||
|
{
|
||||||
|
RawDnsItem = new DNSItem
|
||||||
|
{
|
||||||
|
Id = "dns-raw-1",
|
||||||
|
Remarks = "raw",
|
||||||
|
Enabled = true,
|
||||||
|
CoreType = ECoreType.sing_box,
|
||||||
|
NormalDNS = JsonUtils.Serialize(rawDns),
|
||||||
|
DomainDNSAddress = "1.1.1.1",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
cfg.dns.servers.Should().Contain(s => s.tag == "remote" && s.address == "8.8.8.8");
|
||||||
|
cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag);
|
||||||
|
cfg.dns.rules.Should().Contain(r => r.clash_mode == ERuleMode.Global.ToString());
|
||||||
|
cfg.dns.rules.Should().Contain(r => r.clash_mode == ERuleMode.Direct.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,539 @@
|
||||||
|
using AwesomeAssertions;
|
||||||
|
using ServiceLib.Common;
|
||||||
|
using ServiceLib.Enums;
|
||||||
|
using ServiceLib.Models;
|
||||||
|
using ServiceLib.Services.CoreConfig;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ServiceLib.Tests.CoreConfig.V2ray;
|
||||||
|
|
||||||
|
public class CoreConfigV2rayServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray);
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
result.Data.Should().NotBeNull();
|
||||||
|
|
||||||
|
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString());
|
||||||
|
v2rayConfig.Should().NotBeNull();
|
||||||
|
v2rayConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.protocol == "vmess");
|
||||||
|
v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n1", "node-1");
|
||||||
|
var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n2", "node-2");
|
||||||
|
var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, "g1", "group",
|
||||||
|
[n1.IndexId, n2.IndexId]);
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, group, ECoreType.Xray);
|
||||||
|
context.AllProxiesMap[n1.IndexId] = n1;
|
||||||
|
context.AllProxiesMap[n2.IndexId] = n2;
|
||||||
|
context.AllProxiesMap[group.IndexId] = group;
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
|
||||||
|
cfg.routing.balancers.Should().NotBeNull();
|
||||||
|
cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_ProxyChain_ShouldBuildDialerProxyChain()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n1", "node-1");
|
||||||
|
var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n2", "node-2");
|
||||||
|
var chain = CoreConfigTestFactory.CreateProxyChainNode(ECoreType.Xray, "c1", "chain", [n1.IndexId, n2.IndexId]);
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, chain, ECoreType.Xray);
|
||||||
|
context.AllProxiesMap[n1.IndexId] = n1;
|
||||||
|
context.AllProxiesMap[n2.IndexId] = n2;
|
||||||
|
context.AllProxiesMap[chain.IndexId] = chain;
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
|
||||||
|
var hasDialerChain = cfg.outbounds.Any(o =>
|
||||||
|
o.tag == Global.ProxyTag
|
||||||
|
&& o.streamSettings is not null
|
||||||
|
&& o.streamSettings.sockopt is not null
|
||||||
|
&& (o.streamSettings.sockopt.dialerProxy ?? string.Empty).StartsWith("chain-proxy-1-",
|
||||||
|
StringComparison.Ordinal));
|
||||||
|
hasDialerChain.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n1", "node-1");
|
||||||
|
var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n2", "node-2");
|
||||||
|
var n3 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n3", "node-3");
|
||||||
|
var chain = CoreConfigTestFactory.CreateProxyChainNode(ECoreType.Xray, "c1", "chain", [n1.IndexId, n2.IndexId]);
|
||||||
|
var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, "g1", "group",
|
||||||
|
[chain.IndexId, n3.IndexId]);
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, group, ECoreType.Xray);
|
||||||
|
context.AllProxiesMap[n1.IndexId] = n1;
|
||||||
|
context.AllProxiesMap[n2.IndexId] = n2;
|
||||||
|
context.AllProxiesMap[n3.IndexId] = n3;
|
||||||
|
context.AllProxiesMap[chain.IndexId] = chain;
|
||||||
|
context.AllProxiesMap[group.IndexId] = group;
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
|
||||||
|
cfg.routing.balancers.Should().NotBeNull();
|
||||||
|
cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n1", "node-1");
|
||||||
|
var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n2", "node-2");
|
||||||
|
var n3 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n3", "node-3");
|
||||||
|
var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, "g1", "group",
|
||||||
|
[n1.IndexId, n2.IndexId]);
|
||||||
|
var chain = CoreConfigTestFactory.CreateProxyChainNode(ECoreType.Xray, "c1", "chain",
|
||||||
|
[group.IndexId, n3.IndexId]);
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, chain, ECoreType.Xray);
|
||||||
|
context.AllProxiesMap[n1.IndexId] = n1;
|
||||||
|
context.AllProxiesMap[n2.IndexId] = n2;
|
||||||
|
context.AllProxiesMap[n3.IndexId] = n3;
|
||||||
|
context.AllProxiesMap[group.IndexId] = group;
|
||||||
|
context.AllProxiesMap[chain.IndexId] = chain;
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal));
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
var proxyCloneCount = cfg.outbounds.Count(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal));
|
||||||
|
proxyCloneCount.Should().Be(2);
|
||||||
|
|
||||||
|
var allCloneDialersPointToGroupBranches = cfg.outbounds
|
||||||
|
.Where(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal))
|
||||||
|
.All(o => (o.streamSettings?.sockopt?.dialerProxy ?? string.Empty).StartsWith("chain-proxy-1-group-",
|
||||||
|
StringComparison.Ordinal));
|
||||||
|
allCloneDialersPointToGroupBranches.Should().BeTrue();
|
||||||
|
|
||||||
|
cfg.routing.balancers.Should().NotBeNull();
|
||||||
|
cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray) with
|
||||||
|
{
|
||||||
|
RoutingItem = new RoutingItem
|
||||||
|
{
|
||||||
|
Id = "r-split-1",
|
||||||
|
Remarks = "split-direct-block",
|
||||||
|
RuleSet = JsonUtils.Serialize(new List<RulesItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
RuleType = ERuleType.Routing,
|
||||||
|
OutboundTag = Global.DirectTag,
|
||||||
|
Domain = ["full:direct.example.com"],
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
RuleType = ERuleType.Routing,
|
||||||
|
OutboundTag = Global.BlockTag,
|
||||||
|
Domain = ["full:block.example.com"],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DomainStrategy = Global.AsIs,
|
||||||
|
DomainStrategy4Singbox = string.Empty,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
var hasDirectRule = cfg.routing.rules.Any(r =>
|
||||||
|
r.domain != null
|
||||||
|
&& r.domain.Contains("full:direct.example.com")
|
||||||
|
&& r.outboundTag == Global.DirectTag);
|
||||||
|
hasDirectRule.Should().BeTrue();
|
||||||
|
|
||||||
|
var hasBlockRule = cfg.routing.rules.Any(r =>
|
||||||
|
r.domain != null
|
||||||
|
&& r.domain.Contains("full:block.example.com")
|
||||||
|
&& r.outboundTag == Global.BlockTag);
|
||||||
|
hasBlockRule.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||||
|
var routeNode = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n-route", "route-node");
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray) with
|
||||||
|
{
|
||||||
|
RoutingItem = new RoutingItem
|
||||||
|
{
|
||||||
|
Id = "r-split-2",
|
||||||
|
Remarks = "split-remark",
|
||||||
|
RuleSet = JsonUtils.Serialize(new List<RulesItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
RuleType = ERuleType.Routing,
|
||||||
|
OutboundTag = routeNode.Remarks,
|
||||||
|
Domain = ["full:route.example.com"],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DomainStrategy = Global.AsIs,
|
||||||
|
DomainStrategy4Singbox = string.Empty,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
context.AllProxiesMap[$"remark:{routeNode.Remarks}"] = routeNode;
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
var expectedPrefix = $"{routeNode.IndexId}-{Global.ProxyTag}-{routeNode.Remarks}";
|
||||||
|
|
||||||
|
cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal));
|
||||||
|
var hasRouteRule = cfg.routing.rules.Any(r =>
|
||||||
|
r.domain != null
|
||||||
|
&& r.domain.Contains("full:route.example.com")
|
||||||
|
&& (r.outboundTag ?? string.Empty).StartsWith(expectedPrefix, StringComparison.Ordinal));
|
||||||
|
hasRouteRule.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyExpectedIPsToDirectDnsServer()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn");
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray) with
|
||||||
|
{
|
||||||
|
RoutingItem = new RoutingItem
|
||||||
|
{
|
||||||
|
Id = "r-dns-direct-expected",
|
||||||
|
Remarks = "dns-direct-expected",
|
||||||
|
RuleSet = JsonUtils.Serialize(new List<RulesItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
RuleType = ERuleType.DNS,
|
||||||
|
OutboundTag = Global.DirectTag,
|
||||||
|
Domain = ["geosite:cn"],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DomainStrategy = Global.AsIs,
|
||||||
|
DomainStrategy4Singbox = string.Empty,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
|
||||||
|
|
||||||
|
var dnsServers = dns.servers
|
||||||
|
.Select(s => JsonUtils.Deserialize<DnsServer4Ray>(JsonUtils.Serialize(s)))
|
||||||
|
.Where(s => s is not null)
|
||||||
|
.Cast<DnsServer4Ray>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var hasExpectedServer = dnsServers.Any(s =>
|
||||||
|
(s.tag ?? string.Empty).StartsWith(Global.DirectDnsTag, StringComparison.Ordinal)
|
||||||
|
&& s.domains?.Contains("geosite:cn") == true
|
||||||
|
&& s.expectedIPs?.Contains("192.168.0.0/16") == true
|
||||||
|
&& s.expectedIPs?.Contains("geoip:cn") == true);
|
||||||
|
hasExpectedServer.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_BootstrapDNS_ShouldApplyToDnsServerDomains()
|
||||||
|
{
|
||||||
|
var bootstrapDns = "8.8.8.8";
|
||||||
|
var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.Xray, bootstrapDns);
|
||||||
|
config.SimpleDNSItem.DirectDNS = "https://dns-direct.example/dns-query";
|
||||||
|
config.SimpleDNSItem.RemoteDNS = "https://dns-remote.example/dns-query";
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
|
||||||
|
|
||||||
|
var dnsServers = dns.servers
|
||||||
|
.Select(s => JsonUtils.Deserialize<DnsServer4Ray>(JsonUtils.Serialize(s)))
|
||||||
|
.Where(s => s is not null)
|
||||||
|
.Cast<DnsServer4Ray>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var hasBootstrapServer = dnsServers.Any(s =>
|
||||||
|
s.address == bootstrapDns
|
||||||
|
&& s.domains?.Contains("full:dns-direct.example") == true
|
||||||
|
&& s.domains?.Contains("full:dns-remote.example") == true);
|
||||||
|
hasBootstrapServer.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectDnsServers()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||||
|
config.SimpleDNSItem.DirectDNS = "1.1.1.1";
|
||||||
|
config.SimpleDNSItem.RemoteDNS = "9.9.9.9";
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray) with
|
||||||
|
{
|
||||||
|
RoutingItem = new RoutingItem
|
||||||
|
{
|
||||||
|
Id = "r-direct-final",
|
||||||
|
Remarks = "direct-final",
|
||||||
|
RuleSet = JsonUtils.Serialize(new List<RulesItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
RuleType = ERuleType.Routing,
|
||||||
|
OutboundTag = Global.DirectTag,
|
||||||
|
Ip = ["0.0.0.0/0"],
|
||||||
|
Port = "0-65535",
|
||||||
|
Network = "tcp,udp",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DomainStrategy = Global.AsIs,
|
||||||
|
DomainStrategy4Singbox = string.Empty,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
|
||||||
|
var dnsServers = dns.servers
|
||||||
|
.Select(s => JsonUtils.Deserialize<DnsServer4Ray>(JsonUtils.Serialize(s)))
|
||||||
|
.Where(s => s is not null)
|
||||||
|
.Cast<DnsServer4Ray>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var hasDirectFallback = dnsServers.Any(s =>
|
||||||
|
(s.tag ?? string.Empty).StartsWith(Global.DirectDnsTag, StringComparison.Ordinal)
|
||||||
|
&& s.address == "1.1.1.1");
|
||||||
|
hasDirectFallback.Should().BeTrue();
|
||||||
|
|
||||||
|
var hasRemoteFallback = dnsServers.Any(s => s.address == "9.9.9.9");
|
||||||
|
hasRemoteFallback.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedIPs()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn");
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray) with
|
||||||
|
{
|
||||||
|
RoutingItem = new RoutingItem
|
||||||
|
{
|
||||||
|
Id = "r-dns-direct-unmatched",
|
||||||
|
Remarks = "dns-direct-unmatched",
|
||||||
|
RuleSet = JsonUtils.Serialize(new List<RulesItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
RuleType = ERuleType.DNS,
|
||||||
|
OutboundTag = Global.DirectTag,
|
||||||
|
Domain = ["geosite:us"],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DomainStrategy = Global.AsIs,
|
||||||
|
DomainStrategy4Singbox = string.Empty,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
|
||||||
|
var dnsServers = dns.servers
|
||||||
|
.Select(s => JsonUtils.Deserialize<DnsServer4Ray>(JsonUtils.Serialize(s)))
|
||||||
|
.Where(s => s is not null)
|
||||||
|
.Cast<DnsServer4Ray>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var hasExpectedIPs = dnsServers.Any(s =>
|
||||||
|
s.expectedIPs?.Contains("192.168.0.0/16") == true
|
||||||
|
|| s.expectedIPs?.Contains("geoip:cn") == true);
|
||||||
|
hasExpectedIPs.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("geosite:cn")]
|
||||||
|
[InlineData("geosite:geolocation-cn")]
|
||||||
|
[InlineData("geosite:tld-cn")]
|
||||||
|
public void GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedIPs(string domainTag)
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn");
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray) with
|
||||||
|
{
|
||||||
|
RoutingItem = new RoutingItem
|
||||||
|
{
|
||||||
|
Id = "r-dns-direct-variant",
|
||||||
|
Remarks = "dns-direct-variant",
|
||||||
|
RuleSet = JsonUtils.Serialize(new List<RulesItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Enabled = true, RuleType = ERuleType.DNS, OutboundTag = Global.DirectTag, Domain = [domainTag],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DomainStrategy = Global.AsIs,
|
||||||
|
DomainStrategy4Singbox = string.Empty,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
|
||||||
|
var dnsServers = dns.servers
|
||||||
|
.Select(s => JsonUtils.Deserialize<DnsServer4Ray>(JsonUtils.Serialize(s)))
|
||||||
|
.Where(s => s is not null)
|
||||||
|
.Cast<DnsServer4Ray>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var hasExpectedServer = dnsServers.Any(s =>
|
||||||
|
(s.tag ?? string.Empty).StartsWith(Global.DirectDnsTag, StringComparison.Ordinal)
|
||||||
|
&& s.domains?.Contains(domainTag) == true
|
||||||
|
&& s.expectedIPs?.Contains("192.168.0.0/16") == true
|
||||||
|
&& s.expectedIPs?.Contains("geoip:cn") == true);
|
||||||
|
hasExpectedServer.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_Hosts_ShouldPopulateDnsHosts()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||||
|
config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1";
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
|
||||||
|
|
||||||
|
dns.hosts.Should().NotBeNull();
|
||||||
|
dns.hosts!.Should().ContainKey("resolver.example");
|
||||||
|
JsonUtils.Serialize(dns.hosts!["resolver.example"]).Should().Contain("1.1.1.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsConfig()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray) with
|
||||||
|
{
|
||||||
|
RawDnsItem = new DNSItem
|
||||||
|
{
|
||||||
|
Id = "dns-raw-1",
|
||||||
|
Remarks = "raw",
|
||||||
|
Enabled = true,
|
||||||
|
CoreType = ECoreType.Xray,
|
||||||
|
NormalDNS = "{\"servers\":[\"8.8.8.8\"],\"hosts\":{\"raw.example\":\"1.1.1.1\"}}",
|
||||||
|
DomainStrategy4Freedom = "UseIPv4",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
|
||||||
|
|
||||||
|
JsonUtils.Serialize(dns.servers).Should().Contain("8.8.8.8");
|
||||||
|
dns.hosts.Should().NotBeNull();
|
||||||
|
dns.hosts!.Should().ContainKey("raw.example");
|
||||||
|
JsonUtils.Serialize(dns.hosts!["raw.example"]).Should().Contain("1.1.1.1");
|
||||||
|
|
||||||
|
var directOutbound = cfg.outbounds.FirstOrDefault(o => o.tag == Global.DirectTag && o.protocol == "freedom");
|
||||||
|
directOutbound.Should().NotBeNull();
|
||||||
|
directOutbound!.settings.domainStrategy.Should().Be("UseIPv4");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,222 +0,0 @@
|
||||||
using System.Text.Json.Nodes;
|
|
||||||
using ServiceLib;
|
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Models;
|
|
||||||
using ServiceLib.Services.CoreConfig;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace ServiceLib.Tests;
|
|
||||||
|
|
||||||
public class CoreConfigV2rayServiceTests
|
|
||||||
{
|
|
||||||
private const string SendThrough = "198.51.100.10";
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GenerateClientConfigContent_OnlyAppliesSendThroughToRemoteProxyOutbounds()
|
|
||||||
{
|
|
||||||
var node = CreateProxyNode("proxy-1", "198.51.100.1", 443);
|
|
||||||
var service = new CoreConfigV2rayService(CreateContext(node));
|
|
||||||
|
|
||||||
var result = service.GenerateClientConfigContent();
|
|
||||||
|
|
||||||
Assert.True(result.Success);
|
|
||||||
|
|
||||||
var outbounds = GetOutbounds(result.Data?.ToString());
|
|
||||||
var proxyOutbound = outbounds.Single(outbound => outbound["tag"]!.GetValue<string>() == Global.ProxyTag);
|
|
||||||
var directOutbound = outbounds.Single(outbound => outbound["tag"]!.GetValue<string>() == Global.DirectTag);
|
|
||||||
var blockOutbound = outbounds.Single(outbound => outbound["tag"]!.GetValue<string>() == Global.BlockTag);
|
|
||||||
|
|
||||||
Assert.Equal(SendThrough, proxyOutbound["sendThrough"]?.GetValue<string>());
|
|
||||||
Assert.Null(directOutbound["sendThrough"]);
|
|
||||||
Assert.Null(blockOutbound["sendThrough"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GenerateClientConfigContent_OnlyAppliesSendThroughToChainExitOutbounds()
|
|
||||||
{
|
|
||||||
var exitNode = CreateProxyNode("exit", "198.51.100.2", 443);
|
|
||||||
var entryNode = CreateProxyNode("entry", "198.51.100.3", 443);
|
|
||||||
var chainNode = CreateChainNode("chain", exitNode, entryNode);
|
|
||||||
|
|
||||||
var service = new CoreConfigV2rayService(CreateContext(
|
|
||||||
chainNode,
|
|
||||||
allProxiesMap: new Dictionary<string, ProfileItem>
|
|
||||||
{
|
|
||||||
[exitNode.IndexId] = exitNode,
|
|
||||||
[entryNode.IndexId] = entryNode,
|
|
||||||
}));
|
|
||||||
|
|
||||||
var result = service.GenerateClientConfigContent();
|
|
||||||
|
|
||||||
Assert.True(result.Success);
|
|
||||||
|
|
||||||
var outbounds = GetOutbounds(result.Data?.ToString())
|
|
||||||
.Where(outbound => outbound["protocol"]?.GetValue<string>() is not ("freedom" or "blackhole" or "dns"))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var sendThroughOutbounds = outbounds
|
|
||||||
.Where(outbound => outbound["sendThrough"]?.GetValue<string>() == SendThrough)
|
|
||||||
.ToList();
|
|
||||||
var chainedOutbounds = outbounds
|
|
||||||
.Where(outbound => outbound["streamSettings"]?["sockopt"]?["dialerProxy"] is not null)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
Assert.Single(sendThroughOutbounds);
|
|
||||||
Assert.All(chainedOutbounds, outbound => Assert.Null(outbound["sendThrough"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GenerateClientConfigContent_DoesNotApplySendThroughToTunRelayLoopbackOutbound()
|
|
||||||
{
|
|
||||||
var node = CreateProxyNode("proxy-1", "198.51.100.4", 443);
|
|
||||||
var config = CreateConfig();
|
|
||||||
config.TunModeItem.EnableLegacyProtect = false;
|
|
||||||
|
|
||||||
var service = new CoreConfigV2rayService(CreateContext(
|
|
||||||
node,
|
|
||||||
config,
|
|
||||||
isTunEnabled: true));
|
|
||||||
|
|
||||||
var result = service.GenerateClientConfigContent();
|
|
||||||
|
|
||||||
Assert.True(result.Success);
|
|
||||||
|
|
||||||
var outbounds = GetOutbounds(result.Data?.ToString());
|
|
||||||
Assert.DoesNotContain(outbounds, outbound => outbound["sendThrough"]?.GetValue<string>() == SendThrough);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CoreConfigContext CreateContext(
|
|
||||||
ProfileItem node,
|
|
||||||
Config? config = null,
|
|
||||||
Dictionary<string, ProfileItem>? allProxiesMap = null,
|
|
||||||
bool isTunEnabled = false)
|
|
||||||
{
|
|
||||||
return new CoreConfigContext
|
|
||||||
{
|
|
||||||
Node = node,
|
|
||||||
RunCoreType = ECoreType.Xray,
|
|
||||||
AppConfig = config ?? CreateConfig(),
|
|
||||||
AllProxiesMap = allProxiesMap ?? new(),
|
|
||||||
SimpleDnsItem = new SimpleDNSItem(),
|
|
||||||
IsTunEnabled = isTunEnabled,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Config CreateConfig()
|
|
||||||
{
|
|
||||||
return new Config
|
|
||||||
{
|
|
||||||
IndexId = string.Empty,
|
|
||||||
SubIndexId = string.Empty,
|
|
||||||
CoreBasicItem = new()
|
|
||||||
{
|
|
||||||
LogEnabled = false,
|
|
||||||
Loglevel = "warning",
|
|
||||||
MuxEnabled = false,
|
|
||||||
DefAllowInsecure = false,
|
|
||||||
DefFingerprint = Global.Fingerprints.First(),
|
|
||||||
DefUserAgent = string.Empty,
|
|
||||||
SendThrough = SendThrough,
|
|
||||||
EnableFragment = false,
|
|
||||||
EnableCacheFile4Sbox = true,
|
|
||||||
},
|
|
||||||
TunModeItem = new()
|
|
||||||
{
|
|
||||||
EnableTun = false,
|
|
||||||
AutoRoute = true,
|
|
||||||
StrictRoute = true,
|
|
||||||
Stack = string.Empty,
|
|
||||||
Mtu = 9000,
|
|
||||||
EnableIPv6Address = false,
|
|
||||||
IcmpRouting = Global.TunIcmpRoutingPolicies.First(),
|
|
||||||
EnableLegacyProtect = false,
|
|
||||||
},
|
|
||||||
KcpItem = new(),
|
|
||||||
GrpcItem = new(),
|
|
||||||
RoutingBasicItem = new()
|
|
||||||
{
|
|
||||||
DomainStrategy = Global.DomainStrategies.First(),
|
|
||||||
DomainStrategy4Singbox = Global.DomainStrategies4Sbox.First(),
|
|
||||||
RoutingIndexId = string.Empty,
|
|
||||||
},
|
|
||||||
GuiItem = new(),
|
|
||||||
MsgUIItem = new(),
|
|
||||||
UiItem = new()
|
|
||||||
{
|
|
||||||
CurrentLanguage = "en",
|
|
||||||
CurrentFontFamily = string.Empty,
|
|
||||||
MainColumnItem = [],
|
|
||||||
WindowSizeItem = [],
|
|
||||||
},
|
|
||||||
ConstItem = new(),
|
|
||||||
SpeedTestItem = new(),
|
|
||||||
Mux4RayItem = new()
|
|
||||||
{
|
|
||||||
Concurrency = 8,
|
|
||||||
XudpConcurrency = 8,
|
|
||||||
XudpProxyUDP443 = "reject",
|
|
||||||
},
|
|
||||||
Mux4SboxItem = new()
|
|
||||||
{
|
|
||||||
Protocol = string.Empty,
|
|
||||||
},
|
|
||||||
HysteriaItem = new(),
|
|
||||||
ClashUIItem = new()
|
|
||||||
{
|
|
||||||
ConnectionsColumnItem = [],
|
|
||||||
},
|
|
||||||
SystemProxyItem = new(),
|
|
||||||
WebDavItem = new(),
|
|
||||||
CheckUpdateItem = new(),
|
|
||||||
Fragment4RayItem = null,
|
|
||||||
Inbound = [new InItem
|
|
||||||
{
|
|
||||||
Protocol = EInboundProtocol.socks.ToString(),
|
|
||||||
LocalPort = 10808,
|
|
||||||
UdpEnabled = true,
|
|
||||||
SniffingEnabled = true,
|
|
||||||
RouteOnly = false,
|
|
||||||
}],
|
|
||||||
GlobalHotkeys = [],
|
|
||||||
CoreTypeItem = [],
|
|
||||||
SimpleDNSItem = new(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ProfileItem CreateProxyNode(string indexId, string address, int port)
|
|
||||||
{
|
|
||||||
return new ProfileItem
|
|
||||||
{
|
|
||||||
IndexId = indexId,
|
|
||||||
Remarks = indexId,
|
|
||||||
ConfigType = EConfigType.SOCKS,
|
|
||||||
CoreType = ECoreType.Xray,
|
|
||||||
Address = address,
|
|
||||||
Port = port,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ProfileItem CreateChainNode(string indexId, params ProfileItem[] nodes)
|
|
||||||
{
|
|
||||||
var chainNode = new ProfileItem
|
|
||||||
{
|
|
||||||
IndexId = indexId,
|
|
||||||
Remarks = indexId,
|
|
||||||
ConfigType = EConfigType.ProxyChain,
|
|
||||||
CoreType = ECoreType.Xray,
|
|
||||||
};
|
|
||||||
chainNode.SetProtocolExtra(new ProtocolExtraItem
|
|
||||||
{
|
|
||||||
ChildItems = string.Join(',', nodes.Select(node => node.IndexId)),
|
|
||||||
});
|
|
||||||
return chainNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<JsonObject> GetOutbounds(string? json)
|
|
||||||
{
|
|
||||||
var root = JsonNode.Parse(json ?? throw new InvalidOperationException("Config JSON is missing"))?.AsObject()
|
|
||||||
?? throw new InvalidOperationException("Failed to parse config JSON");
|
|
||||||
return root["outbounds"]?.AsArray().Select(node => node!.AsObject()).ToList()
|
|
||||||
?? throw new InvalidOperationException("Config JSON does not contain outbounds");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
173
v2rayN/ServiceLib.Tests/Fmt/FmtHandlerTests.cs
Normal file
173
v2rayN/ServiceLib.Tests/Fmt/FmtHandlerTests.cs
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
using AwesomeAssertions;
|
||||||
|
using ServiceLib.Handler.Fmt;
|
||||||
|
using ServiceLib.Models;
|
||||||
|
using ServiceLib.Enums;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ServiceLib.Tests.Fmt;
|
||||||
|
|
||||||
|
public class FmtHandlerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GetShareUriAndResolveConfig_Vmess_ShouldRoundTripBasicFields()
|
||||||
|
{
|
||||||
|
var source = CreateVmessProfile();
|
||||||
|
|
||||||
|
var resolved = ExportThenImport(source);
|
||||||
|
|
||||||
|
resolved.ConfigType.Should().Be(EConfigType.VMess);
|
||||||
|
resolved.Remarks.Should().Be(source.Remarks);
|
||||||
|
resolved.Address.Should().Be(source.Address);
|
||||||
|
resolved.Port.Should().Be(source.Port);
|
||||||
|
resolved.Password.Should().Be(source.Password);
|
||||||
|
resolved.GetProtocolExtra().AlterId.Should().Be(source.GetProtocolExtra().AlterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetShareUriAndResolveConfig_Vless_ShouldRoundTripBasicFields()
|
||||||
|
{
|
||||||
|
var source = CreateVlessProfile();
|
||||||
|
|
||||||
|
var resolved = ExportThenImport(source);
|
||||||
|
|
||||||
|
resolved.ConfigType.Should().Be(EConfigType.VLESS);
|
||||||
|
resolved.Remarks.Should().Be(source.Remarks);
|
||||||
|
resolved.Address.Should().Be(source.Address);
|
||||||
|
resolved.Port.Should().Be(source.Port);
|
||||||
|
resolved.Password.Should().Be(source.Password);
|
||||||
|
resolved.GetProtocolExtra().VlessEncryption.Should().Be(Global.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetShareUriAndResolveConfig_Shadowsocks_ShouldRoundTripBasicFields()
|
||||||
|
{
|
||||||
|
var source = CreateShadowsocksProfile();
|
||||||
|
|
||||||
|
var resolved = ExportThenImport(source);
|
||||||
|
|
||||||
|
resolved.ConfigType.Should().Be(EConfigType.Shadowsocks);
|
||||||
|
resolved.Remarks.Should().Be(source.Remarks);
|
||||||
|
resolved.Address.Should().Be(source.Address);
|
||||||
|
resolved.Port.Should().Be(source.Port);
|
||||||
|
resolved.Password.Should().Be(source.Password);
|
||||||
|
resolved.GetProtocolExtra().SsMethod.Should().Be(source.GetProtocolExtra().SsMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetShareUriAndResolveConfig_Socks_ShouldRoundTripBasicFields()
|
||||||
|
{
|
||||||
|
var source = CreateSocksProfile();
|
||||||
|
|
||||||
|
var resolved = ExportThenImport(source);
|
||||||
|
|
||||||
|
resolved.ConfigType.Should().Be(EConfigType.SOCKS);
|
||||||
|
resolved.Remarks.Should().Be(source.Remarks);
|
||||||
|
resolved.Address.Should().Be(source.Address);
|
||||||
|
resolved.Port.Should().Be(source.Port);
|
||||||
|
resolved.Username.Should().Be(source.Username);
|
||||||
|
resolved.Password.Should().Be(source.Password);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveConfig_UnsupportedProtocol_ShouldReturnNull()
|
||||||
|
{
|
||||||
|
var resolved = FmtHandler.ResolveConfig("not-a-share-uri", out var msg);
|
||||||
|
|
||||||
|
resolved.Should().BeNull();
|
||||||
|
msg.Should().NotBeNullOrWhiteSpace();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetShareUri_UnsupportedConfigType_ShouldReturnNull()
|
||||||
|
{
|
||||||
|
var item = new ProfileItem { ConfigType = EConfigType.PolicyGroup, Remarks = "group", };
|
||||||
|
|
||||||
|
var uri = FmtHandler.GetShareUri(item);
|
||||||
|
|
||||||
|
uri.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProfileItem ExportThenImport(ProfileItem source)
|
||||||
|
{
|
||||||
|
var uri = FmtHandler.GetShareUri(source);
|
||||||
|
|
||||||
|
uri.Should().NotBeNullOrWhiteSpace();
|
||||||
|
(uri!.StartsWith(Global.ProtocolShares[source.ConfigType], StringComparison.OrdinalIgnoreCase)).Should()
|
||||||
|
.BeTrue();
|
||||||
|
|
||||||
|
var resolved = FmtHandler.ResolveConfig(uri, out var msg);
|
||||||
|
|
||||||
|
resolved.Should().NotBeNull($"uri: {uri}, msg: {msg}");
|
||||||
|
return resolved!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProfileItem CreateVmessProfile()
|
||||||
|
{
|
||||||
|
var item = new ProfileItem
|
||||||
|
{
|
||||||
|
ConfigType = EConfigType.VMess,
|
||||||
|
Remarks = "vmess demo",
|
||||||
|
Address = "example.com",
|
||||||
|
Port = 443,
|
||||||
|
Password = Guid.NewGuid().ToString(),
|
||||||
|
Network = nameof(ETransport.raw),
|
||||||
|
StreamSecurity = string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
item.SetProtocolExtra(new ProtocolExtraItem { AlterId = "0", VmessSecurity = Global.DefaultSecurity, });
|
||||||
|
item.SetTransportExtra(new TransportExtraItem { RawHeaderType = Global.None, });
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProfileItem CreateVlessProfile()
|
||||||
|
{
|
||||||
|
var item = new ProfileItem
|
||||||
|
{
|
||||||
|
ConfigType = EConfigType.VLESS,
|
||||||
|
Remarks = "vless demo",
|
||||||
|
Address = "vless.example",
|
||||||
|
Port = 8443,
|
||||||
|
Password = Guid.NewGuid().ToString(),
|
||||||
|
Network = nameof(ETransport.raw),
|
||||||
|
StreamSecurity = string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
item.SetProtocolExtra(new ProtocolExtraItem { VlessEncryption = Global.None, });
|
||||||
|
item.SetTransportExtra(new TransportExtraItem { RawHeaderType = Global.None, });
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProfileItem CreateShadowsocksProfile()
|
||||||
|
{
|
||||||
|
var item = new ProfileItem
|
||||||
|
{
|
||||||
|
ConfigType = EConfigType.Shadowsocks,
|
||||||
|
Remarks = "ss demo",
|
||||||
|
Address = "1.2.3.4",
|
||||||
|
Port = 8388,
|
||||||
|
Password = "pass123",
|
||||||
|
Network = nameof(ETransport.raw),
|
||||||
|
StreamSecurity = string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
item.SetProtocolExtra(new ProtocolExtraItem { SsMethod = "aes-128-gcm", });
|
||||||
|
item.SetTransportExtra(new TransportExtraItem { RawHeaderType = Global.None, });
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProfileItem CreateSocksProfile()
|
||||||
|
{
|
||||||
|
return new ProfileItem
|
||||||
|
{
|
||||||
|
ConfigType = EConfigType.SOCKS,
|
||||||
|
Remarks = "socks demo",
|
||||||
|
Address = "127.0.0.1",
|
||||||
|
Port = 1080,
|
||||||
|
Username = "user",
|
||||||
|
Password = "pass",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<IsTestProject>true</IsTestProject>
|
<IsTestProject>true</IsTestProject>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AwesomeAssertions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="xunit" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio">
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="xunit.v3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 17.3.32811.315
|
VisualStudioVersion = 18.4.11620.152 stable
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "v2rayN", "v2rayN\v2rayN.csproj", "{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "v2rayN", "v2rayN\v2rayN.csproj", "{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue