From e71a64160ca83a78f4cb14358cefe7bc43bf85e5 Mon Sep 17 00:00:00 2001 From: Joao Rua Date: Fri, 26 Jul 2024 17:24:22 +0100 Subject: [PATCH 1/3] error handling for domain file store upload --- .../Components/Common/FormButtons.razor.css | 10 ---------- .../EngineeringModel/FileStore/FileForm.razor | 16 +++++++++++++++- .../FileStore/FileForm.razor.cs | 17 +++++++++++++++-- .../FileHandler/FileHandlerViewModel.cs | 19 +++++++++++++++---- .../FileHandler/IFileHandlerViewModel.cs | 6 ++++-- COMETwebapp/wwwroot/css/app.css | 13 ++++++++++++- 6 files changed, 61 insertions(+), 20 deletions(-) delete mode 100644 COMETwebapp/Components/Common/FormButtons.razor.css diff --git a/COMETwebapp/Components/Common/FormButtons.razor.css b/COMETwebapp/Components/Common/FormButtons.razor.css deleted file mode 100644 index 01159ef6..00000000 --- a/COMETwebapp/Components/Common/FormButtons.razor.css +++ /dev/null @@ -1,10 +0,0 @@ -.validation-container { - border: dashed 1px red; - padding-top: 1rem; - padding-right: 1rem; - border-radius: 10px; -} - -.validation-container:not(:has(li)) { - display: none; -} \ No newline at end of file diff --git a/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor b/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor index c6f45809..633295bc 100644 --- a/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor @@ -54,7 +54,21 @@ Copyright (c) 2023-2024 Starion Group S.A.
- + +
+
    + + @if (!this.ViewModel.SelectedFileRevisions.Any()) + { +
  • At least one file revision should exist
  • + } + @if (!string.IsNullOrWhiteSpace(this.ErrorMessage)) + { +
  • @(this.ErrorMessage)
  • + } +
