From fc0f99bcc7d47b1e3752e28c432a02534898b73c Mon Sep 17 00:00:00 2001 From: Audrey SERRA <95615798+audserraCGI@users.noreply.github.com> Date: Fri, 3 Jun 2022 15:13:52 +0200 Subject: [PATCH] Show a processing dialog while waiting for an action to be performed (#640) * Disable button while processing on DeviceDetailPage * Show dialog while processing on DeviceDetailPage * Show dialog while processing on DeviceModelDetailPage * Show dialog while processing on CreateDevicePage * Show dialog while processing on CreateDeviceModelPage * Show dialog while processing on EdgeDeviceDetailPage * Disable button while processing on CreateEdgeDeviceDialog * Show dialog while processing on Concentrator Pages * Show dialog while processing on DeviceTagsPage * Fix unit test on DeviceModelDetailPage * Fix unit test on CreateDeviceModelPage * OnInitializedAsync changed to OnInitialized * Setting DialogParameters changed * Addes unit test on ProcessingDialog * Fix code scanning errors * Unit test on CreateDevicePage * Delete useless variables * Fix rebase error * Add html id on DeviceID field in CreateDevicePage * Fix ClickOnSaveShouldPostDeviceDetailsAsync * Add test on DeviceDetailPage + fix an issue occuring when a DeviceDetail is missing some tags * ClickOnSaveShouldPutEdgeDeviceDetails draft * Fix ClickOnSaveShouldPutEdgeDeviceDetails * Add ClickOnSaveShouldPutEdgeDeviceDetails() on ConcentratorDetailPageTests * Fix ClickOnSaveShouldPutEdgeDeviceDetails() on EdgeDeviceDetailPageTests * Add DeviceDtailPageTests : ClickOnSaveShouldDisplaySnackbarIfValidationError() * EdgeDeviceDetailPage Tests on UpdateDevice() * Add tests on EdgeDeviceDetailPage * Fix unit tests * Added test on EdgeDeviceDetailPage * Add tests on DeviceDetailPage * Fix typo on DeviceDetailPageTest * Fix mockSnackbarService.Setup return type * Add test on Save() function on DeviceTagsPage * Tests on CreateConcentratorPage * Fix code scanning errors * Fix "Dereferenced variable may be null" exceptions --- .../Pages/Devices/CreateDevicePageTests.cs | 169 ++++++ .../Pages/Devices/DeviceDetailPageTests.cs | 493 ++++++++++++++++++ .../Pages/Devices/DevicesDetailPageTests.cs | 107 ---- .../CreateDeviceModelPageTests.cs | 22 + .../DeviceModelDetailsPageTests.cs | 50 +- .../Edge_Devices/EdgeDeviceDetailPageTests.cs | 470 ++++++++++++++++- .../ConcentratorDetailPageTests.cs | 87 +++- .../CreateConcentratorPageTest.cs | 157 ++++++ .../Pages/Settings/DeviceTagsPageTests.cs | 266 ++++++++++ .../Pages/Shared/ProcessingDialogTests.cs | 70 +++ .../DeviceModels/CreateDeviceModelPage.razor | 5 + .../DeviceModels/DeviceModelDetailPage.razor | 7 + .../Pages/Devices/CreateDevicePage.razor | 78 +-- .../Pages/Devices/DeviceDetailPage.razor | 26 +- .../Devices/LoRaWAN/EditLoraDevice.razor | 8 +- .../Edge_Devices/CreateEdgeDeviceDialog.razor | 15 +- .../Edge_Devices/EdgeDeviceDetailPage.razor | 47 +- .../Concentrator/ConcentratorDetailPage.razor | 8 +- .../Concentrator/CreateConcentratorPage.razor | 17 +- .../Pages/Settings/DeviceTagsPage.razor | 14 +- .../Client/Shared/ProcessingDialog.razor | 25 + .../Client/wwwroot/scss/app.css | 78 +++ 22 files changed, 1995 insertions(+), 224 deletions(-) create mode 100644 src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/CreateDevicePageTests.cs create mode 100644 src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/DeviceDetailPageTests.cs delete mode 100644 src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/DevicesDetailPageTests.cs rename src/AzureIoTHub.Portal.Server.Tests.Unit/{ => Pages}/LoRaWan/Concentrator/ConcentratorDetailPageTests.cs (55%) create mode 100644 src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/LoRaWan/Concentrator/CreateConcentratorPageTest.cs create mode 100644 src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Settings/DeviceTagsPageTests.cs create mode 100644 src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Shared/ProcessingDialogTests.cs create mode 100644 src/AzureIoTHub.Portal/Client/Shared/ProcessingDialog.razor create mode 100644 src/AzureIoTHub.Portal/Client/wwwroot/scss/app.css diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/CreateDevicePageTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/CreateDevicePageTests.cs new file mode 100644 index 000000000..c1eceeeb1 --- /dev/null +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/CreateDevicePageTests.cs @@ -0,0 +1,169 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.Tests.Unit.Pages +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + using AzureIoTHub.Portal.Client.Pages.Devices; + using AzureIoTHub.Portal.Client.Shared; + using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Server.Tests.Unit.Helpers; + using Bunit; + using Bunit.TestDoubles; + using Microsoft.AspNetCore.Components; + using Microsoft.Extensions.DependencyInjection; + using Moq; + using MudBlazor; + using MudBlazor.Interop; + using MudBlazor.Services; + using NUnit.Framework; + using RichardSzalay.MockHttp; + + [TestFixture] + public class CreateDevicePageTests : IDisposable + { + private Bunit.TestContext testContext; + private MockHttpMessageHandler mockHttpClient; + + private MockRepository mockRepository; + private Mock mockDialogService; + + private FakeNavigationManager mockNavigationManager; + + private static string ApiBaseUrl => "/api/devices"; + + [SetUp] + public void SetUp() + { + this.testContext = new Bunit.TestContext(); + + this.mockRepository = new MockRepository(MockBehavior.Strict); + this.mockDialogService = this.mockRepository.Create(); + this.mockHttpClient = this.testContext.Services + .AddMockHttpClient(); + + _ = this.testContext.Services.AddSingleton(this.mockDialogService.Object); + + _ = this.testContext.Services.AddMudServices(); + + _ = this.testContext.JSInterop.SetupVoid("mudKeyInterceptor.connect", _ => true); + _ = this.testContext.JSInterop.SetupVoid("mudPopover.connect", _ => true); + _ = this.testContext.JSInterop.SetupVoid("Blazor._internal.InputFile.init", _ => true); + _ = this.testContext.JSInterop.Setup("mudElementRef.getBoundingClientRect", _ => true); + _ = this.testContext.JSInterop.Setup>("mudResizeObserver.connect", _ => true); + + this.mockNavigationManager = this.testContext.Services.GetRequiredService(); + + this.mockHttpClient.AutoFlush = true; + } + + private IRenderedComponent RenderComponent(params ComponentParameter[] parameters) + where TComponent : IComponent + { + return this.testContext.RenderComponent(parameters); + } + + [Test] + public async Task ClickOnSaveShouldPostDeviceDetailsAsync() + { + var mockDeviceModel = new DeviceModel + { + ModelId = Guid.NewGuid().ToString(), + Description = Guid.NewGuid().ToString(), + SupportLoRaFeatures = false, + Name = Guid.NewGuid().ToString() + }; + + var expectedDeviceDetails = new DeviceDetails + { + DeviceName = Guid.NewGuid().ToString(), + ModelId = mockDeviceModel.ModelId, + DeviceID = Guid.NewGuid().ToString(), + }; + + + _ = this.mockHttpClient.When(HttpMethod.Post, $"{ApiBaseUrl}") + .With(m => + { + Assert.IsAssignableFrom>(m.Content); + var objectContent = m.Content as ObjectContent; + Assert.IsNotNull(objectContent); + + Assert.IsAssignableFrom(objectContent.Value); + var deviceDetails = objectContent.Value as DeviceDetails; + Assert.IsNotNull(deviceDetails); + + Assert.AreEqual(expectedDeviceDetails.DeviceID, deviceDetails.DeviceID); + Assert.AreEqual(expectedDeviceDetails.DeviceName, deviceDetails.DeviceName); + Assert.AreEqual(expectedDeviceDetails.ModelId, deviceDetails.ModelId); + + return true; + }) + .RespondText(string.Empty); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/models") + .RespondJson(new DeviceModel[] + { + mockDeviceModel + }); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/settings/device-tags") + .RespondJson(new List() + { + new DeviceTag() + { + Label = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString(), + Required = false, + Searchable = false + } + }); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/models/{mockDeviceModel.ModelId}/properties") + .RespondJson(Array.Empty()); + + _ = this.mockHttpClient.When(HttpMethod.Post, $"{ApiBaseUrl}/{expectedDeviceDetails.DeviceID}/properties") + .RespondText(string.Empty); + + var cut = RenderComponent(); + Thread.Sleep(2500); + var saveButton = cut.WaitForElement("#SaveButton"); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show("Processing", It.IsAny())) + .Returns(mockDialogReference); + + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + + // Act + cut.Find($"#{nameof(DeviceDetails.DeviceName)}").Change(expectedDeviceDetails.DeviceName); + cut.Find($"#{nameof(DeviceDetails.DeviceID)}").Change(expectedDeviceDetails.DeviceID); + await cut.Instance.ChangeModel(mockDeviceModel); + + saveButton.Click(); + Thread.Sleep(2500); + cut.WaitForState(() => + { + return this.mockNavigationManager.Uri.EndsWith("/devices", StringComparison.OrdinalIgnoreCase); + }); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/DeviceDetailPageTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/DeviceDetailPageTests.cs new file mode 100644 index 000000000..d56e2b225 --- /dev/null +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/DeviceDetailPageTests.cs @@ -0,0 +1,493 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.Tests.Unit.Pages +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Threading; + using AzureIoTHub.Portal.Client.Pages.Devices; + using AzureIoTHub.Portal.Client.Shared; + using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Server.Tests.Unit.Helpers; + using Bunit; + using Bunit.TestDoubles; + using Microsoft.AspNetCore.Components; + using Microsoft.Extensions.DependencyInjection; + using Moq; + using MudBlazor; + using MudBlazor.Interop; + using MudBlazor.Services; + using NUnit.Framework; + using RichardSzalay.MockHttp; + + [TestFixture] + public class DeviceDetailPageTests : IDisposable + { + private Bunit.TestContext testContext; + private MockHttpMessageHandler mockHttpClient; + private MockRepository mockRepository; + private Mock mockDialogService; + private Mock mockSnackbarService; + private FakeNavigationManager mockNavigationManager; + + private static string ApiBaseUrl => "/api/devices"; + + [SetUp] + public void SetUp() + { + this.testContext = new Bunit.TestContext(); + + this.mockRepository = new MockRepository(MockBehavior.Strict); + this.mockHttpClient = this.testContext.Services.AddMockHttpClient(); + + this.mockDialogService = this.mockRepository.Create(); + _ = this.testContext.Services.AddSingleton(this.mockDialogService.Object); + + this.mockSnackbarService = this.mockRepository.Create(); + _ = this.testContext.Services.AddSingleton(this.mockSnackbarService.Object); + + _ = this.testContext.Services.AddMudServices(); + + _ = this.testContext.Services.AddSingleton(new PortalSettings { IsLoRaSupported = false }); + + _ = this.testContext.JSInterop.SetupVoid("mudKeyInterceptor.connect", _ => true); + _ = this.testContext.JSInterop.SetupVoid("mudPopover.connect", _ => true); + _ = this.testContext.JSInterop.Setup("mudElementRef.getBoundingClientRect", _ => true); + _ = this.testContext.JSInterop.Setup>("mudResizeObserver.connect", _ => true); + + this.mockNavigationManager = this.testContext.Services.GetRequiredService(); + + this.mockHttpClient.AutoFlush = true; + } + + private IRenderedComponent RenderComponent(params ComponentParameter[] parameters) + where TComponent : IComponent + { + return this.testContext.RenderComponent(parameters); + } + + [Test] + public void ReturnButtonMustNavigateToPreviousPage() + { + + // Arrange + var deviceId = Guid.NewGuid().ToString(); + var modelId = Guid.NewGuid().ToString(); + + + _ = this.mockHttpClient + .When(HttpMethod.Get, $"/api/devices/{deviceId}") + .RespondJson(new DeviceDetails() { ModelId = modelId }); + + _ = this.mockHttpClient + .When(HttpMethod.Get, $"/api/devices/{deviceId}/properties") + .RespondJson(new List()); + + _ = this.mockHttpClient + .When(HttpMethod.Get, $"/api/models/{modelId}") + .RespondJson(new DeviceModel()); + + _ = this.mockHttpClient + .When(HttpMethod.Get, $"/api/settings/device-tags") + .RespondJson(new List()); + + var cut = RenderComponent(ComponentParameter.CreateParameter("DeviceID", deviceId)); + var returnButton = cut.WaitForElement("#returnButton"); + + // Act + returnButton.Click(); + + // Assert + cut.WaitForState(() => this.mockNavigationManager.Uri.EndsWith("/devices", StringComparison.OrdinalIgnoreCase)); + } + + [Test] + public void ClickOnSaveShouldPutDeviceDetails() + { + var mockDeviceModel = new DeviceModel + { + ModelId = Guid.NewGuid().ToString(), + Description = Guid.NewGuid().ToString(), + SupportLoRaFeatures = false, + Name = Guid.NewGuid().ToString() + }; + + var mockTag = new DeviceTag + { + Label = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString(), + Required = false, + Searchable = false + }; + + var mockDeviceDetails = new DeviceDetails + { + DeviceName = Guid.NewGuid().ToString(), + ModelId = mockDeviceModel.ModelId, + DeviceID = Guid.NewGuid().ToString(), + Tags = new Dictionary() + { + {mockTag.Name,Guid.NewGuid().ToString()} + } + }; + + + _ = this.mockHttpClient.When(HttpMethod.Put, $"{ApiBaseUrl}") + .With(m => + { + Assert.IsAssignableFrom>(m.Content); + var objectContent = m.Content as ObjectContent; + Assert.IsNotNull(objectContent); + + Assert.IsAssignableFrom(objectContent.Value); + var deviceDetails = objectContent.Value as DeviceDetails; + Assert.IsNotNull(deviceDetails); + + Assert.AreEqual(mockDeviceDetails.DeviceID, deviceDetails.DeviceID); + Assert.AreEqual(mockDeviceDetails.DeviceName, deviceDetails.DeviceName); + Assert.AreEqual(mockDeviceDetails.ModelId, deviceDetails.ModelId); + + return true; + }) + .RespondText(string.Empty); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/devices/{mockDeviceDetails.DeviceID}") + .RespondJson(mockDeviceDetails); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/models/{mockDeviceDetails.ModelId}") + .RespondJson(mockDeviceModel); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/settings/device-tags") + .RespondJson(new List() + { + mockTag + }); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"{ApiBaseUrl}/{mockDeviceDetails.DeviceID}/properties") + .RespondJson(Array.Empty()); + + _ = this.mockHttpClient.When(HttpMethod.Post, $"{ApiBaseUrl}/{mockDeviceDetails.DeviceID}/properties") + .RespondText(string.Empty); + + var cut = RenderComponent(ComponentParameter.CreateParameter("DeviceID", mockDeviceDetails.DeviceID)); + Thread.Sleep(2500); + + var saveButton = cut.WaitForElement("#saveButton"); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show("Processing", It.IsAny())) + .Returns(mockDialogReference); + + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + + _ = this.mockSnackbarService.Setup(c => c.Add(It.IsAny(), Severity.Success, null)).Returns((Snackbar)null); + + // Act + saveButton.Click(); + Thread.Sleep(2500); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + cut.WaitForState(() => this.mockNavigationManager.Uri.EndsWith("devices", StringComparison.OrdinalIgnoreCase)); + } + + [Test] + public void ClickOnSaveShouldDisplaySnackbarIfValidationError() + { + var mockDeviceModel = new DeviceModel + { + ModelId = Guid.NewGuid().ToString(), + Description = Guid.NewGuid().ToString(), + SupportLoRaFeatures = false, + Name = Guid.NewGuid().ToString() + }; + + var mockTag = new DeviceTag + { + Label = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString(), + Required = false, + Searchable = false + }; + + var mockDeviceDetails = new DeviceDetails + { + DeviceName = Guid.NewGuid().ToString(), + ModelId = mockDeviceModel.ModelId, + DeviceID = Guid.NewGuid().ToString(), + Tags = new Dictionary() + { + {mockTag.Name,Guid.NewGuid().ToString()} + } + }; + + + _ = this.mockHttpClient.When(HttpMethod.Put, $"{ApiBaseUrl}") + .With(m => + { + Assert.IsAssignableFrom>(m.Content); + var objectContent = m.Content as ObjectContent; + Assert.IsNotNull(objectContent); + + Assert.IsAssignableFrom(objectContent.Value); + var deviceDetails = objectContent.Value as DeviceDetails; + Assert.IsNotNull(deviceDetails); + + Assert.AreEqual(mockDeviceDetails.DeviceID, deviceDetails.DeviceID); + Assert.AreEqual(mockDeviceDetails.DeviceName, deviceDetails.DeviceName); + Assert.AreEqual(mockDeviceDetails.ModelId, deviceDetails.ModelId); + + return true; + }) + .RespondText(string.Empty); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/devices/{mockDeviceDetails.DeviceID}") + .RespondJson(mockDeviceDetails); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/models/{mockDeviceDetails.ModelId}") + .RespondJson(mockDeviceModel); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/settings/device-tags") + .RespondJson(new List() + { + mockTag + }); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"{ApiBaseUrl}/{mockDeviceDetails.DeviceID}/properties") + .RespondJson(Array.Empty()); + + _ = this.mockHttpClient.When(HttpMethod.Post, $"{ApiBaseUrl}/{mockDeviceDetails.DeviceID}/properties") + .RespondText(string.Empty); + + var cut = RenderComponent(ComponentParameter.CreateParameter("DeviceID", mockDeviceDetails.DeviceID)); + Thread.Sleep(2500); + + cut.Find($"#{nameof(DeviceDetails.DeviceName)}").Change(""); + var saveButton = cut.WaitForElement("#saveButton"); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show("Processing", It.IsAny())) + .Returns(mockDialogReference); + + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + + _ = this.mockSnackbarService.Setup(c => c.Add("One or more validation errors occurred", Severity.Error, null)).Returns((Snackbar)null); + + // Act + saveButton.Click(); + Thread.Sleep(2500); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + } + + [Test] + public void ClickOnConnectShouldDisplayDeviceCredentials() + { + var mockDeviceModel = new DeviceModel + { + ModelId = Guid.NewGuid().ToString(), + Description = Guid.NewGuid().ToString(), + SupportLoRaFeatures = false, + Name = Guid.NewGuid().ToString() + }; + + var mockTag = new DeviceTag + { + Label = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString(), + Required = false, + Searchable = false + }; + + var mockDeviceDetails = new DeviceDetails + { + DeviceName = Guid.NewGuid().ToString(), + ModelId = mockDeviceModel.ModelId, + DeviceID = Guid.NewGuid().ToString(), + Tags = new Dictionary() + { + {mockTag.Name,Guid.NewGuid().ToString()} + } + }; + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/devices/{mockDeviceDetails.DeviceID}") + .RespondJson(mockDeviceDetails); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/models/{mockDeviceDetails.ModelId}") + .RespondJson(mockDeviceModel); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/settings/device-tags") + .RespondJson(new List() + { + mockTag + }); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"{ApiBaseUrl}/{mockDeviceDetails.DeviceID}/properties") + .RespondJson(Array.Empty()); + + var cut = RenderComponent(ComponentParameter.CreateParameter("DeviceID", mockDeviceDetails.DeviceID)); + Thread.Sleep(2500); + + var connectButton = cut.WaitForElement("#connectButton"); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference); + + // Act + connectButton.Click(); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + } + + [Test] + public void ClickOnDeleteShouldDisplayConfirmationDialogAndReturnIfAborted() + { + var mockDeviceModel = new DeviceModel + { + ModelId = Guid.NewGuid().ToString(), + Description = Guid.NewGuid().ToString(), + SupportLoRaFeatures = false, + Name = Guid.NewGuid().ToString() + }; + + var mockTag = new DeviceTag + { + Label = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString(), + Required = false, + Searchable = false + }; + + var mockDeviceDetails = new DeviceDetails + { + DeviceName = Guid.NewGuid().ToString(), + ModelId = mockDeviceModel.ModelId, + DeviceID = Guid.NewGuid().ToString(), + Tags = new Dictionary() + { + {mockTag.Name,Guid.NewGuid().ToString()} + } + }; + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/devices/{mockDeviceDetails.DeviceID}") + .RespondJson(mockDeviceDetails); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/models/{mockDeviceDetails.ModelId}") + .RespondJson(mockDeviceModel); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/settings/device-tags") + .RespondJson(new List() + { + mockTag + }); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"{ApiBaseUrl}/{mockDeviceDetails.DeviceID}/properties") + .RespondJson(Array.Empty()); + + var cut = RenderComponent(ComponentParameter.CreateParameter("DeviceID", mockDeviceDetails.DeviceID)); + Thread.Sleep(2500); + + var deleteButton = cut.WaitForElement("#deleteButton"); + + var mockDialogReference = this.mockRepository.Create(); + _ = mockDialogReference.Setup(c => c.Result).ReturnsAsync(DialogResult.Cancel()); + + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference.Object); + + // Act + deleteButton.Click(); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + } + + [Test] + public void ClickOnDeleteShouldDisplayConfirmationDialogAndRedirectIfConfirmed() + { + var mockDeviceModel = new DeviceModel + { + ModelId = Guid.NewGuid().ToString(), + Description = Guid.NewGuid().ToString(), + SupportLoRaFeatures = false, + Name = Guid.NewGuid().ToString() + }; + + var mockTag = new DeviceTag + { + Label = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString(), + Required = false, + Searchable = false + }; + + var mockDeviceDetails = new DeviceDetails + { + DeviceName = Guid.NewGuid().ToString(), + ModelId = mockDeviceModel.ModelId, + DeviceID = Guid.NewGuid().ToString(), + Tags = new Dictionary() + { + {mockTag.Name,Guid.NewGuid().ToString()} + } + }; + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/devices/{mockDeviceDetails.DeviceID}") + .RespondJson(mockDeviceDetails); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/models/{mockDeviceDetails.ModelId}") + .RespondJson(mockDeviceModel); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"/api/settings/device-tags") + .RespondJson(new List() + { + mockTag + }); + + _ = this.mockHttpClient.When(HttpMethod.Get, $"{ApiBaseUrl}/{mockDeviceDetails.DeviceID}/properties") + .RespondJson(Array.Empty()); + + var cut = RenderComponent(ComponentParameter.CreateParameter("DeviceID", mockDeviceDetails.DeviceID)); + Thread.Sleep(2500); + + var deleteButton = cut.WaitForElement("#deleteButton"); + + var mockDialogReference = this.mockRepository.Create(); + _ = mockDialogReference.Setup(c => c.Result).ReturnsAsync(DialogResult.Ok("Ok")); + + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference.Object); + + // Act + deleteButton.Click(); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + + cut.WaitForState(() => this.mockNavigationManager.Uri.EndsWith("/devices", StringComparison.OrdinalIgnoreCase)); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/DevicesDetailPageTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/DevicesDetailPageTests.cs deleted file mode 100644 index 56594da90..000000000 --- a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/DevicesDetailPageTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) CGI France. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace AzureIoTHub.Portal.Server.Tests.Unit.Pages -{ - using System; - using System.Collections.Generic; - using System.Net.Http; - using AzureIoTHub.Portal.Client.Pages.Devices; - using AzureIoTHub.Portal.Models.v10; - using AzureIoTHub.Portal.Server.Tests.Unit.Helpers; - using Bunit; - using Bunit.TestDoubles; - using Microsoft.AspNetCore.Components; - using Microsoft.Extensions.DependencyInjection; - using Moq; - using MudBlazor; - using MudBlazor.Interop; - using MudBlazor.Services; - using NUnit.Framework; - using RichardSzalay.MockHttp; - - [TestFixture] - public class DevicesDetailPageTests : IDisposable - { - private Bunit.TestContext testContext; - private MockHttpMessageHandler mockHttpClient; - private MockRepository mockRepository; - private Mock mockDialogService; - [SetUp] - public void SetUp() - { - this.testContext = new Bunit.TestContext(); - - this.mockRepository = new MockRepository(MockBehavior.Strict); - this.mockDialogService = this.mockRepository.Create(); - - this.mockHttpClient = this.testContext.Services.AddMockHttpClient(); - - _ = this.testContext.Services.AddSingleton(this.mockDialogService.Object); - _ = this.testContext.Services.AddMudServices(); - - _ = this.testContext.JSInterop.SetupVoid("mudKeyInterceptor.connect", _ => true); - _ = this.testContext.JSInterop.SetupVoid("mudPopover.connect", _ => true); - _ = this.testContext.JSInterop.Setup("mudElementRef.getBoundingClientRect", _ => true); - _ = this.testContext.JSInterop.Setup>("mudResizeObserver.connect", _ => true); - - this.mockHttpClient.AutoFlush = true; - } - - private IRenderedComponent RenderComponent(params ComponentParameter[] parameters) - where TComponent : IComponent - { - return this.testContext.RenderComponent(parameters); - } - - - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - } - - [Test] - - public void ReturnButtonMustNavigateToPreviousPage() - { - - // Arrange - var deviceId = Guid.NewGuid().ToString(); - var modelId = Guid.NewGuid().ToString(); - - - _ = this.mockHttpClient - .When(HttpMethod.Get, $"/api/devices/{deviceId}") - .RespondJson(new DeviceDetails() { ModelId = modelId }); - - _ = this.mockHttpClient - .When(HttpMethod.Get, $"/api/devices/{deviceId}/properties") - .RespondJson(new List()); - - _ = this.mockHttpClient - .When(HttpMethod.Get, $"/api/models/{modelId}") - .RespondJson(new DeviceModel()); - - _ = this.mockHttpClient - .When(HttpMethod.Get, $"/api/settings/device-tags") - .RespondJson(new List()); - - _ = this.testContext.Services.AddSingleton(new PortalSettings { IsLoRaSupported = false }); - - var cut = RenderComponent(ComponentParameter.CreateParameter("DeviceID", deviceId)); - var returnButton = cut.WaitForElement("#returnButton"); - - // Act - returnButton.Click(); - - // Assert - cut.WaitForState(() => this.testContext.Services.GetRequiredService().Uri.EndsWith("/devices", StringComparison.OrdinalIgnoreCase)); - } - } -} diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/DevicesModels/CreateDeviceModelPageTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/DevicesModels/CreateDeviceModelPageTests.cs index 618ac3cf9..e553c37db 100644 --- a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/DevicesModels/CreateDeviceModelPageTests.cs +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/DevicesModels/CreateDeviceModelPageTests.cs @@ -21,6 +21,7 @@ namespace AzureIoTHub.Portal.Server.Tests.Unit.Pages using MudBlazor.Services; using NUnit.Framework; using RichardSzalay.MockHttp; + using AzureIoTHub.Portal.Client.Shared; [TestFixture] public class CreateDeviceModelPageTests : IDisposable @@ -98,6 +99,13 @@ public void ClickOnSaveShouldPostDeviceModelData() var cut = RenderComponent(); var saveButton = cut.WaitForElement("#SaveButton"); + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show("Processing", It.IsAny())) + .Returns(mockDialogReference); + + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + // Act cut.Find($"#{nameof(DeviceModel.Name)}").Change(modelName); cut.Find($"#{nameof(DeviceModel.Description)}").Change(description); @@ -151,6 +159,13 @@ public void ClickOnAddPropertyShouldAddNewProperty() cut.Find($"#{nameof(DeviceModel.Name)}").Change(Guid.NewGuid().ToString()); cut.Find($"#{nameof(DeviceModel.Description)}").Change(Guid.NewGuid().ToString()); + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show("Processing", It.IsAny())) + .Returns(mockDialogReference); + + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + // Act addPropertyButton.Click(); @@ -205,6 +220,13 @@ public void ClickOnRemovePropertyShouldRemoveTheProperty() var removePropertyButton = cut.WaitForElement("#DeletePropertyButton"); + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show("Processing", It.IsAny())) + .Returns(mockDialogReference); + + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + // Act removePropertyButton.Click(); diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/DevicesModels/DeviceModelDetailsPageTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/DevicesModels/DeviceModelDetailsPageTests.cs index 3ef8db3e8..cbcb27fac 100644 --- a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/DevicesModels/DeviceModelDetailsPageTests.cs +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/DevicesModels/DeviceModelDetailsPageTests.cs @@ -25,14 +25,13 @@ namespace AzureIoTHub.Portal.Server.Tests.Unit.Pages using MudBlazor.Services; using NUnit.Framework; using RichardSzalay.MockHttp; + using AzureIoTHub.Portal.Client.Shared; [TestFixture] public class DeviceModelDetaislPageTests : IDisposable { -#pragma warning disable CA2213 // Disposable fields should be disposed private Bunit.TestContext testContext; private MockHttpMessageHandler mockHttpClient; -#pragma warning restore CA2213 // Disposable fields should be disposed private readonly string mockModelId = Guid.NewGuid().ToString(); @@ -89,7 +88,7 @@ public void ClickOnSaveShouldPostDeviceModelData() var expectedModel = SetupMockDeviceModel(properties: expectedProperties); - _ = this.mockHttpClient.When(HttpMethod.Put, $"{ ApiBaseUrl}") + _ = this.mockHttpClient.When(HttpMethod.Put, $"{ApiBaseUrl}") .With(m => { Assert.IsAssignableFrom(m.Content); @@ -109,7 +108,7 @@ public void ClickOnSaveShouldPostDeviceModelData() .RespondText(string.Empty); _ = this.mockHttpClient - .When(HttpMethod.Post, $"{ ApiBaseUrl}/properties") + .When(HttpMethod.Post, $"{ApiBaseUrl}/properties") .With(m => { Assert.IsAssignableFrom(m.Content); @@ -138,6 +137,13 @@ public void ClickOnSaveShouldPostDeviceModelData() var saveButton = cut.WaitForElement("#SaveButton"); + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show("Processing", It.IsAny())) + .Returns(mockDialogReference); + + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + // Act saveButton.Click(); cut.WaitForState(() => this.mockNavigationManager.Uri.EndsWith("/device-models", StringComparison.OrdinalIgnoreCase)); @@ -153,23 +159,23 @@ public void ClickOnAddPropertyShouldAddNewProperty() var propertyName = Guid.NewGuid().ToString(); var displayName = Guid.NewGuid().ToString(); - _ = this.mockHttpClient.When(HttpMethod.Get, $"{ ApiBaseUrl }") + _ = this.mockHttpClient.When(HttpMethod.Get, $"{ApiBaseUrl}") .RespondJson(new DeviceModel { ModelId = this.mockModelId, Name = Guid.NewGuid().ToString() }); - _ = this.mockHttpClient.When(HttpMethod.Get, $"{ ApiBaseUrl }/avatar") + _ = this.mockHttpClient.When(HttpMethod.Get, $"{ApiBaseUrl}/avatar") .RespondText(string.Empty); - _ = this.mockHttpClient.When(HttpMethod.Get, $"{ ApiBaseUrl }/properties") + _ = this.mockHttpClient.When(HttpMethod.Get, $"{ApiBaseUrl}/properties") .RespondJson(Array.Empty()); - _ = this.mockHttpClient.When(HttpMethod.Put, $"{ ApiBaseUrl }") + _ = this.mockHttpClient.When(HttpMethod.Put, $"{ApiBaseUrl}") .RespondText(string.Empty); - _ = this.mockHttpClient.When(HttpMethod.Post, $"{ ApiBaseUrl}/properties") + _ = this.mockHttpClient.When(HttpMethod.Post, $"{ApiBaseUrl}/properties") .With(m => { Assert.IsAssignableFrom>>(m.Content); @@ -197,6 +203,13 @@ public void ClickOnAddPropertyShouldAddNewProperty() var saveButton = cut.WaitForElement("#SaveButton"); var addPropertyButton = cut.WaitForElement("#addPropertyButton"); + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show("Processing", It.IsAny())) + .Returns(mockDialogReference); + + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + // Act addPropertyButton.Click(); @@ -219,26 +232,26 @@ public void ClickOnAddPropertyShouldAddNewProperty() public void ClickOnRemovePropertyShouldRemoveTheProperty() { // Arrange - _ = this.mockHttpClient.When(HttpMethod.Get, $"{ ApiBaseUrl }") + _ = this.mockHttpClient.When(HttpMethod.Get, $"{ApiBaseUrl}") .RespondJson(new DeviceModel { ModelId = this.mockModelId, Name = Guid.NewGuid().ToString() }); - _ = this.mockHttpClient.When(HttpMethod.Get, $"{ ApiBaseUrl }/avatar") + _ = this.mockHttpClient.When(HttpMethod.Get, $"{ApiBaseUrl}/avatar") .RespondText(string.Empty); - _ = this.mockHttpClient.When(HttpMethod.Get, $"{ ApiBaseUrl }/properties") + _ = this.mockHttpClient.When(HttpMethod.Get, $"{ApiBaseUrl}/properties") .RespondJson(new DeviceProperty[] { new DeviceProperty() }); - _ = this.mockHttpClient.When(HttpMethod.Put, $"{ ApiBaseUrl}") + _ = this.mockHttpClient.When(HttpMethod.Put, $"{ApiBaseUrl}") .RespondText(string.Empty); - _ = this.mockHttpClient.When(HttpMethod.Post, $"{ ApiBaseUrl}/properties") + _ = this.mockHttpClient.When(HttpMethod.Post, $"{ApiBaseUrl}/properties") .With(m => { Assert.IsAssignableFrom>>(m.Content); @@ -259,6 +272,13 @@ public void ClickOnRemovePropertyShouldRemoveTheProperty() var saveButton = cut.WaitForElement("#SaveButton"); var removePropertyButton = cut.WaitForElement("#DeletePropertyButton"); + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show("Processing", It.IsAny())) + .Returns(mockDialogReference); + + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + // Act removePropertyButton.Click(); @@ -299,9 +319,7 @@ public void WhenPresentModelDetailsShouldDisplayProperties() Assert.AreEqual(item.DisplayName, cut.Find($"{propertyCssSelector} #{nameof(item.DisplayName)}").Attributes["value"].Value); Assert.AreEqual(item.Name, cut.Find($"{propertyCssSelector} #{nameof(item.Name)}").Attributes["value"].Value); Assert.AreEqual(item.PropertyType.ToString(), cut.Find($"{propertyCssSelector} #{nameof(item.PropertyType)}").Attributes["value"].Value); -#pragma warning disable CA1308 // Normalize strings to uppercase Assert.AreEqual(item.IsWritable.ToString().ToLowerInvariant(), cut.Find($"{propertyCssSelector} #{nameof(item.IsWritable)}").Attributes["aria-checked"].Value); -#pragma warning restore CA1308 // Normalize strings to uppercase } this.mockHttpClient.VerifyNoOutstandingExpectation(); diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Edge_Devices/EdgeDeviceDetailPageTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Edge_Devices/EdgeDeviceDetailPageTests.cs index 8a6393e63..c7865ed32 100644 --- a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Edge_Devices/EdgeDeviceDetailPageTests.cs +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Edge_Devices/EdgeDeviceDetailPageTests.cs @@ -6,7 +6,11 @@ namespace AzureIoTHub.Portal.Server.Tests.Unit.Pages using System; using System.Collections.Generic; using System.Net.Http; + using System.Net.Mime; + using System.Text; + using System.Threading; using AzureIoTHub.Portal.Client.Pages.Edge_Devices; + using AzureIoTHub.Portal.Client.Shared; using Bunit; using Bunit.TestDoubles; using Helpers; @@ -17,21 +21,23 @@ namespace AzureIoTHub.Portal.Server.Tests.Unit.Pages using MudBlazor; using MudBlazor.Interop; using MudBlazor.Services; + using Newtonsoft.Json; using NUnit.Framework; using RichardSzalay.MockHttp; [TestFixture] public class EdgeDeviceDetailPageTests : IDisposable { -#pragma warning disable CA2213 // Disposable fields should be disposed private Bunit.TestContext testContext; private MockHttpMessageHandler mockHttpClient; -#pragma warning restore CA2213 // Disposable fields should be disposed private MockRepository mockRepository; private Mock mockDialogService; + private Mock mockSnackbarService; private readonly string mockdeviceId = Guid.NewGuid().ToString(); + private FakeNavigationManager mockNavigationManager; + [SetUp] public void SetUp() { @@ -39,15 +45,23 @@ public void SetUp() this.mockRepository = new MockRepository(MockBehavior.Strict); this.mockHttpClient = this.testContext.Services.AddMockHttpClient(); - this.mockDialogService = this.mockRepository.Create(); + this.mockDialogService = this.mockRepository.Create(); _ = this.testContext.Services.AddSingleton(this.mockDialogService.Object); + + this.mockSnackbarService = this.mockRepository.Create(); + _ = this.testContext.Services.AddSingleton(this.mockSnackbarService.Object); + _ = this.testContext.Services.AddMudServices(); + _ = this.testContext.Services.AddSingleton(new PortalSettings { IsLoRaSupported = false }); + _ = this.testContext.JSInterop.SetupVoid("mudKeyInterceptor.connect", _ => true); _ = this.testContext.JSInterop.SetupVoid("mudPopover.connect", _ => true); _ = this.testContext.JSInterop.Setup("mudElementRef.getBoundingClientRect", _ => true); _ = this.testContext.JSInterop.Setup>("mudResizeObserver.connect", _ => true); + + this.mockNavigationManager = this.testContext.Services.GetRequiredService(); } private IRenderedComponent RenderComponent(params ComponentParameter[] parameters) @@ -64,8 +78,6 @@ public void ReturnButtonMustNavigateToPreviousPage() .When(HttpMethod.Get, $"/api/edge/devices/{this.mockdeviceId}") .RespondJson(new IoTEdgeDevice() { ConnectionState = "false" }); - _ = this.testContext.Services.AddSingleton(new PortalSettings { IsLoRaSupported = false }); - var cut = RenderComponent(ComponentParameter.CreateParameter("deviceId", this.mockdeviceId)); var returnButton = cut.WaitForElement("#returnButton"); @@ -73,7 +85,453 @@ public void ReturnButtonMustNavigateToPreviousPage() returnButton.Click(); // Assert - cut.WaitForState(() => this.testContext.Services.GetRequiredService().Uri.EndsWith("/edge/devices", StringComparison.OrdinalIgnoreCase)); + cut.WaitForState(() => this.mockNavigationManager.Uri.EndsWith("/edge/devices", StringComparison.OrdinalIgnoreCase)); + } + + [Test] + public void ClickOnSaveShouldPutEdgeDeviceDetails() + { + var mockIoTEdgeDevice = new IoTEdgeDevice() + { + DeviceId = mockdeviceId, + ConnectionState = "Connected", + Type = "Other" + }; + + _ = this.mockHttpClient + .When(HttpMethod.Get, $"/api/edge/devices/{this.mockdeviceId}") + .RespondJson(mockIoTEdgeDevice); + + _ = this.mockHttpClient + .When(HttpMethod.Put, $"/api/edge/devices/{this.mockdeviceId}") + .With(m => + { + Assert.IsAssignableFrom>(m.Content); + var objectContent = m.Content as ObjectContent; + Assert.IsNotNull(objectContent); + + Assert.IsAssignableFrom(objectContent.Value); + var edgeDevice = objectContent.Value as IoTEdgeDevice; + Assert.IsNotNull(edgeDevice); + + Assert.AreEqual(mockIoTEdgeDevice.DeviceId, edgeDevice.DeviceId); + Assert.AreEqual(mockIoTEdgeDevice.ConnectionState, edgeDevice.ConnectionState); + Assert.AreEqual(mockIoTEdgeDevice.Type, edgeDevice.Type); + + return true; + }) + .RespondText(string.Empty); + + + var cut = RenderComponent(ComponentParameter.CreateParameter("deviceId", this.mockdeviceId)); + Thread.Sleep(2500); + + var saveButton = cut.WaitForElement("#saveButton"); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show("Processing", It.IsAny())) + .Returns(mockDialogReference); + + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + + _ = this.mockSnackbarService.Setup(c => c.Add($"Device {this.mockdeviceId} has been successfully updated!", Severity.Success, null)).Returns((Snackbar)null); + + // Act + saveButton.Click(); + Thread.Sleep(2500); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + } + + [Test] + public void ClickOnSaveShouldDisplaySnackbarIfValidationError() + { + var mockIoTEdgeDevice = new IoTEdgeDevice() + { + DeviceId = mockdeviceId, + ConnectionState = "Connected", + Type = "Other" + }; + + _ = this.mockHttpClient + .When(HttpMethod.Get, $"/api/edge/devices/{this.mockdeviceId}") + .RespondJson(mockIoTEdgeDevice); + + _ = this.mockHttpClient + .When(HttpMethod.Put, $"/api/edge/devices/{this.mockdeviceId}") + .With(m => + { + Assert.IsAssignableFrom>(m.Content); + var objectContent = m.Content as ObjectContent; + Assert.IsNotNull(objectContent); + + Assert.IsAssignableFrom(objectContent.Value); + var edgeDevice = objectContent.Value as IoTEdgeDevice; + Assert.IsNotNull(edgeDevice); + + Assert.AreEqual(mockIoTEdgeDevice.DeviceId, edgeDevice.DeviceId); + Assert.AreEqual(mockIoTEdgeDevice.ConnectionState, edgeDevice.ConnectionState); + Assert.AreEqual(mockIoTEdgeDevice.Type, edgeDevice.Type); + + return true; + }) + .Respond(System.Net.HttpStatusCode.BadRequest); + + var cut = RenderComponent(ComponentParameter.CreateParameter("deviceId", this.mockdeviceId)); + Thread.Sleep(2500); + + var saveButton = cut.WaitForElement("#saveButton"); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show("Processing", It.IsAny())) + .Returns(mockDialogReference); + + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + + _ = this.mockSnackbarService.Setup(c => c.Add("One or more validation errors occurred", Severity.Error, null)).Returns((Snackbar)null); + + // Act + saveButton.Click(); + Thread.Sleep(2500); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + } + + [Test] + public void ClickOnSaveShouldDisplaySnackbarIfUnexpectedError() + { + var mockIoTEdgeDevice = new IoTEdgeDevice() + { + DeviceId = mockdeviceId, + ConnectionState = "Connected", + Type = "Other" + }; + + _ = this.mockHttpClient + .When(HttpMethod.Get, $"/api/edge/devices/{this.mockdeviceId}") + .RespondJson(mockIoTEdgeDevice); + + _ = this.mockHttpClient + .When(HttpMethod.Put, $"/api/edge/devices/{this.mockdeviceId}") + .With(m => + { + Assert.IsAssignableFrom>(m.Content); + var objectContent = m.Content as ObjectContent; + Assert.IsNotNull(objectContent); + + Assert.IsAssignableFrom(objectContent.Value); + var edgeDevice = objectContent.Value as IoTEdgeDevice; + Assert.IsNotNull(edgeDevice); + + Assert.AreEqual(mockIoTEdgeDevice.DeviceId, edgeDevice.DeviceId); + Assert.AreEqual(mockIoTEdgeDevice.ConnectionState, edgeDevice.ConnectionState); + Assert.AreEqual(mockIoTEdgeDevice.Type, edgeDevice.Type); + + return true; + }) + .Respond(System.Net.HttpStatusCode.NotFound); + + var cut = RenderComponent(ComponentParameter.CreateParameter("deviceId", this.mockdeviceId)); + Thread.Sleep(2500); + + var saveButton = cut.WaitForElement("#saveButton"); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show("Processing", It.IsAny())) + .Returns(mockDialogReference); + + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + + _ = this.mockSnackbarService.Setup(c => c.Add("Something unexpected occurred", Severity.Warning, null)).Returns((Snackbar)null); + + // Act + saveButton.Click(); + Thread.Sleep(2500); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + } + + [Test] + public void ClickOnRebootShouldRebootModule() + { + var mockIoTEdgeModule = new IoTEdgeModule() + { + ModuleName = Guid.NewGuid().ToString() + }; + + var mockIoTEdgeDevice = new IoTEdgeDevice() + { + DeviceId = mockdeviceId, + ConnectionState = "Connected", + Type = "Other", + Modules= new List(){mockIoTEdgeModule} + }; + + + _ = this.mockHttpClient + .When(HttpMethod.Get, $"/api/edge/devices/{this.mockdeviceId}") + .RespondJson(mockIoTEdgeDevice); + + _ = this.mockHttpClient + .When(HttpMethod.Post, $"/api/edge/devices/{mockIoTEdgeDevice.DeviceId}/{mockIoTEdgeModule.ModuleName}/RestartModule") + .With(m => + { + Assert.IsAssignableFrom>(m.Content); + var objectContent = m.Content as ObjectContent; + Assert.IsNotNull(objectContent); + + Assert.IsAssignableFrom(objectContent.Value); + var edgeModule = objectContent.Value as IoTEdgeModule; + Assert.IsNotNull(edgeModule); + + Assert.AreEqual(mockIoTEdgeModule.ModuleName, edgeModule.ModuleName); + + return true; + }) + .Respond(System.Net.HttpStatusCode.OK, new StringContent( + JsonConvert.SerializeObject(new C2Dresult() + { + Payload = "ABC", + Status = 200 + }), + Encoding.UTF8, + MediaTypeNames.Application.Json)); + + var cut = RenderComponent(ComponentParameter.CreateParameter("deviceId", this.mockdeviceId)); + Thread.Sleep(2500); + + var rebootButton = cut.WaitForElement("#rebootModule"); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show("Processing", It.IsAny())) + .Returns(mockDialogReference); + + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + + _ = this.mockSnackbarService.Setup(c => c.Add("Command successfully executed.", Severity.Success, null)).Returns((Snackbar)null); + + // Act + rebootButton.Click(); + Thread.Sleep(2500); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + } + + [Test] + public void ClickOnRebootShouldDisplaySnackbarIfError() + { + var mockIoTEdgeModule = new IoTEdgeModule() + { + ModuleName = Guid.NewGuid().ToString() + }; + + var mockIoTEdgeDevice = new IoTEdgeDevice() + { + DeviceId = mockdeviceId, + ConnectionState = "Connected", + Type = "Other", + Modules= new List(){mockIoTEdgeModule} + }; + + + _ = this.mockHttpClient + .When(HttpMethod.Get, $"/api/edge/devices/{this.mockdeviceId}") + .RespondJson(mockIoTEdgeDevice); + + _ = this.mockHttpClient + .When(HttpMethod.Post, $"/api/edge/devices/{mockIoTEdgeDevice.DeviceId}/{mockIoTEdgeModule.ModuleName}/RestartModule") + .With(m => + { + Assert.IsAssignableFrom>(m.Content); + var objectContent = m.Content as ObjectContent; + Assert.IsNotNull(objectContent); + + Assert.IsAssignableFrom(objectContent.Value); + var edgeModule = objectContent.Value as IoTEdgeModule; + Assert.IsNotNull(edgeModule); + + Assert.AreEqual(mockIoTEdgeModule.ModuleName, edgeModule.ModuleName); + + return true; + }) + .Respond(System.Net.HttpStatusCode.InternalServerError, new StringContent( + JsonConvert.SerializeObject(new C2Dresult() + { + Payload = "ABC", + Status = 500 + }), + Encoding.UTF8, + MediaTypeNames.Application.Json)); + + var cut = RenderComponent(ComponentParameter.CreateParameter("deviceId", this.mockdeviceId)); + Thread.Sleep(2500); + + var rebootButton = cut.WaitForElement("#rebootModule"); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show("Processing", It.IsAny())) + .Returns(mockDialogReference); + + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + + _ = this.mockSnackbarService.Setup(c => c.Add(It.IsAny(), Severity.Error, It.IsAny>())).Returns((Snackbar)null); + + // Act + rebootButton.Click(); + Thread.Sleep(2500); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + } + + [Test] + public void ClickOnLogsShouldDisplayLogs() + { + var mockIoTEdgeModule = new IoTEdgeModule() + { + ModuleName = Guid.NewGuid().ToString() + }; + + var mockIoTEdgeDevice = new IoTEdgeDevice() + { + DeviceId = mockdeviceId, + ConnectionState = "Connected", + Type = "Other", + Modules= new List(){mockIoTEdgeModule} + }; + + + _ = this.mockHttpClient + .When(HttpMethod.Get, $"/api/edge/devices/{this.mockdeviceId}") + .RespondJson(mockIoTEdgeDevice); + + var cut = RenderComponent(ComponentParameter.CreateParameter("deviceId", this.mockdeviceId)); + + var logsButton = cut.WaitForElement("#showLogs"); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference); + + // Act + logsButton.Click(); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + } + + [Test] + public void ClickOnConnectShouldDisplayDeviceCredentials() + { + var mockIoTEdgeDevice = new IoTEdgeDevice() + { + DeviceId = mockdeviceId, + ConnectionState = "Connected", + Type = "Other", + }; + + + _ = this.mockHttpClient + .When(HttpMethod.Get, $"/api/edge/devices/{this.mockdeviceId}") + .RespondJson(mockIoTEdgeDevice); + + var cut = RenderComponent(ComponentParameter.CreateParameter("deviceId", this.mockdeviceId)); + + var connectButton = cut.WaitForElement("#connectButton"); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference); + + // Act + connectButton.Click(); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + } + + [Test] + public void ClickOnDeleteShouldDisplayConfirmationDialogAndReturnIfAborted() + { + var mockIoTEdgeDevice = new IoTEdgeDevice() + { + DeviceId = mockdeviceId, + ConnectionState = "Connected", + Type = "Other", + }; + + _ = this.mockHttpClient + .When(HttpMethod.Get, $"/api/edge/devices/{this.mockdeviceId}") + .RespondJson(mockIoTEdgeDevice); + + var cut = RenderComponent(ComponentParameter.CreateParameter("deviceId", this.mockdeviceId)); + + var deleteButton = cut.WaitForElement("#deleteButton"); + + var mockDialogReference = this.mockRepository.Create(); + _ = mockDialogReference.Setup(c => c.Result).ReturnsAsync(DialogResult.Cancel()); + + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference.Object); + + // Act + deleteButton.Click(); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + } + + [Test] + public void ClickOnDeleteShouldDisplayConfirmationDialogAndRedirectIfConfirmed() + { + var mockIoTEdgeDevice = new IoTEdgeDevice() + { + DeviceId = mockdeviceId, + ConnectionState = "Connected", + Type = "Other", + }; + + _ = this.mockHttpClient + .When(HttpMethod.Get, $"/api/edge/devices/{this.mockdeviceId}") + .RespondJson(mockIoTEdgeDevice); + + var cut = RenderComponent(ComponentParameter.CreateParameter("deviceId", this.mockdeviceId)); + + var deleteButton = cut.WaitForElement("#deleteButton"); + + var mockDialogReference = this.mockRepository.Create(); + _ = mockDialogReference.Setup(c => c.Result).ReturnsAsync(DialogResult.Ok("Ok")); + + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference.Object); + + // Act + deleteButton.Click(); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + + cut.WaitForState(() => this.mockNavigationManager.Uri.EndsWith("/edge/devices", StringComparison.OrdinalIgnoreCase)); } public void Dispose() diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/LoRaWan/Concentrator/ConcentratorDetailPageTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/LoRaWan/Concentrator/ConcentratorDetailPageTests.cs similarity index 55% rename from src/AzureIoTHub.Portal.Server.Tests.Unit/LoRaWan/Concentrator/ConcentratorDetailPageTests.cs rename to src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/LoRaWan/Concentrator/ConcentratorDetailPageTests.cs index 525b6c293..b9bd3980c 100644 --- a/src/AzureIoTHub.Portal.Server.Tests.Unit/LoRaWan/Concentrator/ConcentratorDetailPageTests.cs +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/LoRaWan/Concentrator/ConcentratorDetailPageTests.cs @@ -6,7 +6,9 @@ namespace AzureIoTHub.Portal.Server.Tests.Unit.Pages using System; using System.Collections.Generic; using System.Net.Http; + using System.Threading; using AzureIoTHub.Portal.Client.Pages.LoRaWAN.Concentrator; + using AzureIoTHub.Portal.Client.Shared; using AzureIoTHub.Portal.Models.v10; using AzureIoTHub.Portal.Models.v10.LoRaWAN; using AzureIoTHub.Portal.Server.Tests.Unit.Helpers; @@ -31,6 +33,8 @@ public class ConcentratorDetailPageTests : IDisposable private readonly string mockDeviceID = Guid.NewGuid().ToString(); + private FakeNavigationManager mockNavigationManager; + [SetUp] public void SetUp() { @@ -42,6 +46,7 @@ public void SetUp() this.mockHttpClient = this.testContext.Services.AddMockHttpClient(); _ = this.testContext.Services.AddSingleton(this.mockDialogService.Object); + _ = this.testContext.Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); _ = this.testContext.Services.AddMudServices(); _ = this.testContext.JSInterop.SetupVoid("mudKeyInterceptor.connect", _ => true); @@ -51,6 +56,8 @@ public void SetUp() _ = this.testContext.JSInterop.Setup>("mudResizeObserver.connect", _ => true); _ = this.testContext.JSInterop.SetupVoid("mudJsEvent.connect", _ => true); this.mockHttpClient.AutoFlush = true; + + this.mockNavigationManager = this.testContext.Services.GetRequiredService(); } private IRenderedComponent RenderComponent(params ComponentParameter[] parameters) @@ -59,18 +66,6 @@ private IRenderedComponent RenderComponent(params Compon return this.testContext.RenderComponent(parameters); } - - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - } - [Test] public void ReturnButtonMustNavigateToPreviousPage() { @@ -79,9 +74,6 @@ public void ReturnButtonMustNavigateToPreviousPage() .When(HttpMethod.Get, $"/api/lorawan/concentrators/{this.mockDeviceID}") .RespondJson(new Concentrator()); - - _ = this.testContext.Services.AddSingleton(new PortalSettings { IsLoRaSupported = false }); - var cut = RenderComponent(ComponentParameter.CreateParameter("DeviceID", this.mockDeviceID)); var returnButton = cut.WaitForElement("#returnButton"); @@ -89,7 +81,70 @@ public void ReturnButtonMustNavigateToPreviousPage() returnButton.Click(); // Assert - cut.WaitForState(() => this.testContext.Services.GetRequiredService().Uri.EndsWith("/lorawan/concentrators", StringComparison.OrdinalIgnoreCase)); + cut.WaitForState(() => this.mockNavigationManager.Uri.EndsWith("/lorawan/concentrators", StringComparison.OrdinalIgnoreCase)); + } + + [Test] + public void ClickOnSaveShouldPutConcentratorDetails() + { + var mockConcentrator = new Concentrator() + { + DeviceId = "1234567890123456", + DeviceName = Guid.NewGuid().ToString(), + LoraRegion = Guid.NewGuid().ToString() + }; + + _ = this.mockHttpClient + .When(HttpMethod.Get, $"/api/lorawan/concentrators/{mockConcentrator.DeviceId}") + .RespondJson(mockConcentrator); + + _ = this.mockHttpClient + .When(HttpMethod.Put, $"/api/lorawan/concentrators") + .With(m => + { + Assert.IsAssignableFrom>(m.Content); + var objectContent = m.Content as ObjectContent; + Assert.IsNotNull(objectContent); + + Assert.IsAssignableFrom(objectContent.Value); + var concentrator = objectContent.Value as Concentrator; + Assert.IsNotNull(concentrator); + + Assert.AreEqual(mockConcentrator.DeviceId, concentrator.DeviceId); + Assert.AreEqual(mockConcentrator.DeviceName, concentrator.DeviceName); + Assert.AreEqual(mockConcentrator.LoraRegion, concentrator.LoraRegion); + + return true; + }) + .RespondText(string.Empty); + + var cut = RenderComponent(ComponentParameter.CreateParameter("DeviceID", mockConcentrator.DeviceId)); + var saveButton = cut.WaitForElement("#saveButton"); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + + _ = this.mockDialogService.Setup(c => c.Show("Processing", It.IsAny())) + .Returns(mockDialogReference); + + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + + saveButton.Click(); + Thread.Sleep(2500); + cut.WaitForState(() => this.mockNavigationManager.Uri.EndsWith("/lorawan/concentrators", StringComparison.OrdinalIgnoreCase)); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + } + } } diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/LoRaWan/Concentrator/CreateConcentratorPageTest.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/LoRaWan/Concentrator/CreateConcentratorPageTest.cs new file mode 100644 index 000000000..679310954 --- /dev/null +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/LoRaWan/Concentrator/CreateConcentratorPageTest.cs @@ -0,0 +1,157 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.Tests.Unit.Pages.LoRaWan.Concentrator +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Threading; + using AzureIoTHub.Portal.Client.Pages.LoRaWAN.Concentrator; + using AzureIoTHub.Portal.Client.Shared; + using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Models.v10.LoRaWAN; + using AzureIoTHub.Portal.Server.Tests.Unit.Helpers; + using Bunit; + using Bunit.TestDoubles; + using Microsoft.AspNetCore.Components; + using Microsoft.Extensions.DependencyInjection; + using Moq; + using MudBlazor; + using MudBlazor.Interop; + using MudBlazor.Services; + using NUnit.Framework; + using RichardSzalay.MockHttp; + + [TestFixture] + public class CreateConcentratorPageTest : IDisposable + { + private Bunit.TestContext testContext; + private MockHttpMessageHandler mockHttpClient; + private MockRepository mockRepository; + private Mock mockDialogService; + private Mock mockSnackbarService; + + private FakeNavigationManager mockNavigationManager; + + [SetUp] + public void SetUp() + { + this.testContext = new Bunit.TestContext(); + + this.mockRepository = new MockRepository(MockBehavior.Strict); + this.mockHttpClient = this.testContext.Services.AddMockHttpClient(); + + this.mockDialogService = this.mockRepository.Create(); + _ = this.testContext.Services.AddSingleton(this.mockDialogService.Object); + + this.mockSnackbarService = this.mockRepository.Create(); + _ = this.testContext.Services.AddSingleton(this.mockSnackbarService.Object); + + _ = this.testContext.Services.AddMudServices(); + _ = this.testContext.Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); + + _ = this.testContext.JSInterop.SetupVoid("mudKeyInterceptor.connect", _ => true); + _ = this.testContext.JSInterop.SetupVoid("mudPopover.connect", _ => true); + _ = this.testContext.JSInterop.SetupVoid("Blazor._internal.InputFile.init", _ => true); + _ = this.testContext.JSInterop.Setup("mudElementRef.getBoundingClientRect", _ => true); + _ = this.testContext.JSInterop.Setup>("mudResizeObserver.connect", _ => true); + _ = this.testContext.JSInterop.SetupVoid("mudJsEvent.connect", _ => true); + _ = this.testContext.JSInterop.SetupVoid("mudKeyInterceptor.updatekey", _ => true); + this.mockHttpClient.AutoFlush = true; + + this.mockNavigationManager = this.testContext.Services.GetRequiredService(); + } + + private IRenderedComponent RenderComponent(params ComponentParameter[] parameters) + where TComponent : IComponent + { + return this.testContext.RenderComponent(parameters); + } + + [Test] + public void ClickOnSaveShouldPostConcentratorDetails() + { + var mockConcentrator = new Concentrator() + { + DeviceId = "1234567890123456", + DeviceName = Guid.NewGuid().ToString(), + LoraRegion = "CN_470_510_RP2" + }; + + _ = this.mockHttpClient + .When(HttpMethod.Post, $"/api/lorawan/concentrators") + .With(m => + { + Assert.IsAssignableFrom>(m.Content); + var objectContent = m.Content as ObjectContent; + Assert.IsNotNull(objectContent); + + Assert.IsAssignableFrom(objectContent.Value); + var concentrator = objectContent.Value as Concentrator; + Assert.IsNotNull(concentrator); + + Assert.AreEqual(mockConcentrator.DeviceId, concentrator.DeviceId); + Assert.AreEqual(mockConcentrator.DeviceName, concentrator.DeviceName); + Assert.AreEqual(mockConcentrator.LoraRegion, concentrator.LoraRegion); + + return true; + }) + .RespondText(string.Empty); + + var cut = RenderComponent(); + var saveButton = cut.WaitForElement("#saveButton"); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference); + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + + _ = this.mockSnackbarService.Setup(c => c.Add(It.IsAny(), Severity.Success, null)).Returns((Snackbar)null); + + + cut.Find($"#{nameof(Concentrator.DeviceId)}").Change(mockConcentrator.DeviceId); + cut.Find($"#{nameof(Concentrator.DeviceName)}").Change(mockConcentrator.DeviceName); + cut.Instance.ChangeRegion(mockConcentrator.LoraRegion); + + saveButton.Click(); + Thread.Sleep(1000); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + cut.WaitForState(() => this.mockNavigationManager.Uri.EndsWith("/lorawan/concentrators", StringComparison.OrdinalIgnoreCase)); + } + + [Test] + public void ClickOnSaveShouldDisplayErrorSnackbarIfValidationError() + { + var cut = RenderComponent(); + var saveButton = cut.WaitForElement("#saveButton"); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference); + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + + _ = this.mockSnackbarService.Setup(c => c.Add(It.IsAny(), Severity.Error, null)).Returns((Snackbar)null); + + saveButton.Click(); + Thread.Sleep(1000); + + // Assert + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Settings/DeviceTagsPageTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Settings/DeviceTagsPageTests.cs new file mode 100644 index 000000000..00f74e121 --- /dev/null +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Settings/DeviceTagsPageTests.cs @@ -0,0 +1,266 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.Tests.Unit +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Threading; + using AzureIoTHub.Portal.Client.Pages.Settings; + using AzureIoTHub.Portal.Client.Shared; + using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Server.Tests.Unit.Helpers; + using Bunit; + using Bunit.TestDoubles; + using Microsoft.AspNetCore.Components; + using Microsoft.Extensions.DependencyInjection; + using Moq; + using MudBlazor; + using MudBlazor.Interop; + using MudBlazor.Services; + using NUnit.Framework; + using RichardSzalay.MockHttp; + + [TestFixture] + public class DeviceTagsPageTests : IDisposable + { + private Bunit.TestContext testContext; + private MockHttpMessageHandler mockHttpClient; + private MockRepository mockRepository; + private Mock mockDialogService; + private Mock mockSnackbarService; + private FakeNavigationManager mockNavigationManager; + + private static string ApiBaseUrl => "/api/settings/device-tags"; + + [SetUp] + public void SetUp() + { + this.testContext = new Bunit.TestContext(); + + this.mockRepository = new MockRepository(MockBehavior.Strict); + this.mockHttpClient = this.testContext.Services.AddMockHttpClient(); + + this.mockDialogService = this.mockRepository.Create(); + _ = this.testContext.Services.AddSingleton(this.mockDialogService.Object); + + this.mockSnackbarService = this.mockRepository.Create(); + _ = this.testContext.Services.AddSingleton(this.mockSnackbarService.Object); + + _ = this.testContext.Services.AddMudServices(); + + _ = this.testContext.Services.AddSingleton(new PortalSettings { IsLoRaSupported = false }); + + _ = this.testContext.JSInterop.SetupVoid("mudKeyInterceptor.connect", _ => true); + _ = this.testContext.JSInterop.SetupVoid("mudPopover.connect", _ => true); + _ = this.testContext.JSInterop.Setup("mudElementRef.getBoundingClientRect", _ => true); + _ = this.testContext.JSInterop.Setup>("mudResizeObserver.connect", _ => true); + + this.mockNavigationManager = this.testContext.Services.GetRequiredService(); + + this.mockHttpClient.AutoFlush = true; + } + + private IRenderedComponent RenderComponent(params ComponentParameter[] parameters) + where TComponent : IComponent + { + return this.testContext.RenderComponent(parameters); + } + + [Test] + public void ClickOnSaveShouldUpdateTagList() + { + var mockTag = new DeviceTag + { + Label = "Label", + Name = "Name", + Required = false, + Searchable = false + }; + + _ = this.mockHttpClient.When(HttpMethod.Get, $"{ApiBaseUrl}") + .RespondJson(new List(){ + mockTag + }); + + _ = this.mockHttpClient.When(HttpMethod.Post, $"{ApiBaseUrl}") + .With(m => + { + Assert.IsAssignableFrom>>(m.Content); + var objectContent = m.Content as ObjectContent>; + Assert.IsNotNull(objectContent); + + Assert.IsAssignableFrom>(objectContent.Value); + var tags = objectContent.Value as IEnumerable; + Assert.IsNotNull(tags); + + Assert.AreEqual(1, tags.Count()); + + var tag = tags.Single(x => x.Name == mockTag.Name); + + Assert.AreEqual(mockTag.Name, tag.Name); + Assert.AreEqual(mockTag.Label, tag.Label); + Assert.AreEqual(mockTag.Required, tag.Required); + Assert.AreEqual(mockTag.Searchable, tag.Searchable); + + return true; + }) + .RespondText(string.Empty); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference); + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + + _ = this.mockSnackbarService.Setup(c => c.Add(It.IsAny(), Severity.Success, null)).Returns((Snackbar)null); + + + var cut = RenderComponent(); + + var saveButton = cut.WaitForElement("#saveButton"); + + // Act + saveButton.Click(); + Thread.Sleep(1000); + + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + } + + [Test] + public void ClickOnSaveShouldDisplayErrorSnackbarIfDuplicated() + { + var mockTag = new DeviceTag + { + Label = "Label", + Name = "Name", + Required = false, + Searchable = false + }; + + _ = this.mockHttpClient.When(HttpMethod.Get, $"{ApiBaseUrl}") + .RespondJson(new List(){ + mockTag, + mockTag + }); + + _ = this.mockHttpClient.When(HttpMethod.Post, $"{ApiBaseUrl}") + .With(m => + { + Assert.IsAssignableFrom>>(m.Content); + var objectContent = m.Content as ObjectContent>; + Assert.IsNotNull(objectContent); + + Assert.IsAssignableFrom>(objectContent.Value); + var tags = objectContent.Value as IEnumerable; + Assert.IsNotNull(tags); + + Assert.AreEqual(1, tags.Count()); + + var tag = tags.Single(x => x.Name == mockTag.Name); + + Assert.AreEqual(mockTag.Name, tag.Name); + Assert.AreEqual(mockTag.Label, tag.Label); + Assert.AreEqual(mockTag.Required, tag.Required); + Assert.AreEqual(mockTag.Searchable, tag.Searchable); + + return true; + }) + .RespondText(string.Empty); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference); + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + + _ = this.mockSnackbarService.Setup(c => c.Add(It.IsAny(), Severity.Warning, null)).Returns((Snackbar)null); + _ = this.mockSnackbarService.Setup(c => c.Add(It.IsAny(), Severity.Error, null)).Returns((Snackbar)null); + + + var cut = RenderComponent(); + + var saveButton = cut.WaitForElement("#saveButton"); + + // Act + saveButton.Click(); + Thread.Sleep(1000); + + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + } + + [Test] + public void ClickOnSaveShouldDisplayErrorSnackbarIfValidationIssue() + { + var mockTag = new DeviceTag + { + Label = "Label", + Name = "InvalidName!", + Required = false, + Searchable = false + }; + + _ = this.mockHttpClient.When(HttpMethod.Get, $"{ApiBaseUrl}") + .RespondJson(new List(){ + mockTag, + mockTag + }); + + _ = this.mockHttpClient.When(HttpMethod.Post, $"{ApiBaseUrl}") + .With(m => + { + Assert.IsAssignableFrom>>(m.Content); + var objectContent = m.Content as ObjectContent>; + Assert.IsNotNull(objectContent); + + Assert.IsAssignableFrom>(objectContent.Value); + var tags = objectContent.Value as IEnumerable; + Assert.IsNotNull(tags); + + Assert.AreEqual(1, tags.Count()); + + var tag = tags.Single(x => x.Name == mockTag.Name); + + Assert.AreEqual(mockTag.Name, tag.Name); + Assert.AreEqual(mockTag.Label, tag.Label); + Assert.AreEqual(mockTag.Required, tag.Required); + Assert.AreEqual(mockTag.Searchable, tag.Searchable); + + return true; + }) + .RespondText(string.Empty); + + var mockDialogReference = new DialogReference(Guid.NewGuid(), this.mockDialogService.Object); + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference); + _ = this.mockDialogService.Setup(c => c.Close(It.Is(x => x == mockDialogReference))); + + _ = this.mockSnackbarService.Setup(c => c.Add(It.IsAny(), Severity.Warning, null)).Returns((Snackbar)null); + _ = this.mockSnackbarService.Setup(c => c.Add(It.IsAny(), Severity.Error, null)).Returns((Snackbar)null); + + + var cut = RenderComponent(); + + var saveButton = cut.WaitForElement("#saveButton"); + + // Act + saveButton.Click(); + Thread.Sleep(1000); + + this.mockHttpClient.VerifyNoOutstandingExpectation(); + this.mockRepository.VerifyAll(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Shared/ProcessingDialogTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Shared/ProcessingDialogTests.cs new file mode 100644 index 000000000..0d6f58400 --- /dev/null +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Shared/ProcessingDialogTests.cs @@ -0,0 +1,70 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.Tests.Unit.Pages.Shared +{ + using System; + using System.Threading.Tasks; + using AzureIoTHub.Portal.Client.Shared; + using Bunit; + using FluentAssertions; + using Microsoft.Extensions.DependencyInjection; + using MudBlazor; + using MudBlazor.Services; + using NUnit.Framework; + + [TestFixture] + public class ProcessingDialogTests : IDisposable + { + private Bunit.TestContext testContext; + + [SetUp] + public void SetUp() + { + this.testContext = new Bunit.TestContext(); + testContext.JSInterop.Mode = JSRuntimeMode.Loose; + _ = testContext.Services.AddSingleton(); + _ = testContext.Services.AddMudServices(); + } + + [Test] + public async Task DisplayDialog() + { + // Assert + var comp = this.testContext.RenderComponent(); + _ = comp.Markup.Trim().Should().BeEmpty(); + var service = this.testContext.Services.GetService() as DialogService; + _ = service?.Should().NotBe(null); + IDialogReference dialogReference = null; + + // Opens dialog + var parameters = new DialogParameters{{ "ContentText", "Processing" } }; + await comp.InvokeAsync(() => dialogReference = service?.Show("Processing", parameters)); + _ = dialogReference.Should().NotBe(null); + + // Checks dialog content + _ = comp.Find("div.mud-dialog-container").Should().NotBe(null); + _ = comp.Find("p.mud-typography").InnerHtml.Should().Be("Processing"); + _ = comp.Find("#contentText").InnerHtml.Should().Be("Processing"); + + // Dialog should not be closable through backdrop click + comp.Find("div.mud-overlay").Click(); + comp.WaitForAssertion(() => comp.Markup.Trim().Should().NotBeEmpty(), TimeSpan.FromSeconds(5)); + + // Dialog should be closable through a method + await comp.InvokeAsync(() => service?.Close(dialogReference as DialogReference)); + var result = await dialogReference.Result; + _ = result.Cancelled.Should().BeFalse(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/CreateDeviceModelPage.razor b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/CreateDeviceModelPage.razor index 1a71a1676..3f5e41bda 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/CreateDeviceModelPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/CreateDeviceModelPage.razor @@ -256,6 +256,9 @@ private async Task Save() { + var parameters = new DialogParameters{{ "ContentText", "Processing" }}; + MudBlazor.DialogReference processingDialog = DialogService.Show("Processing", parameters) as MudBlazor.DialogReference; + // Displays validation error message for each field await form.Validate(); @@ -290,6 +293,7 @@ duplicated || cmdValidationError))) { + DialogService.Close(processingDialog); Snackbar.Add("One or more validation errors occurred", Severity.Error); return; } @@ -329,6 +333,7 @@ var response = await httpClient.PostAsync($"{ApiUrlBase}/{Model.ModelId}/avatar", content); } + DialogService.Close(processingDialog); Snackbar.Add("Device model successfully created.", Severity.Success); // Go back to the list of devices diff --git a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor index 0b26a79ea..081ea9c34 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor @@ -273,10 +273,15 @@ else private async Task Save() { + var parameters = new DialogParameters{{ "ContentText", "Processing" }}; + MudBlazor.DialogReference processingDialog = DialogService.Show("Processing", parameters) as MudBlazor.DialogReference; + if (!standardValidator.Validate(Model).IsValid || !propertiesValidator.Validate(Properties).IsValid || (IsLoRa && !this.loraValidator.Validate(this.Model as LoRaDeviceModel).IsValid)) { + + DialogService.Close(processingDialog); Snackbar.Add("One or more validation errors occurred", Severity.Error); propertiesValidator.Validate(Properties).Errors.ForEach(x => @@ -312,6 +317,8 @@ else result = await httpClient.PostAsync($"{ApiUrlBase}/avatar", content); } + DialogService.Close(processingDialog); + if (result.IsSuccessStatusCode) { Snackbar.Add("Device model successfully updated.", Severity.Success); diff --git a/src/AzureIoTHub.Portal/Client/Pages/Devices/CreateDevicePage.razor b/src/AzureIoTHub.Portal/Client/Pages/Devices/CreateDevicePage.razor index 1ffe5d374..b5574517b 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/Devices/CreateDevicePage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/Devices/CreateDevicePage.razor @@ -12,6 +12,7 @@ @inject HttpClient Http @inject ISnackbar Snackbar @inject NavigationManager NavManager +@inject IDialogService DialogService Create Device @@ -21,46 +22,47 @@ @(string.IsNullOrEmpty(Device.DeviceName) ? Device.DeviceID : Device.DeviceName) - - - -
- -
-
- - - Save Changes - + + + +
+ +
+
+ + + Save Changes - - - - - - - - Details - - - - + + + + + + + + Details + + + + this.DeviceModel) Variant="Variant.Outlined" - ToStringFunc="@(x => x.Name)" + ToStringFunc="@(x => x?.Name)" ResetValueOnEmptyText=true Immediate=true Clearable=true CoerceText=true CoerceValue=false> - - @context.Name - - @((!string.IsNullOrEmpty(@context.Description) && @context.Description.Length > 100) ? @context.Description.Substring(0, 100) + "..." : @context.Description) + + @context.Name + + @((!string.IsNullOrEmpty(@context.Description) && @context.Description.Length > 100) ? @context.Description.Substring(0, 100) + "..." : @context.Description) @@ -73,6 +75,7 @@ @if (IsLoRa) { c.ToString().ToUpperInvariant()[0]; - [Parameter] - public string DeviceID { get; set; } - private DeviceDetails Device { get; set; } = new DeviceDetails(); private IEnumerable DeviceModelList { get; set; } = new List(); @@ -276,6 +278,9 @@ /// public async void Save() { + var parameters = new DialogParameters { { "ContentText", "Processing" } }; + MudBlazor.DialogReference processingDialog = DialogService.Show("Processing", parameters) as MudBlazor.DialogReference; + await form.Validate(); bool tagValidationError = CheckTagsError(); @@ -283,6 +288,7 @@ || (IsLoRa && !this.loraValidator.Validate(this.Device as LoRaDeviceDetails).IsValid) || tagValidationError) { + DialogService.Close(processingDialog); Snackbar.Add("One or more validation errors occurred", Severity.Error); // Allows to display ValidationError messages for the MudAutocomplete field. @@ -296,13 +302,15 @@ if (IsLoRa) result = await Http.PostAsJsonAsync(ApiUrlBase, Device as LoRaDeviceDetails); else + { result = await Http.PostAsJsonAsync(ApiUrlBase, Device); + result.EnsureSuccessStatusCode(); + result = await Http.PostAsJsonAsync($"{ApiUrlBase}/{Device.DeviceID}/properties", Properties); + } result.EnsureSuccessStatusCode(); - await Http.PostAsJsonAsync($"{ApiUrlBase}/{DeviceID}/properties", Properties); - - result.EnsureSuccessStatusCode(); + DialogService.Close(processingDialog); // Prompts a snack bar to inform the action was successful Snackbar.Add($"Device {Device.DeviceID} has been successfully created!", Severity.Success); @@ -343,7 +351,7 @@ .Where(x => x.Name.StartsWith(value, StringComparison.InvariantCultureIgnoreCase)); } - private async Task ChangeModel(DeviceModel model) + internal async Task ChangeModel(DeviceModel model) { Properties.Clear(); diff --git a/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor b/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor index 26272b32c..b5745df22 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor @@ -38,14 +38,14 @@ @if (isLoaded && (!IsLoRa || !(Device is LoRaDeviceDetails))) { - Connect + Connect } - Delete device - Save Changes + Delete device + Save Changes @@ -61,6 +61,7 @@ + @if (!Device.Tags.ContainsKey(tag.Name)) + { + Device.Tags.Add(tag.Name, ""); + } @@ -251,6 +257,9 @@ /// public async void Save() { + var parameters = new DialogParameters{{ "ContentText", "Processing" }}; + MudBlazor.DialogReference processingDialog = DialogService.Show("Processing", parameters) as MudBlazor.DialogReference; + await form.Validate(); bool tagValidationError = CheckTagsError(); @@ -258,6 +267,7 @@ || (IsLoRa && !this.loraValidator.Validate(this.Device as LoRaDeviceDetails).IsValid) || tagValidationError) { + DialogService.Close(processingDialog); Snackbar.Add("One or more validation errors occurred", Severity.Error); return; @@ -280,18 +290,20 @@ result.EnsureSuccessStatusCode(); - // Prompts a snack bar to inform the action was successful - Snackbar.Add($"Device {Device.DeviceName} has been successfully updated!", Severity.Success); + DialogService.Close(processingDialog); + // Prompts a snack bar to inform the action was successful + Snackbar.Add($"Device {Device.DeviceName} has been successfully updated!", Severity.Success, null); + // Go back to the list of devices NavManager.NavigateTo("devices"); } - public void ShowConnectionString() + public async Task ShowConnectionString() { var parameters = new DialogParameters(); parameters.Add(nameof(ConnectionStringDialog.deviceId), this.DeviceID); - DialogService.Show("Device Credentials", parameters); + _ = await DialogService.Show("Device Credentials", parameters).Result; } private bool CheckTagsError() diff --git a/src/AzureIoTHub.Portal/Client/Pages/Devices/LoRaWAN/EditLoraDevice.razor b/src/AzureIoTHub.Portal/Client/Pages/Devices/LoRaWAN/EditLoraDevice.razor index 1aa5e5d24..6a4535acf 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/Devices/LoRaWAN/EditLoraDevice.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/Devices/LoRaWAN/EditLoraDevice.razor @@ -3,6 +3,7 @@ @inject HttpClient Http @inject ISnackbar Snackbar +@inject IDialogService DialogService @@ -140,7 +141,7 @@ @CommandContext.Name - + @@ -169,8 +170,13 @@ private async Task ExecuteMethod(DeviceModelCommand method) { + var parameters = new DialogParameters{{ "ContentText", "Processing" }}; + MudBlazor.DialogReference processingDialog = DialogService.Show("Processing", parameters) as MudBlazor.DialogReference; + var result = await Http.PostAsJsonAsync($"api/lorawan/devices/{LoRaDevice.DeviceID}/_command/{method.Name}", method); + DialogService.Close(processingDialog); + if (result.IsSuccessStatusCode) { Snackbar.Add($"{method.Name} has been successfully executed!", diff --git a/src/AzureIoTHub.Portal/Client/Pages/Edge_Devices/CreateEdgeDeviceDialog.razor b/src/AzureIoTHub.Portal/Client/Pages/Edge_Devices/CreateEdgeDeviceDialog.razor index 93ac6aa81..505b4c373 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/Edge_Devices/CreateEdgeDeviceDialog.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/Edge_Devices/CreateEdgeDeviceDialog.razor @@ -45,7 +45,17 @@ Cancel - Create + + @if (processingSave) + { + + Processing + + } + else + { + Create + } @@ -56,12 +66,15 @@ [CascadingParameter] MudDialogInstance MudDialog { get; set; } private IoTEdgeDevice gateway = new IoTEdgeDevice(); + private bool processingSave = false; void Cancel() => MudDialog.Cancel(); private async Task OnValidation() { + processingSave = true; var result = await Http.PostAsJsonAsync("api/edge/devices", gateway); + processingSave = false; if (result.IsSuccessStatusCode) { diff --git a/src/AzureIoTHub.Portal/Client/Pages/Edge_Devices/EdgeDeviceDetailPage.razor b/src/AzureIoTHub.Portal/Client/Pages/Edge_Devices/EdgeDeviceDetailPage.razor index 3f7aa0efb..cca1438aa 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/Edge_Devices/EdgeDeviceDetailPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/Edge_Devices/EdgeDeviceDetailPage.razor @@ -39,30 +39,12 @@ else - Connect + Connect - @if (processingDelete) - { - - Processing - - } - else - { - Delete edge device - } - @if (processingUpdate) - { - - Processing - - } - else - { - Save Changes - } + Delete edge device + Save Changes @@ -176,8 +158,8 @@ else @context.Version @context.Status - logs - reboot + logs + reboot @@ -198,8 +180,6 @@ else public string deviceId { get; set; } private bool loading = true; private bool btn_disable = false; - private bool processingUpdate = false; - private bool processingDelete = false; private void Return() => navigationManager.NavigateTo("edge/devices"); private IoTEdgeDevice Gateway; @@ -223,9 +203,14 @@ else public async Task UpdateDevice() { - processingUpdate = true; + + var parameters = new DialogParameters{{ "ContentText", "Processing" }}; + MudBlazor.DialogReference processingDialog = DialogService.Show("Processing", parameters) as MudBlazor.DialogReference; + var result = await Http.PutAsJsonAsync($"api/edge/devices/{Gateway.DeviceId}", Gateway); + DialogService.Close(processingDialog); + if (result.IsSuccessStatusCode) { Snackbar.Add($"Device {Gateway.DeviceId} has been successfully updated!", Severity.Success); @@ -238,14 +223,18 @@ else { Snackbar.Add("Something unexpected occurred", Severity.Warning); } - - processingUpdate = false; } public async Task OnMethod(IoTEdgeModule module, string methodName) { + + var parameters = new DialogParameters{{ "ContentText", "Processing" }}; + MudBlazor.DialogReference processingDialog = DialogService.Show("Processing", parameters) as MudBlazor.DialogReference; + var result = await Http.PostAsJsonAsync($"api/edge/devices/{Gateway.DeviceId}/{module.ModuleName}/{methodName}", module); + DialogService.Close(processingDialog); + var c2dResult = result.Content.ReadFromJsonAsync().Result; if (c2dResult.Status == 200) @@ -282,13 +271,11 @@ else public async Task ShowDeleteModal() { - processingDelete = true; var parameter = new DialogParameters(); parameter.Add(nameof(Gateway.DeviceId), Gateway.DeviceId); var result = await DialogService.Show("Edge device deletion confirmation", parameter).Result; - processingDelete = false; if (result.Cancelled) { diff --git a/src/AzureIoTHub.Portal/Client/Pages/LoRaWAN/Concentrator/ConcentratorDetailPage.razor b/src/AzureIoTHub.Portal/Client/Pages/LoRaWAN/Concentrator/ConcentratorDetailPage.razor index 6397166a5..970410da9 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/LoRaWAN/Concentrator/ConcentratorDetailPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/LoRaWAN/Concentrator/ConcentratorDetailPage.razor @@ -47,7 +47,7 @@ Delete device - Save Changes + Save Changes @@ -157,9 +157,13 @@ public async void SaveDevice() { + var parameters = new DialogParameters{{ "ContentText", "Processing" }}; + MudBlazor.DialogReference processingDialog = DialogService.Show("Processing", parameters) as MudBlazor.DialogReference; + await form.Validate(); if (!this.concentratorValidator.Validate(this.concentrator).IsValid) { + DialogService.Close(processingDialog); Snackbar.Add($"One or more validation errors occurred", Severity.Error); return; } @@ -168,6 +172,8 @@ result.EnsureSuccessStatusCode(); + DialogService.Close(processingDialog); + // Prompts a snack bar to inform the action was successful Snackbar.Add($"Device {concentrator.DeviceId} has been successfully updated!", Severity.Success); diff --git a/src/AzureIoTHub.Portal/Client/Pages/LoRaWAN/Concentrator/CreateConcentratorPage.razor b/src/AzureIoTHub.Portal/Client/Pages/LoRaWAN/Concentrator/CreateConcentratorPage.razor index 389580adf..727bc95b4 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/LoRaWAN/Concentrator/CreateConcentratorPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/LoRaWAN/Concentrator/CreateConcentratorPage.razor @@ -8,6 +8,7 @@ @inject HttpClient Http @inject ISnackbar Snackbar @inject NavigationManager NavManager +@inject IDialogService DialogService LoRaWAN Concentrator @@ -22,7 +23,7 @@ - Save Changes + Save Changes @@ -37,6 +38,7 @@ @@ -55,7 +58,7 @@ @bind-Value="@concentrator.ClientThumbprint" Label="Client Certificate Thumbprint" Variant="Variant.Outlined" /> - + Europe 863-870 MHz United States 902-928 MHz, FSB 1 United States 902-928 MHz, FSB 2 @@ -115,10 +118,14 @@ /// public async void SaveDevice() { + var parameters = new DialogParameters{{ "ContentText", "Processing" }}; + MudBlazor.DialogReference processingDialog = DialogService.Show("Processing", parameters) as MudBlazor.DialogReference; + await form.Validate(); if (!this.concentratorValidator.Validate(this.concentrator).IsValid) { // string errorMsg = await response.Content.ReadAsStringAsync(); + DialogService.Close(processingDialog); Snackbar.Add("One or more validation errors occurred", Severity.Error); return; } @@ -130,10 +137,16 @@ response.EnsureSuccessStatusCode(); + DialogService.Close(processingDialog); + // Prompts a snack bar to inform the action was successful Snackbar.Add($"Device {concentrator.DeviceId} has been successfully created!", Severity.Success); // Go back to the list of concentrator NavManager.NavigateTo("lorawan/concentrators"); } + + // Workaround to change MudSelect value when performing unit tests. + internal void ChangeRegion(string region) { concentrator.LoraRegion = region; } + } diff --git a/src/AzureIoTHub.Portal/Client/Pages/Settings/DeviceTagsPage.razor b/src/AzureIoTHub.Portal/Client/Pages/Settings/DeviceTagsPage.razor index e6a716cd6..126973772 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/Settings/DeviceTagsPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/Settings/DeviceTagsPage.razor @@ -9,6 +9,7 @@ @attribute [Authorize] @inject HttpClient HttpClient @inject ISnackbar Snackbar +@inject IDialogService DialogService @@ -61,7 +62,7 @@ - Add a new Tag + Add a new Tag @@ -72,7 +73,7 @@ - Save Changes + Save Changes @code { @@ -106,6 +107,9 @@ private async Task Save() { + var parameters = new DialogParameters{{ "ContentText", "Processing" }}; + MudBlazor.DialogReference processingDialog = DialogService.Show("Processing", parameters) as MudBlazor.DialogReference; + // Checks empty field and regex validation await FormLabel.Validate(); await FormName.Validate(); @@ -122,16 +126,22 @@ duplicated = true; } + var test = FormLabel.IsValid; + var test2 = FormName.IsValid; + if (FormLabel.IsValid && FormName.IsValid && !duplicated) { var response = await HttpClient.PostAsJsonAsync($"/api/settings/device-tags", Tags); response.EnsureSuccessStatusCode(); + DialogService.Close(processingDialog); + // Prompts a snack bar to inform the action was successful Snackbar.Add($"Settings have been successfully updated!", Severity.Success); } else { + DialogService.Close(processingDialog); Snackbar.Add("One or more validation errors occurred", Severity.Error); return; } diff --git a/src/AzureIoTHub.Portal/Client/Shared/ProcessingDialog.razor b/src/AzureIoTHub.Portal/Client/Shared/ProcessingDialog.razor new file mode 100644 index 000000000..90f137a82 --- /dev/null +++ b/src/AzureIoTHub.Portal/Client/Shared/ProcessingDialog.razor @@ -0,0 +1,25 @@ + + + + + + + + + @ContentText + + + + + +@code { + [CascadingParameter] MudDialogInstance MudDialog { get; set; } + [Parameter] public string ContentText { get; set; } + + protected override void OnInitialized() + { + MudDialog.Options.NoHeader = true; + MudDialog.Options.DisableBackdropClick = true; + MudDialog.SetOptions(MudDialog.Options); + } +} diff --git a/src/AzureIoTHub.Portal/Client/wwwroot/scss/app.css b/src/AzureIoTHub.Portal/Client/wwwroot/scss/app.css new file mode 100644 index 000000000..95e2f2d65 --- /dev/null +++ b/src/AzureIoTHub.Portal/Client/wwwroot/scss/app.css @@ -0,0 +1,78 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap"); +.custom-form { + display: flex !important; + align-items: baseline !important; +} + +.custom-disabled { + background-color: #e2e2e2; +} + +.custom-centered-container { + display: flex !important; + justify-content: center !important; +} + +.custom-centered-item { + margin: auto; +} + +.custom-img-sm { + width: 35px; +} + +.img-device-model { + width: 200px; + height: auto; + margin-top: 1em; +} + +/*//////////////////// Search Panel ////////////////*/ +.search-panel { + /*width: max-content;*/ + /*padding: 5px;*/ + /*margin-left: 25%;*/ + /*justify-content: center;*/ + /*margin-bottom: 15px;*/ + /* .mud-grid-spacing-xs-3 { + display: contents; + }*/ +} + +.mud-dialog-width-sm { + max-width: fit-content; +} +.mud-dialog-width-sm .Add-dialog { + width: fit-content; +} +.mud-dialog-width-sm .Add-dialog .mud-input-control .mud-input-control-input-container { + width: 300px; +} + +.grid-custom .last-deployment { + background: gainsboro; + padding: 4px; +} + +.btn { + font-size: 0.9em; +} +.btn:hover { + color: #000000; +} + +.form-group { + font-size: 0.9em; +} + +.connectionString-modal-card .form-group { + align-items: baseline; + margin: 10px; + width: max-content; +} +.connectionString-modal-card .form-group label { + padding-right: 2px; + font-size: 1.1em; + font-weight: bold; + text-decoration: underline; +}