From 8a6a792c0e786b1a952267f0df29a8e118b167ea Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Fri, 21 Aug 2026 19:15:07 +0100 Subject: [PATCH] better error handling with new tests --- .../ReportRequestHandlerTests.cs | 28 ++++++ .../SupportRequestHandlerTests.cs | 88 ++++++++++++++++++- .../TransactionRequestHandlerTests.cs | 17 +++- .../RequestHandlers/ReportRequestHandler.cs | 8 ++ .../RequestHandlers/SupportRequestHandler.cs | 81 ++++++++++------- .../TransactionRequestHandler.cs | 43 +++++++-- 6 files changed, 225 insertions(+), 40 deletions(-) diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/ReportRequestHandlerTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/ReportRequestHandlerTests.cs index 7638291c..1756205b 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/ReportRequestHandlerTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/ReportRequestHandlerTests.cs @@ -47,6 +47,34 @@ public async Task GetDailyPerformanceSummaryQuery_ReturnsMockedSummaryForToday() Times.Once); } + [Fact] + public async Task GetDailyPerformanceSummaryQuery_InvalidPeriod_ReturnsFailure() + { + Mock reportsService = new(); + Mock applicationCache = new(); + MerchantDetailsModel merchantDetails = new() + { + MerchantReportingId = 12345 + }; + + applicationCache.Setup(a => a.GetMerchantDetails()).Returns(merchantDetails); + + ReportRequestHandler handler = new(reportsService.Object, applicationCache.Object); + + Result result = await handler.Handle( + new ReportQueries.GetDailyPerformanceSummaryQuery((PerformanceSummaryPeriod)999), + CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + reportsService.Verify(r => r.GetDailyPerformanceSummary( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + [Fact] public async Task GetTransactionMixSummaryQuery_ReturnsRequestedBreakdown() { diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/SupportRequestHandlerTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/SupportRequestHandlerTests.cs index c7696e36..13902504 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/SupportRequestHandlerTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/SupportRequestHandlerTests.cs @@ -36,6 +36,27 @@ public async Task SupportRequestHandlerTests_UploadLogsRequest_NoLogs_Handle_IsH response.IsSuccess.ShouldBeTrue(); } + [Fact] + public async Task SupportRequestHandlerTests_UploadLogsRequest_NoConfiguration_ReturnsFailure() + { + Mock configurationService = new Mock(); + Func configurationServiceResolver = _ => configurationService.Object; + + Mock databaseContext = new Mock(); + Mock applicationCache = new Mock(); + applicationCache.Setup(s => s.GetConfiguration()).Returns((Configuration)null); + + SupportRequestHandler handler = new SupportRequestHandler(configurationServiceResolver, databaseContext.Object, applicationCache.Object); + + SupportCommands.UploadLogsCommand request = new(TestData.DeviceIdentifier); + + Result response = await handler.Handle(request, CancellationToken.None); + + response.IsFailed.ShouldBeTrue(); + configurationService.Verify(c => c.PostDiagnosticLogs(It.IsAny(), It.IsAny>(), It.IsAny()), Times.Never); + databaseContext.Verify(d => d.GetLogMessages(It.IsAny(), It.IsAny()), Times.Never); + } + [Fact] public async Task SupportRequestHandlerTests_UploadLogsRequest_LogsToUpload_Only10Messages_Handle_IsHandled() { @@ -117,6 +138,71 @@ public async Task SupportRequestHandlerTests_UploadLogsRequest_LogsToUpload_15Me databaseContext.Verify(d => d.RemoveUploadedMessages(It.IsAny>()), Times.Exactly(2)); } + [Fact] + public async Task SupportRequestHandlerTests_UploadLogsRequest_InvalidLogLevel_ReturnsFailure() + { + Mock configurationService = new Mock(); + Func configurationServiceResolver = _ => configurationService.Object; + + Mock databaseContext = new Mock(); + databaseContext.Setup(d => d.GetLogMessages(It.IsAny(), It.IsAny())).ReturnsAsync(new List + { + new Database.LogMessage { LogLevel = "NotALogLevel" } + }); + + Mock applicationCache = new Mock(); + applicationCache.Setup(s => s.GetConfiguration()).Returns(new Configuration()); + + SupportRequestHandler handler = new SupportRequestHandler(configurationServiceResolver, databaseContext.Object, applicationCache.Object); + + SupportCommands.UploadLogsCommand request = new(TestData.DeviceIdentifier); + + Result response = await handler.Handle(request, CancellationToken.None); + + response.IsFailed.ShouldBeTrue(); + configurationService.Verify(c => c.PostDiagnosticLogs(It.IsAny(), It.IsAny>(), It.IsAny()), Times.Never); + databaseContext.Verify(d => d.RemoveUploadedMessages(It.IsAny>()), Times.Never); + } + + [Fact] + public async Task SupportRequestHandlerTests_UploadLogsRequest_CancellationStopsFurtherProcessing() + { + Mock configurationService = new Mock(); + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + configurationService.Setup(c => c.PostDiagnosticLogs(It.IsAny(), It.IsAny>(), It.IsAny())) + .Callback, CancellationToken>((_, _, _) => + { + cancellationTokenSource.Cancel(); + }) + .ReturnsAsync(Result.Success()); + + Func configurationServiceResolver = _ => configurationService.Object; + + Mock databaseContext = new Mock(); + databaseContext.SetupSequence(d => d.GetLogMessages(It.IsAny(), It.IsAny())) + .ReturnsAsync(new List + { + new Database.LogMessage { LogLevel = LogLevel.Debug.ToString() } + }) + .ReturnsAsync(new List + { + new Database.LogMessage { LogLevel = LogLevel.Debug.ToString() } + }); + + Mock applicationCache = new Mock(); + applicationCache.Setup(s => s.GetConfiguration()).Returns(new Configuration()); + + SupportRequestHandler handler = new SupportRequestHandler(configurationServiceResolver, databaseContext.Object, applicationCache.Object); + + SupportCommands.UploadLogsCommand request = new(TestData.DeviceIdentifier); + + await Should.ThrowAsync(async () => await handler.Handle(request, cancellationTokenSource.Token)); + + configurationService.Verify(c => c.PostDiagnosticLogs(It.IsAny(), It.IsAny>(), cancellationTokenSource.Token), Times.Once); + databaseContext.Verify(d => d.GetLogMessages(It.IsAny(), It.IsAny()), Times.Once); + databaseContext.Verify(d => d.RemoveUploadedMessages(It.IsAny>()), Times.Once); + } + [Theory] [InlineData(true, 4)] [InlineData(false, 3)] @@ -154,4 +240,4 @@ public async Task SupportRequestHandlerTests_ViewLogsRequest_Handle_IsHandled(Bo result.IsSuccess.ShouldBeTrue(); result.Data.Count.ShouldBe(expectedNumberMessages); } -} \ No newline at end of file +} diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/TransactionRequestHandlerTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/TransactionRequestHandlerTests.cs index 1dd34a34..bc64eb14 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/TransactionRequestHandlerTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/TransactionRequestHandlerTests.cs @@ -66,7 +66,7 @@ public async Task TransactionRequestHandler_LogonTransactionRequest_Handle_IsHan public async Task TransactionRequestHandler_LogonTransactionRequest_Handle_LogonFailed_IsHandled() { this.TransactionService.Setup(t => t.PerformLogon(It.IsAny(), It.IsAny())).ReturnsAsync( - Result.Success(TestData.PerformLogonResponseFailedModel)); + Result.Success(TestData.PerformLogonResponseFailedModel)); TransactionCommands.PerformLogonCommand request = new(TestData.TransactionDateTime); @@ -75,6 +75,21 @@ public async Task TransactionRequestHandler_LogonTransactionRequest_Handle_Logon result.IsSuccess.ShouldBeFalse(); } + [Fact] + public async Task TransactionRequestHandler_LogonTransactionRequest_Handle_ServiceFailure_ReturnsFailure_AndDoesNotUpdateTransaction() + { + this.DatabaseContext.Setup(d => d.CreateTransaction(It.IsAny())).ReturnsAsync(1); + this.TransactionService.Setup(t => t.PerformLogon(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Failure("Logon failed")); + + TransactionCommands.PerformLogonCommand request = new(TestData.TransactionDateTime); + + Result result = await this.TransactionRequestHandler.Handle(request, CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + this.DatabaseContext.Verify(d => d.UpdateTransaction(It.IsAny()), Times.Never); + } + [Fact] public async Task TransactionRequestHandler_PerformMobileTopupRequest_Handle_IsHandled() { diff --git a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/ReportRequestHandler.cs b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/ReportRequestHandler.cs index 828cec08..a6299229 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/ReportRequestHandler.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/ReportRequestHandler.cs @@ -23,6 +23,14 @@ public async Task> Handle(ReportQueries.Get DateTime current = DateTime.Now.Date; + if (request.Period is not (PerformanceSummaryPeriod.Today or + PerformanceSummaryPeriod.Yesterday or + PerformanceSummaryPeriod.ThisWeek or + PerformanceSummaryPeriod.MonthToDate)) + { + return Result.Failure("Invalid performance summary period."); + } + (DateTime startDate, DateTime endDate) dates = request.Period switch { PerformanceSummaryPeriod.Today => (current, current), PerformanceSummaryPeriod.Yesterday => (current.AddDays(-1), current.AddDays(-1)), diff --git a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/SupportRequestHandler.cs b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/SupportRequestHandler.cs index 8f53b432..a4012a1f 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/SupportRequestHandler.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/SupportRequestHandler.cs @@ -25,35 +25,36 @@ public SupportRequestHandler(Func configurationS this.ApplicationCache = applicationCache; } - public async Task Handle(SupportCommands.UploadLogsCommand request, CancellationToken cancellationToken) + public async Task Handle(SupportCommands.UploadLogsCommand request, CancellationToken cancellationToken) + { + Boolean useTrainingMode = this.ApplicationCache.GetUseTrainingMode(); + Configuration configuration = this.ApplicationCache.GetConfiguration(); + if (configuration == null) { - Boolean useTrainingMode = this.ApplicationCache.GetUseTrainingMode(); - Configuration configuration = this.ApplicationCache.GetConfiguration(); - while (true) { - IConfigurationService configurationService = this.ConfigurationServiceResolver(useTrainingMode); - - List logEntries = await this.DatabaseContext.GetLogMessages(configuration.LogMessageBatchSize.GetValueOrDefault(10), useTrainingMode); + return Result.Failure("Configuration is not available."); + } - if (logEntries.Any() == false) { - break; - } + while (true) { + cancellationToken.ThrowIfCancellationRequested(); + IConfigurationService configurationService = this.ConfigurationServiceResolver(useTrainingMode); - List logMessageModels = new(); + List logEntries = await this.DatabaseContext.GetLogMessages(configuration.LogMessageBatchSize.GetValueOrDefault(10), useTrainingMode); - logEntries.ForEach(l => logMessageModels.Add(new Models.LogMessage { - LogLevel = Enum.Parse(l.LogLevel), - LogLevelString = l.LogLevel, - Message = l.Message, - EntryDateTime = l.EntryDateTime, - Id = l.Id - })); + if (logEntries.Any() == false) { + break; + } + Result> logMessageModelsResult = this.TryBuildLogMessageModels(logEntries); + if (logMessageModelsResult.IsFailed) + { + return Result.Failure(logMessageModelsResult.Errors.FirstOrDefault() ?? "Unable to map log messages."); + } - Result result = await configurationService.PostDiagnosticLogs(request.DeviceIdentifier, logMessageModels, CancellationToken.None); + Result result = await configurationService.PostDiagnosticLogs(request.DeviceIdentifier, logMessageModelsResult.Data, cancellationToken); - if (result.IsFailed) { - // We have had a failure posting the logs so we will stop trying to upload any more logs - return result; + if (result.IsFailed) { + // We have had a failure posting the logs so we will stop trying to upload any more logs + return result; } // Clear the logs that have been uploaded @@ -70,17 +71,37 @@ public async Task Handle(SupportCommands.UploadLogsCommand request, Canc List logEntries = await this.DatabaseContext.GetLogMessages(50, useTrainingMode); + Result> logMessageModelsResult = this.TryBuildLogMessageModels(logEntries); + if (logMessageModelsResult.IsFailed) + { + return Result.Failure(logMessageModelsResult.Errors.FirstOrDefault() ?? "Unable to map log messages."); + } + + return Result.Success(logMessageModelsResult.Data.OrderByDescending(l => l.EntryDateTime).ToList()); + } + + private Result> TryBuildLogMessageModels(List logEntries) + { List logMessageModels = new(); - logEntries.ForEach(l => logMessageModels.Add(new Models.LogMessage { - LogLevel = Enum.Parse(l.LogLevel), - LogLevelString = l.LogLevel, - Message = l.Message, - EntryDateTime = l.EntryDateTime, - Id = l.Id - })); + foreach (LogMessage logEntry in logEntries) + { + if (Enum.TryParse(logEntry.LogLevel, ignoreCase: true, out Models.LogLevel logLevel) == false) + { + return Result.Failure($"Invalid log level '{logEntry.LogLevel}'."); + } + + logMessageModels.Add(new Models.LogMessage + { + LogLevel = logLevel, + LogLevelString = logEntry.LogLevel, + Message = logEntry.Message, + EntryDateTime = logEntry.EntryDateTime, + Id = logEntry.Id + }); + } - return Result.Success(logMessageModels.OrderByDescending(l => l.EntryDateTime).ToList()); + return Result.Success(logMessageModels); } } } diff --git a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs index 5968e397..f2e1c95b 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs @@ -74,10 +74,14 @@ public async Task> Handle(TransactionCom }; Result result = await transactionService.PerformMobileTopup(model, cancellationToken); - + if (result.IsFailed) + { + return result; + } + await this.UpdateTransactionRecord(transaction.transactionRecord.UpdateFrom(result)); - if (result.IsSuccess && result.Data.IsSuccessful == false) + if (result.Data.IsSuccessful == false) { return Result.Failure("Logon transaction not successful"); } @@ -118,10 +122,13 @@ public async Task> Handle(TransactionCommands. Result result = await transactionService.PerformLogon(model, cancellationToken); + if (result.IsFailed) + return result; + await this.UpdateTransactionRecord(transaction.transactionRecord.UpdateFrom(result)); - if (result.IsSuccess && result.Data.IsSuccessful == false) + if (result.Data.IsSuccessful == false) { - return Result.Failure($"Logon transaction not successful {StringSerialiser.Serialise(result.Data)}"); + return Result.Failure($"Logon transaction not successful {StringSerialiser.Serialise(result.Data)}"); } return Result.Success(result.Data); @@ -151,10 +158,14 @@ public async Task> Handle(TransactionCo }; Result result = await transactionService.PerformVoucherIssue(model, cancellationToken); + if (result.IsFailed) + { + return result; + } await this.UpdateTransactionRecord(transaction.transactionRecord.UpdateFrom(result)); - if (result.IsSuccess && result.Data.IsSuccessful == false) + if (result.Data.IsSuccessful == false) { return Result.Failure("Voucher Issue not successful"); } @@ -183,6 +194,10 @@ public async Task> Handle(Tran }; Result result = await transactionService.PerformBillPaymentGetAccount(model, cancellationToken); + if (result.IsFailed) + { + return result; + } await this.UpdateTransactionRecord(transaction.transactionRecord.UpdateFrom(result)); @@ -275,10 +290,14 @@ public async Task> Handle(Tra }; Result result = await transactionService.PerformBillPaymentMakePayment(model, cancellationToken); + if (result.IsFailed) + { + return result; + } await this.UpdateTransactionRecord(transaction.transactionRecord.UpdateFrom(result)); - if (result.IsSuccess && result.Data.IsSuccessful == false) + if (result.Data.IsSuccessful == false) { return Result.Failure("Bill Payment not successful"); } @@ -311,10 +330,14 @@ public async Task> Handle(Tra }; Result result = await transactionService.PerformBillPaymentMakePayment(model, cancellationToken); + if (result.IsFailed) + { + return result; + } await this.UpdateTransactionRecord(transaction.transactionRecord.UpdateFrom(result)); - if (result.IsSuccess && result.Data.IsSuccessful == false) + if (result.Data.IsSuccessful == false) { return Result.Failure("Bill Payment not successful"); } @@ -343,9 +366,13 @@ public async Task> Handle(Transa }; Result result = await transactionService.PerformBillPaymentGetMeter(model, cancellationToken); + if (result.IsFailed) + { + return result; + } await this.UpdateTransactionRecord(transaction.transactionRecord.UpdateFrom(result)); return result; } -} \ No newline at end of file +}