Files
Joseph Doherty da02784feb feat(datasync): add custom interval support to DataUpdateRepository
Add optional customIntervals parameter to GetSyncStatusAsync to allow
per-pipeline interval overrides instead of hardcoded defaults. This
enables tables like MisData to use longer sync intervals (e.g., 70 days)
while other tables use standard intervals.

Key changes:
- IDataUpdateRepository.GetSyncStatusAsync now accepts an optional
  Dictionary<string, int> for custom intervals keyed by "TableName_UpdateType"
- GetExpectedInterval and IsOverdue made public static for testing and reuse
- Added GetDefaultInterval method for accessing default values
- Updated DataSyncHealthCheck to use new signature
- Added comprehensive unit tests for custom interval behavior
2026-01-07 01:35:28 -05:00

233 lines
7.0 KiB
C#

using System.Data;
using Dapper;
using JdeScoping.Core.Models;
using JdeScoping.Core.Models.Enums;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataSync.Contracts;
using JdeScoping.DataSync.Services;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using Shouldly;
namespace JdeScoping.DataSync.Tests.Services;
/// <summary>
/// Unit tests for DataUpdateRepository.
/// Tests the custom interval functionality for GetSyncStatusAsync.
/// </summary>
public class DataUpdateRepositoryTests
{
private readonly IDbConnectionFactory _connectionFactory;
private readonly DataUpdateRepository _sut;
public DataUpdateRepositoryTests()
{
_connectionFactory = Substitute.For<IDbConnectionFactory>();
_sut = new DataUpdateRepository(
_connectionFactory,
NullLogger<DataUpdateRepository>.Instance);
}
#region GetSyncStatusAsync Custom Intervals
[Fact]
public void GetSyncStatusAsync_WithoutCustomIntervals_UsesDefaultIntervals()
{
// This test verifies backward compatibility - method should work without custom intervals
// The expected defaults are:
// - Hourly: 60 minutes
// - Daily: 1440 minutes (24 hours)
// - Mass: 10080 minutes (7 days)
// Arrange - Get expected intervals from static helper
var hourlyInterval = DataUpdateRepository.GetDefaultInterval(UpdateTypes.Hourly);
var dailyInterval = DataUpdateRepository.GetDefaultInterval(UpdateTypes.Daily);
var massInterval = DataUpdateRepository.GetDefaultInterval(UpdateTypes.Mass);
// Assert expected defaults
hourlyInterval.ShouldBe(60);
dailyInterval.ShouldBe(1440);
massInterval.ShouldBe(10080);
}
[Fact]
public void GetExpectedInterval_WithCustomIntervals_ReturnsCustomValue()
{
// Arrange - UpdateTypes.Mass = 3, UpdateTypes.Daily = 2
var customIntervals = new Dictionary<string, int>
{
{ "MisData_3", 100800 }, // Mass = 3
{ "WorkOrder_2", 120 } // Daily = 2
};
// Act
var misDataMassInterval = DataUpdateRepository.GetExpectedInterval(
"MisData",
UpdateTypes.Mass,
customIntervals);
var workOrderDailyInterval = DataUpdateRepository.GetExpectedInterval(
"WorkOrder",
UpdateTypes.Daily,
customIntervals);
// Assert
misDataMassInterval.ShouldBe(100800);
workOrderDailyInterval.ShouldBe(120);
}
[Fact]
public void GetExpectedInterval_WithNoMatchingCustomInterval_ReturnsDefault()
{
// Arrange - UpdateTypes.Mass = 3
var customIntervals = new Dictionary<string, int>
{
{ "MisData_3", 100800 } // Only MisData_Mass has custom interval
};
// Act - WorkOrder_Daily should fall back to default
var workOrderDailyInterval = DataUpdateRepository.GetExpectedInterval(
"WorkOrder",
UpdateTypes.Daily,
customIntervals);
// Assert - Should return default 1440 for Daily
workOrderDailyInterval.ShouldBe(1440);
}
[Fact]
public void GetExpectedInterval_WithNullCustomIntervals_ReturnsDefault()
{
// Act
var massInterval = DataUpdateRepository.GetExpectedInterval(
"MisData",
UpdateTypes.Mass,
null);
// Assert - Should return default 10080 for Mass
massInterval.ShouldBe(10080);
}
[Fact]
public void GetExpectedInterval_WithEmptyCustomIntervals_ReturnsDefault()
{
// Arrange
var customIntervals = new Dictionary<string, int>();
// Act
var hourlyInterval = DataUpdateRepository.GetExpectedInterval(
"WorkOrder",
UpdateTypes.Hourly,
customIntervals);
// Assert - Should return default 60 for Hourly
hourlyInterval.ShouldBe(60);
}
[Fact]
public void IsOverdue_WithCustomInterval_UsesCustomValue()
{
// Arrange - Custom interval of 100800 minutes (70 days)
// UpdateTypes.Mass = 3
var customIntervals = new Dictionary<string, int>
{
{ "MisData_3", 100800 }
};
// Default Mass interval is 10080 min (7 days) + 50% grace = 10.5 days
// A sync 15 days ago would be overdue with default (15 > 10.5)
// but NOT overdue with custom 70-day interval + 50% grace = 105 days (15 < 105)
var lastSync = DateTime.UtcNow.AddDays(-15);
// Act
var isOverdueWithCustom = DataUpdateRepository.IsOverdue(
lastSync,
"MisData",
UpdateTypes.Mass,
customIntervals);
var isOverdueWithDefault = DataUpdateRepository.IsOverdue(
lastSync,
"MisData",
UpdateTypes.Mass,
null);
// Assert
isOverdueWithCustom.ShouldBeFalse(); // 15 days < 70 days + 50% grace = 105 days
isOverdueWithDefault.ShouldBeTrue(); // 15 days > 7 days + 50% grace = 10.5 days
}
[Fact]
public void IsOverdue_WithNoLastSync_ReturnsTrue()
{
// Act
var isOverdue = DataUpdateRepository.IsOverdue(
null,
"WorkOrder",
UpdateTypes.Daily,
null);
// Assert
isOverdue.ShouldBeTrue();
}
[Fact]
public void IsOverdue_WithRecentSync_ReturnsFalse()
{
// Arrange - Sync completed 5 minutes ago for Hourly (60 min interval)
var lastSync = DateTime.UtcNow.AddMinutes(-5);
// Act
var isOverdue = DataUpdateRepository.IsOverdue(
lastSync,
"WorkOrder",
UpdateTypes.Hourly,
null);
// Assert
isOverdue.ShouldBeFalse();
}
[Fact]
public void IsOverdue_WithOldSync_ReturnsTrue()
{
// Arrange - Sync completed 3 hours ago for Hourly (60 min + 50% grace = 90 min)
var lastSync = DateTime.UtcNow.AddHours(-3);
// Act
var isOverdue = DataUpdateRepository.IsOverdue(
lastSync,
"WorkOrder",
UpdateTypes.Hourly,
null);
// Assert
isOverdue.ShouldBeTrue();
}
#endregion
#region Constructor Tests
[Fact]
public void Constructor_WithNullConnectionFactory_ThrowsArgumentNullException()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new DataUpdateRepository(null!, NullLogger<DataUpdateRepository>.Instance));
}
[Fact]
public void Constructor_WithNullLogger_ThrowsArgumentNullException()
{
// Arrange
var connectionFactory = Substitute.For<IDbConnectionFactory>();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new DataUpdateRepository(connectionFactory, null!));
}
#endregion
}