From f344ece2439b59ea42334ef10c0ae2d576df7613 Mon Sep 17 00:00:00 2001 From: Marvin Hochstetter Date: Wed, 23 Oct 2024 15:40:55 +0200 Subject: [PATCH] Restructured error management in BackendAccess; Introduced BackendMoodleApiUnreachableException.cs and new error message in LmsLoginDialog.razor --- BackendAccess/BackendEntities/ErrorBE.cs | 4 ++ .../BackendServices/UserWebApiServices.cs | 8 ++-- .../BackendServices/UserWebApiServicesUt.cs | 29 +++++++++++- .../BackendHttpRequestException.cs | 15 +++++++ .../BackendMoodleApiUnreachableException.cs | 16 +++++++ .../BackendAccess/ErrorCodes.cs | 44 +++++++++++++++++++ IntegrationTest/Dialogues/LmsLoginDialogIt.cs | 23 ++++++++++ .../Components/Dialogues/LmsLoginDialog.razor | 11 +++++ .../Dialogues/LmsLoginDialog.de.resx | 3 ++ .../Dialogues/LmsLoginDialog.en.resx | 3 ++ 10 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 BusinessLogic/ErrorManagement/BackendAccess/BackendHttpRequestException.cs create mode 100644 BusinessLogic/ErrorManagement/BackendAccess/BackendMoodleApiUnreachableException.cs create mode 100644 BusinessLogic/ErrorManagement/BackendAccess/ErrorCodes.cs diff --git a/BackendAccess/BackendEntities/ErrorBE.cs b/BackendAccess/BackendEntities/ErrorBE.cs index 2ce68db83..d9567a781 100644 --- a/BackendAccess/BackendEntities/ErrorBE.cs +++ b/BackendAccess/BackendEntities/ErrorBE.cs @@ -7,4 +7,8 @@ public class ErrorBE { // ReSharper disable once UnassignedGetOnlyAutoProperty public string? Detail { set; get; } + + public string? Type { set; get; } + + public string? StatusCode { set; get; } } \ No newline at end of file diff --git a/BackendAccess/BackendServices/UserWebApiServices.cs b/BackendAccess/BackendServices/UserWebApiServices.cs index 95528ec0c..bdffb6d7d 100644 --- a/BackendAccess/BackendServices/UserWebApiServices.cs +++ b/BackendAccess/BackendServices/UserWebApiServices.cs @@ -109,10 +109,12 @@ public async Task GetUserInformationAsync(string token) return await SendHttpGetRequestAsync("Users/UserData", parameters); } - catch (HttpRequestException httpReqEx) + catch (BackendHttpRequestException httpReqEx) { - if (httpReqEx.Message == "The provided token is invalid") + if (httpReqEx.ErrorType == ErrorCodes.LmsTokenInvalid) throw new BackendInvalidTokenException(httpReqEx.Message, httpReqEx); + if (httpReqEx.ErrorType == ErrorCodes.LmsError) + throw new BackendMoodleApiUnreachableException(httpReqEx.Message, httpReqEx); throw; } } @@ -379,7 +381,7 @@ private async Task HandleErrorMessage(HttpResponseMessage apiResp) var error = await apiResp.Content.ReadAsStringAsync(); var problemDetails = TryRead(error); - throw new HttpRequestException(problemDetails.Detail, null, apiResp.StatusCode); + throw new BackendHttpRequestException(problemDetails.Detail, null, apiResp.StatusCode, problemDetails.Type); } diff --git a/BackendAccessTest/BackendServices/UserWebApiServicesUt.cs b/BackendAccessTest/BackendServices/UserWebApiServicesUt.cs index 9107c2fb5..d7afbd265 100644 --- a/BackendAccessTest/BackendServices/UserWebApiServicesUt.cs +++ b/BackendAccessTest/BackendServices/UserWebApiServicesUt.cs @@ -274,7 +274,7 @@ public void GetUserTokenAsync_AnyOtherHttpRequestException_ThrowsException() var userWebApiServices = CreateTestableUserWebApiServices(httpClientFactory: mockHttpClientFactory); - var ex = Assert.ThrowsAsync(async () => + var ex = Assert.ThrowsAsync(async () => await userWebApiServices.GetUserTokenAsync("username", "password")); Assert.That(ex!.Message, Is.EqualTo("Error Message")); } @@ -322,7 +322,7 @@ public void GetUserInformationAsync_InvalidToken_ThrowsException() { var mockedHttp = new MockHttpMessageHandler(); - var exception = new HttpRequestException("The provided token is invalid"); + var exception = new BackendHttpRequestException("The provided token is invalid", null, null, ErrorCodes.LmsTokenInvalid); mockedHttp .When("*") .Throw(exception); @@ -338,6 +338,31 @@ public void GetUserInformationAsync_InvalidToken_ThrowsException() Assert.That(ex!.Message, Is.EqualTo("The provided token is invalid")); Assert.That(ex.InnerException, Is.EqualTo(exception)); } + + [Test] + // ANF-ID: [AHO21] + public void GetUserInformationAsync_MoodleUnreachable_ThrowsException() + { + var mockedHttp = new MockHttpMessageHandler(); + + var exception = new BackendHttpRequestException( + "Das Ergebnis der Moodle Web Api konnte nicht gelesen werden. Response string is: 404 page not found\n", + null, null, ErrorCodes.LmsError); + mockedHttp + .When("*") + .Throw(exception); + var mockHttpClientFactory = Substitute.For(); + mockHttpClientFactory + .CreateClient(Arg.Any()) + .Returns(mockedHttp.ToHttpClient()); + + var userWebApiServices = CreateTestableUserWebApiServices(httpClientFactory: mockHttpClientFactory); + + var ex = Assert.ThrowsAsync(async () => + await userWebApiServices.GetUserInformationAsync("token")); + Assert.That(ex!.Message, Is.EqualTo("Das Ergebnis der Moodle Web Api konnte nicht gelesen werden. Response string is: 404 page not found\n")); + Assert.That(ex.InnerException, Is.EqualTo(exception)); + } [Test] // ANF-ID: [AHO21] diff --git a/BusinessLogic/ErrorManagement/BackendAccess/BackendHttpRequestException.cs b/BusinessLogic/ErrorManagement/BackendAccess/BackendHttpRequestException.cs new file mode 100644 index 000000000..2a96474fa --- /dev/null +++ b/BusinessLogic/ErrorManagement/BackendAccess/BackendHttpRequestException.cs @@ -0,0 +1,15 @@ +using System.Net; + +namespace BusinessLogic.ErrorManagement.BackendAccess; + +public class BackendHttpRequestException : HttpRequestException +{ + + public string? ErrorType { get; set; } + + public BackendHttpRequestException(string? message, Exception? inner, HttpStatusCode? statusCode, string? type) + : base(message, inner, statusCode) + { + ErrorType = type; + } +} \ No newline at end of file diff --git a/BusinessLogic/ErrorManagement/BackendAccess/BackendMoodleApiUnreachableException.cs b/BusinessLogic/ErrorManagement/BackendAccess/BackendMoodleApiUnreachableException.cs new file mode 100644 index 000000000..2a9a79e93 --- /dev/null +++ b/BusinessLogic/ErrorManagement/BackendAccess/BackendMoodleApiUnreachableException.cs @@ -0,0 +1,16 @@ +namespace BusinessLogic.ErrorManagement.BackendAccess; + +public class BackendMoodleApiUnreachableException : Exception +{ + public BackendMoodleApiUnreachableException() + { + } + + public BackendMoodleApiUnreachableException(string message) : base(message) + { + } + + public BackendMoodleApiUnreachableException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/BusinessLogic/ErrorManagement/BackendAccess/ErrorCodes.cs b/BusinessLogic/ErrorManagement/BackendAccess/ErrorCodes.cs new file mode 100644 index 000000000..d4673310f --- /dev/null +++ b/BusinessLogic/ErrorManagement/BackendAccess/ErrorCodes.cs @@ -0,0 +1,44 @@ +namespace BusinessLogic.ErrorManagement.BackendAccess; + +public static class ErrorCodes +{ + /// + /// Error code for using an invalid token + /// + public const string LmsTokenInvalid = "invalid_token"; + + /// + /// Error code for wrong username or password + /// + public const string InvalidLogin = "invalid_login"; + + /// + /// Error if a resource is not found + /// + public const string NotFound = "not_found"; + + /// + /// Error if a resource is forbidden + /// + public const string Forbidden = "forbidden"; + + /// + /// Error if a request is invalid + /// + public const string ValidationError = "validation_error"; + + /// + /// Error code for unknown errors + /// + public const string UnknownError = "unknown_error"; + + /// + /// Error code for generic LMS errors + /// + public const string LmsError = "lms_error"; + + /// + /// Error code when a world could not be created + /// + public const string WorldCreationErrorDuplicate = "world_creation_error"; +} \ No newline at end of file diff --git a/IntegrationTest/Dialogues/LmsLoginDialogIt.cs b/IntegrationTest/Dialogues/LmsLoginDialogIt.cs index b0ffe37f8..5f24eca08 100644 --- a/IntegrationTest/Dialogues/LmsLoginDialogIt.cs +++ b/IntegrationTest/Dialogues/LmsLoginDialogIt.cs @@ -175,6 +175,29 @@ public async Task DialogCreated_IsLmsConnectedThrowsBackendApiUnreachableExcepti Assert.That(errorText, Is.EqualTo("API is unreachable")); }); } + + [Test] + // ANF-ID: [AHO21] + public async Task DialogCreated_IsLmsConnectedThrowsMoodleUnreachableException_ShowsErrorMessage() + { + _presentationLogic.IsLmsConnected().Throws(x => throw new BackendMoodleApiUnreachableException()); + + Localizer["DialogContent.Error.MoodleUnreachable"] + .Returns(new LocalizedString("DialogContent.Error.MoodleUnreachable","Moodle is unreachable")); + + await OpenDialogAndGetDialogReferenceAsync(); + + var mudTexts = DialogProvider.FindComponents(); + Assert.That(mudTexts, Has.Count.EqualTo(5)); + DialogProvider.WaitForAssertion(() => + { + var errorElement = DialogProvider.Find("h6.mud-error-text"); + Assert.That(errorElement, Is.Not.Null); + + var errorText = errorElement.TextContent.Trim(); + Assert.That(errorText, Is.EqualTo("Moodle is unreachable")); + }); + } [Test] // ANF-ID: [AHO21] diff --git a/Presentation/Components/Dialogues/LmsLoginDialog.razor b/Presentation/Components/Dialogues/LmsLoginDialog.razor index 8856733c8..0bf155aac 100644 --- a/Presentation/Components/Dialogues/LmsLoginDialog.razor +++ b/Presentation/Components/Dialogues/LmsLoginDialog.razor @@ -178,6 +178,12 @@ @Localizer["DialogContent.Error.TokenInvalid"] } + @if (_showErrorMoodleUnreachable) + { + + }
@@ -262,6 +268,7 @@ private bool _isPasswordVisible = false; private bool _showErrorTokenInvalid = false; + private bool _showErrorMoodleUnreachable = false; private bool _spinnerActive = false; private InputType PasswordInputType => _isPasswordVisible ? InputType.Text : InputType.Password; private string ShowPasswordIcon => _isPasswordVisible ? Icons.Material.Filled.Visibility : Icons.Material.Filled.VisibilityOff; @@ -307,6 +314,10 @@ _showErrorTokenInvalid = true; Logout(); } + catch (BackendMoodleApiUnreachableException) + { + _showErrorMoodleUnreachable = true; + } finally { StopSpinner(); diff --git a/Presentation/Resources/Components/Dialogues/LmsLoginDialog.de.resx b/Presentation/Resources/Components/Dialogues/LmsLoginDialog.de.resx index bd4101e9a..e9a798184 100644 --- a/Presentation/Resources/Components/Dialogues/LmsLoginDialog.de.resx +++ b/Presentation/Resources/Components/Dialogues/LmsLoginDialog.de.resx @@ -132,4 +132,7 @@ Mit dieser ausgeführten Aktion des Löschens werden die Kurse unwiderruflich au Fehler beim Versuch, die Liste der LMS-Welten abzurufen + + Moodle ist nicht erreichbar. + \ No newline at end of file diff --git a/Presentation/Resources/Components/Dialogues/LmsLoginDialog.en.resx b/Presentation/Resources/Components/Dialogues/LmsLoginDialog.en.resx index 559fe7064..b365c3001 100644 --- a/Presentation/Resources/Components/Dialogues/LmsLoginDialog.en.resx +++ b/Presentation/Resources/Components/Dialogues/LmsLoginDialog.en.resx @@ -120,4 +120,7 @@ This deletion action also irrevocably removes the courses from Moodle. Error while trying to get the LMS world list + + Couldn't reach Moodle. + \ No newline at end of file