Initial commit: JDE Scoping Tool migration project
Set up repository with legacy .NET Framework 4.8 source (OLD/), new .NET 10 Blazor solution (NEW/), OpenSpec specifications, documentation, and project configuration.
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
using JdeScoping.Core.Models.Enums;
|
||||
using JdeScoping.DataSync.Contracts;
|
||||
using JdeScoping.DataSync.HealthChecks;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Shouldly;
|
||||
|
||||
namespace JdeScoping.DataSync.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for DataSyncHealthCheck.
|
||||
/// Tests health check scenarios: Healthy, Degraded, Unhealthy.
|
||||
/// </summary>
|
||||
public class DataSyncHealthCheckTests
|
||||
{
|
||||
private readonly IDataUpdateRepository _repository;
|
||||
private readonly DataSyncHealthCheck _sut;
|
||||
|
||||
public DataSyncHealthCheckTests()
|
||||
{
|
||||
_repository = Substitute.For<IDataUpdateRepository>();
|
||||
_sut = new DataSyncHealthCheck(_repository);
|
||||
}
|
||||
|
||||
#region Healthy Scenarios
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_AllSyncsCurrent_ReturnsHealthy()
|
||||
{
|
||||
// Arrange
|
||||
var statuses = new List<TableSyncStatus>
|
||||
{
|
||||
CreateSyncStatus("WorkOrder", UpdateTypes.Mass, isOverdue: false, recentFailures: 0),
|
||||
CreateSyncStatus("WorkOrder", UpdateTypes.Daily, isOverdue: false, recentFailures: 0),
|
||||
CreateSyncStatus("LotUsage", UpdateTypes.Mass, isOverdue: false, recentFailures: 0)
|
||||
};
|
||||
|
||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(statuses);
|
||||
|
||||
// Act
|
||||
var result = await _sut.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
// Assert
|
||||
result.Status.ShouldBe(HealthStatus.Healthy);
|
||||
result.Description.ShouldBe("All syncs current");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_SlightlyOverdueButProgressing_ReturnsHealthyWithNote()
|
||||
{
|
||||
// Arrange: Some tables slightly overdue but no failures
|
||||
var statuses = new List<TableSyncStatus>
|
||||
{
|
||||
CreateSyncStatus("WorkOrder", UpdateTypes.Mass, isOverdue: false, recentFailures: 0),
|
||||
CreateSyncStatus("WorkOrder", UpdateTypes.Daily, isOverdue: true, recentFailures: 0),
|
||||
CreateSyncStatus("LotUsage", UpdateTypes.Mass, isOverdue: false, recentFailures: 0),
|
||||
CreateSyncStatus("LotUsage", UpdateTypes.Daily, isOverdue: false, recentFailures: 0)
|
||||
};
|
||||
|
||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(statuses);
|
||||
|
||||
// Act
|
||||
var result = await _sut.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
// Assert: With only 1 of 4 overdue (< half), still healthy
|
||||
result.Status.ShouldBe(HealthStatus.Healthy);
|
||||
result.Description!.ShouldContain("slightly overdue");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Degraded Scenarios
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_MajorityOverdue_ReturnsDegraded()
|
||||
{
|
||||
// Arrange: More than half of tables overdue
|
||||
var statuses = new List<TableSyncStatus>
|
||||
{
|
||||
CreateSyncStatus("WorkOrder", UpdateTypes.Mass, isOverdue: true, recentFailures: 0),
|
||||
CreateSyncStatus("WorkOrder", UpdateTypes.Daily, isOverdue: true, recentFailures: 0),
|
||||
CreateSyncStatus("LotUsage", UpdateTypes.Mass, isOverdue: true, recentFailures: 0),
|
||||
CreateSyncStatus("LotUsage", UpdateTypes.Daily, isOverdue: false, recentFailures: 0)
|
||||
};
|
||||
|
||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(statuses);
|
||||
|
||||
// Act
|
||||
var result = await _sut.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
// Assert
|
||||
result.Status.ShouldBe(HealthStatus.Degraded);
|
||||
result.Description!.ShouldContain("overdue");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_SingleRecentFailure_ReturnsDegraded()
|
||||
{
|
||||
// Arrange: One table with recent failures
|
||||
var statuses = new List<TableSyncStatus>
|
||||
{
|
||||
CreateSyncStatus("WorkOrder", UpdateTypes.Mass, isOverdue: false, recentFailures: 1),
|
||||
CreateSyncStatus("WorkOrder", UpdateTypes.Daily, isOverdue: false, recentFailures: 0),
|
||||
CreateSyncStatus("LotUsage", UpdateTypes.Mass, isOverdue: false, recentFailures: 0)
|
||||
};
|
||||
|
||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(statuses);
|
||||
|
||||
// Act
|
||||
var result = await _sut.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
// Assert
|
||||
result.Status.ShouldBe(HealthStatus.Degraded);
|
||||
result.Description!.ShouldContain("failures");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_TwoTablesWithFailures_ReturnsDegraded()
|
||||
{
|
||||
// Arrange: Two tables with failures (at threshold)
|
||||
var statuses = new List<TableSyncStatus>
|
||||
{
|
||||
CreateSyncStatus("WorkOrder", UpdateTypes.Mass, isOverdue: false, recentFailures: 2),
|
||||
CreateSyncStatus("LotUsage", UpdateTypes.Mass, isOverdue: false, recentFailures: 1),
|
||||
CreateSyncStatus("Item", UpdateTypes.Mass, isOverdue: false, recentFailures: 0)
|
||||
};
|
||||
|
||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(statuses);
|
||||
|
||||
// Act
|
||||
var result = await _sut.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
// Assert
|
||||
result.Status.ShouldBe(HealthStatus.Degraded);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unhealthy Scenarios
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_MultipleRecentFailures_ReturnsUnhealthy()
|
||||
{
|
||||
// Arrange: More than 2 tables with recent failures
|
||||
var statuses = new List<TableSyncStatus>
|
||||
{
|
||||
CreateSyncStatus("WorkOrder", UpdateTypes.Mass, isOverdue: false, recentFailures: 1),
|
||||
CreateSyncStatus("LotUsage", UpdateTypes.Mass, isOverdue: false, recentFailures: 1),
|
||||
CreateSyncStatus("Item", UpdateTypes.Mass, isOverdue: false, recentFailures: 1),
|
||||
CreateSyncStatus("Lot", UpdateTypes.Mass, isOverdue: false, recentFailures: 0)
|
||||
};
|
||||
|
||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(statuses);
|
||||
|
||||
// Act
|
||||
var result = await _sut.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
// Assert
|
||||
result.Status.ShouldBe(HealthStatus.Unhealthy);
|
||||
result.Description!.ShouldContain("Multiple recent sync failures");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_RepositoryThrows_ReturnsUnhealthy()
|
||||
{
|
||||
// Arrange
|
||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
||||
.ThrowsAsync(new Exception("Database connection failed"));
|
||||
|
||||
// Act
|
||||
var result = await _sut.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
// Assert
|
||||
result.Status.ShouldBe(HealthStatus.Unhealthy);
|
||||
result.Description.ShouldBe("Unable to check sync status");
|
||||
result.Exception.ShouldNotBeNull();
|
||||
result.Exception.Message.ShouldBe("Database connection failed");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Diagnostic Data Scenarios
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_IncludesPerTableDiagnostics()
|
||||
{
|
||||
// Arrange
|
||||
var now = DateTime.UtcNow;
|
||||
var statuses = new List<TableSyncStatus>
|
||||
{
|
||||
new("WorkOrder", UpdateTypes.Mass, now.AddHours(-1), true, 10080, false, 0),
|
||||
new("WorkOrder", UpdateTypes.Daily, now.AddHours(-12), true, 1440, false, 0)
|
||||
};
|
||||
|
||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(statuses);
|
||||
|
||||
// Act
|
||||
var result = await _sut.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
// Assert
|
||||
result.Data.ShouldNotBeNull();
|
||||
result.Data.ShouldContainKey("WorkOrder_Mass_LastSync");
|
||||
result.Data.ShouldContainKey("WorkOrder_Mass_Status");
|
||||
result.Data.ShouldContainKey("WorkOrder_Mass_RecentFailures");
|
||||
result.Data.ShouldContainKey("WorkOrder_Daily_LastSync");
|
||||
result.Data.ShouldContainKey("TotalTables");
|
||||
result.Data.ShouldContainKey("OverdueCount");
|
||||
result.Data.ShouldContainKey("FailedCount");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_NeverSynced_ShowsNeverInDiagnostics()
|
||||
{
|
||||
// Arrange
|
||||
var statuses = new List<TableSyncStatus>
|
||||
{
|
||||
new("WorkOrder", UpdateTypes.Mass, null, false, 10080, true, 0)
|
||||
};
|
||||
|
||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(statuses);
|
||||
|
||||
// Act
|
||||
var result = await _sut.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
// Assert
|
||||
result.Data["WorkOrder_Mass_LastSync"].ShouldBe("Never");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_OverdueTable_ShowsOverdueInDiagnostics()
|
||||
{
|
||||
// Arrange
|
||||
var statuses = new List<TableSyncStatus>
|
||||
{
|
||||
CreateSyncStatus("WorkOrder", UpdateTypes.Daily, isOverdue: true, recentFailures: 0)
|
||||
};
|
||||
|
||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(statuses);
|
||||
|
||||
// Act
|
||||
var result = await _sut.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
// Assert
|
||||
result.Data["WorkOrder_Daily_Status"].ShouldBe("Overdue");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_EmptyStatusList_ReturnsHealthy()
|
||||
{
|
||||
// Arrange: No tables configured
|
||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<TableSyncStatus>());
|
||||
|
||||
// Act
|
||||
var result = await _sut.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
// Assert
|
||||
result.Status.ShouldBe(HealthStatus.Healthy);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static TableSyncStatus CreateSyncStatus(
|
||||
string tableName,
|
||||
UpdateTypes updateType,
|
||||
bool isOverdue,
|
||||
int recentFailures)
|
||||
{
|
||||
return new TableSyncStatus(
|
||||
TableName: tableName,
|
||||
UpdateType: updateType,
|
||||
LastSyncTime: DateTime.UtcNow.AddHours(-1),
|
||||
WasSuccessful: recentFailures == 0,
|
||||
ExpectedIntervalMinutes: 1440,
|
||||
IsOverdue: isOverdue,
|
||||
RecentFailures: recentFailures);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user