using JdeScoping.Core.Models; using JdeScoping.Core.Models.Enums; using JdeScoping.Core.Models.Inventory; using JdeScoping.Core.Models.Search; using JdeScoping.Core.ViewModels; using JdeScoping.DataAccess.Options; using JdeScoping.DataAccess.Exceptions; using JdeScoping.DataAccess.Interfaces; using JdeScoping.DataAccess.Repositories; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NSubstitute; using NSubstitute.ExceptionExtensions; using Shouldly; using Xunit; namespace JdeScoping.DataAccess.Tests; /// /// Unit tests for LotFinderRepository. /// public class LotFinderRepositoryTests { private readonly IDbConnectionFactory _connectionFactory; private readonly ILogger _logger; private readonly IOptions _options; public LotFinderRepositoryTests() { _connectionFactory = Substitute.For(); _logger = Substitute.For>(); _options = Microsoft.Extensions.Options.Options.Create(new DataAccessOptions { DefaultTimeoutSeconds = 30, RebuildIndexTimeoutSeconds = 60 }); } #region Constructor Tests [Fact] public void Constructor_NullConnectionFactory_ThrowsArgumentNullException() { // Act & Assert Should.Throw( () => new LotFinderRepository(null!, _logger, _options)) .ParamName.ShouldBe("connectionFactory"); } [Fact] public void Constructor_NullLogger_ThrowsArgumentNullException() { // Act & Assert Should.Throw( () => new LotFinderRepository(_connectionFactory, null!, _options)) .ParamName.ShouldBe("logger"); } [Fact] public void Constructor_NullOptions_ThrowsArgumentNullException() { // Act & Assert Should.Throw( () => new LotFinderRepository(_connectionFactory, _logger, null!)) .ParamName.ShouldBe("options"); } [Fact] public void Constructor_ValidParameters_CreatesInstance() { // Act var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Assert repository.ShouldNotBeNull(); } #endregion #region RebuildIndicesAsync - Table Name Validation Tests [Theory] [InlineData("Branch")] [InlineData("DataUpdate")] [InlineData("FunctionCode")] [InlineData("Item")] [InlineData("JdeUser")] [InlineData("Lot")] [InlineData("LotLocation")] [InlineData("LotUsage_Curr")] [InlineData("LotUsage_Hist")] [InlineData("MisData")] [InlineData("OrgHierarchy")] [InlineData("ProfitCenter")] [InlineData("RouteMaster")] [InlineData("Search")] [InlineData("StatusCode")] [InlineData("WorkCenter")] [InlineData("WorkOrder_Curr")] [InlineData("WorkOrder_Hist")] [InlineData("WorkOrderComponent_Curr")] [InlineData("WorkOrderComponent_Hist")] [InlineData("WorkOrderRouting")] [InlineData("WorkOrderStep_Curr")] [InlineData("WorkOrderStep_Hist")] [InlineData("WorkOrderTime_Curr")] [InlineData("WorkOrderTime_Hist")] public async Task RebuildIndicesAsync_ValidTableName_DoesNotThrowArgumentException(string tableName) { // Arrange - expect connection exception since we have no real connection _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert - should throw QueryException (wrapped ConnectionException), not ArgumentException var ex = await Should.ThrowAsync( async () => await repository.RebuildIndicesAsync(tableName)); ex.QueryName.ShouldBe("SQL_REBUILD_INDICES"); } [Theory] [InlineData("InvalidTable")] [InlineData("DropTable")] [InlineData("Users")] [InlineData("sys.tables")] [InlineData("'; DROP TABLE Users; --")] [InlineData("WorkOrder")] [InlineData("branch")] // Case-insensitive should still work public async Task RebuildIndicesAsync_InvalidTableName_ThrowsArgumentException(string tableName) { // Arrange var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert // Note: "branch" is case-insensitive match for "Branch", so it should NOT throw if (tableName.Equals("branch", StringComparison.OrdinalIgnoreCase)) { // Case-insensitive match - will try to connect and throw QueryException _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection", "LotFinderDB")); await Should.ThrowAsync( async () => await repository.RebuildIndicesAsync(tableName)); } else { var ex = await Should.ThrowAsync( async () => await repository.RebuildIndicesAsync(tableName)); ex.ParamName.ShouldBe("tableName"); ex.Message.ShouldContain($"Invalid table name: {tableName}"); } } #endregion #region TruncateTableAsync - Table Name Validation Tests [Theory] [InlineData("Branch")] [InlineData("Item")] [InlineData("WorkOrder_Curr")] public async Task TruncateTableAsync_ValidTableName_DoesNotThrowArgumentException(string tableName) { // Arrange - expect connection exception since we have no real connection _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert - should throw QueryException (wrapped ConnectionException), not ArgumentException await Should.ThrowAsync( async () => await repository.TruncateTableAsync(tableName)); } [Theory] [InlineData("InvalidTable")] [InlineData("'; DELETE FROM Users; --")] public async Task TruncateTableAsync_InvalidTableName_ThrowsArgumentException(string tableName) { // Arrange var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.TruncateTableAsync(tableName)); ex.ParamName.ShouldBe("tableName"); ex.Message.ShouldContain($"Invalid table name: {tableName}"); } #endregion #region BulkInsertAsync - Table Name Validation Tests [Theory] [InlineData("Branch")] [InlineData("Item")] public async Task BulkInsertAsync_ValidTableName_DoesNotThrowArgumentException(string tableName) { // Arrange - expect connection exception since we have no real connection _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); var records = new List(); // Act & Assert - should throw QueryException (wrapped ConnectionException), not ArgumentException await Should.ThrowAsync( async () => await repository.BulkInsertAsync(tableName, records)); } [Theory] [InlineData("InvalidTable")] [InlineData("'; TRUNCATE TABLE Users; --")] public async Task BulkInsertAsync_InvalidTableName_ThrowsArgumentException(string tableName) { // Arrange var repository = new LotFinderRepository(_connectionFactory, _logger, _options); var records = new List(); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.BulkInsertAsync(tableName, records)); ex.ParamName.ShouldBe("tableName"); ex.Message.ShouldContain($"Invalid table name: {tableName}"); } #endregion #region Connection Exception Handling Tests [Fact] public async Task GetUserSearchesAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.GetUserSearchesAsync("testuser")); ex.QueryName.ShouldBe("SQL_GET_USER_SEARCHES"); ex.Repository.ShouldBe("LotFinderRepository"); } [Fact] public async Task GetQueuedSearchesAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.GetQueuedSearchesAsync()); ex.QueryName.ShouldBe("SQL_GET_QUEUED_SEARCHES"); } [Fact] public async Task GetSearchAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.GetSearchAsync(1)); ex.QueryName.ShouldBe("SQL_GET_SEARCH"); } [Fact] public async Task GetSearchResultsAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.GetSearchResultsAsync(1)); ex.QueryName.ShouldBe("SQL_GET_SEARCH_RESULTS"); } [Fact] public async Task SubmitSearchAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); var search = new Search { UserName = "testuser", Name = "Test Search" }; // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.SubmitSearchAsync(search)); ex.QueryName.ShouldBe(SqlObjects.SubmitSearch); } [Fact] public async Task UpdateSearchStatusAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.UpdateSearchStatusAsync(1, SearchStatus.Running)); ex.QueryName.ShouldBe("SQL_UPDATE_SEARCH_STATUS"); } [Fact] public async Task UpdateSearchResultsAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.UpdateSearchResultsAsync(1, [1, 2, 3])); ex.QueryName.ShouldBe("SQL_UPDATE_SEARCH_RESULTS"); } #endregion #region Reference Data Lookup Exception Handling Tests [Fact] public async Task SearchItemsAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.SearchItemsAsync("test")); ex.QueryName.ShouldBe("SQL_SEARCH_ITEMS"); } [Fact] public async Task LookupItemsAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.LookupItemsAsync(["ITEM001"])); ex.QueryName.ShouldBe("SQL_LOOKUP_ITEMS"); } [Fact] public async Task LookupWorkordersAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.LookupWorkordersAsync([12345])); ex.QueryName.ShouldBe("SQL_LOOKUP_WORKORDERS"); } [Fact] public async Task SearchWorkCentersAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.SearchWorkCentersAsync("test")); ex.QueryName.ShouldBe("SQL_SEARCH_WORK_CENTERS"); } [Fact] public async Task LookupWorkCentersAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.LookupWorkCentersAsync(["WC01"])); ex.QueryName.ShouldBe("SQL_LOOKUP_WORK_CENTERS"); } [Fact] public async Task SearchProfitCentersAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.SearchProfitCentersAsync("test")); ex.QueryName.ShouldBe("SQL_SEARCH_PROFIT_CENTERS"); } [Fact] public async Task LookupProfitCentersAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.LookupProfitCentersAsync(["PC01"])); ex.QueryName.ShouldBe("SQL_LOOKUP_PROFIT_CENTERS"); } [Fact] public async Task SearchUsersAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.SearchUsersAsync("test")); ex.QueryName.ShouldBe("SQL_SEARCH_USERS"); } [Fact] public async Task LookupUsersAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.LookupUsersAsync(["USER01"])); ex.QueryName.ShouldBe("SQL_LOOKUP_USERS"); } [Fact] public async Task LookupLotsAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); var lots = new List { new LotViewModel { LotNumber = "LOT001", ItemNumber = "ITEM001" } }; // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.LookupLotsAsync(lots)); ex.QueryName.ShouldBe("SQL_LOOKUP_LOTS"); } #endregion #region Data Sync Exception Handling Tests [Fact] public async Task GetLastDataUpdatesAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.GetLastDataUpdatesAsync()); ex.QueryName.ShouldBe("SQL_GET_LAST_DATA_UPDATES"); } [Fact] public async Task GetTableSpecAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.GetTableSpecAsync("Item")); ex.QueryName.ShouldBe("SQL_GET_TABLE_COLUMNS"); } [Fact] public async Task PostProcessMisDataAsync_ConnectionFails_ThrowsQueryException() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert var ex = await Should.ThrowAsync( async () => await repository.PostProcessMisDataAsync()); ex.QueryName.ShouldBe("SQL_POSTPROCESS_MISDATA"); } #endregion #region Cancellation Tests [Fact] public async Task GetUserSearchesAsync_CancellationRequested_ThrowsOperationCanceledException() { // Arrange using var cts = new CancellationTokenSource(); cts.Cancel(); _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new OperationCanceledException(cts.Token)); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert await Should.ThrowAsync( async () => await repository.GetUserSearchesAsync("testuser", cts.Token)); } [Fact] public async Task GetQueuedSearchesAsync_CancellationRequested_ThrowsOperationCanceledException() { // Arrange using var cts = new CancellationTokenSource(); cts.Cancel(); _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new OperationCanceledException(cts.Token)); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act & Assert await Should.ThrowAsync( async () => await repository.GetQueuedSearchesAsync(cts.Token)); } #endregion #region Logging Tests [Fact] public async Task GetUserSearchesAsync_ConnectionFails_LogsError() { // Arrange _connectionFactory.CreateLotFinderConnectionAsync(Arg.Any()) .ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB")); var repository = new LotFinderRepository(_connectionFactory, _logger, _options); // Act try { await repository.GetUserSearchesAsync("testuser"); } catch (QueryException) { // Expected } // Assert - verify logging was called _logger.ReceivedWithAnyArgs(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>()); } #endregion }