Initial import of the CBDDC codebase with docs and tests. Add a .NET-focused gitignore to keep generated artifacts out of source control.
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
256
tests/ZB.MOM.WW.CBDDC.Hosting.Tests/CBDDCHealthCheckTests.cs
Normal file
256
tests/ZB.MOM.WW.CBDDC.Hosting.Tests/CBDDCHealthCheckTests.cs
Normal file
@@ -0,0 +1,256 @@
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using ZB.MOM.WW.CBDDC.Core;
|
||||
using ZB.MOM.WW.CBDDC.Core.Storage;
|
||||
using ZB.MOM.WW.CBDDC.Hosting.Configuration;
|
||||
using ZB.MOM.WW.CBDDC.Hosting.HealthChecks;
|
||||
|
||||
namespace ZB.MOM.WW.CBDDC.Hosting.Tests;
|
||||
|
||||
public class CBDDCHealthCheckTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that health is reported as healthy when persistence is available and all peers are within lag thresholds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_WhenPersistenceOkAndPeersWithinLagThreshold_ReturnsHealthyWithPayload()
|
||||
{
|
||||
var store = Substitute.For<IOplogStore>();
|
||||
var confirmationStore = Substitute.For<IPeerOplogConfirmationStore>();
|
||||
var peer1LastUpdate = DateTimeOffset.UtcNow.AddSeconds(-5);
|
||||
var peer2LastUpdate = DateTimeOffset.UtcNow.AddSeconds(-2);
|
||||
|
||||
store.GetLatestTimestampAsync(Arg.Any<CancellationToken>()).Returns(new HlcTimestamp(1_000, 0, "node-1"));
|
||||
confirmationStore.GetActiveTrackedPeersAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IEnumerable<string>>(new[] { "peer-1", "peer-2" }));
|
||||
confirmationStore.GetConfirmationsForPeerAsync("peer-1", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IEnumerable<PeerOplogConfirmation>>(new[]
|
||||
{
|
||||
new PeerOplogConfirmation
|
||||
{
|
||||
PeerNodeId = "peer-1",
|
||||
SourceNodeId = "source-1",
|
||||
ConfirmedWall = 995,
|
||||
ConfirmedLogic = 0,
|
||||
LastConfirmedUtc = peer1LastUpdate,
|
||||
IsActive = true
|
||||
}
|
||||
}));
|
||||
confirmationStore.GetConfirmationsForPeerAsync("peer-2", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IEnumerable<PeerOplogConfirmation>>(new[]
|
||||
{
|
||||
new PeerOplogConfirmation
|
||||
{
|
||||
PeerNodeId = "peer-2",
|
||||
SourceNodeId = "source-1",
|
||||
ConfirmedWall = 990,
|
||||
ConfirmedLogic = 0,
|
||||
LastConfirmedUtc = peer2LastUpdate,
|
||||
IsActive = true
|
||||
}
|
||||
}));
|
||||
|
||||
var healthCheck = new CBDDCHealthCheck(
|
||||
store,
|
||||
confirmationStore,
|
||||
CreateOptions(lagThresholdMs: 20, criticalLagThresholdMs: 50));
|
||||
|
||||
var result = await healthCheck.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
result.Status.ShouldBe(HealthStatus.Healthy);
|
||||
result.Data["trackedPeerCount"].ShouldBe(2);
|
||||
result.Data["maxLagMs"].ShouldBe(10L);
|
||||
result.Data["laggingPeers"].ShouldBeOfType<List<string>>().Count.ShouldBe(0);
|
||||
result.Data["peersWithNoConfirmation"].ShouldBeOfType<List<string>>().Count.ShouldBe(0);
|
||||
|
||||
var lastUpdates = result.Data["lastSuccessfulConfirmationUpdateByPeer"]
|
||||
.ShouldBeOfType<Dictionary<string, DateTimeOffset?>>();
|
||||
lastUpdates["peer-1"].ShouldBe(peer1LastUpdate);
|
||||
lastUpdates["peer-2"].ShouldBe(peer2LastUpdate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that health is reported as degraded when at least one peer is lagging or has no confirmation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_WhenPeersLaggingOrUnconfirmed_ReturnsDegradedWithPayload()
|
||||
{
|
||||
var store = Substitute.For<IOplogStore>();
|
||||
var confirmationStore = Substitute.For<IPeerOplogConfirmationStore>();
|
||||
var peer1LastUpdate = DateTimeOffset.UtcNow.AddSeconds(-10);
|
||||
|
||||
store.GetLatestTimestampAsync(Arg.Any<CancellationToken>()).Returns(new HlcTimestamp(1_000, 0, "node-1"));
|
||||
confirmationStore.GetActiveTrackedPeersAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IEnumerable<string>>(new[] { "peer-1", "peer-2", "peer-3" }));
|
||||
confirmationStore.GetConfirmationsForPeerAsync("peer-1", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IEnumerable<PeerOplogConfirmation>>(new[]
|
||||
{
|
||||
new PeerOplogConfirmation
|
||||
{
|
||||
PeerNodeId = "peer-1",
|
||||
SourceNodeId = "source-1",
|
||||
ConfirmedWall = 960,
|
||||
ConfirmedLogic = 0,
|
||||
LastConfirmedUtc = peer1LastUpdate,
|
||||
IsActive = true
|
||||
}
|
||||
}));
|
||||
confirmationStore.GetConfirmationsForPeerAsync("peer-2", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IEnumerable<PeerOplogConfirmation>>(Array.Empty<PeerOplogConfirmation>()));
|
||||
confirmationStore.GetConfirmationsForPeerAsync("peer-3", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IEnumerable<PeerOplogConfirmation>>(new[]
|
||||
{
|
||||
new PeerOplogConfirmation
|
||||
{
|
||||
PeerNodeId = "peer-3",
|
||||
SourceNodeId = "source-1",
|
||||
ConfirmedWall = 995,
|
||||
ConfirmedLogic = 0,
|
||||
LastConfirmedUtc = DateTimeOffset.UtcNow.AddSeconds(-4),
|
||||
IsActive = true
|
||||
}
|
||||
}));
|
||||
|
||||
var healthCheck = new CBDDCHealthCheck(
|
||||
store,
|
||||
confirmationStore,
|
||||
CreateOptions(lagThresholdMs: 30, criticalLagThresholdMs: 100));
|
||||
|
||||
var result = await healthCheck.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
result.Status.ShouldBe(HealthStatus.Degraded);
|
||||
result.Data["trackedPeerCount"].ShouldBe(3);
|
||||
result.Data["maxLagMs"].ShouldBe(40L);
|
||||
result.Data["laggingPeers"].ShouldBeOfType<List<string>>().ShouldContain("peer-1");
|
||||
result.Data["peersWithNoConfirmation"].ShouldBeOfType<List<string>>().ShouldContain("peer-2");
|
||||
|
||||
var lastUpdates = result.Data["lastSuccessfulConfirmationUpdateByPeer"]
|
||||
.ShouldBeOfType<Dictionary<string, DateTimeOffset?>>();
|
||||
lastUpdates["peer-1"].ShouldBe(peer1LastUpdate);
|
||||
lastUpdates["peer-2"].ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that health is reported as unhealthy when critical lag threshold is exceeded.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_WhenCriticalLagBreached_ReturnsUnhealthyWithPayload()
|
||||
{
|
||||
var store = Substitute.For<IOplogStore>();
|
||||
var confirmationStore = Substitute.For<IPeerOplogConfirmationStore>();
|
||||
|
||||
store.GetLatestTimestampAsync(Arg.Any<CancellationToken>()).Returns(new HlcTimestamp(1_000, 0, "node-1"));
|
||||
confirmationStore.GetActiveTrackedPeersAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IEnumerable<string>>(new[] { "peer-critical" }));
|
||||
confirmationStore.GetConfirmationsForPeerAsync("peer-critical", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IEnumerable<PeerOplogConfirmation>>(new[]
|
||||
{
|
||||
new PeerOplogConfirmation
|
||||
{
|
||||
PeerNodeId = "peer-critical",
|
||||
SourceNodeId = "source-1",
|
||||
ConfirmedWall = 850,
|
||||
ConfirmedLogic = 0,
|
||||
LastConfirmedUtc = DateTimeOffset.UtcNow.AddMinutes(-1),
|
||||
IsActive = true
|
||||
}
|
||||
}));
|
||||
|
||||
var healthCheck = new CBDDCHealthCheck(
|
||||
store,
|
||||
confirmationStore,
|
||||
CreateOptions(lagThresholdMs: 30, criticalLagThresholdMs: 80));
|
||||
|
||||
var result = await healthCheck.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
result.Status.ShouldBe(HealthStatus.Unhealthy);
|
||||
result.Data["maxLagMs"].ShouldBe(150L);
|
||||
result.Data["laggingPeers"].ShouldBeOfType<List<string>>().ShouldContain("peer-critical");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that worst-case lag is used when a peer has multiple source confirmations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_WhenPeerHasMultipleSourceConfirmations_UsesWorstCaseLag()
|
||||
{
|
||||
var store = Substitute.For<IOplogStore>();
|
||||
var confirmationStore = Substitute.For<IPeerOplogConfirmationStore>();
|
||||
|
||||
store.GetLatestTimestampAsync(Arg.Any<CancellationToken>()).Returns(new HlcTimestamp(1_000, 0, "node-1"));
|
||||
confirmationStore.GetActiveTrackedPeersAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IEnumerable<string>>(new[] { "peer-1" }));
|
||||
confirmationStore.GetConfirmationsForPeerAsync("peer-1", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IEnumerable<PeerOplogConfirmation>>(new[]
|
||||
{
|
||||
new PeerOplogConfirmation
|
||||
{
|
||||
PeerNodeId = "peer-1",
|
||||
SourceNodeId = "source-fast",
|
||||
ConfirmedWall = 995,
|
||||
ConfirmedLogic = 0,
|
||||
LastConfirmedUtc = DateTimeOffset.UtcNow.AddSeconds(-1),
|
||||
IsActive = true
|
||||
},
|
||||
new PeerOplogConfirmation
|
||||
{
|
||||
PeerNodeId = "peer-1",
|
||||
SourceNodeId = "source-slow",
|
||||
ConfirmedWall = 900,
|
||||
ConfirmedLogic = 0,
|
||||
LastConfirmedUtc = DateTimeOffset.UtcNow.AddSeconds(-10),
|
||||
IsActive = true
|
||||
}
|
||||
}));
|
||||
|
||||
var healthCheck = new CBDDCHealthCheck(
|
||||
store,
|
||||
confirmationStore,
|
||||
CreateOptions(lagThresholdMs: 80, criticalLagThresholdMs: 150));
|
||||
|
||||
var result = await healthCheck.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
result.Status.ShouldBe(HealthStatus.Degraded);
|
||||
result.Data["maxLagMs"].ShouldBe(100L);
|
||||
result.Data["laggingPeers"].ShouldBeOfType<List<string>>().ShouldContain("peer-1");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that health is reported as unhealthy when the persistence store throws.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_WhenStoreThrows_ReturnsUnhealthy()
|
||||
{
|
||||
var store = Substitute.For<IOplogStore>();
|
||||
var confirmationStore = Substitute.For<IPeerOplogConfirmationStore>();
|
||||
var error = new InvalidOperationException("store unavailable");
|
||||
|
||||
store.GetLatestTimestampAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromException<HlcTimestamp>(error));
|
||||
|
||||
var healthCheck = new CBDDCHealthCheck(
|
||||
store,
|
||||
confirmationStore,
|
||||
CreateOptions());
|
||||
|
||||
var result = await healthCheck.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
result.Status.ShouldBe(HealthStatus.Unhealthy);
|
||||
result.Exception.ShouldBe(error);
|
||||
result.Description.ShouldNotBeNull();
|
||||
result.Description.ShouldContain("persistence layer is unavailable");
|
||||
}
|
||||
|
||||
private static CBDDCHostingOptions CreateOptions(
|
||||
long lagThresholdMs = 30_000,
|
||||
long criticalLagThresholdMs = 120_000)
|
||||
{
|
||||
return new CBDDCHostingOptions
|
||||
{
|
||||
Cluster = new ClusterOptions
|
||||
{
|
||||
PeerConfirmationLagThresholdMs = lagThresholdMs,
|
||||
PeerConfirmationCriticalLagThresholdMs = criticalLagThresholdMs
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.CBDDC.Hosting.Configuration;
|
||||
using ZB.MOM.WW.CBDDC.Hosting.HostedServices;
|
||||
using ZB.MOM.WW.CBDDC.Hosting.Services;
|
||||
using ZB.MOM.WW.CBDDC.Network;
|
||||
|
||||
namespace ZB.MOM.WW.CBDDC.Hosting.Tests;
|
||||
|
||||
public class CBDDCHostingExtensionsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that adding CBDDC hosting throws when the service collection is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddCBDDCHosting_WithNullServices_ThrowsArgumentNullException()
|
||||
{
|
||||
Should.Throw<ArgumentNullException>(() =>
|
||||
CBDDCHostingExtensions.AddCBDDCHosting(null!, _ => { }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that adding CBDDC hosting throws when the configuration delegate is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddCBDDCHosting_WithNullConfigure_ThrowsArgumentNullException()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
Should.Throw<ArgumentNullException>(() => services.AddCBDDCHosting(null!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that single-cluster hosting registers expected services and configured options.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddCBDDCHostingSingleCluster_RegistersExpectedServicesAndOptions()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddCBDDCHostingSingleCluster(options =>
|
||||
{
|
||||
options.NodeId = "node-1";
|
||||
options.TcpPort = 5055;
|
||||
options.PeerConfirmationLagThresholdMs = 45_000;
|
||||
options.PeerConfirmationCriticalLagThresholdMs = 180_000;
|
||||
});
|
||||
|
||||
var optionsDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(CBDDCHostingOptions));
|
||||
optionsDescriptor.ShouldNotBeNull();
|
||||
var options = optionsDescriptor.ImplementationInstance.ShouldBeOfType<CBDDCHostingOptions>();
|
||||
|
||||
options.Cluster.NodeId.ShouldBe("node-1");
|
||||
options.Cluster.TcpPort.ShouldBe(5055);
|
||||
options.Cluster.PeerConfirmationLagThresholdMs.ShouldBe(45_000);
|
||||
options.Cluster.PeerConfirmationCriticalLagThresholdMs.ShouldBe(180_000);
|
||||
|
||||
ShouldContainService<IDiscoveryService, NoOpDiscoveryService>(services);
|
||||
ShouldContainService<ISyncOrchestrator, SyncOrchestrator>(services);
|
||||
ShouldContainHostedService<TcpSyncServerHostedService>(services);
|
||||
ShouldContainHostedService<DiscoveryServiceHostedService>(services);
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var healthChecks = provider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
var registration = healthChecks.Registrations.SingleOrDefault(r => r.Name == "cbddc");
|
||||
|
||||
registration.ShouldNotBeNull();
|
||||
registration.FailureStatus.ShouldBe(HealthStatus.Unhealthy);
|
||||
registration.Tags.ShouldContain("db");
|
||||
registration.Tags.ShouldContain("ready");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that single-cluster hosting uses default options when no configuration delegate is provided.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddCBDDCHostingSingleCluster_WithNullConfigure_UsesDefaults()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddCBDDCHostingSingleCluster();
|
||||
|
||||
var optionsDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(CBDDCHostingOptions));
|
||||
optionsDescriptor.ShouldNotBeNull();
|
||||
var options = optionsDescriptor.ImplementationInstance.ShouldBeOfType<CBDDCHostingOptions>();
|
||||
|
||||
options.Cluster.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that health check registration is skipped when health checks are disabled.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddCBDDCHosting_WithHealthChecksDisabled_DoesNotRegisterCBDDCHealthCheck()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddCBDDCHosting(options =>
|
||||
{
|
||||
options.EnableHealthChecks = false;
|
||||
});
|
||||
|
||||
services.Any(d => d.ServiceType == typeof(IConfigureOptions<HealthCheckServiceOptions>))
|
||||
.ShouldBeFalse();
|
||||
}
|
||||
|
||||
private static void ShouldContainService<TService, TImplementation>(IServiceCollection services)
|
||||
{
|
||||
services.Any(d =>
|
||||
d.ServiceType == typeof(TService) &&
|
||||
d.ImplementationType == typeof(TImplementation))
|
||||
.ShouldBeTrue();
|
||||
}
|
||||
|
||||
private static void ShouldContainHostedService<THostedService>(IServiceCollection services)
|
||||
{
|
||||
services.Any(d =>
|
||||
d.ServiceType == typeof(IHostedService) &&
|
||||
d.ImplementationType == typeof(THostedService))
|
||||
.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
2
tests/ZB.MOM.WW.CBDDC.Hosting.Tests/GlobalUsings.cs
Normal file
2
tests/ZB.MOM.WW.CBDDC.Hosting.Tests/GlobalUsings.cs
Normal file
@@ -0,0 +1,2 @@
|
||||
global using NSubstitute;
|
||||
global using Shouldly;
|
||||
42
tests/ZB.MOM.WW.CBDDC.Hosting.Tests/HostedServicesTests.cs
Normal file
42
tests/ZB.MOM.WW.CBDDC.Hosting.Tests/HostedServicesTests.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.CBDDC.Hosting.HostedServices;
|
||||
using ZB.MOM.WW.CBDDC.Network;
|
||||
|
||||
namespace ZB.MOM.WW.CBDDC.Hosting.Tests;
|
||||
|
||||
public class HostedServicesTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that the TCP sync server hosted service starts and stops the server lifecycle.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task TcpSyncServerHostedService_StartAndStop_CallsServerLifecycle()
|
||||
{
|
||||
var syncServer = Substitute.For<ISyncServer>();
|
||||
var logger = Substitute.For<ILogger<TcpSyncServerHostedService>>();
|
||||
var hostedService = new TcpSyncServerHostedService(syncServer, logger);
|
||||
|
||||
await hostedService.StartAsync(CancellationToken.None);
|
||||
await hostedService.StopAsync(CancellationToken.None);
|
||||
|
||||
await syncServer.Received(1).Start();
|
||||
await syncServer.Received(1).Stop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the discovery hosted service starts and stops the discovery lifecycle.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DiscoveryServiceHostedService_StartAndStop_CallsDiscoveryLifecycle()
|
||||
{
|
||||
var discoveryService = Substitute.For<IDiscoveryService>();
|
||||
var logger = Substitute.For<ILogger<DiscoveryServiceHostedService>>();
|
||||
var hostedService = new DiscoveryServiceHostedService(discoveryService, logger);
|
||||
|
||||
await hostedService.StartAsync(CancellationToken.None);
|
||||
await hostedService.StopAsync(CancellationToken.None);
|
||||
|
||||
await discoveryService.Received(1).Start();
|
||||
await discoveryService.Received(1).Stop();
|
||||
}
|
||||
}
|
||||
35
tests/ZB.MOM.WW.CBDDC.Hosting.Tests/NoOpServicesTests.cs
Normal file
35
tests/ZB.MOM.WW.CBDDC.Hosting.Tests/NoOpServicesTests.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using ZB.MOM.WW.CBDDC.Hosting.Services;
|
||||
|
||||
namespace ZB.MOM.WW.CBDDC.Hosting.Tests;
|
||||
|
||||
public class NoOpServicesTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that no-op discovery service lifecycle calls complete and no peers are returned.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task NoOpDiscoveryService_ReturnsNoPeers_AndCompletesLifecycleCalls()
|
||||
{
|
||||
var service = new NoOpDiscoveryService();
|
||||
|
||||
service.GetActivePeers().ShouldBeEmpty();
|
||||
(await Record.ExceptionAsync(() => service.Start())).ShouldBeNull();
|
||||
(await Record.ExceptionAsync(() => service.Stop())).ShouldBeNull();
|
||||
|
||||
service.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that no-op sync orchestrator lifecycle calls complete without exceptions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task NoOpSyncOrchestrator_CompletesLifecycleCalls()
|
||||
{
|
||||
var orchestrator = new NoOpSyncOrchestrator();
|
||||
|
||||
(await Record.ExceptionAsync(() => orchestrator.Start())).ShouldBeNull();
|
||||
(await Record.ExceptionAsync(() => orchestrator.Stop())).ShouldBeNull();
|
||||
|
||||
orchestrator.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>ZB.MOM.WW.CBDDC.Hosting.Tests</AssemblyName>
|
||||
<RootNamespace>ZB.MOM.WW.CBDDC.Hosting.Tests</RootNamespace>
|
||||
<PackageId>ZB.MOM.WW.CBDDC.Hosting.Tests</PackageId>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<NoWarn>$(NoWarn);xUnit1031;xUnit1051</NoWarn>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
<PackageReference Include="xunit.v3" Version="3.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.CBDDC.Hosting\ZB.MOM.WW.CBDDC.Hosting.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user