91b516e197
- SearchRepository: Search table operations with Dapper - SearchExecutionService: Search pipeline with proper cancellation handling - WorkProcessor: Unified BackgroundService for syncs and searches - SearchNotificationService: SignalR notifications in Api layer All 45 new tests pass. Proper shutdown vs timeout distinction prevents marking searches as error on host shutdown.
673 lines
23 KiB
C#
673 lines
23 KiB
C#
using System.Diagnostics.Metrics;
|
|
using JdeScoping.Core.Interfaces;
|
|
using JdeScoping.Core.Models.Enums;
|
|
using JdeScoping.Core.Models.Search;
|
|
using JdeScoping.DataSync.Contracts;
|
|
using JdeScoping.DataSync.Models;
|
|
using JdeScoping.DataSync.Options;
|
|
using JdeScoping.DataSync.Telemetry;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
using NSubstitute.ExceptionExtensions;
|
|
using Shouldly;
|
|
|
|
using MsOptions = Microsoft.Extensions.Options.Options;
|
|
|
|
namespace JdeScoping.DataSync.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for WorkProcessor background service.
|
|
/// </summary>
|
|
public class WorkProcessorTests
|
|
{
|
|
#region Disabled Service
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenDisabled_StopsImmediately()
|
|
{
|
|
// Arrange
|
|
var options = MsOptions.Create(new WorkProcessorOptions { Enabled = false });
|
|
var scopeFactory = Substitute.For<IServiceScopeFactory>();
|
|
var metrics = CreateMetrics();
|
|
|
|
var sut = new WorkProcessor(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<WorkProcessor>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
|
|
|
|
// Act
|
|
await sut.StartAsync(cts.Token);
|
|
await Task.Delay(50);
|
|
await sut.StopAsync(CancellationToken.None);
|
|
|
|
// Assert - should not throw and scope factory should not be called
|
|
scopeFactory.DidNotReceive().CreateAsyncScope();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Startup Cleanup
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_CallsStartupCleanup_CloseOpenUpdateEntries()
|
|
{
|
|
// Arrange
|
|
var dataUpdateRepo = Substitute.For<IDataUpdateRepository>();
|
|
var searchRepo = Substitute.For<ISearchRepository>();
|
|
var scheduleChecker = Substitute.For<IScheduleChecker>();
|
|
var notificationService = Substitute.For<ISearchNotificationService>();
|
|
|
|
scheduleChecker.GetPendingTasksAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<DataUpdateTask>());
|
|
|
|
var scopeFactory = SetupScopeFactory(
|
|
dataUpdateRepo,
|
|
searchRepo,
|
|
scheduleChecker,
|
|
notificationService);
|
|
|
|
var options = MsOptions.Create(new WorkProcessorOptions
|
|
{
|
|
Enabled = true,
|
|
WorkInterval = TimeSpan.FromMilliseconds(100)
|
|
});
|
|
var metrics = CreateMetrics();
|
|
|
|
var sut = new WorkProcessor(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<WorkProcessor>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
|
|
|
|
// Act
|
|
await sut.StartAsync(cts.Token);
|
|
await Task.Delay(150);
|
|
await sut.StopAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
await dataUpdateRepo.Received().CloseOpenUpdateEntriesAsync(Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_CallsStartupCleanup_ResetPartialSearches()
|
|
{
|
|
// Arrange
|
|
var dataUpdateRepo = Substitute.For<IDataUpdateRepository>();
|
|
var searchRepo = Substitute.For<ISearchRepository>();
|
|
var scheduleChecker = Substitute.For<IScheduleChecker>();
|
|
var notificationService = Substitute.For<ISearchNotificationService>();
|
|
|
|
scheduleChecker.GetPendingTasksAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<DataUpdateTask>());
|
|
|
|
var scopeFactory = SetupScopeFactory(
|
|
dataUpdateRepo,
|
|
searchRepo,
|
|
scheduleChecker,
|
|
notificationService);
|
|
|
|
var options = MsOptions.Create(new WorkProcessorOptions
|
|
{
|
|
Enabled = true,
|
|
WorkInterval = TimeSpan.FromMilliseconds(100)
|
|
});
|
|
var metrics = CreateMetrics();
|
|
|
|
var sut = new WorkProcessor(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<WorkProcessor>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
|
|
|
|
// Act
|
|
await sut.StartAsync(cts.Token);
|
|
await Task.Delay(150);
|
|
await sut.StopAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
await searchRepo.Received().ResetPartialSearchesAsync(Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenCloseOpenEntriesThrows_ContinuesStarting()
|
|
{
|
|
// Arrange
|
|
var dataUpdateRepo = Substitute.For<IDataUpdateRepository>();
|
|
dataUpdateRepo.CloseOpenUpdateEntriesAsync(Arg.Any<CancellationToken>())
|
|
.ThrowsAsync(new Exception("Database error"));
|
|
|
|
var searchRepo = Substitute.For<ISearchRepository>();
|
|
var scheduleChecker = Substitute.For<IScheduleChecker>();
|
|
scheduleChecker.GetPendingTasksAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<DataUpdateTask>());
|
|
var notificationService = Substitute.For<ISearchNotificationService>();
|
|
|
|
var scopeFactory = SetupScopeFactory(
|
|
dataUpdateRepo,
|
|
searchRepo,
|
|
scheduleChecker,
|
|
notificationService);
|
|
|
|
var options = MsOptions.Create(new WorkProcessorOptions
|
|
{
|
|
Enabled = true,
|
|
WorkInterval = TimeSpan.FromMilliseconds(100)
|
|
});
|
|
var metrics = CreateMetrics();
|
|
|
|
var sut = new WorkProcessor(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<WorkProcessor>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
|
|
|
|
// Act - should not throw
|
|
await sut.StartAsync(cts.Token);
|
|
await Task.Delay(150);
|
|
await sut.StopAsync(CancellationToken.None);
|
|
|
|
// Assert - search repo should still be called (startup continues)
|
|
await searchRepo.Received().ResetPartialSearchesAsync(Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenResetPartialSearchesThrows_ContinuesRunning()
|
|
{
|
|
// Arrange
|
|
var dataUpdateRepo = Substitute.For<IDataUpdateRepository>();
|
|
var searchRepo = Substitute.For<ISearchRepository>();
|
|
searchRepo.ResetPartialSearchesAsync(Arg.Any<CancellationToken>())
|
|
.ThrowsAsync(new Exception("Database error"));
|
|
|
|
var scheduleChecker = Substitute.For<IScheduleChecker>();
|
|
var callCount = 0;
|
|
scheduleChecker.GetPendingTasksAsync(Arg.Any<CancellationToken>())
|
|
.Returns(x =>
|
|
{
|
|
callCount++;
|
|
return new List<DataUpdateTask>();
|
|
});
|
|
var notificationService = Substitute.For<ISearchNotificationService>();
|
|
|
|
var scopeFactory = SetupScopeFactory(
|
|
dataUpdateRepo,
|
|
searchRepo,
|
|
scheduleChecker,
|
|
notificationService);
|
|
|
|
var options = MsOptions.Create(new WorkProcessorOptions
|
|
{
|
|
Enabled = true,
|
|
WorkInterval = TimeSpan.FromMilliseconds(50)
|
|
});
|
|
var metrics = CreateMetrics();
|
|
|
|
var sut = new WorkProcessor(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<WorkProcessor>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
|
|
|
|
// Act - should not throw
|
|
await sut.StartAsync(cts.Token);
|
|
await Task.Delay(150);
|
|
await sut.StopAsync(CancellationToken.None);
|
|
|
|
// Assert - service continues running after startup error
|
|
callCount.ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Priority Processing
|
|
|
|
[Fact]
|
|
public async Task DoWorkAsync_WhenPendingTasks_ExecutesSyncs()
|
|
{
|
|
// Arrange
|
|
var dataUpdateRepo = Substitute.For<IDataUpdateRepository>();
|
|
var searchRepo = Substitute.For<ISearchRepository>();
|
|
var orchestrator = Substitute.For<ISyncOrchestrator>();
|
|
var scheduleChecker = Substitute.For<IScheduleChecker>();
|
|
scheduleChecker.GetPendingTasksAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<DataUpdateTask> { CreateTask("TestTable", UpdateTypes.Daily) });
|
|
var notificationService = Substitute.For<ISearchNotificationService>();
|
|
|
|
var scopeFactory = SetupScopeFactory(
|
|
dataUpdateRepo,
|
|
searchRepo,
|
|
scheduleChecker,
|
|
notificationService,
|
|
orchestrator: orchestrator);
|
|
|
|
var options = MsOptions.Create(new WorkProcessorOptions
|
|
{
|
|
Enabled = true,
|
|
WorkInterval = TimeSpan.FromMilliseconds(100)
|
|
});
|
|
var metrics = CreateMetrics();
|
|
|
|
var sut = new WorkProcessor(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<WorkProcessor>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
|
|
|
|
// Act
|
|
await sut.StartAsync(cts.Token);
|
|
await Task.Delay(150);
|
|
await sut.StopAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
await orchestrator.Received().ExecutePendingSyncsAsync(Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DoWorkAsync_WhenNoPendingTasks_ChecksForQueuedSearches()
|
|
{
|
|
// Arrange
|
|
var dataUpdateRepo = Substitute.For<IDataUpdateRepository>();
|
|
var searchRepo = Substitute.For<ISearchRepository>();
|
|
var scheduleChecker = Substitute.For<IScheduleChecker>();
|
|
scheduleChecker.GetPendingTasksAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<DataUpdateTask>());
|
|
var notificationService = Substitute.For<ISearchNotificationService>();
|
|
|
|
var scopeFactory = SetupScopeFactory(
|
|
dataUpdateRepo,
|
|
searchRepo,
|
|
scheduleChecker,
|
|
notificationService);
|
|
|
|
var options = MsOptions.Create(new WorkProcessorOptions
|
|
{
|
|
Enabled = true,
|
|
WorkInterval = TimeSpan.FromMilliseconds(100)
|
|
});
|
|
var metrics = CreateMetrics();
|
|
|
|
var sut = new WorkProcessor(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<WorkProcessor>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
|
|
|
|
// Act
|
|
await sut.StartAsync(cts.Token);
|
|
await Task.Delay(150);
|
|
await sut.StopAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
await searchRepo.Received().GetNextQueuedSearchAsync(Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DoWorkAsync_WhenQueuedSearchExists_ExecutesSearch()
|
|
{
|
|
// Arrange
|
|
var dataUpdateRepo = Substitute.For<IDataUpdateRepository>();
|
|
var searchRepo = Substitute.For<ISearchRepository>();
|
|
var queuedSearch = new Search { Id = 42 };
|
|
searchRepo.GetNextQueuedSearchAsync(Arg.Any<CancellationToken>())
|
|
.Returns(queuedSearch);
|
|
|
|
var searchExecution = Substitute.For<ISearchExecutionService>();
|
|
var scheduleChecker = Substitute.For<IScheduleChecker>();
|
|
scheduleChecker.GetPendingTasksAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<DataUpdateTask>());
|
|
var notificationService = Substitute.For<ISearchNotificationService>();
|
|
|
|
var scopeFactory = SetupScopeFactory(
|
|
dataUpdateRepo,
|
|
searchRepo,
|
|
scheduleChecker,
|
|
notificationService,
|
|
searchExecution: searchExecution);
|
|
|
|
var options = MsOptions.Create(new WorkProcessorOptions
|
|
{
|
|
Enabled = true,
|
|
WorkInterval = TimeSpan.FromMilliseconds(100)
|
|
});
|
|
var metrics = CreateMetrics();
|
|
|
|
var sut = new WorkProcessor(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<WorkProcessor>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
|
|
|
|
// Act
|
|
await sut.StartAsync(cts.Token);
|
|
await Task.Delay(150);
|
|
await sut.StopAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
await searchExecution.Received().ExecuteSearchAsync(
|
|
Arg.Is<Search>(s => s.Id == 42),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DoWorkAsync_WhenPendingTasks_DoesNotProcessSearches()
|
|
{
|
|
// Arrange
|
|
var dataUpdateRepo = Substitute.For<IDataUpdateRepository>();
|
|
var searchRepo = Substitute.For<ISearchRepository>();
|
|
var orchestrator = Substitute.For<ISyncOrchestrator>();
|
|
var scheduleChecker = Substitute.For<IScheduleChecker>();
|
|
scheduleChecker.GetPendingTasksAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<DataUpdateTask> { CreateTask("TestTable", UpdateTypes.Daily) });
|
|
var notificationService = Substitute.For<ISearchNotificationService>();
|
|
|
|
var scopeFactory = SetupScopeFactory(
|
|
dataUpdateRepo,
|
|
searchRepo,
|
|
scheduleChecker,
|
|
notificationService,
|
|
orchestrator: orchestrator);
|
|
|
|
var options = MsOptions.Create(new WorkProcessorOptions
|
|
{
|
|
Enabled = true,
|
|
WorkInterval = TimeSpan.FromMilliseconds(100)
|
|
});
|
|
var metrics = CreateMetrics();
|
|
|
|
var sut = new WorkProcessor(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<WorkProcessor>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
|
|
|
|
// Act
|
|
await sut.StartAsync(cts.Token);
|
|
await Task.Delay(150);
|
|
await sut.StopAsync(CancellationToken.None);
|
|
|
|
// Assert - when syncs are pending, searches are not processed
|
|
await searchRepo.DidNotReceive().GetNextQueuedSearchAsync(Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Error Handling
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenDoWorkThrows_ContinuesLoop()
|
|
{
|
|
// Arrange
|
|
var dataUpdateRepo = Substitute.For<IDataUpdateRepository>();
|
|
var searchRepo = Substitute.For<ISearchRepository>();
|
|
var callCount = 0;
|
|
var scheduleChecker = Substitute.For<IScheduleChecker>();
|
|
scheduleChecker.GetPendingTasksAsync(Arg.Any<CancellationToken>())
|
|
.Returns(x =>
|
|
{
|
|
callCount++;
|
|
if (callCount == 1)
|
|
{
|
|
throw new Exception("Test error");
|
|
}
|
|
return new List<DataUpdateTask>();
|
|
});
|
|
var notificationService = Substitute.For<ISearchNotificationService>();
|
|
|
|
var scopeFactory = SetupScopeFactory(
|
|
dataUpdateRepo,
|
|
searchRepo,
|
|
scheduleChecker,
|
|
notificationService);
|
|
|
|
var options = MsOptions.Create(new WorkProcessorOptions
|
|
{
|
|
Enabled = true,
|
|
WorkInterval = TimeSpan.FromMilliseconds(50)
|
|
});
|
|
var metrics = CreateMetrics();
|
|
|
|
var sut = new WorkProcessor(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<WorkProcessor>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(300));
|
|
|
|
// Act
|
|
await sut.StartAsync(cts.Token);
|
|
await Task.Delay(250);
|
|
await sut.StopAsync(CancellationToken.None);
|
|
|
|
// Assert - should have been called multiple times despite first error
|
|
callCount.ShouldBeGreaterThan(1);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Status Notifications
|
|
|
|
[Fact]
|
|
public async Task NotifyStatusSafeAsync_WhenNotificationThrows_DoesNotCrash()
|
|
{
|
|
// Arrange
|
|
var dataUpdateRepo = Substitute.For<IDataUpdateRepository>();
|
|
var searchRepo = Substitute.For<ISearchRepository>();
|
|
var scheduleChecker = Substitute.For<IScheduleChecker>();
|
|
scheduleChecker.GetPendingTasksAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<DataUpdateTask>());
|
|
var notificationService = Substitute.For<ISearchNotificationService>();
|
|
notificationService.NotifyStatusAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.ThrowsAsync(new Exception("SignalR error"));
|
|
|
|
var scopeFactory = SetupScopeFactory(
|
|
dataUpdateRepo,
|
|
searchRepo,
|
|
scheduleChecker,
|
|
notificationService);
|
|
|
|
var options = MsOptions.Create(new WorkProcessorOptions
|
|
{
|
|
Enabled = true,
|
|
WorkInterval = TimeSpan.FromMilliseconds(100)
|
|
});
|
|
var metrics = CreateMetrics();
|
|
|
|
var sut = new WorkProcessor(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<WorkProcessor>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
|
|
|
|
// Act - should not throw
|
|
await sut.StartAsync(cts.Token);
|
|
await Task.Delay(150);
|
|
await sut.StopAsync(CancellationToken.None);
|
|
|
|
// Assert - no exception thrown, service runs
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Graceful Shutdown
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenCancelled_StopsGracefully()
|
|
{
|
|
// Arrange
|
|
var dataUpdateRepo = Substitute.For<IDataUpdateRepository>();
|
|
var searchRepo = Substitute.For<ISearchRepository>();
|
|
var scheduleChecker = Substitute.For<IScheduleChecker>();
|
|
scheduleChecker.GetPendingTasksAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<DataUpdateTask>());
|
|
var notificationService = Substitute.For<ISearchNotificationService>();
|
|
|
|
var scopeFactory = SetupScopeFactory(
|
|
dataUpdateRepo,
|
|
searchRepo,
|
|
scheduleChecker,
|
|
notificationService);
|
|
|
|
var options = MsOptions.Create(new WorkProcessorOptions
|
|
{
|
|
Enabled = true,
|
|
WorkInterval = TimeSpan.FromSeconds(10) // Long interval
|
|
});
|
|
var metrics = CreateMetrics();
|
|
|
|
var sut = new WorkProcessor(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<WorkProcessor>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await sut.StartAsync(cts.Token);
|
|
await Task.Delay(50);
|
|
cts.Cancel();
|
|
|
|
var stopTask = sut.StopAsync(CancellationToken.None);
|
|
var completed = await Task.WhenAny(stopTask, Task.Delay(2000));
|
|
|
|
// Assert - should complete without hanging
|
|
completed.ShouldBe(stopTask);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenCancelledDuringWork_HandlesOperationCanceledException()
|
|
{
|
|
// Arrange
|
|
var dataUpdateRepo = Substitute.For<IDataUpdateRepository>();
|
|
var searchRepo = Substitute.For<ISearchRepository>();
|
|
var orchestrator = Substitute.For<ISyncOrchestrator>();
|
|
var cts = new CancellationTokenSource();
|
|
|
|
orchestrator.ExecutePendingSyncsAsync(Arg.Any<CancellationToken>())
|
|
.Returns(async x =>
|
|
{
|
|
cts.Cancel();
|
|
await Task.Delay(100, x.Arg<CancellationToken>());
|
|
});
|
|
|
|
var scheduleChecker = Substitute.For<IScheduleChecker>();
|
|
scheduleChecker.GetPendingTasksAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<DataUpdateTask> { CreateTask("TestTable", UpdateTypes.Daily) });
|
|
var notificationService = Substitute.For<ISearchNotificationService>();
|
|
|
|
var scopeFactory = SetupScopeFactory(
|
|
dataUpdateRepo,
|
|
searchRepo,
|
|
scheduleChecker,
|
|
notificationService,
|
|
orchestrator: orchestrator);
|
|
|
|
var options = MsOptions.Create(new WorkProcessorOptions
|
|
{
|
|
Enabled = true,
|
|
WorkInterval = TimeSpan.FromMilliseconds(100)
|
|
});
|
|
var metrics = CreateMetrics();
|
|
|
|
var sut = new WorkProcessor(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<WorkProcessor>.Instance,
|
|
metrics);
|
|
|
|
// Act
|
|
await sut.StartAsync(cts.Token);
|
|
await Task.Delay(200);
|
|
var stopTask = sut.StopAsync(CancellationToken.None);
|
|
var completed = await Task.WhenAny(stopTask, Task.Delay(2000));
|
|
|
|
// Assert - should complete gracefully
|
|
completed.ShouldBe(stopTask);
|
|
cts.Dispose();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private static DataSyncMetrics CreateMetrics()
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddMetrics();
|
|
var provider = services.BuildServiceProvider();
|
|
var meterFactory = provider.GetRequiredService<IMeterFactory>();
|
|
return new DataSyncMetrics(meterFactory);
|
|
}
|
|
|
|
private static IServiceScopeFactory SetupScopeFactory(
|
|
IDataUpdateRepository dataUpdateRepo,
|
|
ISearchRepository searchRepo,
|
|
IScheduleChecker scheduleChecker,
|
|
ISearchNotificationService notificationService,
|
|
ISyncOrchestrator? orchestrator = null,
|
|
ISearchExecutionService? searchExecution = null)
|
|
{
|
|
orchestrator ??= Substitute.For<ISyncOrchestrator>();
|
|
searchExecution ??= Substitute.For<ISearchExecutionService>();
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddScoped(_ => dataUpdateRepo);
|
|
services.AddScoped(_ => searchRepo);
|
|
services.AddScoped(_ => scheduleChecker);
|
|
services.AddScoped(_ => notificationService);
|
|
services.AddScoped(_ => orchestrator);
|
|
services.AddScoped(_ => searchExecution);
|
|
|
|
var serviceProvider = services.BuildServiceProvider();
|
|
return serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
|
}
|
|
|
|
private static DataUpdateTask CreateTask(string tableName, UpdateTypes updateType)
|
|
{
|
|
return new DataUpdateTask
|
|
{
|
|
TableName = tableName,
|
|
SourceSystem = "JDE",
|
|
SourceData = tableName.ToUpper(),
|
|
UpdateType = updateType,
|
|
MinimumDt = null,
|
|
Config = new DataSourceConfig
|
|
{
|
|
TableName = tableName,
|
|
SourceSystem = "JDE",
|
|
SourceData = tableName.ToUpper(),
|
|
IsEnabled = true,
|
|
MassConfig = new ScheduleConfig { Enabled = true, IntervalMinutes = 10080 },
|
|
DailyConfig = new ScheduleConfig { Enabled = true, IntervalMinutes = 1440 },
|
|
HourlyConfig = new ScheduleConfig { Enabled = true, IntervalMinutes = 60 }
|
|
}
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
}
|