+
+
/// Support class for the /// @@ -47,14 +49,25 @@ public partial class FileForm : SelectedDataItemForm /// public bool IsDeletePopupVisible { get; private set; } + /// + /// Gets the error message from the , if any + /// + public string ErrorMessage { get; private set; } = string.Empty; + /// /// Method that is executed when there is a valid submit /// /// A protected override async Task OnValidSubmit() { - await this.ViewModel.CreateOrEditFile(this.ShouldCreate); - await base.OnValidSubmit(); + this.ErrorMessage = string.Empty; + var result = await this.ViewModel.CreateOrEditFile(this.ShouldCreate); + this.ErrorMessage = string.Join(", ", result.Reasons.OfType().Select(x => x.Exception.Message)); + + if (string.IsNullOrWhiteSpace(this.ErrorMessage)) + { + await base.OnValidSubmit(); + } } /// diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs index 655baa4e..db85d132 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs @@ -36,6 +36,8 @@ namespace COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileHandl using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileRevisionHandler; + using FluentResults; + using Microsoft.AspNetCore.Components; /// @@ -149,8 +151,8 @@ public async Task MoveFile(File file, Folder targetFolder) /// Creates or edits a file /// /// The condition to check if the file should be created - /// A - public async Task CreateOrEditFile(bool shouldCreate) + /// A containing a + public async Task CreateOrEditFile(bool shouldCreate) { this.IsLoading = true; this.logger.LogInformation("Creating or editing file"); @@ -193,11 +195,20 @@ public async Task CreateOrEditFile(bool shouldCreate) } thingsToUpdate.Add(this.CurrentThing); + var result = await this.SessionService.CreateOrUpdateThings(fileStoreClone, thingsToUpdate, newFileRevisions.Select(x => x.LocalPath).ToList()); - await this.SessionService.CreateOrUpdateThings(fileStoreClone, thingsToUpdate, newFileRevisions.Select(x => x.LocalPath).ToList()); + if (result.IsSuccess) + { + this.logger.LogInformation("File with iid {iid} updated successfully", this.CurrentThing.Iid); + } + else + { + this.logger.LogWarning("File could not be created. {warning}", string.Join(", ", string.Join(", ", result.Reasons.Select(x => x.Message)))); + } - this.logger.LogInformation("File with iid {iid} updated successfully", this.CurrentThing.Iid); this.IsLoading = false; + + return result; } /// diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/IFileHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/IFileHandlerViewModel.cs index aef92ea7..dd1eb662 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/IFileHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/IFileHandlerViewModel.cs @@ -32,6 +32,8 @@ namespace COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileHandl using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileRevisionHandler; + using FluentResults; + /// /// View model used to manage the files in Filestores /// @@ -96,8 +98,8 @@ public interface IFileHandlerViewModel : IApplicationBaseViewModel /// Creates a file into a target folder /// /// - /// A - Task CreateOrEditFile(bool shouldCreate); + /// A containing a + Task CreateOrEditFile(bool shouldCreate); /// /// Deletes the current file diff --git a/COMETwebapp/wwwroot/css/app.css b/COMETwebapp/wwwroot/css/app.css index dd2b31a0..5fbf33b6 100644 --- a/COMETwebapp/wwwroot/css/app.css +++ b/COMETwebapp/wwwroot/css/app.css @@ -290,4 +290,15 @@ sub { .model-editor-details { flex: 1 1 40% !important; -} \ No newline at end of file +} + +.validation-container { + border: dashed 1px red; + padding-top: 1rem; + padding-right: 1rem; + border-radius: 10px; +} + + .validation-container:not(:has(li)) { + display: none; + } \ No newline at end of file From 4f94992d21595d85d3d2bede6124046ade606254 Mon Sep 17 00:00:00 2001 From: Joao Rua Date: Mon, 29 Jul 2024 09:40:15 +0100 Subject: [PATCH 2/3] unit tests --- .../FileStore/FileFormTestFixture.cs | 42 +++++++-- .../FileHandlerViewModelTestFixture.cs | 88 +++++++++++-------- 2 files changed, 83 insertions(+), 47 deletions(-) diff --git a/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileFormTestFixture.cs b/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileFormTestFixture.cs index d1bf4e3e..e4812c8e 100644 --- a/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileFormTestFixture.cs +++ b/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileFormTestFixture.cs @@ -36,6 +36,8 @@ namespace COMETwebapp.Tests.Components.EngineeringModel.FileStore using DevExpress.Blazor; + using FluentResults; + using Microsoft.AspNetCore.Components.Forms; using Moq; @@ -55,10 +57,14 @@ public class FileFormTestFixture public void SetUp() { this.context = new TestContext(); + + var domainSelectorViewModel = new Mock(); + this.viewModel = new Mock(); this.viewModel.Setup(x => x.CurrentThing).Returns(new File()); - var domainSelectorViewModel = new Mock(); this.viewModel.Setup(x => x.DomainOfExpertiseSelectorViewModel).Returns(domainSelectorViewModel.Object); + this.viewModel.Setup(x => x.CreateOrEditFile(It.IsAny())).ReturnsAsync(new Result()); + this.context.ConfigureDevExpressBlazor(); this.renderer = this.context.RenderComponent(parameters => { parameters.Add(p => p.ViewModel, this.viewModel.Object); }); @@ -72,14 +78,8 @@ public void Teardown() } [Test] - public async Task VerifyOnValidSubmitAndDelete() + public async Task VerifyFileFormDelete() { - Assert.That(this.renderer.Instance.IsDeletePopupVisible, Is.EqualTo(false)); - - var form = this.renderer.FindComponent(); - await this.renderer.InvokeAsync(form.Instance.OnValidSubmit.InvokeAsync); - this.viewModel.Verify(x => x.CreateOrEditFile(It.IsAny()), Times.Once); - var deleteFileButton = this.renderer.FindComponents().First(x => x.Instance.Id == "deleteFileButton"); await this.renderer.InvokeAsync(deleteFileButton.Instance.Click.InvokeAsync); Assert.That(this.renderer.Instance.IsDeletePopupVisible, Is.EqualTo(true)); @@ -93,5 +93,31 @@ public async Task VerifyOnValidSubmitAndDelete() this.viewModel.Verify(x => x.DeleteFile(), Times.Once); }); } + + [Test] + public async Task VerifyOnValidSubmitAndDelete() + { + Assert.That(this.renderer.Instance.IsDeletePopupVisible, Is.EqualTo(false)); + + var form = this.renderer.FindComponent(); + await this.renderer.InvokeAsync(form.Instance.OnValidSubmit.InvokeAsync); + + Assert.Multiple(() => + { + this.viewModel.Verify(x => x.CreateOrEditFile(It.IsAny()), Times.Once); + Assert.That(this.renderer.Instance.ErrorMessage, Is.Null.Or.Empty); + }); + + var failingResult = new Result(); + failingResult.Reasons.Add(new ExceptionalError(new Exception("Invalid"))); + this.viewModel.Setup(x => x.CreateOrEditFile(It.IsAny())).ReturnsAsync(failingResult); + await this.renderer.InvokeAsync(form.Instance.OnValidSubmit.InvokeAsync); + + Assert.Multiple(() => + { + this.viewModel.Verify(x => x.CreateOrEditFile(It.IsAny()), Times.Exactly(2)); + Assert.That(this.renderer.Instance.ErrorMessage, Is.Not.Null.Or.Empty); + }); + } } } diff --git a/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FileHandlerViewModelTestFixture.cs b/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FileHandlerViewModelTestFixture.cs index 495f77fc..0db7f6f1 100644 --- a/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FileHandlerViewModelTestFixture.cs +++ b/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FileHandlerViewModelTestFixture.cs @@ -1,18 +1,18 @@ // -------------------------------------------------------------------------------------------------------------------- // -// Copyright (c) 2023-2024 Starion Group S.A. +// Copyright (c) 2024 Starion Group S.A. // -// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua // -// This file is part of CDP4-COMET WEB Community Edition -// The CDP4-COMET WEB Community Edition is the Starion Web Application implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This file is part of COMET WEB Community Edition +// The COMET WEB Community Edition is the Starion Group Web Application implementation of ECSS-E-TM-10-25 Annex A and Annex C. // -// The CDP4-COMET WEB Community Edition is free software; you can redistribute it and/or +// The COMET WEB Community Edition is free software; you can redistribute it and/or // modify it under the terms of the GNU Affero General Public // License as published by the Free Software Foundation; either // version 3 of the License, or (at your option) any later version. // -// The CDP4-COMET WEB Community Edition is distributed in the hope that it will be useful, +// The COMET WEB Community Edition is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Affero General Public License for more details. @@ -35,6 +35,8 @@ namespace COMETwebapp.Tests.ViewModels.Components.EngineeringModel.FileStore using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileHandler; using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileRevisionHandler; + using FluentResults; + using Microsoft.Extensions.Logging; using Moq; @@ -63,11 +65,11 @@ public void Setup() this.iteration = new Iteration(); var person = new Person(); - var siteDirectory = new SiteDirectory() + var siteDirectory = new SiteDirectory { Domain = { - new DomainOfExpertise() + new DomainOfExpertise { ShortName = "doe", Name = "Domain Of Expertise" @@ -75,24 +77,24 @@ public void Setup() } }; - var engineeringModelSetup = new EngineeringModelSetup() + var engineeringModelSetup = new EngineeringModelSetup { Participant = { new Participant { Person = person } } }; - var folder1 = new Folder() + var folder1 = new Folder { Iid = Guid.NewGuid(), Name = "folder 1" }; - var file1 = new File() + var file1 = new File { - Iid = Guid.NewGuid(), + Iid = Guid.NewGuid(), CurrentContainingFolder = folder1 }; - var file2 = new File() + var file2 = new File { Iid = Guid.NewGuid(), CurrentContainingFolder = folder1 @@ -100,7 +102,7 @@ public void Setup() file1.FileRevision.Add(new FileRevision()); - this.commonFileStore = new CommonFileStore() + this.commonFileStore = new CommonFileStore { Name = "CFS", Folder = { folder1 }, @@ -111,6 +113,8 @@ public void Setup() this.sessionService.Setup(x => x.GetSiteDirectory()).Returns(siteDirectory); this.sessionService.Setup(x => x.Session.ActivePerson).Returns(person); + this.sessionService.Setup(x => x.CreateOrUpdateThings(It.IsAny(), It.IsAny>(), It.IsAny>())).ReturnsAsync(new Result()); + this.viewModel = new FileHandlerViewModel(this.sessionService.Object, this.messageBus, this.logger.Object, this.fileRevisionHandlerViewModel.Object); } @@ -121,30 +125,11 @@ public void TearDown() this.messageBus.Dispose(); } - [Test] - public void VerifyInitializeViewModel() - { - this.viewModel.InitializeViewModel(this.commonFileStore, this.iteration); - Assert.That(this.viewModel.DomainOfExpertiseSelectorViewModel.AvailableDomainsOfExpertise.Count(), Is.EqualTo(1)); - } - - [Test] - public async Task VerifyMoveAndDeleteFile() - { - this.viewModel.InitializeViewModel(this.commonFileStore, this.iteration); - this.viewModel.CurrentThing = this.commonFileStore.File[0]; - - await this.viewModel.MoveFile(this.commonFileStore.File[0], this.commonFileStore.Folder[0]); - this.sessionService.Verify(x => x.CreateOrUpdateThings(It.IsAny(), It.IsAny>()), Times.Once); - - await this.viewModel.DeleteFile(); - this.sessionService.Verify(x => x.DeleteThings(It.IsAny(), It.IsAny>()), Times.Once); - } - [Test] public async Task VerifyCreateOrEditFolder() { this.viewModel.InitializeViewModel(this.commonFileStore, this.iteration); + this.viewModel.CurrentThing = this.commonFileStore.File.First(); var domain = new DomainOfExpertise(); this.viewModel.DomainOfExpertiseSelectorViewModel.SelectedDomainOfExpertise = domain; @@ -153,20 +138,45 @@ public async Task VerifyCreateOrEditFolder() await this.viewModel.CreateOrEditFile(false); this.sessionService.Verify(x => x.CreateOrUpdateThings(It.IsAny(), It.Is>(c => !c.OfType().Any()), It.IsAny>()), Times.Once); - this.viewModel.SelectedFileRevisions = [new FileRevision(){ LocalPath = "/localpath" }]; + this.viewModel.SelectedFileRevisions = [new FileRevision { LocalPath = "/localpath" }]; this.viewModel.IsLocked = true; + + var failingResult = new Result(); + failingResult.Reasons.Add(new ExceptionalError(new Exception("Invalid"))); + this.sessionService.Setup(x => x.CreateOrUpdateThings(It.IsAny(), It.IsAny>(), It.IsAny>())).ReturnsAsync(failingResult); + await this.viewModel.CreateOrEditFile(true); Assert.Multiple(() => { this.sessionService.Verify(x => x.CreateOrUpdateThings( - It.IsAny(), - It.Is>(c => c.OfType().Any()), - It.Is>(c => c.Contains("/localpath"))) - , Times.Once); + It.IsAny(), + It.Is>(c => c.OfType().Any()), + It.Is>(c => c.Contains("/localpath"))) + , Times.Once); Assert.That(this.viewModel.CurrentThing.LockedBy, Is.Not.Null); }); } + + [Test] + public void VerifyInitializeViewModel() + { + this.viewModel.InitializeViewModel(this.commonFileStore, this.iteration); + Assert.That(this.viewModel.DomainOfExpertiseSelectorViewModel.AvailableDomainsOfExpertise.Count(), Is.EqualTo(1)); + } + + [Test] + public async Task VerifyMoveAndDeleteFile() + { + this.viewModel.InitializeViewModel(this.commonFileStore, this.iteration); + this.viewModel.CurrentThing = this.commonFileStore.File[0]; + + await this.viewModel.MoveFile(this.commonFileStore.File[0], this.commonFileStore.Folder[0]); + this.sessionService.Verify(x => x.CreateOrUpdateThings(It.IsAny(), It.IsAny>()), Times.Once); + + await this.viewModel.DeleteFile(); + this.sessionService.Verify(x => x.DeleteThings(It.IsAny(), It.IsAny>()), Times.Once); + } } } From ebf9754a86ebfc521c347acf5271c182b7f86ec5 Mon Sep 17 00:00:00 2001 From: Joao Rua Date: Mon, 29 Jul 2024 09:56:38 +0100 Subject: [PATCH 3/3] small adjustments --- .../FileStore/FileHandler/FileHandlerViewModel.cs | 3 +-- COMETwebapp/wwwroot/css/app.css | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs index db85d132..bf1b3e4f 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs @@ -203,11 +203,10 @@ public async Task CreateOrEditFile(bool shouldCreate) } else { - this.logger.LogWarning("File could not be created. {warning}", string.Join(", ", string.Join(", ", result.Reasons.Select(x => x.Message)))); + this.logger.LogWarning("File could not be created. {warning}", string.Join(", ", result.Reasons.Select(x => x.Message))); } this.IsLoading = false; - return result; } diff --git a/COMETwebapp/wwwroot/css/app.css b/COMETwebapp/wwwroot/css/app.css index 5fbf33b6..2f4bcb17 100644 --- a/COMETwebapp/wwwroot/css/app.css +++ b/COMETwebapp/wwwroot/css/app.css @@ -299,6 +299,6 @@ sub { border-radius: 10px; } - .validation-container:not(:has(li)) { - display: none; - } \ No newline at end of file +.validation-container:not(:has(li)) { + display: none; +}