From d67321eed0628a7a1a3c8d937a4596455b7da263 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Mon, 20 Apr 2026 10:58:36 +0000 Subject: [PATCH] Add more test (#9162) * Add test Add more test and fmt test * Update to xunit.v3 --- v2rayN/Directory.Packages.props | 5 +- .../Context/CoreConfigContextBuilderTests.cs | 113 ++++ .../CoreConfig/CoreConfigTestFactory.cs | 197 +++++++ .../Singbox/CoreConfigSingboxServiceTests.cs | 511 +++++++++++++++++ .../V2ray/CoreConfigV2rayServiceTests.cs | 539 ++++++++++++++++++ .../CoreConfigV2rayServiceTests.cs | 222 -------- .../ServiceLib.Tests/Fmt/FmtHandlerTests.cs | 173 ++++++ .../ServiceLib.Tests/ServiceLib.Tests.csproj | 4 +- v2rayN/v2rayN.sln | 6 +- 9 files changed, 1542 insertions(+), 228 deletions(-) create mode 100644 v2rayN/ServiceLib.Tests/CoreConfig/Context/CoreConfigContextBuilderTests.cs create mode 100644 v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs create mode 100644 v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs create mode 100644 v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs delete mode 100644 v2rayN/ServiceLib.Tests/CoreConfigV2rayServiceTests.cs create mode 100644 v2rayN/ServiceLib.Tests/Fmt/FmtHandlerTests.cs diff --git a/v2rayN/Directory.Packages.props b/v2rayN/Directory.Packages.props index 453c4d40..2345daf2 100644 --- a/v2rayN/Directory.Packages.props +++ b/v2rayN/Directory.Packages.props @@ -9,11 +9,12 @@ + - + @@ -27,8 +28,8 @@ - + diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/Context/CoreConfigContextBuilderTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/Context/CoreConfigContextBuilderTests.cs new file mode 100644 index 00000000..5f185a9f --- /dev/null +++ b/v2rayN/ServiceLib.Tests/CoreConfig/Context/CoreConfigContextBuilderTests.cs @@ -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(); + foreach (var profile in profiles) + { + await SQLiteHelper.Instance.ReplaceAsync(profile); + } + } +} diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs b/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs new file mode 100644 index 00000000..c5acd854 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs @@ -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 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 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 { [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; + } +} diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs new file mode 100644 index 00000000..02aaac6f --- /dev/null +++ b/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs @@ -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(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(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(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(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(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 + { + 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(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 + { + 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(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 + { + 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(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(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 + { + 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(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 + { + 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(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 + { + 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(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(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(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()); + } +} diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs new file mode 100644 index 00000000..29b26648 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs @@ -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(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(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(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(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(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 + { + 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(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 + { + 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(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 + { + 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(result.Data!.ToString())!; + var dns = JsonUtils.Deserialize(JsonUtils.Serialize(cfg.dns))!; + + var dnsServers = dns.servers + .Select(s => JsonUtils.Deserialize(JsonUtils.Serialize(s))) + .Where(s => s is not null) + .Cast() + .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(result.Data!.ToString())!; + var dns = JsonUtils.Deserialize(JsonUtils.Serialize(cfg.dns))!; + + var dnsServers = dns.servers + .Select(s => JsonUtils.Deserialize(JsonUtils.Serialize(s))) + .Where(s => s is not null) + .Cast() + .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 + { + 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(result.Data!.ToString())!; + var dns = JsonUtils.Deserialize(JsonUtils.Serialize(cfg.dns))!; + var dnsServers = dns.servers + .Select(s => JsonUtils.Deserialize(JsonUtils.Serialize(s))) + .Where(s => s is not null) + .Cast() + .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 + { + 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(result.Data!.ToString())!; + var dns = JsonUtils.Deserialize(JsonUtils.Serialize(cfg.dns))!; + var dnsServers = dns.servers + .Select(s => JsonUtils.Deserialize(JsonUtils.Serialize(s))) + .Where(s => s is not null) + .Cast() + .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 + { + 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(result.Data!.ToString())!; + var dns = JsonUtils.Deserialize(JsonUtils.Serialize(cfg.dns))!; + var dnsServers = dns.servers + .Select(s => JsonUtils.Deserialize(JsonUtils.Serialize(s))) + .Where(s => s is not null) + .Cast() + .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(result.Data!.ToString())!; + var dns = JsonUtils.Deserialize(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(result.Data!.ToString())!; + var dns = JsonUtils.Deserialize(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"); + } +} diff --git a/v2rayN/ServiceLib.Tests/CoreConfigV2rayServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfigV2rayServiceTests.cs deleted file mode 100644 index 7d9ecdcb..00000000 --- a/v2rayN/ServiceLib.Tests/CoreConfigV2rayServiceTests.cs +++ /dev/null @@ -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() == Global.ProxyTag); - var directOutbound = outbounds.Single(outbound => outbound["tag"]!.GetValue() == Global.DirectTag); - var blockOutbound = outbounds.Single(outbound => outbound["tag"]!.GetValue() == Global.BlockTag); - - Assert.Equal(SendThrough, proxyOutbound["sendThrough"]?.GetValue()); - 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 - { - [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() is not ("freedom" or "blackhole" or "dns")) - .ToList(); - - var sendThroughOutbounds = outbounds - .Where(outbound => outbound["sendThrough"]?.GetValue() == 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() == SendThrough); - } - - private static CoreConfigContext CreateContext( - ProfileItem node, - Config? config = null, - Dictionary? 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 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"); - } -} diff --git a/v2rayN/ServiceLib.Tests/Fmt/FmtHandlerTests.cs b/v2rayN/ServiceLib.Tests/Fmt/FmtHandlerTests.cs new file mode 100644 index 00000000..56fb0f97 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/Fmt/FmtHandlerTests.cs @@ -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", + }; + } +} diff --git a/v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj b/v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj index 4e1ed1a7..f86ca573 100644 --- a/v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj +++ b/v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj @@ -1,17 +1,19 @@ + Exe false true + - all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/v2rayN/v2rayN.sln b/v2rayN/v2rayN.sln index adf2cbf1..3c53a0f0 100644 --- a/v2rayN/v2rayN.sln +++ b/v2rayN/v2rayN.sln @@ -1,7 +1,7 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.3.32811.315 +# Visual Studio Version 18 +VisualStudioVersion = 18.4.11620.152 stable MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "v2rayN", "v2rayN\v2rayN.csproj", "{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}" EndProject