using System.Net; using System.Net.Sockets; using System.Net.WebSockets; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; using XFEExtension.NetCore.CyberComm; using XFEExtension.NetCore.ServerInteractive.Models; using Xunit; using XFEExtension.NetCore.ServerInteractive.Models.UserModels; using XFEExtension.NetCore.ServerInteractive.Utilities.Extensions; using XFEExtension.NetCore.ServerInteractive.Utilities.Helpers; using XFEExtension.NetCore.ServerInteractive.Utilities.Requester; using XFEExtension.NetCore.ServerInteractive.Utilities.Server; namespace XFEExtension.NetCore.ServerInteractive.Test; public sealed class CyberCommIntegrationTests { [Fact] public async Task HttpServer_ProcessesPostAndStopsCleanly() { var port = GetFreePort(); await using var server = CreateServer(port); server.HttpRequestHandler = async (context, _) => { await context.Response.WriteTextAsync(context.RequestBody, HttpStatusCode.OK, "application/json"); context.Response.Complete(); }; await server.StartAsync(); using var client = new HttpClient(); using var response = await client.PostAsync($"http://127.0.0.1:{port}/echo", new StringContent("{\"ok\":true}", Encoding.UTF8, "application/json")); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("{\"ok\":true}", await response.Content.ReadAsStringAsync()); await server.StopAsync(); Assert.Equal(CyberCommServerState.Stopped, server.State); } [Fact] public async Task HttpServer_RejectsConflictingContentLength() { var port = GetFreePort(); await using var server = CreateServer(port); await server.StartAsync(); using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, port); await using var stream = client.GetStream(); var request = $"POST / HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nContent-Length: 1\r\nContent-Length: 2\r\nConnection: close\r\n\r\nx"; await stream.WriteAsync(Encoding.ASCII.GetBytes(request)); var buffer = new byte[1024]; var count = await stream.ReadAsync(buffer); Assert.StartsWith("HTTP/1.1 400", Encoding.ASCII.GetString(buffer, 0, count)); } [Fact] public async Task HttpServer_ReadsChunkedRequestBody() { var port = GetFreePort(); await using var server = CreateServer(port); server.HttpRequestHandler = async (context, _) => { await context.Response.WriteTextAsync(context.RequestBody); context.Response.Complete(); }; await server.StartAsync(); using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, port); await using var stream = client.GetStream(); var request = $"POST /chunk HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n5\r\nhello\r\n0\r\n\r\n"; await stream.WriteAsync(Encoding.ASCII.GetBytes(request)); using var output = new MemoryStream(); var buffer = new byte[1024]; int count; while ((count = await stream.ReadAsync(buffer)) > 0) await output.WriteAsync(buffer.AsMemory(0, count)); Assert.EndsWith("hello", Encoding.UTF8.GetString(output.ToArray())); } [Fact] public async Task WebSocketClientAndServer_EchoAndDisconnectWithoutReconnect() { var port = GetFreePort(); await using var server = CreateServer(port); server.WebSocketMessageHandler = async (args, _) => { if (args.MessageType == BackMessageType.Text) await args.ReplyMessage(args.TextMessage!); }; await server.StartAsync(); await using var client = new CyberCommClient(new CyberCommClientOptions { ServerUri = new Uri($"ws://127.0.0.1:{port}/ws"), Reconnect = new() { Enabled = false } }); var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); client.MessageHandler = (args, _) => { if (args.MessageType == BackMessageType.Text) received.TrySetResult(args.TextMessage!); return ValueTask.CompletedTask; }; await client.ConnectAsync(); Assert.True((await client.SendTextAsync("echo")).IsSuccess); Assert.Equal("echo", await received.Task.WaitAsync(TimeSpan.FromSeconds(5))); await Task.WhenAll(Enumerable.Range(0, 8).Select(_ => client.DisconnectAsync())); Assert.Equal(CyberCommClientState.Disconnected, client.State); await Task.Delay(200); Assert.False(client.IsConnected); } [Fact] public async Task WebSocketClient_AbnormalDisconnectCreatesANewSocketForReconnect() { var port = GetFreePort(); await using var server = CreateServer(port); server.WebSocketMessageHandler = (args, _) => { if (args.TextMessage == "drop") args.CurrentWebSocket.Abort(); return ValueTask.CompletedTask; }; await server.StartAsync(); await using var client = new CyberCommClient(new CyberCommClientOptions { ServerUri = new Uri($"ws://127.0.0.1:{port}/ws"), Reconnect = new() { Enabled = true, MaxAttempts = 10, InitialDelay = TimeSpan.FromMilliseconds(20), MaxDelay = TimeSpan.FromMilliseconds(100), JitterRatio = 0 } }); var connectionCount = 0; var reconnected = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); client.ConnectedHandler = _ => { if (Interlocked.Increment(ref connectionCount) == 2) reconnected.TrySetResult(); return ValueTask.CompletedTask; }; await client.ConnectAsync(); var firstSocket = client.ClientWebSocket; Assert.True((await client.SendTextAsync("drop")).IsSuccess); await reconnected.Task.WaitAsync(TimeSpan.FromSeconds(5)); Assert.NotSame(firstSocket, client.ClientWebSocket); Assert.Equal(CyberCommClientState.Connected, client.State); } [Fact] public async Task HttpsAndWss_UseTheSameBclTlsTransport() { var port = GetFreePort(); using var certificate = CreateCertificate(); await using var server = new CyberCommServer(new CyberCommServerOptions { ListenUrls = [$"https://127.0.0.1:{port}/"], Tls = new() { Certificate = certificate } }); server.HttpRequestHandler = async (context, _) => { await context.Response.WriteTextAsync("secure"); context.Response.Complete(); }; server.ErrorHandler = Console.WriteLine; server.WebSocketMessageHandler = async (args, _) => await args.ReplyMessage(args.TextMessage!); await server.StartAsync(); using var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = static (_, _, _, _) => true }; using var httpClient = new HttpClient(handler); Assert.Equal("secure", await httpClient.GetStringAsync($"https://127.0.0.1:{port}/health")); await using var webSocketClient = new CyberCommClient(new CyberCommClientOptions { ServerUri = new Uri($"wss://127.0.0.1:{port}/ws"), Reconnect = new() { Enabled = false }, RemoteCertificateValidationCallback = static (_, _, _, _) => true }); var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); webSocketClient.MessageHandler = (args, _) => { if (args.TextMessage is not null) received.TrySetResult(args.TextMessage); return ValueTask.CompletedTask; }; await webSocketClient.ConnectAsync(); Assert.True((await webSocketClient.SendTextAsync("wss")).IsSuccess); Assert.Equal("wss", await received.Task.WaitAsync(TimeSpan.FromSeconds(5))); } [Fact] public async Task TlsOptions_LoadExternalPfxAndPemFiles() { var testDirectory = Path.Combine(Path.GetTempPath(), $"cybercomm-tls-{Guid.NewGuid():N}"); Directory.CreateDirectory(testDirectory); try { using var certificate = CreateCertificate(); const string password = "test-only-password"; var pfxPath = Path.Combine(testDirectory, "server.pfx"); var certificatePath = Path.Combine(testDirectory, "server.crt.pem"); var privateKeyPath = Path.Combine(testDirectory, "server.key.pem"); await File.WriteAllBytesAsync(pfxPath, certificate.Export(X509ContentType.Pkcs12, password)); await File.WriteAllTextAsync(certificatePath, certificate.ExportCertificatePem()); using (var privateKey = certificate.GetRSAPrivateKey()) await File.WriteAllTextAsync(privateKeyPath, privateKey!.ExportPkcs8PrivateKeyPem()); await AssertTlsConfigurationStartsAsync(new CyberCommTlsOptions { PfxPath = pfxPath, PfxPassword = password }); await AssertTlsConfigurationStartsAsync(new CyberCommTlsOptions { CertificatePemPath = certificatePath, PrivateKeyPemPath = privateKeyPath }); } finally { Directory.Delete(testDirectory, true); } } [Fact] public async Task Lifecycle_ConcurrentCallsShareOneTransition() { var port = GetFreePort(); await using var server = CreateServer(port); await Task.WhenAll(Enumerable.Range(0, 8).Select(_ => server.StartAsync())); Assert.Equal(CyberCommServerState.Running, server.State); await Task.WhenAll(Enumerable.Range(0, 8).Select(_ => server.StopAsync())); Assert.Equal(CyberCommServerState.Stopped, server.State); } [Fact] public async Task StopAsync_WaitsForActiveRequestButNotForKeepAliveIdleTimeout() { var port = GetFreePort(); await using var server = new CyberCommServer(new CyberCommServerOptions { ListenUrls = [$"http://127.0.0.1:{port}/"], Limits = new() { HandlerTimeout = TimeSpan.FromSeconds(5), IdleTimeout = TimeSpan.FromSeconds(5), CloseTimeout = TimeSpan.FromSeconds(2) } }); var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); server.HttpRequestHandler = async (context, cancellationToken) => { started.TrySetResult(); await release.Task.WaitAsync(cancellationToken); await context.Response.WriteTextAsync("finished"); context.Response.Complete(); }; await server.StartAsync(); using var client = new HttpClient(); var request = client.GetAsync($"http://127.0.0.1:{port}/work"); await started.Task.WaitAsync(TimeSpan.FromSeconds(2)); var stop = server.StopAsync(); await Task.Delay(50); Assert.False(stop.IsCompleted); release.TrySetResult(); using var response = await request; Assert.Equal("finished", await response.Content.ReadAsStringAsync()); await stop.WaitAsync(TimeSpan.FromSeconds(1)); Assert.Equal(CyberCommServerState.Stopped, server.State); } [Fact] public async Task ServerInteractive_SharesOneCyberCommListenerAcrossBasePaths() { var port = GetFreePort(); var listenUrl = $"http://0.0.0.0:{port}/"; var firstCore = XFEServerCoreBuilder.CreateBuilder() .AddConnectService() .Build(options => { options.ServerCoreName = "first"; options.MainEntryPoint = "first"; options.AcceptGet = true; options.BindIP(listenUrl); }); var secondCore = XFEServerCoreBuilder.CreateBuilder() .AddConnectService() .Build(options => { options.ServerCoreName = "second"; options.MainEntryPoint = "second"; options.AcceptGet = true; options.BindIP(listenUrl); }); await using var server = XFEServerBuilder.CreateBuilder() .UseXFEServer() .AddServerCore(firstCore) .AddServerCore(secondCore) .Build(); await server.StartAsync(); using var client = new HttpClient(); using var firstResponse = await client.GetAsync($"http://127.0.0.1:{port}/first/check_connect"); using var secondResponse = await client.GetAsync($"http://127.0.0.1:{port}/second/check_connect"); using var missingResponse = await client.GetAsync($"http://127.0.0.1:{port}/missing/check_connect"); Assert.Equal(HttpStatusCode.OK, firstResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, secondResponse.StatusCode); Assert.Equal(HttpStatusCode.NotFound, missingResponse.StatusCode); } [Fact] public async Task ServerInteractive_RoutesDirectIpDnsAndEdgeOneRequestsByOriginPort() { var originPort = GetFreePort(); var core = XFEServerCoreBuilder.CreateBuilder() .AddConnectService() .Build(options => { options.ServerCoreName = "edge-one-origin"; options.MainEntryPoint = "api"; options.AcceptGet = true; options.BindIP($"http://*:{originPort}/"); }); await using var server = XFEServerBuilder.CreateBuilder() .AddXFEExceptionProcess() .AddXFECoreProcessor() .AddServerCore(core) .Build(); await server.StartAsync(); var directIpResponse = await SendRawAsync(originPort, "GET /api/check_connect HTTP/1.1\r\n" + $"Host: 127.0.0.1:{originPort}\r\n" + "Connection: close\r\n\r\n"); var directDnsResponse = await SendRawAsync(originPort, "GET /api/check_connect HTTP/1.1\r\n" + $"Host: toolbox.api.xfe.studio:{originPort}\r\n" + "Connection: close\r\n\r\n"); var edgeOneResponse = await SendRawAsync(originPort, "GET /api/check_connect HTTP/1.1\r\n" + "Host: toolbox.api.xfe.studio\r\n" + "X-Forwarded-Proto: http\r\n" + "Connection: close\r\n\r\n"); Assert.StartsWith("HTTP/1.1 200", directIpResponse); Assert.StartsWith("HTTP/1.1 200", directDnsResponse); Assert.StartsWith("HTTP/1.1 200", edgeOneResponse); } [Fact] public async Task TableRequester_AcceptsCreatedAndUsesPageSize() { var port = GetFreePort(); await using var server = CreateServer(port); var getRequestBody = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); server.HttpRequestHandler = async (context, _) => { if (context.RequestUri.AbsolutePath.EndsWith("/add/userFaceInfo", StringComparison.Ordinal)) await context.Response.WriteTextAsync("{\"id\":\"1\",\"version\":1}", HttpStatusCode.Created, "application/json"); else { getRequestBody.TrySetResult(context.RequestBody); await context.Response.WriteTextAsync("{\"totalCount\":0,\"lastPage\":0,\"dataList\":[]}", HttpStatusCode.OK, "application/json"); } context.Response.Complete(); }; await server.StartAsync(); var requester = new TableRequester { RequestAddress = $"http://127.0.0.1:{port}/api/" }; Assert.True(await requester.Add(new UserFaceInfo { Id = "1", Version = 1 })); _ = await requester.Get(2, 1); var body = await getRequestBody.Task.WaitAsync(TimeSpan.FromSeconds(2)); Assert.Contains("\"pageSize\":2", body, StringComparison.Ordinal); Assert.DoesNotContain("pageCount", body, StringComparison.Ordinal); } [Fact] public async Task Client_InitialFailureWithoutReconnectBecomesFaulted() { var port = GetFreePort(); await using var client = new CyberCommClient(new CyberCommClientOptions { ServerUri = new Uri($"ws://127.0.0.1:{port}/"), Reconnect = new() { Enabled = false } }); await Assert.ThrowsAsync(() => client.ConnectAsync()); Assert.Equal(CyberCommClientState.Faulted, client.State); } [Fact] public async Task HttpServer_RejectsUnsupportedMethodWith405() { var port = GetFreePort(); await using var server = CreateServer(port); await server.StartAsync(); var response = await SendRawAsync(port, $"TRACE / HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nConnection: close\r\n\r\n"); Assert.StartsWith("HTTP/1.1 405", response); Assert.Contains("Allow:", response); } [Fact] public async Task HttpServer_HandlerWithoutResponseGetsDeterministic404() { var port = GetFreePort(); await using var server = CreateServer(port); server.HttpRequestHandler = static (_, _) => ValueTask.CompletedTask; await server.StartAsync(); using var client = new HttpClient(); using var response = await client.GetAsync($"http://127.0.0.1:{port}/missing"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task HttpServer_RejectsOversizedBodyBeforeReadingIt() { var port = GetFreePort(); await using var server = new CyberCommServer(new CyberCommServerOptions { ListenUrls = [$"http://127.0.0.1:{port}/"], Limits = new() { MaxHttpBodyBytes = 10, MaxHttpChunkBytes = 10 } }); await server.StartAsync(); var response = await SendRawAsync(port, $"POST / HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nContent-Length: 11\r\nConnection: close\r\n\r\n"); Assert.StartsWith("HTTP/1.1 413", response); } [Fact] public async Task Server_InvalidOptionsBecomeFaultedInsteadOfStayingStarting() { await using var server = new CyberCommServer(new CyberCommServerOptions()); await Assert.ThrowsAsync(() => server.StartAsync()); Assert.Equal(CyberCommServerState.Faulted, server.State); } [Fact] public async Task WebSocket_UnassembledModeDoesNotDropFirstFragment() { var port = GetFreePort(); await using var server = new CyberCommServer(new CyberCommServerOptions { ListenUrls = [$"http://127.0.0.1:{port}/"], AssembleWebSocketMessages = false }); var fragments = new List(); var completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); server.WebSocketMessageHandler = (args, _) => { fragments.Add(args.TextMessage ?? string.Empty); if (args.EndOfMessage) completed.TrySetResult(); return ValueTask.CompletedTask; }; await server.StartAsync(); using var client = new ClientWebSocket(); await client.ConnectAsync(new Uri($"ws://127.0.0.1:{port}/ws"), CancellationToken.None); await client.SendAsync("first-"u8.ToArray(), WebSocketMessageType.Text, false, CancellationToken.None); await client.SendAsync("second"u8.ToArray(), WebSocketMessageType.Text, true, CancellationToken.None); await completed.Task.WaitAsync(TimeSpan.FromSeconds(5)); Assert.Equal(new[] { "first-", "second" }, fragments); } [Theory] [InlineData("v1/*", "v1/test", true)] [InlineData("v1/*", "v1/test/more", false)] [InlineData("v1/*", "v1", false)] [InlineData("v1/**", "v1", true)] [InlineData("v1/**", "v1/test/more", true)] public void WildcardRoute_HasBoundedSemantics(string pattern, string route, bool expected) => Assert.Equal(expected, RouteMatchHelper.MatchWildcardRoute(pattern, route)); [Fact] public void PasswordCredential_VerifiesMigratesAndPublicDtoOmitsSecrets() { var credential = PasswordHasher.Hash("correct horse battery staple", 100_000); Assert.True(PasswordHasher.Verify("correct horse battery staple", credential)); Assert.False(PasswordHasher.Verify("wrong", credential)); Assert.False(PasswordHasher.Verify("anything", new PasswordCredential { Iterations = 600_000, Salt = Convert.ToBase64String(new byte[16]), Hash = string.Empty })); var user = new User { UserName = "demo", Password = "legacy" }; Assert.Equal(UserOperateResult.Success, UserHelper.GetUser("demo", "legacy", [user], out _, out var upgraded)); Assert.True(upgraded); Assert.Empty(user.Password); Assert.NotNull(user.PasswordCredential); var json = JsonSerializer.Serialize(UserPublicDto.From(user)); Assert.DoesNotContain("legacy", json, StringComparison.Ordinal); Assert.DoesNotContain("PasswordCredential", json, StringComparison.Ordinal); } private static CyberCommServer CreateServer(int port) => new(new CyberCommServerOptions { ListenUrls = [$"http://127.0.0.1:{port}/"], Limits = new() { HandlerTimeout = TimeSpan.FromSeconds(5), ReceiveTimeout = TimeSpan.FromSeconds(5), IdleTimeout = TimeSpan.FromSeconds(5) } }); private static int GetFreePort() { var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; listener.Stop(); return port; } private static async Task SendRawAsync(int port, string request) { using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, port); await using var stream = client.GetStream(); await stream.WriteAsync(Encoding.ASCII.GetBytes(request)); using var output = new MemoryStream(); var buffer = new byte[1024]; int count; while ((count = await stream.ReadAsync(buffer)) > 0) await output.WriteAsync(buffer.AsMemory(0, count)); return Encoding.ASCII.GetString(output.ToArray()); } private static async Task AssertTlsConfigurationStartsAsync(CyberCommTlsOptions tlsOptions) { var port = GetFreePort(); await using var server = new CyberCommServer(new CyberCommServerOptions { ListenUrls = [$"https://127.0.0.1:{port}/"], Tls = tlsOptions }); server.HttpRequestHandler = async (context, _) => { await context.Response.WriteTextAsync("secure"); context.Response.Complete(); }; await server.StartAsync(); using var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = static (_, _, _, _) => true }; using var client = new HttpClient(handler); Assert.Equal("secure", await client.GetStringAsync($"https://127.0.0.1:{port}/")); } private static X509Certificate2 CreateCertificate() { using var rsa = RSA.Create(2048); var request = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false)); request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, false)); request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new("1.3.6.1.5.5.7.3.1") }, false)); var names = new SubjectAlternativeNameBuilder(); names.AddDnsName("localhost"); names.AddIpAddress(IPAddress.Loopback); request.CertificateExtensions.Add(names.Build()); using var ephemeral = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddDays(1)); return X509CertificateLoader.LoadPkcs12(ephemeral.Export(X509ContentType.Pkcs12), null, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet); } }