Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" >
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Router.Transport.Tls\StellaOps.Router.Transport.Tls.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Router.Common\StellaOps.Router.Common.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Using Include="FluentAssertions" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,515 @@
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Common.Frames;
|
||||
using StellaOps.Router.Common.Models;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Transport.Tls.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Transport compliance tests for TLS transport.
|
||||
/// Tests: roundtrip over TLS, certificate validation, protocol handling.
|
||||
/// </summary>
|
||||
public sealed class TlsTransportComplianceTests
|
||||
{
|
||||
#region TLS Options Compliance Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TlsOptions_DefaultProtocols_SecureDefaults()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new TlsTransportOptions();
|
||||
|
||||
// Assert - Should default to TLS 1.2 and 1.3 (no legacy protocols)
|
||||
options.EnabledProtocols.Should().Be(SslProtocols.Tls12 | SslProtocols.Tls13);
|
||||
|
||||
// Should NOT include legacy protocols
|
||||
#pragma warning disable SYSLIB0039, CS0618 // Intentionally testing obsolete protocols are disabled
|
||||
options.EnabledProtocols.HasFlag(SslProtocols.Tls).Should().BeFalse();
|
||||
options.EnabledProtocols.HasFlag(SslProtocols.Tls11).Should().BeFalse();
|
||||
options.EnabledProtocols.HasFlag(SslProtocols.Ssl2).Should().BeFalse();
|
||||
options.EnabledProtocols.HasFlag(SslProtocols.Ssl3).Should().BeFalse();
|
||||
#pragma warning restore SYSLIB0039, CS0618
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TlsOptions_RequireClientCertificate_DefaultFalse()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new TlsTransportOptions();
|
||||
|
||||
// Assert
|
||||
options.RequireClientCertificate.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TlsOptions_AllowSelfSigned_DefaultFalse()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new TlsTransportOptions();
|
||||
|
||||
// Assert
|
||||
options.AllowSelfSigned.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TlsOptions_CheckCertificateRevocation_DefaultFalse()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new TlsTransportOptions();
|
||||
|
||||
// Assert
|
||||
options.CheckCertificateRevocation.Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Certificate Loading Compliance Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CertificateLoading_DirectCertificate_Preferred()
|
||||
{
|
||||
// Arrange
|
||||
var cert = CreateTestCertificate("direct-cert");
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
ServerCertificate = cert,
|
||||
ServerCertificatePath = "/should/be/ignored"
|
||||
};
|
||||
|
||||
// Act
|
||||
var loaded = CertificateLoader.LoadServerCertificate(options);
|
||||
|
||||
// Assert - Direct certificate should be used
|
||||
loaded.Should().BeSameAs(cert);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CertificateLoading_NoCertificate_ThrowsForServer()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TlsTransportOptions();
|
||||
|
||||
// Act & Assert
|
||||
var action = () => CertificateLoader.LoadServerCertificate(options);
|
||||
action.Should().Throw<InvalidOperationException>();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CertificateLoading_NoCertificate_ReturnsNullForClient()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TlsTransportOptions();
|
||||
|
||||
// Act
|
||||
var result = CertificateLoader.LoadClientCertificate(options);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CertificateLoading_ClientCertificate_LoadsSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var cert = CreateTestCertificate("client-cert");
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
ClientCertificate = cert
|
||||
};
|
||||
|
||||
// Act
|
||||
var loaded = CertificateLoader.LoadClientCertificate(options);
|
||||
|
||||
// Assert
|
||||
loaded.Should().BeSameAs(cert);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Protocol Negotiation Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(SslProtocols.Tls12)]
|
||||
[InlineData(SslProtocols.Tls13)]
|
||||
[InlineData(SslProtocols.Tls12 | SslProtocols.Tls13)]
|
||||
public void ProtocolNegotiation_SupportedProtocols_Configurable(SslProtocols protocols)
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
EnabledProtocols = protocols
|
||||
};
|
||||
|
||||
// Assert
|
||||
options.EnabledProtocols.Should().Be(protocols);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ProtocolNegotiation_Tls12Only_Configurable()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
EnabledProtocols = SslProtocols.Tls12
|
||||
};
|
||||
|
||||
// Assert
|
||||
options.EnabledProtocols.Should().Be(SslProtocols.Tls12);
|
||||
options.EnabledProtocols.HasFlag(SslProtocols.Tls13).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ProtocolNegotiation_Tls13Only_Configurable()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
EnabledProtocols = SslProtocols.Tls13
|
||||
};
|
||||
|
||||
// Assert
|
||||
options.EnabledProtocols.Should().Be(SslProtocols.Tls13);
|
||||
options.EnabledProtocols.HasFlag(SslProtocols.Tls12).Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Frame Roundtrip Tests (via TcpFrameProtocol shared)
|
||||
|
||||
// TLS uses the same frame protocol as TCP after the TLS handshake
|
||||
// These tests verify frames are correctly serialized before TLS encryption
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FrameRoundtrip_RequestFrame_PreTlsEncryption()
|
||||
{
|
||||
// Arrange - Test frame serialization (TLS encrypts the result)
|
||||
using var stream = new MemoryStream();
|
||||
|
||||
var request = new RequestFrame
|
||||
{
|
||||
RequestId = "tls-req-12345",
|
||||
CorrelationId = "tls-corr-67890",
|
||||
Method = "POST",
|
||||
Path = "/api/secure",
|
||||
Headers = new Dictionary<string, string>
|
||||
{
|
||||
["Content-Type"] = "application/json",
|
||||
["Authorization"] = "Bearer secure-token"
|
||||
},
|
||||
Payload = Encoding.UTF8.GetBytes(@"{""sensitive"":""data""}"),
|
||||
TimeoutSeconds = 30,
|
||||
SupportsStreaming = false
|
||||
};
|
||||
|
||||
var frame = FrameConverter.ToFrame(request);
|
||||
|
||||
// Act - Simulate frame protocol write (what gets encrypted by TLS)
|
||||
await TcpFrameProtocolWrapper.WriteFrameAsync(stream, frame, CancellationToken.None);
|
||||
stream.Position = 0;
|
||||
var readFrame = await TcpFrameProtocolWrapper.ReadFrameAsync(stream, 16 * 1024 * 1024, CancellationToken.None);
|
||||
var restored = FrameConverter.ToRequestFrame(readFrame!);
|
||||
|
||||
// Assert
|
||||
restored.Should().NotBeNull();
|
||||
restored!.RequestId.Should().Be(request.RequestId);
|
||||
restored.Method.Should().Be(request.Method);
|
||||
restored.Path.Should().Be(request.Path);
|
||||
restored.Headers.Should().BeEquivalentTo(request.Headers);
|
||||
Encoding.UTF8.GetString(restored.Payload.Span).Should().Be(@"{""sensitive"":""data""}");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FrameRoundtrip_BinaryPayload_NotCorrupted()
|
||||
{
|
||||
// Arrange - Binary data should survive serialization before TLS encryption
|
||||
using var stream = new MemoryStream();
|
||||
|
||||
var binaryPayload = Enumerable.Range(0, 256).Select(i => (byte)i).ToArray();
|
||||
var frame = new Frame
|
||||
{
|
||||
Type = FrameType.Request,
|
||||
CorrelationId = "binary-tls",
|
||||
Payload = binaryPayload
|
||||
};
|
||||
|
||||
// Act
|
||||
await TcpFrameProtocolWrapper.WriteFrameAsync(stream, frame, CancellationToken.None);
|
||||
stream.Position = 0;
|
||||
var readFrame = await TcpFrameProtocolWrapper.ReadFrameAsync(stream, 16 * 1024 * 1024, CancellationToken.None);
|
||||
|
||||
// Assert - All bytes preserved
|
||||
readFrame!.Payload.ToArray().Should().BeEquivalentTo(binaryPayload);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Hostname Verification Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HostnameVerification_ExpectedHostname_Configurable()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
ExpectedServerHostname = "api.stellaops.io"
|
||||
};
|
||||
|
||||
// Assert
|
||||
options.ExpectedServerHostname.Should().Be("api.stellaops.io");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HostnameVerification_NotSet_UsesHost()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
Host = "gateway.local",
|
||||
ExpectedServerHostname = null
|
||||
};
|
||||
|
||||
// Assert
|
||||
options.ExpectedServerHostname.Should().BeNull();
|
||||
options.Host.Should().Be("gateway.local");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Certificate Path Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CertificatePath_Server_Configurable()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
ServerCertificatePath = "/etc/stellaops/certs/server.pfx",
|
||||
ServerCertificatePassword = "secure-password"
|
||||
};
|
||||
|
||||
// Assert
|
||||
options.ServerCertificatePath.Should().Be("/etc/stellaops/certs/server.pfx");
|
||||
options.ServerCertificatePassword.Should().Be("secure-password");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CertificatePath_Client_Configurable()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
ClientCertificatePath = "/etc/stellaops/certs/client.pfx",
|
||||
ClientCertificatePassword = "client-password"
|
||||
};
|
||||
|
||||
// Assert
|
||||
options.ClientCertificatePath.Should().Be("/etc/stellaops/certs/client.pfx");
|
||||
options.ClientCertificatePassword.Should().Be("client-password");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Timeout and Buffer Configuration Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Timeouts_DefaultValues_Reasonable()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new TlsTransportOptions();
|
||||
|
||||
// Assert - Sensible defaults for secure connections
|
||||
options.ConnectTimeout.Should().Be(TimeSpan.FromSeconds(10));
|
||||
options.KeepAliveInterval.Should().Be(TimeSpan.FromSeconds(30));
|
||||
options.MaxReconnectAttempts.Should().Be(10);
|
||||
options.MaxReconnectBackoff.Should().Be(TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Buffers_DefaultValues_Reasonable()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new TlsTransportOptions();
|
||||
|
||||
// Assert
|
||||
options.ReceiveBufferSize.Should().Be(64 * 1024); // 64KB
|
||||
options.SendBufferSize.Should().Be(64 * 1024); // 64KB
|
||||
options.MaxFrameSize.Should().Be(16 * 1024 * 1024); // 16MB
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(8 * 1024)]
|
||||
[InlineData(64 * 1024)]
|
||||
[InlineData(256 * 1024)]
|
||||
public void Buffers_Customizable(int bufferSize)
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
ReceiveBufferSize = bufferSize,
|
||||
SendBufferSize = bufferSize
|
||||
};
|
||||
|
||||
// Assert
|
||||
options.ReceiveBufferSize.Should().Be(bufferSize);
|
||||
options.SendBufferSize.Should().Be(bufferSize);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region mTLS Configuration Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void MutualTls_ClientCertRequired_Configurable()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
RequireClientCertificate = true
|
||||
};
|
||||
|
||||
// Assert
|
||||
options.RequireClientCertificate.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void MutualTls_FullConfiguration_AllOptionsCombine()
|
||||
{
|
||||
// Arrange
|
||||
var serverCert = CreateTestCertificate("server");
|
||||
var clientCert = CreateTestCertificate("client");
|
||||
|
||||
// Act
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
ServerCertificate = serverCert,
|
||||
ClientCertificate = clientCert,
|
||||
RequireClientCertificate = true,
|
||||
CheckCertificateRevocation = true,
|
||||
AllowSelfSigned = false,
|
||||
EnabledProtocols = SslProtocols.Tls13,
|
||||
ExpectedServerHostname = "mtls.stellaops.io"
|
||||
};
|
||||
|
||||
// Assert
|
||||
options.ServerCertificate.Should().BeSameAs(serverCert);
|
||||
options.ClientCertificate.Should().BeSameAs(clientCert);
|
||||
options.RequireClientCertificate.Should().BeTrue();
|
||||
options.CheckCertificateRevocation.Should().BeTrue();
|
||||
options.AllowSelfSigned.Should().BeFalse();
|
||||
options.EnabledProtocols.Should().Be(SslProtocols.Tls13);
|
||||
options.ExpectedServerHostname.Should().Be("mtls.stellaops.io");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Determinism Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Determinism_SameInput_SameOutput()
|
||||
{
|
||||
// Arrange - Frame serialization should be deterministic
|
||||
for (int run = 0; run < 10; run++)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
|
||||
var request = new RequestFrame
|
||||
{
|
||||
RequestId = "tls-deterministic",
|
||||
Method = "GET",
|
||||
Path = "/api/secure",
|
||||
Headers = new Dictionary<string, string> { ["X-Run"] = $"{run}" },
|
||||
Payload = Encoding.UTF8.GetBytes("test-payload")
|
||||
};
|
||||
|
||||
var frame = FrameConverter.ToFrame(request);
|
||||
|
||||
// Act
|
||||
await TcpFrameProtocolWrapper.WriteFrameAsync(stream, frame, CancellationToken.None);
|
||||
stream.Position = 0;
|
||||
var readFrame = await TcpFrameProtocolWrapper.ReadFrameAsync(stream, 16 * 1024 * 1024, CancellationToken.None);
|
||||
var restored = FrameConverter.ToRequestFrame(readFrame!);
|
||||
|
||||
// Assert
|
||||
restored!.RequestId.Should().Be("tls-deterministic");
|
||||
restored.Path.Should().Be("/api/secure");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Helpers
|
||||
|
||||
private static X509Certificate2 CreateTestCertificate(string subject)
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
var request = new CertificateRequest(
|
||||
$"CN={subject}",
|
||||
rsa,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
|
||||
request.CertificateExtensions.Add(
|
||||
new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true));
|
||||
|
||||
var certificate = request.CreateSelfSigned(
|
||||
DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
DateTimeOffset.UtcNow.AddYears(1));
|
||||
|
||||
var pfxBytes = certificate.Export(X509ContentType.Pfx);
|
||||
return X509CertificateLoader.LoadPkcs12(
|
||||
pfxBytes,
|
||||
null,
|
||||
X509KeyStorageFlags.MachineKeySet);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper to access TCP frame protocol (shared between TCP and TLS after handshake).
|
||||
/// </summary>
|
||||
private static class TcpFrameProtocolWrapper
|
||||
{
|
||||
public static Task WriteFrameAsync(Stream stream, Frame frame, CancellationToken ct)
|
||||
{
|
||||
// TLS transport uses the same frame protocol as TCP
|
||||
return Router.Transport.Tcp.FrameProtocol.WriteFrameAsync(stream, frame, ct);
|
||||
}
|
||||
|
||||
public static Task<Frame?> ReadFrameAsync(Stream stream, int maxFrameSize, CancellationToken ct)
|
||||
{
|
||||
return Router.Transport.Tcp.FrameProtocol.ReadFrameAsync(stream, maxFrameSize, ct);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,824 @@
|
||||
using System.Net;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Router.Common.Models;
|
||||
using StellaOps.Router.Transport.Tls;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Transport.Tls.Tests;
|
||||
|
||||
#region TlsTransportOptions Tests
|
||||
|
||||
public class TlsTransportOptionsTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DefaultOptions_HaveCorrectValues()
|
||||
{
|
||||
// Act
|
||||
var options = new TlsTransportOptions();
|
||||
|
||||
// Assert
|
||||
options.Port.Should().Be(5101);
|
||||
options.ReceiveBufferSize.Should().Be(64 * 1024);
|
||||
options.SendBufferSize.Should().Be(64 * 1024);
|
||||
options.KeepAliveInterval.Should().Be(TimeSpan.FromSeconds(30));
|
||||
options.ConnectTimeout.Should().Be(TimeSpan.FromSeconds(10));
|
||||
options.MaxReconnectAttempts.Should().Be(10);
|
||||
options.MaxReconnectBackoff.Should().Be(TimeSpan.FromMinutes(1));
|
||||
options.MaxFrameSize.Should().Be(16 * 1024 * 1024);
|
||||
options.RequireClientCertificate.Should().BeFalse();
|
||||
options.AllowSelfSigned.Should().BeFalse();
|
||||
options.CheckCertificateRevocation.Should().BeFalse();
|
||||
options.EnabledProtocols.Should().Be(SslProtocols.Tls12 | SslProtocols.Tls13);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Host_CanBeSet()
|
||||
{
|
||||
// Act
|
||||
var options = new TlsTransportOptions { Host = "tls.gateway.local" };
|
||||
|
||||
// Assert
|
||||
options.Host.Should().Be("tls.gateway.local");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Port_CanBeSet()
|
||||
{
|
||||
// Act
|
||||
var options = new TlsTransportOptions { Port = 443 };
|
||||
|
||||
// Assert
|
||||
options.Port.Should().Be(443);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void RequireClientCertificate_CanBeSet(bool required)
|
||||
{
|
||||
// Act
|
||||
var options = new TlsTransportOptions { RequireClientCertificate = required };
|
||||
|
||||
// Assert
|
||||
options.RequireClientCertificate.Should().Be(required);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void AllowSelfSigned_CanBeSet(bool allowed)
|
||||
{
|
||||
// Act
|
||||
var options = new TlsTransportOptions { AllowSelfSigned = allowed };
|
||||
|
||||
// Assert
|
||||
options.AllowSelfSigned.Should().Be(allowed);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void CheckCertificateRevocation_CanBeSet(bool check)
|
||||
{
|
||||
// Act
|
||||
var options = new TlsTransportOptions { CheckCertificateRevocation = check };
|
||||
|
||||
// Assert
|
||||
options.CheckCertificateRevocation.Should().Be(check);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(SslProtocols.Tls12)]
|
||||
[InlineData(SslProtocols.Tls13)]
|
||||
[InlineData(SslProtocols.Tls12 | SslProtocols.Tls13)]
|
||||
public void EnabledProtocols_CanBeSet(SslProtocols protocols)
|
||||
{
|
||||
// Act
|
||||
var options = new TlsTransportOptions { EnabledProtocols = protocols };
|
||||
|
||||
// Assert
|
||||
options.EnabledProtocols.Should().Be(protocols);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExpectedServerHostname_CanBeSet()
|
||||
{
|
||||
// Act
|
||||
var options = new TlsTransportOptions { ExpectedServerHostname = "expected.host.name" };
|
||||
|
||||
// Assert
|
||||
options.ExpectedServerHostname.Should().Be("expected.host.name");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ServerCertificatePath_CanBeSet()
|
||||
{
|
||||
// Act
|
||||
var options = new TlsTransportOptions { ServerCertificatePath = "/etc/certs/server.pfx" };
|
||||
|
||||
// Assert
|
||||
options.ServerCertificatePath.Should().Be("/etc/certs/server.pfx");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ClientCertificatePath_CanBeSet()
|
||||
{
|
||||
// Act
|
||||
var options = new TlsTransportOptions { ClientCertificatePath = "/etc/certs/client.pfx" };
|
||||
|
||||
// Assert
|
||||
options.ClientCertificatePath.Should().Be("/etc/certs/client.pfx");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CertificateLoader Tests
|
||||
|
||||
public class CertificateLoaderTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LoadServerCertificate_WithDirectCertificate_ReturnsCertificate()
|
||||
{
|
||||
// Arrange
|
||||
var cert = CreateSelfSignedCertificate("TestServer");
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
ServerCertificate = cert
|
||||
};
|
||||
|
||||
// Act
|
||||
var loaded = CertificateLoader.LoadServerCertificate(options);
|
||||
|
||||
// Assert
|
||||
loaded.Should().BeSameAs(cert);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LoadServerCertificate_WithNoCertificate_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TlsTransportOptions();
|
||||
|
||||
// Act & Assert
|
||||
var action = () => CertificateLoader.LoadServerCertificate(options);
|
||||
action.Should().Throw<InvalidOperationException>();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LoadClientCertificate_WithNoCertificate_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TlsTransportOptions();
|
||||
|
||||
// Act
|
||||
var result = CertificateLoader.LoadClientCertificate(options);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LoadClientCertificate_WithDirectCertificate_ReturnsCertificate()
|
||||
{
|
||||
// Arrange
|
||||
var cert = CreateSelfSignedCertificate("TestClient");
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
ClientCertificate = cert
|
||||
};
|
||||
|
||||
// Act
|
||||
var loaded = CertificateLoader.LoadClientCertificate(options);
|
||||
|
||||
// Assert
|
||||
loaded.Should().BeSameAs(cert);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LoadServerCertificate_WithInvalidPath_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
ServerCertificatePath = "/nonexistent/path/cert.pfx"
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var action = () => CertificateLoader.LoadServerCertificate(options);
|
||||
action.Should().Throw<Exception>();
|
||||
}
|
||||
|
||||
private static X509Certificate2 CreateSelfSignedCertificate(string subject)
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
var request = new CertificateRequest(
|
||||
$"CN={subject}",
|
||||
rsa,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
|
||||
request.CertificateExtensions.Add(
|
||||
new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true));
|
||||
|
||||
var certificate = request.CreateSelfSigned(
|
||||
DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
DateTimeOffset.UtcNow.AddYears(1));
|
||||
|
||||
// Export and re-import to get the private key
|
||||
var pfxBytes = certificate.Export(X509ContentType.Pfx);
|
||||
return X509CertificateLoader.LoadPkcs12(
|
||||
pfxBytes,
|
||||
null,
|
||||
X509KeyStorageFlags.MachineKeySet);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TlsTransportServer Tests
|
||||
|
||||
public class TlsTransportServerTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StartAsync_WithValidCertificate_StartsListening()
|
||||
{
|
||||
// Arrange
|
||||
var cert = CreateSelfSignedCertificate("TestServer");
|
||||
var options = Options.Create(new TlsTransportOptions
|
||||
{
|
||||
Port = 0,
|
||||
ServerCertificate = cert
|
||||
});
|
||||
|
||||
await using var server = new TlsTransportServer(options, NullLogger<TlsTransportServer>.Instance);
|
||||
|
||||
// Act
|
||||
await server.StartAsync(CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
server.ConnectionCount.Should().Be(0);
|
||||
|
||||
await server.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StartAsync_WithNoCertificate_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var options = Options.Create(new TlsTransportOptions { Port = 0 });
|
||||
await using var server = new TlsTransportServer(options, NullLogger<TlsTransportServer>.Instance);
|
||||
|
||||
// Act & Assert
|
||||
var action = () => server.StartAsync(CancellationToken.None);
|
||||
await action.Should().ThrowAsync<InvalidOperationException>();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StopAsync_CanBeCalledWithoutStart()
|
||||
{
|
||||
// Arrange
|
||||
var cert = CreateSelfSignedCertificate("TestServer");
|
||||
var options = Options.Create(new TlsTransportOptions
|
||||
{
|
||||
Port = 0,
|
||||
ServerCertificate = cert
|
||||
});
|
||||
await using var server = new TlsTransportServer(options, NullLogger<TlsTransportServer>.Instance);
|
||||
|
||||
// Act
|
||||
var action = () => server.StopAsync(CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await action.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ConnectionCount_InitiallyZero()
|
||||
{
|
||||
// Arrange
|
||||
var cert = CreateSelfSignedCertificate("TestServer");
|
||||
var options = Options.Create(new TlsTransportOptions
|
||||
{
|
||||
Port = 0,
|
||||
ServerCertificate = cert
|
||||
});
|
||||
await using var server = new TlsTransportServer(options, NullLogger<TlsTransportServer>.Instance);
|
||||
|
||||
// Assert
|
||||
server.ConnectionCount.Should().Be(0);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DisposeAsync_CanBeCalledMultipleTimes()
|
||||
{
|
||||
// Arrange
|
||||
var cert = CreateSelfSignedCertificate("TestServer");
|
||||
var options = Options.Create(new TlsTransportOptions
|
||||
{
|
||||
Port = 0,
|
||||
ServerCertificate = cert
|
||||
});
|
||||
var server = new TlsTransportServer(options, NullLogger<TlsTransportServer>.Instance);
|
||||
|
||||
// Act
|
||||
var action = async () =>
|
||||
{
|
||||
await server.DisposeAsync();
|
||||
await server.DisposeAsync();
|
||||
await server.DisposeAsync();
|
||||
};
|
||||
|
||||
// Assert
|
||||
await action.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
private static X509Certificate2 CreateSelfSignedCertificate(string subject)
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
var request = new CertificateRequest(
|
||||
$"CN={subject}",
|
||||
rsa,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
|
||||
request.CertificateExtensions.Add(
|
||||
new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, critical: true));
|
||||
|
||||
request.CertificateExtensions.Add(
|
||||
new X509EnhancedKeyUsageExtension(
|
||||
new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") },
|
||||
critical: true));
|
||||
|
||||
var certificate = request.CreateSelfSigned(
|
||||
DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
DateTimeOffset.UtcNow.AddYears(1));
|
||||
|
||||
var pfxBytes = certificate.Export(X509ContentType.Pfx);
|
||||
return X509CertificateLoader.LoadPkcs12(
|
||||
pfxBytes,
|
||||
null,
|
||||
X509KeyStorageFlags.MachineKeySet);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TlsTransportClient Tests
|
||||
|
||||
public class TlsTransportClientTests
|
||||
{
|
||||
private TlsTransportClient CreateClient(TlsTransportOptions? options = null)
|
||||
{
|
||||
var opts = options ?? new TlsTransportOptions { Host = "localhost", Port = 5101 };
|
||||
return new TlsTransportClient(
|
||||
Options.Create(opts),
|
||||
NullLogger<TlsTransportClient>.Instance);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Constructor_InitializesCorrectly()
|
||||
{
|
||||
// Act
|
||||
await using var client = CreateClient();
|
||||
|
||||
// Assert - No exception means it initialized correctly
|
||||
client.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ConnectAsync_WithoutHost_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TlsTransportOptions { Host = null, Port = 5101 };
|
||||
await using var client = CreateClient(options);
|
||||
var instance = new InstanceDescriptor
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
InstanceId = "test-1",
|
||||
Region = "local"
|
||||
};
|
||||
|
||||
// Act
|
||||
var action = () => client.ConnectAsync(instance, [], CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await action.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*Host is not configured*");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ConnectAsync_WithEmptyHost_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TlsTransportOptions { Host = "", Port = 5101 };
|
||||
await using var client = CreateClient(options);
|
||||
var instance = new InstanceDescriptor
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
InstanceId = "test-1",
|
||||
Region = "local"
|
||||
};
|
||||
|
||||
// Act
|
||||
var action = () => client.ConnectAsync(instance, [], CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await action.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*Host is not configured*");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DisposeAsync_CanBeCalledMultipleTimes()
|
||||
{
|
||||
// Arrange
|
||||
var client = CreateClient();
|
||||
|
||||
// Act
|
||||
var action = async () =>
|
||||
{
|
||||
await client.DisposeAsync();
|
||||
await client.DisposeAsync();
|
||||
await client.DisposeAsync();
|
||||
};
|
||||
|
||||
// Assert
|
||||
await action.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DisconnectAsync_WithoutConnect_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
await using var client = CreateClient();
|
||||
|
||||
// Act
|
||||
var action = () => client.DisconnectAsync();
|
||||
|
||||
// Assert
|
||||
await action.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CancelAllInflight_WithNoInflight_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
await using var client = CreateClient();
|
||||
|
||||
// Act
|
||||
var action = () => client.CancelAllInflight("test shutdown");
|
||||
|
||||
// Assert
|
||||
action.Should().NotThrow();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CertificateWatcher Tests
|
||||
|
||||
public class CertificateWatcherTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_LoadsServerCertificate()
|
||||
{
|
||||
// Arrange
|
||||
var cert = CreateSelfSignedCertificate("TestServer");
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
ServerCertificate = cert
|
||||
};
|
||||
|
||||
// Act
|
||||
using var watcher = new CertificateWatcher(options, NullLogger<CertificateWatcher>.Instance);
|
||||
|
||||
// Assert
|
||||
watcher.ServerCertificate.Should().BeSameAs(cert);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_LoadsClientCertificate()
|
||||
{
|
||||
// Arrange
|
||||
var cert = CreateSelfSignedCertificate("TestClient");
|
||||
var options = new TlsTransportOptions
|
||||
{
|
||||
ClientCertificate = cert
|
||||
};
|
||||
|
||||
// Act
|
||||
using var watcher = new CertificateWatcher(options, NullLogger<CertificateWatcher>.Instance);
|
||||
|
||||
// Assert
|
||||
watcher.ClientCertificate.Should().BeSameAs(cert);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Dispose_CanBeCalledMultipleTimes()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TlsTransportOptions();
|
||||
var watcher = new CertificateWatcher(options, NullLogger<CertificateWatcher>.Instance);
|
||||
|
||||
// Act
|
||||
var action = () =>
|
||||
{
|
||||
watcher.Dispose();
|
||||
watcher.Dispose();
|
||||
watcher.Dispose();
|
||||
};
|
||||
|
||||
// Assert
|
||||
action.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void OnServerCertificateReloaded_CanBeSubscribed()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TlsTransportOptions();
|
||||
using var watcher = new CertificateWatcher(options, NullLogger<CertificateWatcher>.Instance);
|
||||
var eventRaised = false;
|
||||
|
||||
// Act
|
||||
watcher.OnServerCertificateReloaded += _ => eventRaised = true;
|
||||
|
||||
// Assert - no exception during subscription
|
||||
eventRaised.Should().BeFalse();
|
||||
watcher.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void OnClientCertificateReloaded_CanBeSubscribed()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TlsTransportOptions();
|
||||
using var watcher = new CertificateWatcher(options, NullLogger<CertificateWatcher>.Instance);
|
||||
var eventRaised = false;
|
||||
|
||||
// Act
|
||||
watcher.OnClientCertificateReloaded += _ => eventRaised = true;
|
||||
|
||||
// Assert - no exception during subscription
|
||||
eventRaised.Should().BeFalse();
|
||||
watcher.Should().NotBeNull();
|
||||
}
|
||||
|
||||
private static X509Certificate2 CreateSelfSignedCertificate(string subject)
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
var request = new CertificateRequest(
|
||||
$"CN={subject}",
|
||||
rsa,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
|
||||
request.CertificateExtensions.Add(
|
||||
new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true));
|
||||
|
||||
var certificate = request.CreateSelfSigned(
|
||||
DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
DateTimeOffset.UtcNow.AddYears(1));
|
||||
|
||||
var pfxBytes = certificate.Export(X509ContentType.Pfx);
|
||||
return X509CertificateLoader.LoadPkcs12(
|
||||
pfxBytes,
|
||||
null,
|
||||
X509KeyStorageFlags.MachineKeySet);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TlsIntegration Tests
|
||||
|
||||
public class TlsIntegrationTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ServerAndClient_CanEstablishConnection()
|
||||
{
|
||||
// Arrange - Create self-signed server certificate
|
||||
var serverCert = CreateSelfSignedServerCertificate("localhost");
|
||||
|
||||
var serverOptions = Options.Create(new TlsTransportOptions
|
||||
{
|
||||
Port = 0, // Auto-assign
|
||||
ServerCertificate = serverCert,
|
||||
RequireClientCertificate = false
|
||||
});
|
||||
|
||||
await using var server = new TlsTransportServer(serverOptions, NullLogger<TlsTransportServer>.Instance);
|
||||
|
||||
// Act
|
||||
await server.StartAsync(CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
server.ConnectionCount.Should().Be(0);
|
||||
|
||||
await server.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ServerWithMtls_RequiresClientCertificate()
|
||||
{
|
||||
// Arrange
|
||||
var serverCert = CreateSelfSignedServerCertificate("localhost");
|
||||
|
||||
var serverOptions = Options.Create(new TlsTransportOptions
|
||||
{
|
||||
Port = 0,
|
||||
ServerCertificate = serverCert,
|
||||
RequireClientCertificate = true,
|
||||
AllowSelfSigned = true
|
||||
});
|
||||
|
||||
await using var server = new TlsTransportServer(serverOptions, NullLogger<TlsTransportServer>.Instance);
|
||||
|
||||
// Act
|
||||
await server.StartAsync(CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
serverOptions.Value.RequireClientCertificate.Should().BeTrue();
|
||||
|
||||
await server.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
private static X509Certificate2 CreateSelfSignedServerCertificate(string hostname)
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
var request = new CertificateRequest(
|
||||
$"CN={hostname}",
|
||||
rsa,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
|
||||
// Key usage for server auth
|
||||
request.CertificateExtensions.Add(
|
||||
new X509KeyUsageExtension(
|
||||
X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment,
|
||||
critical: true));
|
||||
|
||||
// Server authentication EKU
|
||||
request.CertificateExtensions.Add(
|
||||
new X509EnhancedKeyUsageExtension(
|
||||
new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") },
|
||||
critical: true));
|
||||
|
||||
// Subject Alternative Name
|
||||
var sanBuilder = new SubjectAlternativeNameBuilder();
|
||||
sanBuilder.AddDnsName(hostname);
|
||||
sanBuilder.AddIpAddress(IPAddress.Loopback);
|
||||
request.CertificateExtensions.Add(sanBuilder.Build());
|
||||
|
||||
var certificate = request.CreateSelfSigned(
|
||||
DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
DateTimeOffset.UtcNow.AddYears(1));
|
||||
|
||||
var pfxBytes = certificate.Export(X509ContentType.Pfx);
|
||||
return X509CertificateLoader.LoadPkcs12(
|
||||
pfxBytes,
|
||||
null,
|
||||
X509KeyStorageFlags.MachineKeySet);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ServiceCollection Extensions Tests
|
||||
|
||||
public class ServiceCollectionExtensionsTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddTlsTransportServer_RegistersServices()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
|
||||
// Act
|
||||
services.AddTlsTransportServer(options =>
|
||||
{
|
||||
options.Port = 5101;
|
||||
});
|
||||
|
||||
// Assert
|
||||
var provider = services.BuildServiceProvider();
|
||||
var server = provider.GetService<TlsTransportServer>();
|
||||
server.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddTlsTransportClient_RegistersServices()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
|
||||
// Act
|
||||
services.AddTlsTransportClient(options =>
|
||||
{
|
||||
options.Host = "localhost";
|
||||
options.Port = 5101;
|
||||
});
|
||||
|
||||
// Assert
|
||||
var provider = services.BuildServiceProvider();
|
||||
var client = provider.GetService<TlsTransportClient>();
|
||||
client.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddTlsTransportServer_WithOptions_ConfiguresOptions()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
|
||||
// Act
|
||||
services.AddTlsTransportServer(options =>
|
||||
{
|
||||
options.Port = 9443;
|
||||
options.RequireClientCertificate = true;
|
||||
});
|
||||
|
||||
// Assert
|
||||
var provider = services.BuildServiceProvider();
|
||||
var optionsService = provider.GetService<IOptions<TlsTransportOptions>>();
|
||||
optionsService.Should().NotBeNull();
|
||||
optionsService!.Value.Port.Should().Be(9443);
|
||||
optionsService.Value.RequireClientCertificate.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddTlsTransportClient_WithOptions_ConfiguresOptions()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
|
||||
// Act
|
||||
services.AddTlsTransportClient(options =>
|
||||
{
|
||||
options.Host = "secure.gateway.local";
|
||||
options.Port = 8443;
|
||||
options.AllowSelfSigned = true;
|
||||
});
|
||||
|
||||
// Assert
|
||||
var provider = services.BuildServiceProvider();
|
||||
var optionsService = provider.GetService<IOptions<TlsTransportOptions>>();
|
||||
optionsService.Should().NotBeNull();
|
||||
optionsService!.Value.Host.Should().Be("secure.gateway.local");
|
||||
optionsService.Value.Port.Should().Be(8443);
|
||||
optionsService.Value.AllowSelfSigned.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
Reference in New Issue
Block a user