Fix E2E test gaps and add comprehensive E2E + parity test suites
- Fix pull consumer fetch: send original stream subject in HMSG (not inbox) so NATS client distinguishes data messages from control messages - Fix MaxAge expiry: add background timer in StreamManager for periodic pruning - Fix JetStream wire format: Go-compatible anonymous objects with string enums, proper offset-based pagination for stream/consumer list APIs - Add 42 E2E black-box tests (core messaging, auth, TLS, accounts, JetStream) - Add ~1000 parity tests across all subsystems (gaps closure) - Update gap inventory docs to reflect implementation status
This commit is contained in:
49
tests/NATS.E2E.Tests/Infrastructure/AccountServerFixture.cs
Normal file
49
tests/NATS.E2E.Tests/Infrastructure/AccountServerFixture.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using NATS.Client.Core;
|
||||
|
||||
namespace NATS.E2E.Tests.Infrastructure;
|
||||
|
||||
public sealed class AccountServerFixture : IAsyncLifetime
|
||||
{
|
||||
private NatsServerProcess _server = null!;
|
||||
|
||||
public int Port => _server.Port;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
var config = """
|
||||
accounts {
|
||||
ACCT_A {
|
||||
users = [
|
||||
{ user: "user_a", password: "pass_a" }
|
||||
]
|
||||
}
|
||||
ACCT_B {
|
||||
users = [
|
||||
{ user: "user_b", password: "pass_b" }
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
_server = NatsServerProcess.WithConfig(config);
|
||||
await _server.StartAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _server.DisposeAsync();
|
||||
}
|
||||
|
||||
public NatsConnection CreateClientA()
|
||||
{
|
||||
return new NatsConnection(new NatsOpts { Url = $"nats://user_a:pass_a@127.0.0.1:{Port}" });
|
||||
}
|
||||
|
||||
public NatsConnection CreateClientB()
|
||||
{
|
||||
return new NatsConnection(new NatsOpts { Url = $"nats://user_b:pass_b@127.0.0.1:{Port}" });
|
||||
}
|
||||
}
|
||||
|
||||
[CollectionDefinition("E2E-Accounts")]
|
||||
public class AccountsCollection : ICollectionFixture<AccountServerFixture>;
|
||||
82
tests/NATS.E2E.Tests/Infrastructure/AuthServerFixture.cs
Normal file
82
tests/NATS.E2E.Tests/Infrastructure/AuthServerFixture.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using NATS.Client.Core;
|
||||
using NATS.NKeys;
|
||||
|
||||
namespace NATS.E2E.Tests.Infrastructure;
|
||||
|
||||
public sealed class AuthServerFixture : IAsyncLifetime
|
||||
{
|
||||
private NatsServerProcess _server = null!;
|
||||
|
||||
public string NKeyPublicKey { get; }
|
||||
public string NKeySeed { get; }
|
||||
|
||||
public int Port => _server.Port;
|
||||
|
||||
public AuthServerFixture()
|
||||
{
|
||||
var kp = KeyPair.CreatePair(PrefixByte.User);
|
||||
NKeyPublicKey = kp.GetPublicKey();
|
||||
NKeySeed = kp.GetSeed();
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
var config = $$"""
|
||||
max_payload: 512
|
||||
|
||||
authorization {
|
||||
users: [
|
||||
{ user: "testuser", password: "testpass" },
|
||||
{
|
||||
user: "pubonly",
|
||||
password: "pubpass",
|
||||
permissions: {
|
||||
publish: { allow: ["allowed.>"] },
|
||||
subscribe: { allow: ["_INBOX.>"] }
|
||||
}
|
||||
},
|
||||
{
|
||||
user: "subonly",
|
||||
password: "subpass",
|
||||
permissions: {
|
||||
subscribe: { allow: ["allowed.>", "_INBOX.>"] },
|
||||
publish: { allow: ["_INBOX.>"] }
|
||||
}
|
||||
},
|
||||
{ user: "limited", password: "limpass" },
|
||||
{ nkey: "{{NKeyPublicKey}}" }
|
||||
]
|
||||
}
|
||||
|
||||
max_subs: 5
|
||||
""";
|
||||
|
||||
_server = NatsServerProcess.WithConfig(config);
|
||||
await _server.StartAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _server.DisposeAsync();
|
||||
}
|
||||
|
||||
public NatsConnection CreateClient(string user, string password)
|
||||
{
|
||||
var opts = new NatsOpts
|
||||
{
|
||||
Url = $"nats://127.0.0.1:{Port}",
|
||||
AuthOpts = new NatsAuthOpts
|
||||
{
|
||||
Username = user,
|
||||
Password = password,
|
||||
},
|
||||
};
|
||||
return new NatsConnection(opts);
|
||||
}
|
||||
|
||||
public NatsConnection CreateClient()
|
||||
=> new(new NatsOpts { Url = $"nats://127.0.0.1:{Port}" });
|
||||
}
|
||||
|
||||
[CollectionDefinition("E2E-Auth")]
|
||||
public class AuthCollection : ICollectionFixture<AuthServerFixture>;
|
||||
4
tests/NATS.E2E.Tests/Infrastructure/Collections.cs
Normal file
4
tests/NATS.E2E.Tests/Infrastructure/Collections.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace NATS.E2E.Tests.Infrastructure;
|
||||
|
||||
[CollectionDefinition("E2E")]
|
||||
public class E2ECollection : ICollectionFixture<NatsServerFixture>;
|
||||
15
tests/NATS.E2E.Tests/Infrastructure/E2ETestHelper.cs
Normal file
15
tests/NATS.E2E.Tests/Infrastructure/E2ETestHelper.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using NATS.Client.Core;
|
||||
|
||||
namespace NATS.E2E.Tests.Infrastructure;
|
||||
|
||||
public static class E2ETestHelper
|
||||
{
|
||||
public static NatsConnection CreateClient(int port)
|
||||
=> new(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
|
||||
|
||||
public static NatsConnection CreateClient(int port, NatsOpts opts)
|
||||
=> new(opts with { Url = $"nats://127.0.0.1:{port}" });
|
||||
|
||||
public static CancellationToken Timeout(int seconds = 10)
|
||||
=> new CancellationTokenSource(TimeSpan.FromSeconds(seconds)).Token;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using NATS.Client.Core;
|
||||
|
||||
namespace NATS.E2E.Tests.Infrastructure;
|
||||
|
||||
public sealed class JetStreamServerFixture : IAsyncLifetime
|
||||
{
|
||||
private NatsServerProcess _server = null!;
|
||||
private string _storeDir = null!;
|
||||
|
||||
public int Port => _server.Port;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_storeDir = Path.Combine(Path.GetTempPath(), "nats-e2e-js-" + Guid.NewGuid().ToString("N")[..8]);
|
||||
Directory.CreateDirectory(_storeDir);
|
||||
|
||||
var config = $$"""
|
||||
jetstream {
|
||||
store_dir: "{{_storeDir}}"
|
||||
max_mem_store: 64mb
|
||||
max_file_store: 256mb
|
||||
}
|
||||
""";
|
||||
|
||||
_server = NatsServerProcess.WithConfig(config);
|
||||
await _server.StartAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _server.DisposeAsync();
|
||||
|
||||
if (_storeDir is not null && Directory.Exists(_storeDir))
|
||||
{
|
||||
try { Directory.Delete(_storeDir, recursive: true); }
|
||||
catch { /* best-effort cleanup */ }
|
||||
}
|
||||
}
|
||||
|
||||
public NatsConnection CreateClient()
|
||||
=> new(new NatsOpts { Url = $"nats://127.0.0.1:{Port}" });
|
||||
}
|
||||
|
||||
[CollectionDefinition("E2E-JetStream")]
|
||||
public class JetStreamCollection : ICollectionFixture<JetStreamServerFixture>;
|
||||
36
tests/NATS.E2E.Tests/Infrastructure/MonitorServerFixture.cs
Normal file
36
tests/NATS.E2E.Tests/Infrastructure/MonitorServerFixture.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using NATS.Client.Core;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace NATS.E2E.Tests.Infrastructure;
|
||||
|
||||
public sealed class MonitorServerFixture : IAsyncLifetime
|
||||
{
|
||||
private NatsServerProcess _server = null!;
|
||||
|
||||
public int Port => _server.Port;
|
||||
|
||||
public int MonitorPort => _server.MonitorPort!.Value;
|
||||
|
||||
public HttpClient MonitorClient { get; private set; } = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_server = new NatsServerProcess(enableMonitoring: true);
|
||||
await _server.StartAsync();
|
||||
MonitorClient = new HttpClient { BaseAddress = new Uri($"http://127.0.0.1:{MonitorPort}") };
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
MonitorClient?.Dispose();
|
||||
await _server.DisposeAsync();
|
||||
}
|
||||
|
||||
public NatsConnection CreateClient()
|
||||
{
|
||||
return new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{Port}" });
|
||||
}
|
||||
}
|
||||
|
||||
[CollectionDefinition("E2E-Monitor")]
|
||||
public class MonitorCollection : ICollectionFixture<MonitorServerFixture>;
|
||||
31
tests/NATS.E2E.Tests/Infrastructure/NatsServerFixture.cs
Normal file
31
tests/NATS.E2E.Tests/Infrastructure/NatsServerFixture.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using NATS.Client.Core;
|
||||
|
||||
namespace NATS.E2E.Tests.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// xUnit fixture that manages a single NATS server process shared across a test collection.
|
||||
/// </summary>
|
||||
public sealed class NatsServerFixture : IAsyncLifetime
|
||||
{
|
||||
private NatsServerProcess _server = null!;
|
||||
|
||||
public int Port => _server.Port;
|
||||
|
||||
public string ServerOutput => _server.Output;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_server = new NatsServerProcess();
|
||||
await _server.StartAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _server.DisposeAsync();
|
||||
}
|
||||
|
||||
public NatsConnection CreateClient()
|
||||
{
|
||||
return new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{Port}" });
|
||||
}
|
||||
}
|
||||
205
tests/NATS.E2E.Tests/Infrastructure/NatsServerProcess.cs
Normal file
205
tests/NATS.E2E.Tests/Infrastructure/NatsServerProcess.cs
Normal file
@@ -0,0 +1,205 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace NATS.E2E.Tests.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Manages a NATS.Server.Host child process for E2E testing.
|
||||
/// Launches the server on an ephemeral port and polls TCP readiness.
|
||||
/// </summary>
|
||||
public sealed class NatsServerProcess : IAsyncDisposable
|
||||
{
|
||||
private Process? _process;
|
||||
private readonly StringBuilder _output = new();
|
||||
private readonly object _outputLock = new();
|
||||
private readonly string[]? _extraArgs;
|
||||
private readonly string? _configContent;
|
||||
private readonly bool _enableMonitoring;
|
||||
private string? _configFilePath;
|
||||
|
||||
public int Port { get; }
|
||||
|
||||
public int? MonitorPort { get; }
|
||||
|
||||
public string Output
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_outputLock)
|
||||
return _output.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public NatsServerProcess(string[]? extraArgs = null, string? configContent = null, bool enableMonitoring = false)
|
||||
{
|
||||
Port = AllocateFreePort();
|
||||
_extraArgs = extraArgs;
|
||||
_configContent = configContent;
|
||||
_enableMonitoring = enableMonitoring;
|
||||
|
||||
if (_enableMonitoring)
|
||||
MonitorPort = AllocateFreePort();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience factory for creating a server with a config file.
|
||||
/// </summary>
|
||||
public static NatsServerProcess WithConfig(string configContent, bool enableMonitoring = false)
|
||||
=> new(configContent: configContent, enableMonitoring: enableMonitoring);
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
var hostDll = ResolveHostDll();
|
||||
|
||||
// Write config file if provided
|
||||
if (_configContent is not null)
|
||||
{
|
||||
_configFilePath = Path.Combine(Path.GetTempPath(), $"nats-e2e-{Guid.NewGuid():N}.conf");
|
||||
await File.WriteAllTextAsync(_configFilePath, _configContent);
|
||||
}
|
||||
|
||||
// Build argument string
|
||||
var args = new StringBuilder($"exec \"{hostDll}\" -p {Port}");
|
||||
if (_configFilePath is not null)
|
||||
args.Append($" -c \"{_configFilePath}\"");
|
||||
if (_enableMonitoring && MonitorPort.HasValue)
|
||||
args.Append($" -m {MonitorPort.Value}");
|
||||
if (_extraArgs is not null)
|
||||
{
|
||||
foreach (var arg in _extraArgs)
|
||||
args.Append($" {arg}");
|
||||
}
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "dotnet",
|
||||
Arguments = args.ToString(),
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
_process = new Process { StartInfo = psi, EnableRaisingEvents = true };
|
||||
|
||||
_process.OutputDataReceived += (_, e) =>
|
||||
{
|
||||
if (e.Data is not null)
|
||||
lock (_outputLock) _output.AppendLine(e.Data);
|
||||
};
|
||||
_process.ErrorDataReceived += (_, e) =>
|
||||
{
|
||||
if (e.Data is not null)
|
||||
lock (_outputLock) _output.AppendLine(e.Data);
|
||||
};
|
||||
|
||||
_process.Start();
|
||||
_process.BeginOutputReadLine();
|
||||
_process.BeginErrorReadLine();
|
||||
|
||||
await WaitForTcpReadyAsync();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_process is not null)
|
||||
{
|
||||
if (!_process.HasExited)
|
||||
{
|
||||
_process.Kill(entireProcessTree: true);
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
try
|
||||
{
|
||||
await _process.WaitForExitAsync(cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Already killed the tree above; nothing more to do
|
||||
}
|
||||
}
|
||||
|
||||
_process.Dispose();
|
||||
_process = null;
|
||||
}
|
||||
|
||||
// Clean up temp config file
|
||||
if (_configFilePath is not null && File.Exists(_configFilePath))
|
||||
{
|
||||
File.Delete(_configFilePath);
|
||||
_configFilePath = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WaitForTcpReadyAsync()
|
||||
{
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
|
||||
while (!timeout.Token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await socket.ConnectAsync(new IPEndPoint(IPAddress.Loopback, Port), timeout.Token);
|
||||
return; // Connected — server is ready
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
await Task.Delay(100, timeout.Token);
|
||||
}
|
||||
}
|
||||
|
||||
throw new TimeoutException(
|
||||
$"NATS server did not become ready on port {Port} within 10s.\n\nServer output:\n{Output}");
|
||||
}
|
||||
|
||||
private static string ResolveHostDll()
|
||||
{
|
||||
// Walk up from test output directory to find solution root (contains NatsDotNet.slnx)
|
||||
var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (dir is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(dir.FullName, "NatsDotNet.slnx")))
|
||||
{
|
||||
var dll = Path.Combine(dir.FullName, "src", "NATS.Server.Host", "bin", "Debug", "net10.0", "NATS.Server.Host.dll");
|
||||
if (File.Exists(dll))
|
||||
return dll;
|
||||
|
||||
// DLL not found — build it
|
||||
var build = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "dotnet",
|
||||
Arguments = "build src/NATS.Server.Host/NATS.Server.Host.csproj -c Debug",
|
||||
WorkingDirectory = dir.FullName,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
});
|
||||
build!.WaitForExit();
|
||||
|
||||
if (build.ExitCode != 0)
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to build NATS.Server.Host:\n{build.StandardError.ReadToEnd()}");
|
||||
|
||||
if (File.Exists(dll))
|
||||
return dll;
|
||||
|
||||
throw new FileNotFoundException($"Built NATS.Server.Host but DLL not found at: {dll}");
|
||||
}
|
||||
|
||||
dir = dir.Parent;
|
||||
}
|
||||
|
||||
throw new FileNotFoundException(
|
||||
"Could not find solution root (NatsDotNet.slnx) walking up from " + AppContext.BaseDirectory);
|
||||
}
|
||||
|
||||
internal static int AllocateFreePort()
|
||||
{
|
||||
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
return ((IPEndPoint)socket.LocalEndPoint!).Port;
|
||||
}
|
||||
}
|
||||
111
tests/NATS.E2E.Tests/Infrastructure/TlsServerFixture.cs
Normal file
111
tests/NATS.E2E.Tests/Infrastructure/TlsServerFixture.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using NATS.Client.Core;
|
||||
|
||||
namespace NATS.E2E.Tests.Infrastructure;
|
||||
|
||||
public sealed class TlsServerFixture : IAsyncLifetime
|
||||
{
|
||||
private NatsServerProcess _server = null!;
|
||||
private string _tempDir = null!;
|
||||
|
||||
public int Port => _server.Port;
|
||||
public string CaCertPath { get; private set; } = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), $"nats-e2e-tls-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
|
||||
var caCertPath = Path.Combine(_tempDir, "ca.pem");
|
||||
var serverCertPath = Path.Combine(_tempDir, "server-cert.pem");
|
||||
var serverKeyPath = Path.Combine(_tempDir, "server-key.pem");
|
||||
|
||||
GenerateCertificates(caCertPath, serverCertPath, serverKeyPath);
|
||||
|
||||
CaCertPath = caCertPath;
|
||||
|
||||
var config = $$"""
|
||||
tls {
|
||||
cert_file: "{{serverCertPath}}"
|
||||
key_file: "{{serverKeyPath}}"
|
||||
ca_file: "{{caCertPath}}"
|
||||
}
|
||||
""";
|
||||
|
||||
_server = NatsServerProcess.WithConfig(config);
|
||||
await _server.StartAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _server.DisposeAsync();
|
||||
|
||||
if (_tempDir is not null && Directory.Exists(_tempDir))
|
||||
Directory.Delete(_tempDir, recursive: true);
|
||||
}
|
||||
|
||||
public NatsConnection CreateTlsClient()
|
||||
{
|
||||
var opts = new NatsOpts
|
||||
{
|
||||
Url = $"nats://127.0.0.1:{Port}",
|
||||
TlsOpts = new NatsTlsOpts
|
||||
{
|
||||
Mode = TlsMode.Require,
|
||||
InsecureSkipVerify = true,
|
||||
},
|
||||
};
|
||||
return new NatsConnection(opts);
|
||||
}
|
||||
|
||||
public NatsConnection CreatePlainClient()
|
||||
{
|
||||
return new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{Port}" });
|
||||
}
|
||||
|
||||
private static void GenerateCertificates(string caCertPath, string serverCertPath, string serverKeyPath)
|
||||
{
|
||||
// Generate CA key and self-signed certificate
|
||||
using var caKey = RSA.Create(2048);
|
||||
var caReq = new CertificateRequest(
|
||||
"CN=E2E Test CA",
|
||||
caKey,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
caReq.CertificateExtensions.Add(
|
||||
new X509BasicConstraintsExtension(certificateAuthority: true, hasPathLengthConstraint: false, pathLengthConstraint: 0, critical: true));
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
using var caCert = caReq.CreateSelfSigned(now.AddMinutes(-5), now.AddDays(1));
|
||||
|
||||
// Generate server key and certificate signed by CA
|
||||
using var serverKey = RSA.Create(2048);
|
||||
var serverReq = new CertificateRequest(
|
||||
"CN=localhost",
|
||||
serverKey,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
|
||||
var sanBuilder = new SubjectAlternativeNameBuilder();
|
||||
sanBuilder.AddIpAddress(System.Net.IPAddress.Loopback);
|
||||
sanBuilder.AddDnsName("localhost");
|
||||
serverReq.CertificateExtensions.Add(sanBuilder.Build());
|
||||
serverReq.CertificateExtensions.Add(
|
||||
new X509BasicConstraintsExtension(certificateAuthority: false, hasPathLengthConstraint: false, pathLengthConstraint: 0, critical: false));
|
||||
|
||||
using var serverCert = serverReq.Create(caCert, now.AddMinutes(-5), now.AddDays(1), [1, 2, 3, 4]);
|
||||
|
||||
// Export CA cert to PEM
|
||||
File.WriteAllText(caCertPath, caCert.ExportCertificatePem());
|
||||
|
||||
// Export server cert to PEM
|
||||
File.WriteAllText(serverCertPath, serverCert.ExportCertificatePem());
|
||||
|
||||
// Export server private key to PEM
|
||||
File.WriteAllText(serverKeyPath, serverKey.ExportRSAPrivateKeyPem());
|
||||
}
|
||||
}
|
||||
|
||||
[CollectionDefinition("E2E-TLS")]
|
||||
public class TlsCollection : ICollectionFixture<TlsServerFixture>;
|
||||
Reference in New Issue
Block a user