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
This commit is contained in:
@@ -64,9 +64,16 @@ public interface IDataUpdateRepository
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets sync status for health check purposes.
|
/// Gets sync status for health check purposes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="customIntervals">
|
||||||
|
/// Optional dictionary of custom intervals per table/updateType.
|
||||||
|
/// Key format: "{TableName}_{UpdateType}" where UpdateType is the numeric enum value (e.g., "MisData_3" for Mass).
|
||||||
|
/// Value: interval in minutes.
|
||||||
|
/// </param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
/// <returns>List of table sync status records.</returns>
|
/// <returns>List of table sync status records.</returns>
|
||||||
Task<List<TableSyncStatus>> GetSyncStatusAsync(CancellationToken cancellationToken = default);
|
Task<List<TableSyncStatus>> GetSyncStatusAsync(
|
||||||
|
Dictionary<string, int>? customIntervals = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class DataSyncHealthCheck : IHealthCheck
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var statuses = await _repository.GetSyncStatusAsync(cancellationToken);
|
var statuses = await _repository.GetSyncStatusAsync(customIntervals: null, cancellationToken);
|
||||||
var data = new Dictionary<string, object>();
|
var data = new Dictionary<string, object>();
|
||||||
|
|
||||||
foreach (var status in statuses)
|
foreach (var status in statuses)
|
||||||
|
|||||||
@@ -158,7 +158,9 @@ WHERE StartDT < DATEADD(DAY, -@retentionDays, GETUTCDATE())";
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task<List<TableSyncStatus>> GetSyncStatusAsync(CancellationToken cancellationToken = default)
|
public async Task<List<TableSyncStatus>> GetSyncStatusAsync(
|
||||||
|
Dictionary<string, int>? customIntervals = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
const string sql = @"
|
const string sql = @"
|
||||||
WITH LastSuccessful AS (
|
WITH LastSuccessful AS (
|
||||||
@@ -182,16 +184,18 @@ FROM LastSuccessful";
|
|||||||
(UpdateTypes)r.UpdateType,
|
(UpdateTypes)r.UpdateType,
|
||||||
r.LastSuccessfulSync,
|
r.LastSuccessfulSync,
|
||||||
r.LastSuccessfulSync.HasValue,
|
r.LastSuccessfulSync.HasValue,
|
||||||
GetExpectedInterval((UpdateTypes)r.UpdateType),
|
GetExpectedInterval(r.TableName, (UpdateTypes)r.UpdateType, customIntervals),
|
||||||
IsOverdue(r.LastSuccessfulSync, (UpdateTypes)r.UpdateType),
|
IsOverdue(r.LastSuccessfulSync, r.TableName, (UpdateTypes)r.UpdateType, customIntervals),
|
||||||
r.RecentFailures))
|
r.RecentFailures))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the expected interval in minutes for an update type.
|
/// Gets the default interval in minutes for an update type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static int GetExpectedInterval(UpdateTypes updateType)
|
/// <param name="updateType">The update type.</param>
|
||||||
|
/// <returns>The default interval in minutes.</returns>
|
||||||
|
public static int GetDefaultInterval(UpdateTypes updateType)
|
||||||
{
|
{
|
||||||
return updateType switch
|
return updateType switch
|
||||||
{
|
{
|
||||||
@@ -202,20 +206,54 @@ FROM LastSuccessful";
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the expected interval in minutes for a table and update type.
|
||||||
|
/// Uses custom interval if provided, otherwise falls back to default.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tableName">The table name.</param>
|
||||||
|
/// <param name="updateType">The update type.</param>
|
||||||
|
/// <param name="customIntervals">Optional dictionary of custom intervals per table/updateType.</param>
|
||||||
|
/// <returns>The expected interval in minutes.</returns>
|
||||||
|
public static int GetExpectedInterval(
|
||||||
|
string tableName,
|
||||||
|
UpdateTypes updateType,
|
||||||
|
Dictionary<string, int>? customIntervals)
|
||||||
|
{
|
||||||
|
if (customIntervals is not null)
|
||||||
|
{
|
||||||
|
var key = $"{tableName}_{(int)updateType}";
|
||||||
|
if (customIntervals.TryGetValue(key, out var customInterval))
|
||||||
|
{
|
||||||
|
return customInterval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetDefaultInterval(updateType);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if a sync is overdue based on last successful sync time.
|
/// Checks if a sync is overdue based on last successful sync time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool IsOverdue(DateTime? lastSync, UpdateTypes updateType)
|
/// <param name="lastSync">The last successful sync time.</param>
|
||||||
|
/// <param name="tableName">The table name.</param>
|
||||||
|
/// <param name="updateType">The update type.</param>
|
||||||
|
/// <param name="customIntervals">Optional dictionary of custom intervals per table/updateType.</param>
|
||||||
|
/// <returns>True if the sync is overdue; otherwise, false.</returns>
|
||||||
|
public static bool IsOverdue(
|
||||||
|
DateTime? lastSync,
|
||||||
|
string tableName,
|
||||||
|
UpdateTypes updateType,
|
||||||
|
Dictionary<string, int>? customIntervals)
|
||||||
{
|
{
|
||||||
if (!lastSync.HasValue)
|
if (!lastSync.HasValue)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var expectedInterval = GetExpectedInterval(updateType);
|
var expectedInterval = GetExpectedInterval(tableName, updateType, customIntervals);
|
||||||
var grace = expectedInterval * 0.5; // 50% grace period
|
var grace = expectedInterval * 0.5; // 50% grace period
|
||||||
var overdueTreshold = DateTime.UtcNow.AddMinutes(-(expectedInterval + grace));
|
var overdueThreshold = DateTime.UtcNow.AddMinutes(-(expectedInterval + grace));
|
||||||
|
|
||||||
return lastSync.Value < overdueTreshold;
|
return lastSync.Value < overdueThreshold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public class DataSyncHealthCheckTests
|
|||||||
CreateSyncStatus("LotUsage", UpdateTypes.Mass, isOverdue: false, recentFailures: 0)
|
CreateSyncStatus("LotUsage", UpdateTypes.Mass, isOverdue: false, recentFailures: 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
_repository.GetSyncStatusAsync(Arg.Any<Dictionary<string, int>?>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(statuses);
|
.Returns(statuses);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -59,7 +59,7 @@ public class DataSyncHealthCheckTests
|
|||||||
CreateSyncStatus("LotUsage", UpdateTypes.Daily, isOverdue: false, recentFailures: 0)
|
CreateSyncStatus("LotUsage", UpdateTypes.Daily, isOverdue: false, recentFailures: 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
_repository.GetSyncStatusAsync(Arg.Any<Dictionary<string, int>?>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(statuses);
|
.Returns(statuses);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -86,7 +86,7 @@ public class DataSyncHealthCheckTests
|
|||||||
CreateSyncStatus("LotUsage", UpdateTypes.Daily, isOverdue: false, recentFailures: 0)
|
CreateSyncStatus("LotUsage", UpdateTypes.Daily, isOverdue: false, recentFailures: 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
_repository.GetSyncStatusAsync(Arg.Any<Dictionary<string, int>?>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(statuses);
|
.Returns(statuses);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -108,7 +108,7 @@ public class DataSyncHealthCheckTests
|
|||||||
CreateSyncStatus("LotUsage", UpdateTypes.Mass, isOverdue: false, recentFailures: 0)
|
CreateSyncStatus("LotUsage", UpdateTypes.Mass, isOverdue: false, recentFailures: 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
_repository.GetSyncStatusAsync(Arg.Any<Dictionary<string, int>?>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(statuses);
|
.Returns(statuses);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -130,7 +130,7 @@ public class DataSyncHealthCheckTests
|
|||||||
CreateSyncStatus("Item", UpdateTypes.Mass, isOverdue: false, recentFailures: 0)
|
CreateSyncStatus("Item", UpdateTypes.Mass, isOverdue: false, recentFailures: 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
_repository.GetSyncStatusAsync(Arg.Any<Dictionary<string, int>?>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(statuses);
|
.Returns(statuses);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -156,7 +156,7 @@ public class DataSyncHealthCheckTests
|
|||||||
CreateSyncStatus("Lot", UpdateTypes.Mass, isOverdue: false, recentFailures: 0)
|
CreateSyncStatus("Lot", UpdateTypes.Mass, isOverdue: false, recentFailures: 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
_repository.GetSyncStatusAsync(Arg.Any<Dictionary<string, int>?>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(statuses);
|
.Returns(statuses);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -171,7 +171,7 @@ public class DataSyncHealthCheckTests
|
|||||||
public async Task CheckHealthAsync_RepositoryThrows_ReturnsUnhealthy()
|
public async Task CheckHealthAsync_RepositoryThrows_ReturnsUnhealthy()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
_repository.GetSyncStatusAsync(Arg.Any<Dictionary<string, int>?>(), Arg.Any<CancellationToken>())
|
||||||
.ThrowsAsync(new Exception("Database connection failed"));
|
.ThrowsAsync(new Exception("Database connection failed"));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -199,7 +199,7 @@ public class DataSyncHealthCheckTests
|
|||||||
new("WorkOrder", UpdateTypes.Daily, now.AddHours(-12), true, 1440, false, 0)
|
new("WorkOrder", UpdateTypes.Daily, now.AddHours(-12), true, 1440, false, 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
_repository.GetSyncStatusAsync(Arg.Any<Dictionary<string, int>?>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(statuses);
|
.Returns(statuses);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -225,7 +225,7 @@ public class DataSyncHealthCheckTests
|
|||||||
new("WorkOrder", UpdateTypes.Mass, null, false, 10080, true, 0)
|
new("WorkOrder", UpdateTypes.Mass, null, false, 10080, true, 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
_repository.GetSyncStatusAsync(Arg.Any<Dictionary<string, int>?>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(statuses);
|
.Returns(statuses);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -244,7 +244,7 @@ public class DataSyncHealthCheckTests
|
|||||||
CreateSyncStatus("WorkOrder", UpdateTypes.Daily, isOverdue: true, recentFailures: 0)
|
CreateSyncStatus("WorkOrder", UpdateTypes.Daily, isOverdue: true, recentFailures: 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
_repository.GetSyncStatusAsync(Arg.Any<Dictionary<string, int>?>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(statuses);
|
.Returns(statuses);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -262,7 +262,7 @@ public class DataSyncHealthCheckTests
|
|||||||
public async Task CheckHealthAsync_EmptyStatusList_ReturnsHealthy()
|
public async Task CheckHealthAsync_EmptyStatusList_ReturnsHealthy()
|
||||||
{
|
{
|
||||||
// Arrange: No tables configured
|
// Arrange: No tables configured
|
||||||
_repository.GetSyncStatusAsync(Arg.Any<CancellationToken>())
|
_repository.GetSyncStatusAsync(Arg.Any<Dictionary<string, int>?>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(new List<TableSyncStatus>());
|
.Returns(new List<TableSyncStatus>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user