This commit is contained in:
DHR60
2026-08-19 01:54:51 +00:00
committed by GitHub
parent 7be264c65c
commit c9e843aa3b
11 changed files with 448 additions and 471 deletions

View File

@@ -9,6 +9,10 @@ on:
- 'v2rayN/ServiceLib/Handler/Fmt/**'
- '.github/workflows/test.yml'
permissions:
checks: write
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
@@ -27,4 +31,11 @@ jobs:
- name: Test Code
working-directory: ./v2rayN
run: dotnet run --project ./ServiceLib.Tests -c Release
run: dotnet test ./ServiceLib.Tests -c Release --no-build --results-directory ./TestResults -- --report-trx
- name: Comment PR with results
if: always()
uses: EnricoMi/publish-unit-test-result-action@v2
with:
files: ./v2rayN/TestResults/*.trx
comment_mode: failures

View File

@@ -28,10 +28,12 @@
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="TUnit" Version="1.65.0" />
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.0-beta" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.v3" Version="4.0.0" />
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
</ItemGroup>
</Project>
</Project>

View File

@@ -1,15 +1,8 @@
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]
[Test]
public async Task ResolveNodeAsync_DirectCycleDependency_ShouldFailWithCycleError()
{
var config = CoreConfigTestFactory.CreateConfig();
@@ -27,13 +20,13 @@ public class CoreConfigContextBuilderTests
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);
await validatorResult.Success.Should().BeFalse();
await validatorResult.Errors.Should().Contain(ContainsCycleDependencyMessage);
await context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
await context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
}
[Fact]
[Test]
public async Task ResolveNodeAsync_IndirectCycleDependency_ShouldFailWithCycleError()
{
var config = CoreConfigTestFactory.CreateConfig();
@@ -53,14 +46,14 @@ public class CoreConfigContextBuilderTests
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);
await validatorResult.Success.Should().BeFalse();
await validatorResult.Errors.Should().Contain(ContainsCycleDependencyMessage);
await context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
await context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
await context.AllProxiesMap.Should().NotContainKey(groupC.IndexId);
}
[Fact]
[Test]
public async Task ResolveNodeAsync_CycleWithValidBranch_ShouldSkipCycleAndKeepValidChild()
{
var config = CoreConfigTestFactory.CreateConfig();
@@ -80,14 +73,14 @@ public class CoreConfigContextBuilderTests
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
validatorResult.Success.Should().BeTrue();
validatorResult.Errors.Should().BeEmpty();
validatorResult.Warnings.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
await validatorResult.Success.Should().BeTrue();
await validatorResult.Errors.Should().BeEmpty();
await validatorResult.Warnings.Should().Contain(ContainsCycleDependencyMessage);
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);
await context.AllProxiesMap.Should().ContainKey(leaf.IndexId);
await context.AllProxiesMap.Should().ContainKey(groupA.IndexId);
await context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
await groupA.GetProtocolExtra().ChildItems.Should().BeEqualTo(leaf.IndexId);
}
private static string NewId(string prefix)

View File

@@ -1,19 +1,9 @@
using AwesomeAssertions;
using ServiceLib.Common;
using ServiceLib.Enums;
using ServiceLib.Handler.Fmt;
using ServiceLib.Manager;
using ServiceLib.Models;
using ServiceLib.Models.Dto;
using ServiceLib.Services.CoreConfig;
using Xunit;
namespace ServiceLib.Tests.CoreConfig.Singbox;
public class CoreConfigSingboxServiceTests
{
[Fact]
public void GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
[Test]
public async Task GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -22,17 +12,17 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
result.Data.Should().NotBeNull();
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
await 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));
await singboxConfig.Should().NotBeNull();
await singboxConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks");
await singboxConfig.inbounds.Should().Contain(i => i.type == nameof(EInboundProtocol.mixed));
}
[Fact]
public void GenerateClientConfigContent_TunWithLoopbackPreSocks_ShouldKeepMixedInbound()
[Test]
public async Task GenerateClientConfigContent_TunWithLoopbackPreSocks_ShouldKeepMixedInbound()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -46,18 +36,18 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.inbounds.Should().Contain(i =>
await cfg.inbounds.Should().Contain(i =>
i.type == nameof(EInboundProtocol.mixed)
&& i.listen == Global.Loopback
&& i.listen_port == AppManager.Instance.GetLocalPort(EInboundProtocol.socks));
cfg.inbounds.Should().Contain(i => i.type == "tun");
await cfg.inbounds.Should().Contain(i => i.type == "tun");
}
[Fact]
public void GenerateClientConfigContent_TunEnabled_ShouldKeepEmbeddedTunRules()
[Test]
public async Task GenerateClientConfigContent_TunEnabled_ShouldKeepEmbeddedTunRules()
{
// The embedded tun rules reject local-network noise (NetBIOS/mDNS, multicast).
// They are deserialized into List<Rule4Sbox>, so a schema mismatch in the
@@ -75,22 +65,22 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.route.rules.Should().Contain(
await cfg.route.rules.Should().Contain(
r => r.action == "reject"
&& r.network != null && r.network.Contains("udp")
&& r.port != null && r.port.Contains(5353),
"the embedded tun rules must reject mDNS/NetBIOS noise");
cfg.route.rules.Should().Contain(
await cfg.route.rules.Should().Contain(
r => r.action == "reject"
&& r.ip_cidr != null && r.ip_cidr.Contains("224.0.0.0/3"),
"the embedded tun rules must reject multicast traffic");
}
[Fact]
public void GenerateClientConfigContent_TunEnabled_ShouldRejectTrafficToTunOwnAddresses()
[Test]
public async Task GenerateClientConfigContent_TunEnabled_ShouldRejectTrafficToTunOwnAddresses()
{
// Regression test: traffic addressed to the TUN interface's own addresses must
// never reach an outbound. auto_route hijacks the default route, so `direct`
@@ -111,17 +101,19 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var tun = cfg.inbounds.First(i => i.type == "tun");
tun.address.Should().NotBeNullOrEmpty();
//tun.address.Should().NotBeNullOrEmpty();
await tun.address.Should().NotBeNull();
await tun.address.Should().NotBeEmpty();
foreach (var address in tun.address!)
{
var self = IPAddress.Parse(address.Split('/').First());
var hostBits = self.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32;
var expected = $"{self}/{hostBits}";
cfg.route.rules.Should().Contain(
await cfg.route.rules.Should().Contain(
r => r.action == "reject" && r.ip_cidr != null && r.ip_cidr.Contains(expected),
$"traffic to the TUN's own address '{address}' must be rejected, not routed");
}
@@ -131,12 +123,13 @@ public class CoreConfigSingboxServiceTests
// here leaves room for it, so a prefix match would drop system name lookups too.
var dropRule = cfg.route.rules.First(r =>
r.action == "reject" && r.method == "drop" && r.ip_cidr?.Count > 0);
dropRule.ip_cidr!.Should().OnlyContain(c =>
c.EndsWith("/32", StringComparison.Ordinal) || c.EndsWith("/128", StringComparison.Ordinal));
//dropRule.ip_cidr!.Should().OnlyContain(c =>
// c.EndsWith("/32", StringComparison.Ordinal) || c.EndsWith("/128", StringComparison.Ordinal));
await dropRule.ip_cidr.Should().All(c => c.EndsWith("/32", StringComparison.Ordinal) || c.EndsWith("/128", StringComparison.Ordinal));
}
[Fact]
public void GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface()
[Test]
public async Task GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.CoreBasicItem.BindInterface = "eth0";
@@ -150,16 +143,16 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
proxy.bind_interface.Should().Be("eth0");
proxy.detour.Should().BeNullOrEmpty();
await proxy.bind_interface.Should().BeEqualTo("eth0");
await proxy.detour.Should().BeNull().Or.BeEmpty();
}
[Fact]
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildSelector()
[Test]
public async Task GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildSelector()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -176,17 +169,17 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"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));
await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
await cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
}
[Fact]
public void GenerateClientConfigContent_ProxyChain_ShouldBuildDetourChain()
[Test]
public async Task GenerateClientConfigContent_ProxyChain_ShouldBuildDetourChain()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -203,18 +196,18 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"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 =>
await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks");
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
await 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()
[Test]
public async Task GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -236,18 +229,18 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"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));
await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
await cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
}
[Fact]
public void GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
[Test]
public async Task GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -269,25 +262,25 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"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));
await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
await cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal));
await 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);
await proxyCloneCount.Should().BeEqualTo(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();
await allCloneDetoursPointToGroupBranches.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
[Test]
public async Task GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -323,24 +316,24 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"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();
await 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();
await hasBlockRule.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
[Test]
public async Task GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -372,21 +365,21 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"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));
await 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();
await hasRouteRule.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyGeoipAndCidrToDirectDnsRule()
[Test]
public async Task GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyGeoipAndCidrToDirectDnsRule()
{
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(
ECoreType.sing_box,
@@ -417,7 +410,7 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var hasExpectedRule = cfg.dns.rules?.Any(r =>
@@ -426,11 +419,11 @@ public class CoreConfigSingboxServiceTests
&& r.rule_set?.Contains("geosite-cn") == true
&& r.rule_set?.Contains("geoip-cn") == true) ?? false;
hasExpectedRule.Should().BeTrue();
await hasExpectedRule.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_BootstrapDNS_ShouldConfigurePureIPResolver()
[Test]
public async Task GenerateClientConfigContent_BootstrapDNS_ShouldConfigurePureIPResolver()
{
var bootstrapDns = "8.8.8.8";
var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.sing_box, bootstrapDns);
@@ -441,17 +434,17 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
config.SimpleDNSItem.BootstrapDNS.Should().Be(bootstrapDns);
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
await config.SimpleDNSItem.BootstrapDNS.Should().BeEqualTo(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);
var bootstrapServer = cfg.dns?.servers.FirstOrDefault(s => s.tag == Global.SingboxLocalDNSTag);
await bootstrapServer.Should().NotBeNull();
await bootstrapServer!.server.Should().Contain(bootstrapDns);
}
[Fact]
public void GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectFinalDns()
[Test]
public async Task GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectFinalDns()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.SimpleDNSItem.DirectDNS = "1.1.1.1";
@@ -484,14 +477,14 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.dns.final.Should().Be(Global.SingboxDirectDNSTag);
await cfg.dns.final.Should().BeEqualTo(Global.SingboxDirectDNSTag);
}
[Fact]
public void GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedRule()
[Test]
public async Task GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedRule()
{
var config =
CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.sing_box, "192.168.0.0/16,geoip:cn");
@@ -521,21 +514,21 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"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();
await 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,
[Test]
[Arguments("geosite:cn", "geosite-cn")]
[Arguments("geosite:geolocation-cn", "geosite-geolocation-cn")]
[Arguments("geosite:tld-cn", "geosite-tld-cn")]
public async Task GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedRule(string domainTag,
string expectedRuleSetTag)
{
var config =
@@ -563,7 +556,7 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var hasExpectedRule = cfg.dns.rules?.Any(r =>
@@ -571,11 +564,11 @@ public class CoreConfigSingboxServiceTests
&& 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();
await hasExpectedRule.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_Hosts_ShouldPopulateHostsServerAndDomainResolver()
[Test]
public async Task GenerateClientConfigContent_Hosts_ShouldPopulateHostsServerAndDomainResolver()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1";
@@ -587,21 +580,21 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"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");
await hostsServer.Should().NotBeNull();
await hostsServer!.predefined.Should().ContainKey("resolver.example");
await 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);
await directServer.Should().NotBeNull();
await directServer!.domain_resolver.Should().BeEqualTo(Global.SingboxHostsDNSTag);
}
[Fact]
public void GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsAndInjectLocalResolver()
[Test]
public async Task GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsAndInjectLocalResolver()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -630,22 +623,22 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.dns.servers.Should().Contain(s => s.tag == "remote" && s.type == "udp" && s.server == "8.8.8.8");
cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag);
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
await cfg.dns.servers.Should().Contain(s => s.tag == "remote" && s.type == "udp" && s.server == "8.8.8.8");
await cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag);
await cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
await cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
}
[Fact]
public void GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl()
[Test]
public async Task GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl()
{
var shareLink =
"hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
var node = Hysteria2Fmt.ResolveRealm(shareLink, out _);
node.Should().NotBeNull();
await node.Should().NotBeNull();
node!.CoreType = ECoreType.sing_box;
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
@@ -658,22 +651,22 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
proxy.type.Should().Be("hysteria2");
proxy.realm.Should().NotBeNull();
proxy.realm!.server_url.Should().StartWith("https://");
proxy.realm.server_url.Should().Contain("realm.hy2.io");
proxy.realm.token.Should().Be("public");
proxy.realm.realm_id.Should().Be("my-realm-id");
proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
proxy.server.Should().BeNull();
await proxy.type.Should().BeEqualTo("hysteria2");
await proxy.realm.Should().NotBeNull();
await proxy.realm!.server_url.Should().StartWith("https://");
await proxy.realm.server_url.Should().Contain("realm.hy2.io");
await proxy.realm.token.Should().BeEqualTo("public");
await proxy.realm.realm_id.Should().BeEqualTo("my-realm-id");
await proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
await proxy.server.Should().BeNull();
}
[Fact]
public void GenerateClientConfigContent_TunSystemStackWithIpv6_ShouldUsePrefixWithPeerAddress()
[Test]
public async Task GenerateClientConfigContent_TunSystemStackWithIpv6_ShouldUsePrefixWithPeerAddress()
{
// Regression test for #9820: sing-box fails with "need one more IPv6 address in
// first prefix for system stack" when the TUN inbound uses a /128 IPv6 prefix.
@@ -691,23 +684,24 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var tun = cfg.inbounds.First(i => i.type == "tun");
tun.address.Should().NotBeNullOrEmpty();
await tun.address.Should().NotBeNull();
await tun.address.Should().NotBeEmpty();
foreach (var address in tun.address!)
{
var prefixLength = int.Parse(address[(address.LastIndexOf('/') + 1)..]);
var isIpv6 = address.Contains(':');
prefixLength.Should().BeLessThanOrEqualTo(isIpv6 ? 126 : 30,
await prefixLength.Should().BeLessThanOrEqualTo(isIpv6 ? 126 : 30,
$"'{address}' must leave room for the peer address the system stack derives");
}
}
[Fact]
public void GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
[Test]
public async Task GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -728,17 +722,17 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
result.Data.Should().NotBeNull();
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
await result.Data.Should().NotBeNull();
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString());
cfg.Should().NotBeNull();
await cfg.Should().NotBeNull();
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
proxyOutbound.Should().NotBeNull();
proxyOutbound!.type.Should().Be("shadowsocks");
proxyOutbound.server.Should().Be("1.2.3.4");
proxyOutbound.server_port.Should().Be(8388);
proxyOutbound.method.Should().Be("aes-128-gcm");
proxyOutbound.password.Should().Be("custom_password");
await proxyOutbound.Should().NotBeNull();
await proxyOutbound!.type.Should().BeEqualTo("shadowsocks");
await proxyOutbound.server.Should().BeEqualTo("1.2.3.4");
await proxyOutbound.server_port.Should().BeEqualTo(8388);
await proxyOutbound.method.Should().BeEqualTo("aes-128-gcm");
await proxyOutbound.password.Should().BeEqualTo("custom_password");
}
}

View File

@@ -1,16 +1,9 @@
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()
[Test]
public async Task GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -19,17 +12,17 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
result.Data.Should().NotBeNull();
await result.Success.Should().BeTrue();
await 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));
await v2rayConfig.Should().NotBeNull();
await v2rayConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.protocol == "vmess");
await v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed));
}
[Fact]
public void GenerateClientConfigContent_HttpOutbound_ShouldEmitHeadersInSettings()
[Test]
public async Task GenerateClientConfigContent_HttpOutbound_ShouldEmitHeadersInSettings()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -42,27 +35,27 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var outbound = cfg.outbounds.First(o => o.tag == Global.ProxyTag && o.protocol == "http");
outbound.settings.address?.ToString().Should().Be("proxy.example.com");
outbound.settings.port.Should().Be(8080);
outbound.settings.user.Should().Be("user");
outbound.settings.pass.Should().Be("pass");
outbound.settings.level.Should().Be(1);
outbound.settings.headers.Should().NotBeNull();
var headers = JsonUtils.ParseJson(outbound.settings.headers.ToString());
headers["User-Agent"]!.GetValue<string>().Should().Be("v2rayN");
headers["Set-Cookie"]!.AsArray()
await outbound.settings.address!.ToString().Should().BeEqualTo("proxy.example.com");
await outbound.settings.port.Should().BeEqualTo(8080);
await outbound.settings.user.Should().BeEqualTo("user");
await outbound.settings.pass.Should().BeEqualTo("pass");
await outbound.settings.level.Should().BeEqualTo(1);
await outbound.settings.headers.Should().NotBeNull();
var headers = JsonUtils.ParseJson(outbound.settings.headers!.ToString());
await headers["User-Agent"]!.GetValue<string>().Should().BeEqualTo("v2rayN");
await headers["Set-Cookie"]!.AsArray()
.Select(item => item!.GetValue<string>())
.Should().Equal("a=1", "b=2");
outbound.settings.servers.Should().BeNull();
outbound.settings.vnext.Should().BeNull();
.Should().BeEquivalentTo(["a=1", "b=2"]);
await outbound.settings.servers.Should().BeNull();
await outbound.settings.vnext.Should().BeNull();
}
[Fact]
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
[Test]
public async Task GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -79,17 +72,17 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await 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);
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
await cfg.routing.balancers.Should().NotBeNull();
await cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
}
[Fact]
public void GenerateClientConfigContent_ProxyChain_ShouldBuildDialerProxyChain()
[Test]
public async Task GenerateClientConfigContent_ProxyChain_ShouldBuildDialerProxyChain()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -105,21 +98,21 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await 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));
await 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();
await hasDialerChain.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds()
[Test]
public async Task GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -140,18 +133,18 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await 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);
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
await cfg.routing.balancers.Should().NotBeNull();
await cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
}
[Fact]
public void GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
[Test]
public async Task GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -173,27 +166,27 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await 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));
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal));
await 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);
await proxyCloneCount.Should().BeEqualTo(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();
await allCloneDialersPointToGroupBranches.Should().BeTrue();
cfg.routing.balancers.Should().NotBeNull();
cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
await cfg.routing.balancers.Should().NotBeNull();
await cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
}
[Fact]
public void GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
[Test]
public async Task GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -229,24 +222,24 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await 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();
await 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();
await hasBlockRule.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
[Test]
public async Task GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -278,20 +271,20 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await 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));
await 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();
await hasRouteRule.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyExpectedIPsToDirectDnsServer()
[Test]
public async Task GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyExpectedIPsToDirectDnsServer()
{
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn");
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -320,7 +313,7 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
@@ -335,11 +328,11 @@ public class CoreConfigV2rayServiceTests
&& s.domains?.Contains("geosite:cn") == true
&& s.expectedIPs?.Contains("192.168.0.0/16") == true
&& s.expectedIPs?.Contains("geoip:cn") == true);
hasExpectedServer.Should().BeTrue();
await hasExpectedServer.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_BootstrapDNS_ShouldApplyToDnsServerDomains()
[Test]
public async Task GenerateClientConfigContent_BootstrapDNS_ShouldApplyToDnsServerDomains()
{
var bootstrapDns = "8.8.8.8";
var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.Xray, bootstrapDns);
@@ -352,7 +345,7 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
@@ -366,11 +359,11 @@ public class CoreConfigV2rayServiceTests
s.address == bootstrapDns
&& s.domains?.Contains("full:dns-direct.example") == true
&& s.domains?.Contains("full:dns-remote.example") == true);
hasBootstrapServer.Should().BeTrue();
await hasBootstrapServer.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectDnsServers()
[Test]
public async Task GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectDnsServers()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
config.SimpleDNSItem.DirectDNS = "1.1.1.1";
@@ -403,7 +396,7 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await 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
@@ -415,14 +408,14 @@ public class CoreConfigV2rayServiceTests
var hasDirectFallback = dnsServers.Any(s =>
(s.tag ?? string.Empty).StartsWith(Global.DirectDnsTag, StringComparison.Ordinal)
&& s.address == "1.1.1.1");
hasDirectFallback.Should().BeTrue();
await hasDirectFallback.Should().BeTrue();
var hasRemoteFallback = dnsServers.Any(s => s.address == "9.9.9.9");
hasRemoteFallback.Should().BeFalse();
await hasRemoteFallback.Should().BeFalse();
}
[Fact]
public void GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedIPs()
[Test]
public async Task GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedIPs()
{
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn");
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -451,7 +444,7 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await 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
@@ -463,14 +456,14 @@ public class CoreConfigV2rayServiceTests
var hasExpectedIPs = dnsServers.Any(s =>
s.expectedIPs?.Contains("192.168.0.0/16") == true
|| s.expectedIPs?.Contains("geoip:cn") == true);
hasExpectedIPs.Should().BeFalse();
await hasExpectedIPs.Should().BeFalse();
}
[Theory]
[InlineData("geosite:cn")]
[InlineData("geosite:geolocation-cn")]
[InlineData("geosite:tld-cn")]
public void GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedIPs(string domainTag)
[Test]
[Arguments("geosite:cn")]
[Arguments("geosite:geolocation-cn")]
[Arguments("geosite:tld-cn")]
public async Task GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedIPs(string domainTag)
{
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn");
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -496,7 +489,7 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await 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
@@ -510,11 +503,11 @@ public class CoreConfigV2rayServiceTests
&& s.domains?.Contains(domainTag) == true
&& s.expectedIPs?.Contains("192.168.0.0/16") == true
&& s.expectedIPs?.Contains("geoip:cn") == true);
hasExpectedServer.Should().BeTrue();
await hasExpectedServer.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_Hosts_ShouldPopulateDnsHosts()
[Test]
public async Task GenerateClientConfigContent_Hosts_ShouldPopulateDnsHosts()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1";
@@ -525,17 +518,17 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await 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");
await dns.hosts.Should().NotBeNull();
await dns.hosts!.Should().ContainKey("resolver.example");
await JsonUtils.Serialize(dns.hosts!["resolver.example"]).Should().Contain("1.1.1.1");
}
[Fact]
public void GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsConfig()
[Test]
public async Task GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsConfig()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -556,24 +549,24 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await 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");
await JsonUtils.Serialize(dns.servers).Should().Contain("8.8.8.8");
await dns.hosts.Should().NotBeNull();
await dns.hosts!.Should().ContainKey("raw.example");
await 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!.streamSettings.sockopt!.domainStrategy.Should().Be("UseIPv4");
await directOutbound.Should().NotBeNull();
await directOutbound!.streamSettings.sockopt!.domainStrategy.Should().BeEqualTo("UseIPv4");
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GenerateClientConfigContent_Tun_ShouldRouteIPv6IntoTunnel(bool enableIPv6Address)
[Test]
[Arguments(false)]
[Arguments(true)]
public async Task GenerateClientConfigContent_Tun_ShouldRouteIPv6IntoTunnel(bool enableIPv6Address)
{
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -583,20 +576,20 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
tunInbound.Should().NotBeNull();
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("0.0.0.0/0");
tunInbound.settings.autoSystemRoutingTable.Should().Contain("::/0");
await tunInbound.Should().NotBeNull();
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("0.0.0.0/0");
await tunInbound.settings.autoSystemRoutingTable.Should().Contain("::/0");
// EnableIPv6Address governs the interface address only, never the routing table.
tunInbound.settings.gateway.Should().HaveCount(enableIPv6Address ? 2 : 1);
await tunInbound.settings.gateway.Should().HaveCount(enableIPv6Address ? 2 : 1);
}
[Fact]
public void GenerateClientConfigContent_TunRouteExcludeAddress_ShouldIncludeIPv6Ranges()
[Test]
public async Task GenerateClientConfigContent_TunRouteExcludeAddress_ShouldIncludeIPv6Ranges()
{
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
config.TunModeItem.EnableIPv6Address = false;
@@ -607,16 +600,16 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
tunInbound.Should().NotBeNull();
tunInbound!.settings.autoSystemRoutingTable.Should().Contain(x => x.Contains(':'));
await tunInbound.Should().NotBeNull();
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain(x => x.Contains(':'));
}
[Fact]
public void GenerateClientConfigContent_TunRouteExcludeAddress()
[Test]
public async Task GenerateClientConfigContent_TunRouteExcludeAddress()
{
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -626,20 +619,20 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
tunInbound.Should().NotBeNull();
await tunInbound.Should().NotBeNull();
tunInbound!.settings.autoSystemRoutingTable.Should().NotContain("0.0.0.0/0");
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32");
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31");
await tunInbound!.settings.autoSystemRoutingTable.Should().NotContain("0.0.0.0/0");
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32");
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31");
}
[Fact]
public void GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
[Test]
public async Task GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -666,14 +659,14 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
result.Data.Should().NotBeNull();
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
await result.Data.Should().NotBeNull();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString());
cfg.Should().NotBeNull();
await cfg.Should().NotBeNull();
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
proxyOutbound.Should().NotBeNull();
proxyOutbound!.protocol.Should().Be("shadowsocks");
proxyOutbound.settings.servers.Should().NotBeNull();
await proxyOutbound.Should().NotBeNull();
await proxyOutbound!.protocol.Should().BeEqualTo("shadowsocks");
await proxyOutbound.settings.servers.Should().NotBeNull();
}
}

View File

@@ -1,103 +1,99 @@
using AwesomeAssertions;
using ServiceLib.Enums;
using ServiceLib.Handler.Fmt;
using ServiceLib.Models;
using Xunit;
namespace ServiceLib.Tests.Fmt;
public class FmtHandlerTests
{
[Fact]
public void GetShareUriAndResolveConfig_Vmess_ShouldRoundTripBasicFields()
[Test]
public async Task GetShareUriAndResolveConfig_Vmess_ShouldRoundTripBasicFields()
{
var source = CreateVmessProfile();
var resolved = ExportThenImport(source);
var resolved = await 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);
await resolved.ConfigType.Should().BeEqualTo(EConfigType.VMess);
await resolved.Remarks.Should().BeEqualTo(source.Remarks);
await resolved.Address.Should().BeEqualTo(source.Address);
await resolved.Port.Should().BeEqualTo(source.Port);
await resolved.Password.Should().BeEqualTo(source.Password);
await resolved.GetProtocolExtra().AlterId.Should().BeEqualTo(source.GetProtocolExtra().AlterId);
}
[Fact]
public void GetShareUriAndResolveConfig_Vless_ShouldRoundTripBasicFields()
[Test]
public async Task GetShareUriAndResolveConfig_Vless_ShouldRoundTripBasicFields()
{
var source = CreateVlessProfile();
var resolved = ExportThenImport(source);
var resolved = await 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);
await resolved.ConfigType.Should().BeEqualTo(EConfigType.VLESS);
await resolved.Remarks.Should().BeEqualTo(source.Remarks);
await resolved.Address.Should().BeEqualTo(source.Address);
await resolved.Port.Should().BeEqualTo(source.Port);
await resolved.Password.Should().BeEqualTo(source.Password);
await resolved.GetProtocolExtra().VlessEncryption.Should().BeEqualTo(Global.None);
}
[Fact]
public void GetShareUriAndResolveConfig_Shadowsocks_ShouldRoundTripBasicFields()
[Test]
public async Task GetShareUriAndResolveConfig_Shadowsocks_ShouldRoundTripBasicFields()
{
var source = CreateShadowsocksProfile();
var resolved = ExportThenImport(source);
var resolved = await 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);
await resolved.ConfigType.Should().BeEqualTo(EConfigType.Shadowsocks);
await resolved.Remarks.Should().BeEqualTo(source.Remarks);
await resolved.Address.Should().BeEqualTo(source.Address);
await resolved.Port.Should().BeEqualTo(source.Port);
await resolved.Password.Should().BeEqualTo(source.Password);
await resolved.GetProtocolExtra().SsMethod.Should().BeEqualTo(source.GetProtocolExtra().SsMethod);
}
[Fact]
public void GetShareUriAndResolveConfig_Socks_ShouldRoundTripBasicFields()
[Test]
public async Task GetShareUriAndResolveConfig_Socks_ShouldRoundTripBasicFields()
{
var source = CreateSocksProfile();
var resolved = ExportThenImport(source);
var resolved = await 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);
await resolved.ConfigType.Should().BeEqualTo(EConfigType.SOCKS);
await resolved.Remarks.Should().BeEqualTo(source.Remarks);
await resolved.Address.Should().BeEqualTo(source.Address);
await resolved.Port.Should().BeEqualTo(source.Port);
await resolved.Username.Should().BeEqualTo(source.Username);
await resolved.Password.Should().BeEqualTo(source.Password);
}
[Fact]
public void ResolveConfig_UnsupportedProtocol_ShouldReturnNull()
[Test]
public async Task ResolveConfig_UnsupportedProtocol_ShouldReturnNull()
{
var resolved = FmtHandler.ResolveConfig("not-a-share-uri", out var msg);
resolved.Should().BeNull();
msg.Should().NotBeNullOrWhiteSpace();
await resolved.Should().BeNull();
await msg.Should().NotBeNull();
await msg.Should().NotBeEmpty();
}
[Fact]
public void GetShareUri_UnsupportedConfigType_ShouldReturnNull()
[Test]
public async Task GetShareUri_UnsupportedConfigType_ShouldReturnNull()
{
var item = new ProfileItem { ConfigType = EConfigType.PolicyGroup, Remarks = "group", };
var uri = FmtHandler.GetShareUri(item);
uri.Should().BeNull();
await uri.Should().BeNull();
}
private static ProfileItem ExportThenImport(ProfileItem source)
private static async Task<ProfileItem> ExportThenImport(ProfileItem source)
{
var uri = FmtHandler.GetShareUri(source);
uri.Should().NotBeNullOrWhiteSpace();
uri!.StartsWith(Global.ProtocolShares[source.ConfigType], StringComparison.OrdinalIgnoreCase).Should()
await uri.Should().NotBeNull();
await uri.Should().NotBeEmpty();
await 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}");
await resolved.Should().NotBeNull().Because($"uri: {uri}, msg: {msg}");
return resolved!;
}

View File

@@ -1,32 +1,27 @@
using AwesomeAssertions;
using ServiceLib.Handler.Fmt;
using ServiceLib.Models.Dto;
using Xunit;
namespace ServiceLib.Tests.Fmt;
public class HyRealmTests
{
[Fact]
public void TryParse_ShouldParseValidRealm()
[Test]
public async Task TryParse_ShouldParseValidRealm()
{
var str = "realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478";
var result = HyRealm.TryParse(str, out var realm);
result.Should().BeTrue();
realm.Should().NotBeNull();
await result.Should().BeTrue();
await realm.Should().NotBeNull();
realm.IsHttp.Should().BeFalse();
realm.Token.Should().Be("public");
realm.RendezvousHost.Should().Be("realm.hy2.io");
realm.RendezvousPort.Should().Be(443);
realm.RealmName.Should().Be("57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
realm.StunList.Should().HaveCount(2);
realm.StunList.Should().Contain("example.stun:3478");
realm.StunList.Should().Contain("example2.stun:3478");
await realm.IsHttp.Should().BeFalse();
await realm.Token.Should().BeEqualTo("public");
await realm.RendezvousHost.Should().BeEqualTo("realm.hy2.io");
await realm.RendezvousPort.Should().BeEqualTo(443);
await realm.RealmName.Should().BeEqualTo("57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
await realm.StunList.Should().HaveCount(2);
await realm.StunList.Should().Contain("example.stun:3478");
await realm.StunList.Should().Contain("example2.stun:3478");
}
[Fact]
public void ToUri_ShouldGenerateValidUri()
[Test]
public async Task ToUri_ShouldGenerateValidUri()
{
var realm = new HyRealm(
IsHttp: false,
@@ -37,32 +32,32 @@ public class HyRealmTests
StunList: ["example.stun:3478", "example2.stun:3478"]
);
var uri = realm.ToUri();
uri.Should().Contain("realm://public@realm.hy2.io");
uri.Should().Contain("/57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
uri.Should().Contain("stun=example.stun:3478");
uri.Should().Contain("stun=example2.stun:3478");
await uri.Should().Contain("realm://public@realm.hy2.io");
await uri.Should().Contain("/57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
await uri.Should().Contain("stun=example.stun:3478");
await uri.Should().Contain("stun=example2.stun:3478");
}
[Fact]
public void GetShareUriAndResolveConfig_Hy2Realm_ShouldRoundTripBasicFields()
[Test]
public async Task GetShareUriAndResolveConfig_Hy2Realm_ShouldRoundTripBasicFields()
{
var str = "hysteria2+realm://mytoken@rendezvous.example.com/my-cabin-1f3a8c2e9b?auth=your_password&insecure=1&pinSHA256=deadbeef#remark";
var resolved = Hysteria2Fmt.ResolveRealm(str, out var msg);
resolved.Should().NotBeNull();
resolved.Password.Should().Be("your_password");
await resolved.Should().NotBeNull();
await resolved.Password.Should().BeEqualTo("your_password");
var result = HyRealm.TryParse(resolved.GetProtocolExtra().Hy2RealmUrl, out var realm);
result.Should().BeTrue();
realm.Should().NotBeNull();
realm.Token.Should().Be("mytoken");
await result.Should().BeTrue();
await realm.Should().NotBeNull();
await realm.Token.Should().BeEqualTo("mytoken");
// To uri
var uri = Hysteria2Fmt.ToUri(resolved);
uri.Should().Contain("hysteria2+realm://mytoken@rendezvous.example.com");
uri.Should().EndWith("#remark");
await uri.Should().Contain("hysteria2+realm://mytoken@rendezvous.example.com");
await uri.Should().EndWith("#remark");
}
[Fact]
public void ToServerUrl_ShouldIncludeSchemeForSingbox()
[Test]
public async Task ToServerUrl_ShouldIncludeSchemeForSingbox()
{
var realm = new HyRealm(
IsHttp: false,
@@ -73,19 +68,19 @@ public class HyRealmTests
StunList: ["turn.cloudflare.com:3478"]
);
realm.ToServerUrl().Should().Be("https://realm.hy2.io:443");
await realm.ToServerUrl().Should().BeEqualTo("https://realm.hy2.io:443");
}
[Fact]
public void ResolveRealm_Issue9635_ShouldProduceHttpsServerUrl()
[Test]
public async Task ResolveRealm_Issue9635_ShouldProduceHttpsServerUrl()
{
var str = "hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
var resolved = Hysteria2Fmt.ResolveRealm(str, out _);
resolved.Should().NotBeNull();
await resolved.Should().NotBeNull();
HyRealm.TryParse(resolved!.GetProtocolExtra().Hy2RealmUrl, out var realm).Should().BeTrue();
realm!.ToServerUrl().Should().StartWith("https://");
realm.ToServerUrl().Should().Contain("realm.hy2.io");
realm.StunList.Should().Contain("turn.cloudflare.com:3478");
await HyRealm.TryParse(resolved!.GetProtocolExtra().Hy2RealmUrl, out var realm).Should().BeTrue();
await realm!.ToServerUrl().Should().StartWith("https://");
await realm.ToServerUrl().Should().Contain("realm.hy2.io");
await realm.StunList.Should().Contain("turn.cloudflare.com:3478");
}
}

View File

@@ -1,15 +1,11 @@
using AwesomeAssertions;
using ServiceLib.Enums;
using ServiceLib.Handler.Fmt;
using ServiceLib.Tests.CoreConfig;
using Xunit;
namespace ServiceLib.Tests.Fmt;
public class InnerFmtTests
{
[Fact]
public void ToUriAndResolve_ShouldRoundTripPolicyGroupReferences()
[Test]
public async Task ToUriAndResolve_ShouldRoundTripPolicyGroupReferences()
{
var childA = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "child-a", "child-a");
var childB = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "child-b", "child-b");
@@ -19,19 +15,20 @@ public class InnerFmtTests
var uri = InnerFmt.ToUri([group, childA, childB]);
uri.Should().NotBeNullOrWhiteSpace();
await uri.Should().NotBeNull();
await uri.Should().NotBeEmpty();
var resolved = InnerFmt.Resolve(uri!, "sub-123");
resolved.Should().NotBeNull();
resolved.Should().HaveCount(3);
await resolved.Should().NotBeNull();
await resolved.Should().HaveCount(3);
var resolvedGroup = resolved!.Single(x => x.Remarks == group.Remarks);
var resolvedChildA = resolved.Single(x => x.Remarks == childA.Remarks);
var resolvedChildB = resolved.Single(x => x.Remarks == childB.Remarks);
resolvedGroup.ConfigType.Should().Be(EConfigType.PolicyGroup);
resolvedGroup.GetProtocolExtra().SubChildItems.Should().Be("sub-123");
resolvedGroup.GetProtocolExtra().ChildItems.Should().Be($"{resolvedChildA.IndexId},{resolvedChildB.IndexId}");
await resolvedGroup.ConfigType.Should().BeEqualTo(EConfigType.PolicyGroup);
await resolvedGroup.GetProtocolExtra().SubChildItems.Should().BeEqualTo("sub-123");
await resolvedGroup.GetProtocolExtra().ChildItems.Should().BeEqualTo($"{resolvedChildA.IndexId},{resolvedChildB.IndexId}");
}
}

View File

@@ -1,13 +1,9 @@
using AwesomeAssertions;
using ServiceLib.Handler.Fmt;
using Xunit;
namespace ServiceLib.Tests.Fmt;
public class WireguardFmtTests
{
[Fact]
public void ResolveConfig_ShouldParsePeersAndIgnoreInlineComments()
[Test]
public async Task ResolveConfig_ShouldParsePeersAndIgnoreInlineComments()
{
const string config =
"""
@@ -29,19 +25,19 @@ public class WireguardFmtTests
var resolved = WireguardFmt.ResolveConfig(config);
resolved.Should().NotBeNull();
resolved.Should().HaveCount(2);
await resolved.Should().NotBeNull();
await resolved.Should().HaveCount(2);
var first = resolved![0];
first.Address.Should().Be("2001:db8::1");
first.Port.Should().Be(51820);
first.Password.Should().Be("interface-private-key");
first.GetProtocolExtra().WgReserved.Should().Be("1, 2, 3");
first.GetProtocolExtra().WgInterfaceAddress.Should().Be("10.0.0.2/32, fd00::2/128");
first.GetProtocolExtra().WgMtu.Should().Be(1420);
await first.Address.Should().BeEqualTo("2001:db8::1");
await first.Port.Should().BeEqualTo(51820);
await first.Password.Should().BeEqualTo("interface-private-key");
await first.GetProtocolExtra().WgReserved.Should().BeEqualTo("1, 2, 3");
await first.GetProtocolExtra().WgInterfaceAddress.Should().BeEqualTo("10.0.0.2/32, fd00::2/128");
await first.GetProtocolExtra().WgMtu.Should().BeEqualTo(1420);
var second = resolved[1];
second.Address.Should().Be("example.com");
second.Port.Should().Be(12345);
await second.Address.Should().BeEqualTo("example.com");
await second.Port.Should().BeEqualTo(12345);
}
}

View File

@@ -1,44 +1,39 @@
using AwesomeAssertions;
using ServiceLib.Enums;
using ServiceLib.Manager;
using Xunit;
namespace ServiceLib.Tests.Manager;
public class CoreManagerTests
{
[Theory]
[InlineData(ECoreType.sing_box)]
[InlineData(ECoreType.mihomo)]
[InlineData(ECoreType.Xray)]
public void ShouldRunAsSudo_TunLaunchOnNonWindows_RequiresElevation(ECoreType coreType)
[Test]
[Arguments(ECoreType.sing_box)]
[Arguments(ECoreType.mihomo)]
[Arguments(ECoreType.Xray)]
public async Task ShouldRunAsSudo_TunLaunchOnNonWindows_RequiresElevation(ECoreType coreType)
{
CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeTrue();
await CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeTrue();
}
[Fact]
public void ShouldRunAsSudo_NonTunLaunch_ShouldNotElevate()
[Test]
public async Task ShouldRunAsSudo_NonTunLaunch_ShouldNotElevate()
{
// Regression guard for the macOS TUN failure: the elevation decision must follow
// the context snapshot that generated the config. A launch whose snapshot has TUN
// disabled must never elevate, and a launch whose snapshot has TUN enabled must
// elevate regardless of later changes to the live config.
CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.sing_box, isNonWindows: true).Should().BeFalse();
CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.Xray, isNonWindows: true).Should().BeFalse();
await CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.sing_box, isNonWindows: true).Should().BeFalse();
await CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.Xray, isNonWindows: true).Should().BeFalse();
}
[Fact]
public void ShouldRunAsSudo_OnWindows_ShouldNotElevate()
[Test]
public async Task ShouldRunAsSudo_OnWindows_ShouldNotElevate()
{
CoreManager.ShouldRunAsSudo(isTunLaunch: true, ECoreType.sing_box, isNonWindows: false).Should().BeFalse();
await CoreManager.ShouldRunAsSudo(isTunLaunch: true, ECoreType.sing_box, isNonWindows: false).Should().BeFalse();
}
[Theory]
[InlineData(ECoreType.v2fly)]
[InlineData(ECoreType.hysteria)]
[InlineData(null)]
public void ShouldRunAsSudo_UnsupportedCoreType_ShouldNotElevate(ECoreType? coreType)
[Test]
[Arguments(ECoreType.v2fly)]
[Arguments(ECoreType.hysteria)]
[Arguments(null)]
public async Task ShouldRunAsSudo_UnsupportedCoreType_ShouldNotElevate(ECoreType? coreType)
{
CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeFalse();
await CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeFalse();
}
}

View File

@@ -1,18 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AwesomeAssertions" />
<PackageReference Include="xunit.v3" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="TUnit" />
<PackageReference Include="TUnit.Assertions.Should" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ServiceLib\ServiceLib.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ServiceLib\ServiceLib.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="TUnit.Assertions.Should" />
<Using Include="TUnit.Assertions.Should.Extensions" />
</ItemGroup>
</Project>