From dabb327c25b86fa094ad5e5daa84476930ade391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20TREMBLAY?= Date: Tue, 30 Aug 2022 16:11:21 +0200 Subject: [PATCH 01/12] Fix unsaved commands in the edge module #1139 --- .../Server/Entities/EdgeModuleCommand.cs | 13 +++++ .../Server/Factories/ITableClientFactory.cs | 3 ++ .../Server/Factories/TableClientFactory.cs | 5 ++ .../Server/Services/EdgeModelService.cs | 49 +++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs diff --git a/src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs b/src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs new file mode 100644 index 000000000..c810d9c3f --- /dev/null +++ b/src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs @@ -0,0 +1,13 @@ +// 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.Entities +{ + public class EdgeModuleCommand : EntityBase + { + /// + /// The property name + /// + public string Name { get; set; } + } +} diff --git a/src/AzureIoTHub.Portal/Server/Factories/ITableClientFactory.cs b/src/AzureIoTHub.Portal/Server/Factories/ITableClientFactory.cs index ac7ad6911..9c493e148 100644 --- a/src/AzureIoTHub.Portal/Server/Factories/ITableClientFactory.cs +++ b/src/AzureIoTHub.Portal/Server/Factories/ITableClientFactory.cs @@ -12,6 +12,7 @@ public interface ITableClientFactory const string EdgeDeviceTemplateTableName = "EdgeDeviceTemplates"; const string DeviceTagSettingTableName = "DeviceTagSettings"; const string DeviceTemplatePropertiesTableName = "DeviceTemplateProperties"; + const string EdgeModuleCommandsTableName = "EdgeModuleCommands"; TableClient GetDeviceCommands(); @@ -24,5 +25,7 @@ public interface ITableClientFactory TableClient GetDeviceTagSettings(); public TableClient GetTemplatesHealthCheck(); + + TableClient GetEdgeModuleCommands(); } } diff --git a/src/AzureIoTHub.Portal/Server/Factories/TableClientFactory.cs b/src/AzureIoTHub.Portal/Server/Factories/TableClientFactory.cs index 208941364..48a4e99da 100644 --- a/src/AzureIoTHub.Portal/Server/Factories/TableClientFactory.cs +++ b/src/AzureIoTHub.Portal/Server/Factories/TableClientFactory.cs @@ -64,5 +64,10 @@ public TableClient GetTemplatesHealthCheck() { return CreateClient("tableHealthCheck"); } + + public TableClient GetEdgeModuleCommands() + { + return CreateClient(ITableClientFactory.EdgeModuleCommandsTableName); + } } } diff --git a/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs b/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs index d05e2fa99..b1996fde1 100644 --- a/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs +++ b/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs @@ -15,6 +15,7 @@ namespace AzureIoTHub.Portal.Server.Services using Microsoft.AspNetCore.Http; using System.Threading.Tasks; using System; + using AzureIoTHub.Portal.Server.Entities; public class EdgeModelService : IEdgeModelService { @@ -91,6 +92,20 @@ public async Task GetEdgeModel(string modelId) var modules = await this.configService.GetConfigModuleList(modelId); + var commands = this.tableClientFactory.GetEdgeModuleCommands() + .Query($"PartitionKey eq '{modelId}'").ToArray(); + + foreach (var command in commands) + { + foreach (var module in modules) + { + if ((module.ModuleName + "-" + command.Name).Equals(command.RowKey, StringComparison.Ordinal)) + { + module.Commands.Add(new Shared.Models.v10.IoTEdgeModuleCommand { Name = command.Name }); + } + } + } + return this.edgeDeviceModelMapper.CreateEdgeDeviceModel(query.Value, modules); } catch (RequestFailedException e) @@ -295,6 +310,8 @@ private async Task SaveEntity(TableEntity entity, IoTEdgeModel deviceModelObject { this.edgeDeviceModelMapper.UpdateTableEntity(entity, deviceModelObject); + await SaveModuleCommands(deviceModelObject); + try { _ = await this.tableClientFactory @@ -308,5 +325,37 @@ private async Task SaveEntity(TableEntity entity, IoTEdgeModel deviceModelObject await this.configService.RollOutEdgeModelConfiguration(deviceModelObject); } + + /// + /// Saves the module commands for a specific model object. + /// + /// The device model object. + /// + /// + private async Task SaveModuleCommands(IoTEdgeModel deviceModelObject) + { + IEnumerable moduleCommands = deviceModelObject.EdgeModules + .SelectMany(x => x.Commands.Select(cmd => new EdgeModuleCommand + { + PartitionKey = deviceModelObject.ModelId, + RowKey = x.ModuleName + "-" + cmd.Name, + Timestamp = DateTime.Now, + Name = cmd.Name, + })).ToArray(); + + try + { + foreach (var moduleCommand in moduleCommands) + { + _ = this.tableClientFactory + .GetEdgeModuleCommands() + .UpsertEntity(moduleCommand); + } + } + catch (RequestFailedException e) + { + throw new InternalServerErrorException("Unable to save device module commands", e); + } + } } } From c3195a8e950e9d7834c8182601ba091088ec9526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20TREMBLAY?= Date: Tue, 30 Aug 2022 16:50:32 +0200 Subject: [PATCH 02/12] Fix the problem of the command deletion #1139 --- .../Server/Mappers/EdgeModelMapper.cs | 21 ++++++++++++-- .../Server/Mappers/IEdgeDeviceModelMapper.cs | 3 +- .../Server/Services/EdgeModelService.cs | 28 +++++++------------ 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/AzureIoTHub.Portal/Server/Mappers/EdgeModelMapper.cs b/src/AzureIoTHub.Portal/Server/Mappers/EdgeModelMapper.cs index 4ef2819dc..76482a6d9 100644 --- a/src/AzureIoTHub.Portal/Server/Mappers/EdgeModelMapper.cs +++ b/src/AzureIoTHub.Portal/Server/Mappers/EdgeModelMapper.cs @@ -5,9 +5,12 @@ namespace AzureIoTHub.Portal.Server.Mappers { using System; using System.Collections.Generic; + using System.Linq; using Azure.Data.Tables; using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Server.Entities; using AzureIoTHub.Portal.Server.Managers; + using AzureIoTHub.Portal.Shared.Models.v10; public class EdgeModelMapper : IEdgeDeviceModelMapper { @@ -36,11 +39,10 @@ public IoTEdgeModelListItem CreateEdgeDeviceModelListItem(TableEntity entity) }; } - public IoTEdgeModel CreateEdgeDeviceModel(TableEntity entity, List ioTEdgeModules) + public IoTEdgeModel CreateEdgeDeviceModel(TableEntity entity, List ioTEdgeModules, IEnumerable commands) { ArgumentNullException.ThrowIfNull(entity, nameof(entity)); - - return new IoTEdgeModel + var result = new IoTEdgeModel { ModelId = entity.RowKey, ImageUrl = this.deviceModelImageManager.ComputeImageUri(entity.RowKey), @@ -48,6 +50,19 @@ public IoTEdgeModel CreateEdgeDeviceModel(TableEntity entity, List (x.ModuleName + "-" + command.Name).Equals(command.RowKey, StringComparison.Ordinal)); + if (module == null) + { + continue; + } + module.Commands.Add(new IoTEdgeModuleCommand + { + Name = command.Name, + }); + } + return result; } public void UpdateTableEntity(TableEntity entity, IoTEdgeModel model) diff --git a/src/AzureIoTHub.Portal/Server/Mappers/IEdgeDeviceModelMapper.cs b/src/AzureIoTHub.Portal/Server/Mappers/IEdgeDeviceModelMapper.cs index 2feb1a1f6..314fd3c2e 100644 --- a/src/AzureIoTHub.Portal/Server/Mappers/IEdgeDeviceModelMapper.cs +++ b/src/AzureIoTHub.Portal/Server/Mappers/IEdgeDeviceModelMapper.cs @@ -6,12 +6,13 @@ namespace AzureIoTHub.Portal.Server.Mappers using System.Collections.Generic; using Azure.Data.Tables; using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Server.Entities; public interface IEdgeDeviceModelMapper { IoTEdgeModelListItem CreateEdgeDeviceModelListItem(TableEntity entity); - IoTEdgeModel CreateEdgeDeviceModel(TableEntity entity, List ioTEdgeModules); + IoTEdgeModel CreateEdgeDeviceModel(TableEntity entity, List ioTEdgeModules, IEnumerable commands); void UpdateTableEntity(TableEntity entity, IoTEdgeModel model); } diff --git a/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs b/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs index b1996fde1..62ccbba9c 100644 --- a/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs +++ b/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs @@ -89,24 +89,11 @@ public async Task GetEdgeModel(string modelId) var query = await this.tableClientFactory .GetEdgeDeviceTemplates() .GetEntityAsync(DefaultPartitionKey, modelId); - var modules = await this.configService.GetConfigModuleList(modelId); - var commands = this.tableClientFactory.GetEdgeModuleCommands() - .Query($"PartitionKey eq '{modelId}'").ToArray(); - - foreach (var command in commands) - { - foreach (var module in modules) - { - if ((module.ModuleName + "-" + command.Name).Equals(command.RowKey, StringComparison.Ordinal)) - { - module.Commands.Add(new Shared.Models.v10.IoTEdgeModuleCommand { Name = command.Name }); - } - } - } - - return this.edgeDeviceModelMapper.CreateEdgeDeviceModel(query.Value, modules); + .Query(c => c.PartitionKey == modelId) + .ToArray(); + return this.edgeDeviceModelMapper.CreateEdgeDeviceModel(query.Value, modules, commands); } catch (RequestFailedException e) { @@ -114,7 +101,6 @@ public async Task GetEdgeModel(string modelId) { throw new ResourceNotFoundException($"The edge model with id {modelId} doesn't exist"); } - throw new InternalServerErrorException($"Unable to get the edge model with id {modelId}", e); } } @@ -342,9 +328,15 @@ private async Task SaveModuleCommands(IoTEdgeModel deviceModelObject) Timestamp = DateTime.Now, Name = cmd.Name, })).ToArray(); - try { + var existingCommands = this.tableClientFactory.GetEdgeModuleCommands() + .Query(c => c.PartitionKey == deviceModelObject.ModelId) + .ToArray(); + foreach (var command in existingCommands.Where(c => !moduleCommands.Any(x => x.RowKey == c.RowKey))) + { + _ = await this.tableClientFactory.GetEdgeModuleCommands().DeleteEntityAsync(command.PartitionKey, command.RowKey); + } foreach (var moduleCommand in moduleCommands) { _ = this.tableClientFactory From 5a91ec4956bc3d76b9ad6a9f53368b15fd0aff4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20TREMBLAY?= Date: Wed, 31 Aug 2022 09:06:45 +0200 Subject: [PATCH 03/12] Fix save and cancel buttons in the command page #1139 --- .../Client/Pages/EdgeModels/EdgeModule/ModuleDialog.razor | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/AzureIoTHub.Portal/Client/Pages/EdgeModels/EdgeModule/ModuleDialog.razor b/src/AzureIoTHub.Portal/Client/Pages/EdgeModels/EdgeModule/ModuleDialog.razor index 496687084..cd8b5e5fb 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/EdgeModels/EdgeModule/ModuleDialog.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/EdgeModels/EdgeModule/ModuleDialog.razor @@ -42,8 +42,7 @@ - Save - Cancel + OK From 80b08c1c12a52417025901bf3277a1af379de56a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20TREMBLAY?= Date: Wed, 31 Aug 2022 15:09:00 +0200 Subject: [PATCH 04/12] Fix unit tests #1139 --- .../EdgeModule/ModuleDialogTests.cs | 44 --------- .../Server/Mappers/EdgeModelMapperTest.cs | 22 ++++- .../Server/Services/EdgeModelServiceTest.cs | 91 ++++++++++++++++++- 3 files changed, 110 insertions(+), 47 deletions(-) diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Configurations/EdgeModule/ModuleDialogTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Configurations/EdgeModule/ModuleDialogTests.cs index 3d80a0ddb..5148ac692 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Configurations/EdgeModule/ModuleDialogTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Configurations/EdgeModule/ModuleDialogTests.cs @@ -105,49 +105,5 @@ public async Task ClickOnSubmitShouldUpdateModuleValues() cut.WaitForAssertion(() => module.ModuleName.Should().Be("newModuleNameValue")); cut.WaitForAssertion(() => module.ImageURI.Should().Be("newModuleImageUriValue")); } - - [Test] - public async Task ClickOnCancelShouldNotChangeModuleValues() - { - //Arrange - var moduleName = Guid.NewGuid().ToString(); - var moduleVersion = Guid.NewGuid().ToString(); - var moduleImageUri = Guid.NewGuid().ToString(); - - var module = new IoTEdgeModule() - { - ModuleName = moduleName, - Version = moduleVersion, - Status = "running", - ImageURI = moduleImageUri, - EnvironmentVariables = new List(), - ModuleIdentityTwinSettings = new List(), - Commands = new List() - }; - - var cut = RenderComponent(); - var service = Services.GetService() as DialogService; - - var parameters = new DialogParameters - { - { - "module", module - } - }; - - // Act - await cut.InvokeAsync(() => service?.Show(string.Empty, parameters)); - - cut.WaitForAssertion(() => cut.Find("div.mud-dialog-container").Should().NotBeNull()); - - cut.WaitForAssertion(() => cut.Find($"#{nameof(IoTEdgeModule.ModuleName)}").Change("newModuleNameValue")); - cut.WaitForAssertion(() => cut.Find($"#{nameof(IoTEdgeModule.ImageURI)}").Change("newModuleImageUriValue")); - - var cancelButton = cut.WaitForElement("#CancelButton"); - cancelButton.Click(); - - cut.WaitForAssertion(() => module.ModuleName.Should().Be(moduleName)); - cut.WaitForAssertion(() => module.ImageURI.Should().Be(moduleImageUri)); - } } } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Mappers/EdgeModelMapperTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Mappers/EdgeModelMapperTest.cs index 27861bdb3..70fe68802 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Mappers/EdgeModelMapperTest.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Mappers/EdgeModelMapperTest.cs @@ -5,8 +5,10 @@ namespace AzureIoTHub.Portal.Tests.Unit.Server.Mappers { using System; using System.Collections.Generic; + using System.Linq; using Azure.Data.Tables; using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Server.Entities; using AzureIoTHub.Portal.Server.Managers; using AzureIoTHub.Portal.Server.Mappers; using Moq; @@ -81,16 +83,32 @@ public void CreateEdgeDeviceModelShouldReturnIoTEdgeModelObject() ["Description"] = "description_test", }; - var modules = new List(){ new IoTEdgeModule()}; + var modules = new List() + { + new IoTEdgeModule + { + ModuleName = "module" + } + }; + var commands = new List() + { + new EdgeModuleCommand + { + PartitionKey = partitionKey, + Name = "Test", + RowKey = modules.First().ModuleName + "-" + "Test", + } + }; // Act - var result = edgeModelMapper.CreateEdgeDeviceModel(entity, modules); + var result = edgeModelMapper.CreateEdgeDeviceModel(entity, modules, commands); // Assert Assert.IsNotNull(result); Assert.AreEqual(rowKey, result.ModelId); Assert.AreEqual("test-name", result.Name); Assert.AreEqual(1, result.EdgeModules.Count); + Assert.AreEqual(1, result.EdgeModules.First().Commands.Count); this.mockRepository.VerifyAll(); } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs index d81ffc366..f4a45974d 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs @@ -23,6 +23,8 @@ namespace AzureIoTHub.Portal.Tests.Unit.Server.Services using FluentAssertions; using AzureIoTHub.Portal.Server.Exceptions; using Microsoft.Azure.Devices; + using AzureIoTHub.Portal.Server.Entities; + using System.Linq.Expressions; [TestFixture] public class EdgeModelServiceTest @@ -34,6 +36,7 @@ public class EdgeModelServiceTest private Mock mockEdgeModelMapper; private Mock mockDeviceModelImageManager; private Mock mockEdgeDeviceTemplatesTableClient; + private Mock mockEdgeModuleCommands; [SetUp] public void SetUp() @@ -45,6 +48,7 @@ public void SetUp() this.mockEdgeModelMapper = this.mockRepository.Create(); this.mockDeviceModelImageManager = this.mockRepository.Create(); this.mockEdgeDeviceTemplatesTableClient = this.mockRepository.Create(); + this.mockEdgeModuleCommands = this.mockRepository.Create(); } public EdgeModelService CreateEdgeModelService() @@ -125,6 +129,23 @@ public async Task GetEdgeModelShouldReturnValue() var mockResponse = this.mockRepository.Create>(); _ = mockResponse.Setup(c => c.Value).Returns(new TableEntity(Guid.NewGuid().ToString(), Guid.NewGuid().ToString())); + var mockModuleCommandResponse = this.mockRepository.Create(); + + var mockModuleCommands = Pageable.FromPages(new[] + { + Page.FromValues(Array.Empty(), null, mockModuleCommandResponse.Object) + }); + + _ = this.mockEdgeModuleCommands.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockModuleCommands); + + _ = this.mockTableClientFactory.Setup(c => c.GetEdgeModuleCommands()) + .Returns(this.mockEdgeModuleCommands.Object); + _ = this.mockEdgeDeviceTemplatesTableClient.Setup(c => c.GetEntityAsync( It.Is(p => p == EdgeModelService.DefaultPartitionKey), It.Is(k => k == "test"), @@ -140,7 +161,7 @@ public async Task GetEdgeModelShouldReturnValue() .ReturnsAsync(new List()); _ = this.mockEdgeModelMapper - .Setup(x => x.CreateEdgeDeviceModel(It.IsAny(), It.IsAny>())) + .Setup(x => x.CreateEdgeDeviceModel(It.IsAny(), It.IsAny>(), It.IsAny>())) .Returns(new IoTEdgeModel()); // Act @@ -212,6 +233,23 @@ public async Task CreateEdgeModelShouldCreateEdgeModelTemplate() var mockResponse = this.mockRepository.Create(); + var mockModuleCommandResponse = this.mockRepository.Create(); + + var mockModuleCommands = Pageable.FromPages(new[] + { + Page.FromValues(Array.Empty(), null, mockModuleCommandResponse.Object) + }); + + _ = this.mockEdgeModuleCommands.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockModuleCommands); + + _ = this.mockTableClientFactory.Setup(c => c.GetEdgeModuleCommands()) + .Returns(this.mockEdgeModuleCommands.Object); + _ = this.mockEdgeModelMapper .Setup(x => x.UpdateTableEntity(It.IsAny(), It.IsAny())); @@ -310,6 +348,23 @@ public void WhenUpsertEntityAsyncFailedCreateEdgeModelShoulThrowInternalServerEr ModelId = Guid.NewGuid().ToString() }; + var mockModuleCommandResponse = this.mockRepository.Create(); + + var mockModuleCommands = Pageable.FromPages(new[] + { + Page.FromValues(Array.Empty(), null, mockModuleCommandResponse.Object) + }); + + _ = this.mockEdgeModuleCommands.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockModuleCommands); + + _ = this.mockTableClientFactory.Setup(c => c.GetEdgeModuleCommands()) + .Returns(this.mockEdgeModuleCommands.Object); + _ = this.mockEdgeModelMapper .Setup(x => x.UpdateTableEntity(It.IsAny(), It.IsAny())); @@ -343,6 +398,23 @@ public async Task UpdateEdgeModelShouldUpdateEdgeModelTemplate() ModelId = Guid.NewGuid().ToString() }; + var mockModuleCommandResponse = this.mockRepository.Create(); + + var mockModuleCommands = Pageable.FromPages(new[] + { + Page.FromValues(Array.Empty(), null, mockModuleCommandResponse.Object) + }); + + _ = this.mockEdgeModuleCommands.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockModuleCommands); + + _ = this.mockTableClientFactory.Setup(c => c.GetEdgeModuleCommands()) + .Returns(this.mockEdgeModuleCommands.Object); + var mockResponse = this.mockRepository.Create>(); _ = mockResponse.Setup(c => c.Value).Returns(new TableEntity(Guid.NewGuid().ToString(), Guid.NewGuid().ToString())); @@ -437,6 +509,23 @@ public void WhenUpsertEntityAsyncFailedUpdateEdgeModelShoulThrowInternalServerEr ModelId = Guid.NewGuid().ToString() }; + var mockModuleCommandResponse = this.mockRepository.Create(); + + var mockModuleCommands = Pageable.FromPages(new[] + { + Page.FromValues(Array.Empty(), null, mockModuleCommandResponse.Object) + }); + + _ = this.mockEdgeModuleCommands.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockModuleCommands); + + _ = this.mockTableClientFactory.Setup(c => c.GetEdgeModuleCommands()) + .Returns(this.mockEdgeModuleCommands.Object); + var mockResponse = this.mockRepository.Create>(); _ = mockResponse.Setup(c => c.Value).Returns(new TableEntity(Guid.NewGuid().ToString(), Guid.NewGuid().ToString())); From 9d78bc7cee85a1ce1b7813b7062ee923e0a009eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20TREMBLAY?= Date: Thu, 1 Sep 2022 14:08:07 +0200 Subject: [PATCH 05/12] Add unit tests for the save commands method #1139 --- .../Server/Services/EdgeModelServiceTest.cs | 155 ++++++++++++++++-- .../Server/Entities/EdgeModuleCommand.cs | 3 + .../Server/Services/EdgeModelService.cs | 3 +- 3 files changed, 146 insertions(+), 15 deletions(-) diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs index f4a45974d..fdc6967ab 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs @@ -3,28 +3,29 @@ namespace AzureIoTHub.Portal.Tests.Unit.Server.Services { - using Azure; - using System.Collections.Generic; using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Linq.Expressions; + using System.Threading; + using System.Threading.Tasks; + using Azure; using Azure.Data.Tables; + using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Server.Controllers.V10.LoRaWAN; + using AzureIoTHub.Portal.Server.Entities; + using AzureIoTHub.Portal.Server.Exceptions; using AzureIoTHub.Portal.Server.Factories; using AzureIoTHub.Portal.Server.Managers; using AzureIoTHub.Portal.Server.Mappers; using AzureIoTHub.Portal.Server.Services; - using Moq; - using NUnit.Framework; - using AzureIoTHub.Portal.Models.v10; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - using AzureIoTHub.Portal.Server.Controllers.V10.LoRaWAN; - using System.IO; + using AzureIoTHub.Portal.Shared.Models.v10; using FluentAssertions; - using AzureIoTHub.Portal.Server.Exceptions; + using Microsoft.AspNetCore.Http; using Microsoft.Azure.Devices; - using AzureIoTHub.Portal.Server.Entities; - using System.Linq.Expressions; + using Moq; + using NUnit.Framework; [TestFixture] public class EdgeModelServiceTest @@ -721,6 +722,132 @@ public async Task UpdateEdgeModelAvatarShouldUpdateValue() this.mockRepository.VerifyAll(); } + [Test] + public async Task SaveModuleCommandsShouldUpsertModuleCommandTemplates() + { + // Arrange + var edgeModelService = CreateEdgeModelService(); + + var modules = new List() + { + new IoTEdgeModule + { + ModuleName = "Test", + Commands = new List + { + new IoTEdgeModuleCommand + { + Name = "Command" + } + } + } + }; + + var iotEdgeModel = new IoTEdgeModel + { + ModelId = Guid.NewGuid().ToString(), + EdgeModules = modules, + }; + + var mockModuleCommandResponse = this.mockRepository.Create(); + + var mockModuleCommands = Pageable.FromPages(new[] + { + Page.FromValues(Array.Empty(), null, mockModuleCommandResponse.Object) + }); + + _ = this.mockEdgeModuleCommands.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockModuleCommands); + + _ = this.mockEdgeModuleCommands + .Setup(c => c.UpsertEntity( + It.Is(x => x.RowKey == "Test-Command" && x.PartitionKey == iotEdgeModel.ModelId), + It.IsAny(), + It.IsAny())) + .Returns((Response)null); + + _ = this.mockTableClientFactory.Setup(c => c.GetEdgeModuleCommands()) + .Returns(this.mockEdgeModuleCommands.Object); + + // Act + await edgeModelService.SaveModuleCommands(iotEdgeModel); + + // Assert + this.mockEdgeModuleCommands.Verify(c => c.UpsertEntity(It.IsAny(), TableUpdateMode.Merge, default), Times.Once); + this.mockTableClientFactory.Verify(c => c.GetEdgeModuleCommands(), Times.Exactly(2)); + this.mockRepository.VerifyAll(); + } + + [Test] + public async Task SaveModuleCommandsShouldDeleteModuleCommandTemplates() + { + // Arrange + var edgeModelService = CreateEdgeModelService(); + + var modules = new List() + { + new IoTEdgeModule + { + ModuleName = "Test", + Commands = new List() + } + }; + + var iotEdgeModel = new IoTEdgeModel + { + ModelId = Guid.NewGuid().ToString(), + EdgeModules = modules, + }; + + var mockModuleCommandResponse = this.mockRepository.Create(); + + var mockModuleCommands = Pageable.FromPages(new[] + { + Page.FromValues(new EdgeModuleCommand[1] + { + new EdgeModuleCommand + { + PartitionKey = iotEdgeModel.ModelId, + RowKey = "Test-Command", + Timestamp = DateTime.Now, + Name = "Command", + } + }, null, mockModuleCommandResponse.Object) + }); + + var mockResponse = this.mockRepository.Create(); + + _ = this.mockEdgeModuleCommands.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockModuleCommands); + + _ = this.mockEdgeModuleCommands + .Setup(c => c.DeleteEntityAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(mockResponse.Object); + + _ = this.mockTableClientFactory.Setup(c => c.GetEdgeModuleCommands()) + .Returns(this.mockEdgeModuleCommands.Object); + + // Act + await edgeModelService.SaveModuleCommands(iotEdgeModel); + + // Assert + this.mockEdgeModuleCommands.Verify(c => c.DeleteEntityAsync(It.IsAny(), It.IsAny(), default, default), Times.Once); + this.mockTableClientFactory.Verify(c => c.GetEdgeModuleCommands(), Times.Exactly(2)); + this.mockRepository.VerifyAll(); + } + [Test] public void WhenGetEntityAsyncFailedUpdateEdgeModelAvatarShouldThrowInternalServerErrorException() { diff --git a/src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs b/src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs index c810d9c3f..8129f855d 100644 --- a/src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs +++ b/src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs @@ -3,6 +3,9 @@ namespace AzureIoTHub.Portal.Server.Entities { + /// + /// Represents an edge module command. + /// public class EdgeModuleCommand : EntityBase { /// diff --git a/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs b/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs index 62ccbba9c..480e1e635 100644 --- a/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs +++ b/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs @@ -318,7 +318,7 @@ private async Task SaveEntity(TableEntity entity, IoTEdgeModel deviceModelObject /// The device model object. /// /// - private async Task SaveModuleCommands(IoTEdgeModel deviceModelObject) + public async Task SaveModuleCommands(IoTEdgeModel deviceModelObject) { IEnumerable moduleCommands = deviceModelObject.EdgeModules .SelectMany(x => x.Commands.Select(cmd => new EdgeModuleCommand @@ -328,6 +328,7 @@ private async Task SaveModuleCommands(IoTEdgeModel deviceModelObject) Timestamp = DateTime.Now, Name = cmd.Name, })).ToArray(); + try { var existingCommands = this.tableClientFactory.GetEdgeModuleCommands() From 7febdd89cf8449dc7f4205ffa32f763d0d55c269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20TREMBLAY?= Date: Tue, 30 Aug 2022 16:11:21 +0200 Subject: [PATCH 06/12] Fix unsaved commands in the edge module #1139 --- .../Server/Entities/EdgeModuleCommand.cs | 13 +++++ .../Server/Factories/ITableClientFactory.cs | 3 ++ .../Server/Factories/TableClientFactory.cs | 5 ++ .../Server/Services/EdgeModelService.cs | 49 +++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs diff --git a/src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs b/src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs new file mode 100644 index 000000000..c810d9c3f --- /dev/null +++ b/src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs @@ -0,0 +1,13 @@ +// 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.Entities +{ + public class EdgeModuleCommand : EntityBase + { + /// + /// The property name + /// + public string Name { get; set; } + } +} diff --git a/src/AzureIoTHub.Portal/Server/Factories/ITableClientFactory.cs b/src/AzureIoTHub.Portal/Server/Factories/ITableClientFactory.cs index ac7ad6911..9c493e148 100644 --- a/src/AzureIoTHub.Portal/Server/Factories/ITableClientFactory.cs +++ b/src/AzureIoTHub.Portal/Server/Factories/ITableClientFactory.cs @@ -12,6 +12,7 @@ public interface ITableClientFactory const string EdgeDeviceTemplateTableName = "EdgeDeviceTemplates"; const string DeviceTagSettingTableName = "DeviceTagSettings"; const string DeviceTemplatePropertiesTableName = "DeviceTemplateProperties"; + const string EdgeModuleCommandsTableName = "EdgeModuleCommands"; TableClient GetDeviceCommands(); @@ -24,5 +25,7 @@ public interface ITableClientFactory TableClient GetDeviceTagSettings(); public TableClient GetTemplatesHealthCheck(); + + TableClient GetEdgeModuleCommands(); } } diff --git a/src/AzureIoTHub.Portal/Server/Factories/TableClientFactory.cs b/src/AzureIoTHub.Portal/Server/Factories/TableClientFactory.cs index 208941364..48a4e99da 100644 --- a/src/AzureIoTHub.Portal/Server/Factories/TableClientFactory.cs +++ b/src/AzureIoTHub.Portal/Server/Factories/TableClientFactory.cs @@ -64,5 +64,10 @@ public TableClient GetTemplatesHealthCheck() { return CreateClient("tableHealthCheck"); } + + public TableClient GetEdgeModuleCommands() + { + return CreateClient(ITableClientFactory.EdgeModuleCommandsTableName); + } } } diff --git a/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs b/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs index d05e2fa99..b1996fde1 100644 --- a/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs +++ b/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs @@ -15,6 +15,7 @@ namespace AzureIoTHub.Portal.Server.Services using Microsoft.AspNetCore.Http; using System.Threading.Tasks; using System; + using AzureIoTHub.Portal.Server.Entities; public class EdgeModelService : IEdgeModelService { @@ -91,6 +92,20 @@ public async Task GetEdgeModel(string modelId) var modules = await this.configService.GetConfigModuleList(modelId); + var commands = this.tableClientFactory.GetEdgeModuleCommands() + .Query($"PartitionKey eq '{modelId}'").ToArray(); + + foreach (var command in commands) + { + foreach (var module in modules) + { + if ((module.ModuleName + "-" + command.Name).Equals(command.RowKey, StringComparison.Ordinal)) + { + module.Commands.Add(new Shared.Models.v10.IoTEdgeModuleCommand { Name = command.Name }); + } + } + } + return this.edgeDeviceModelMapper.CreateEdgeDeviceModel(query.Value, modules); } catch (RequestFailedException e) @@ -295,6 +310,8 @@ private async Task SaveEntity(TableEntity entity, IoTEdgeModel deviceModelObject { this.edgeDeviceModelMapper.UpdateTableEntity(entity, deviceModelObject); + await SaveModuleCommands(deviceModelObject); + try { _ = await this.tableClientFactory @@ -308,5 +325,37 @@ private async Task SaveEntity(TableEntity entity, IoTEdgeModel deviceModelObject await this.configService.RollOutEdgeModelConfiguration(deviceModelObject); } + + /// + /// Saves the module commands for a specific model object. + /// + /// The device model object. + /// + /// + private async Task SaveModuleCommands(IoTEdgeModel deviceModelObject) + { + IEnumerable moduleCommands = deviceModelObject.EdgeModules + .SelectMany(x => x.Commands.Select(cmd => new EdgeModuleCommand + { + PartitionKey = deviceModelObject.ModelId, + RowKey = x.ModuleName + "-" + cmd.Name, + Timestamp = DateTime.Now, + Name = cmd.Name, + })).ToArray(); + + try + { + foreach (var moduleCommand in moduleCommands) + { + _ = this.tableClientFactory + .GetEdgeModuleCommands() + .UpsertEntity(moduleCommand); + } + } + catch (RequestFailedException e) + { + throw new InternalServerErrorException("Unable to save device module commands", e); + } + } } } From e6c866333b047754bc0c7e1cd56ae7f410525ab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20TREMBLAY?= Date: Tue, 30 Aug 2022 16:50:32 +0200 Subject: [PATCH 07/12] Fix the problem of the command deletion #1139 --- .../Server/Mappers/EdgeModelMapper.cs | 21 ++++++++++++-- .../Server/Mappers/IEdgeDeviceModelMapper.cs | 3 +- .../Server/Services/EdgeModelService.cs | 28 +++++++------------ 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/AzureIoTHub.Portal/Server/Mappers/EdgeModelMapper.cs b/src/AzureIoTHub.Portal/Server/Mappers/EdgeModelMapper.cs index 4ef2819dc..76482a6d9 100644 --- a/src/AzureIoTHub.Portal/Server/Mappers/EdgeModelMapper.cs +++ b/src/AzureIoTHub.Portal/Server/Mappers/EdgeModelMapper.cs @@ -5,9 +5,12 @@ namespace AzureIoTHub.Portal.Server.Mappers { using System; using System.Collections.Generic; + using System.Linq; using Azure.Data.Tables; using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Server.Entities; using AzureIoTHub.Portal.Server.Managers; + using AzureIoTHub.Portal.Shared.Models.v10; public class EdgeModelMapper : IEdgeDeviceModelMapper { @@ -36,11 +39,10 @@ public IoTEdgeModelListItem CreateEdgeDeviceModelListItem(TableEntity entity) }; } - public IoTEdgeModel CreateEdgeDeviceModel(TableEntity entity, List ioTEdgeModules) + public IoTEdgeModel CreateEdgeDeviceModel(TableEntity entity, List ioTEdgeModules, IEnumerable commands) { ArgumentNullException.ThrowIfNull(entity, nameof(entity)); - - return new IoTEdgeModel + var result = new IoTEdgeModel { ModelId = entity.RowKey, ImageUrl = this.deviceModelImageManager.ComputeImageUri(entity.RowKey), @@ -48,6 +50,19 @@ public IoTEdgeModel CreateEdgeDeviceModel(TableEntity entity, List (x.ModuleName + "-" + command.Name).Equals(command.RowKey, StringComparison.Ordinal)); + if (module == null) + { + continue; + } + module.Commands.Add(new IoTEdgeModuleCommand + { + Name = command.Name, + }); + } + return result; } public void UpdateTableEntity(TableEntity entity, IoTEdgeModel model) diff --git a/src/AzureIoTHub.Portal/Server/Mappers/IEdgeDeviceModelMapper.cs b/src/AzureIoTHub.Portal/Server/Mappers/IEdgeDeviceModelMapper.cs index 2feb1a1f6..314fd3c2e 100644 --- a/src/AzureIoTHub.Portal/Server/Mappers/IEdgeDeviceModelMapper.cs +++ b/src/AzureIoTHub.Portal/Server/Mappers/IEdgeDeviceModelMapper.cs @@ -6,12 +6,13 @@ namespace AzureIoTHub.Portal.Server.Mappers using System.Collections.Generic; using Azure.Data.Tables; using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Server.Entities; public interface IEdgeDeviceModelMapper { IoTEdgeModelListItem CreateEdgeDeviceModelListItem(TableEntity entity); - IoTEdgeModel CreateEdgeDeviceModel(TableEntity entity, List ioTEdgeModules); + IoTEdgeModel CreateEdgeDeviceModel(TableEntity entity, List ioTEdgeModules, IEnumerable commands); void UpdateTableEntity(TableEntity entity, IoTEdgeModel model); } diff --git a/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs b/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs index b1996fde1..62ccbba9c 100644 --- a/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs +++ b/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs @@ -89,24 +89,11 @@ public async Task GetEdgeModel(string modelId) var query = await this.tableClientFactory .GetEdgeDeviceTemplates() .GetEntityAsync(DefaultPartitionKey, modelId); - var modules = await this.configService.GetConfigModuleList(modelId); - var commands = this.tableClientFactory.GetEdgeModuleCommands() - .Query($"PartitionKey eq '{modelId}'").ToArray(); - - foreach (var command in commands) - { - foreach (var module in modules) - { - if ((module.ModuleName + "-" + command.Name).Equals(command.RowKey, StringComparison.Ordinal)) - { - module.Commands.Add(new Shared.Models.v10.IoTEdgeModuleCommand { Name = command.Name }); - } - } - } - - return this.edgeDeviceModelMapper.CreateEdgeDeviceModel(query.Value, modules); + .Query(c => c.PartitionKey == modelId) + .ToArray(); + return this.edgeDeviceModelMapper.CreateEdgeDeviceModel(query.Value, modules, commands); } catch (RequestFailedException e) { @@ -114,7 +101,6 @@ public async Task GetEdgeModel(string modelId) { throw new ResourceNotFoundException($"The edge model with id {modelId} doesn't exist"); } - throw new InternalServerErrorException($"Unable to get the edge model with id {modelId}", e); } } @@ -342,9 +328,15 @@ private async Task SaveModuleCommands(IoTEdgeModel deviceModelObject) Timestamp = DateTime.Now, Name = cmd.Name, })).ToArray(); - try { + var existingCommands = this.tableClientFactory.GetEdgeModuleCommands() + .Query(c => c.PartitionKey == deviceModelObject.ModelId) + .ToArray(); + foreach (var command in existingCommands.Where(c => !moduleCommands.Any(x => x.RowKey == c.RowKey))) + { + _ = await this.tableClientFactory.GetEdgeModuleCommands().DeleteEntityAsync(command.PartitionKey, command.RowKey); + } foreach (var moduleCommand in moduleCommands) { _ = this.tableClientFactory From 2e1daaf59581614ae9514bc736c41d2912b066e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20TREMBLAY?= Date: Wed, 31 Aug 2022 09:06:45 +0200 Subject: [PATCH 08/12] Fix save and cancel buttons in the command page #1139 --- .../Client/Pages/EdgeModels/EdgeModule/ModuleDialog.razor | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/AzureIoTHub.Portal/Client/Pages/EdgeModels/EdgeModule/ModuleDialog.razor b/src/AzureIoTHub.Portal/Client/Pages/EdgeModels/EdgeModule/ModuleDialog.razor index 496687084..cd8b5e5fb 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/EdgeModels/EdgeModule/ModuleDialog.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/EdgeModels/EdgeModule/ModuleDialog.razor @@ -42,8 +42,7 @@ - Save - Cancel + OK From ea7bb580513e614c5796634cfbac643850db44f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20TREMBLAY?= Date: Wed, 31 Aug 2022 15:09:00 +0200 Subject: [PATCH 09/12] Fix unit tests #1139 --- .../EdgeModule/ModuleDialogTests.cs | 44 --------- .../Server/Mappers/EdgeModelMapperTest.cs | 22 ++++- .../Server/Services/EdgeModelServiceTest.cs | 91 ++++++++++++++++++- 3 files changed, 110 insertions(+), 47 deletions(-) diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Configurations/EdgeModule/ModuleDialogTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Configurations/EdgeModule/ModuleDialogTests.cs index 3d80a0ddb..5148ac692 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Configurations/EdgeModule/ModuleDialogTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Configurations/EdgeModule/ModuleDialogTests.cs @@ -105,49 +105,5 @@ public async Task ClickOnSubmitShouldUpdateModuleValues() cut.WaitForAssertion(() => module.ModuleName.Should().Be("newModuleNameValue")); cut.WaitForAssertion(() => module.ImageURI.Should().Be("newModuleImageUriValue")); } - - [Test] - public async Task ClickOnCancelShouldNotChangeModuleValues() - { - //Arrange - var moduleName = Guid.NewGuid().ToString(); - var moduleVersion = Guid.NewGuid().ToString(); - var moduleImageUri = Guid.NewGuid().ToString(); - - var module = new IoTEdgeModule() - { - ModuleName = moduleName, - Version = moduleVersion, - Status = "running", - ImageURI = moduleImageUri, - EnvironmentVariables = new List(), - ModuleIdentityTwinSettings = new List(), - Commands = new List() - }; - - var cut = RenderComponent(); - var service = Services.GetService() as DialogService; - - var parameters = new DialogParameters - { - { - "module", module - } - }; - - // Act - await cut.InvokeAsync(() => service?.Show(string.Empty, parameters)); - - cut.WaitForAssertion(() => cut.Find("div.mud-dialog-container").Should().NotBeNull()); - - cut.WaitForAssertion(() => cut.Find($"#{nameof(IoTEdgeModule.ModuleName)}").Change("newModuleNameValue")); - cut.WaitForAssertion(() => cut.Find($"#{nameof(IoTEdgeModule.ImageURI)}").Change("newModuleImageUriValue")); - - var cancelButton = cut.WaitForElement("#CancelButton"); - cancelButton.Click(); - - cut.WaitForAssertion(() => module.ModuleName.Should().Be(moduleName)); - cut.WaitForAssertion(() => module.ImageURI.Should().Be(moduleImageUri)); - } } } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Mappers/EdgeModelMapperTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Mappers/EdgeModelMapperTest.cs index 27861bdb3..70fe68802 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Mappers/EdgeModelMapperTest.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Mappers/EdgeModelMapperTest.cs @@ -5,8 +5,10 @@ namespace AzureIoTHub.Portal.Tests.Unit.Server.Mappers { using System; using System.Collections.Generic; + using System.Linq; using Azure.Data.Tables; using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Server.Entities; using AzureIoTHub.Portal.Server.Managers; using AzureIoTHub.Portal.Server.Mappers; using Moq; @@ -81,16 +83,32 @@ public void CreateEdgeDeviceModelShouldReturnIoTEdgeModelObject() ["Description"] = "description_test", }; - var modules = new List(){ new IoTEdgeModule()}; + var modules = new List() + { + new IoTEdgeModule + { + ModuleName = "module" + } + }; + var commands = new List() + { + new EdgeModuleCommand + { + PartitionKey = partitionKey, + Name = "Test", + RowKey = modules.First().ModuleName + "-" + "Test", + } + }; // Act - var result = edgeModelMapper.CreateEdgeDeviceModel(entity, modules); + var result = edgeModelMapper.CreateEdgeDeviceModel(entity, modules, commands); // Assert Assert.IsNotNull(result); Assert.AreEqual(rowKey, result.ModelId); Assert.AreEqual("test-name", result.Name); Assert.AreEqual(1, result.EdgeModules.Count); + Assert.AreEqual(1, result.EdgeModules.First().Commands.Count); this.mockRepository.VerifyAll(); } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs index d81ffc366..f4a45974d 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs @@ -23,6 +23,8 @@ namespace AzureIoTHub.Portal.Tests.Unit.Server.Services using FluentAssertions; using AzureIoTHub.Portal.Server.Exceptions; using Microsoft.Azure.Devices; + using AzureIoTHub.Portal.Server.Entities; + using System.Linq.Expressions; [TestFixture] public class EdgeModelServiceTest @@ -34,6 +36,7 @@ public class EdgeModelServiceTest private Mock mockEdgeModelMapper; private Mock mockDeviceModelImageManager; private Mock mockEdgeDeviceTemplatesTableClient; + private Mock mockEdgeModuleCommands; [SetUp] public void SetUp() @@ -45,6 +48,7 @@ public void SetUp() this.mockEdgeModelMapper = this.mockRepository.Create(); this.mockDeviceModelImageManager = this.mockRepository.Create(); this.mockEdgeDeviceTemplatesTableClient = this.mockRepository.Create(); + this.mockEdgeModuleCommands = this.mockRepository.Create(); } public EdgeModelService CreateEdgeModelService() @@ -125,6 +129,23 @@ public async Task GetEdgeModelShouldReturnValue() var mockResponse = this.mockRepository.Create>(); _ = mockResponse.Setup(c => c.Value).Returns(new TableEntity(Guid.NewGuid().ToString(), Guid.NewGuid().ToString())); + var mockModuleCommandResponse = this.mockRepository.Create(); + + var mockModuleCommands = Pageable.FromPages(new[] + { + Page.FromValues(Array.Empty(), null, mockModuleCommandResponse.Object) + }); + + _ = this.mockEdgeModuleCommands.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockModuleCommands); + + _ = this.mockTableClientFactory.Setup(c => c.GetEdgeModuleCommands()) + .Returns(this.mockEdgeModuleCommands.Object); + _ = this.mockEdgeDeviceTemplatesTableClient.Setup(c => c.GetEntityAsync( It.Is(p => p == EdgeModelService.DefaultPartitionKey), It.Is(k => k == "test"), @@ -140,7 +161,7 @@ public async Task GetEdgeModelShouldReturnValue() .ReturnsAsync(new List()); _ = this.mockEdgeModelMapper - .Setup(x => x.CreateEdgeDeviceModel(It.IsAny(), It.IsAny>())) + .Setup(x => x.CreateEdgeDeviceModel(It.IsAny(), It.IsAny>(), It.IsAny>())) .Returns(new IoTEdgeModel()); // Act @@ -212,6 +233,23 @@ public async Task CreateEdgeModelShouldCreateEdgeModelTemplate() var mockResponse = this.mockRepository.Create(); + var mockModuleCommandResponse = this.mockRepository.Create(); + + var mockModuleCommands = Pageable.FromPages(new[] + { + Page.FromValues(Array.Empty(), null, mockModuleCommandResponse.Object) + }); + + _ = this.mockEdgeModuleCommands.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockModuleCommands); + + _ = this.mockTableClientFactory.Setup(c => c.GetEdgeModuleCommands()) + .Returns(this.mockEdgeModuleCommands.Object); + _ = this.mockEdgeModelMapper .Setup(x => x.UpdateTableEntity(It.IsAny(), It.IsAny())); @@ -310,6 +348,23 @@ public void WhenUpsertEntityAsyncFailedCreateEdgeModelShoulThrowInternalServerEr ModelId = Guid.NewGuid().ToString() }; + var mockModuleCommandResponse = this.mockRepository.Create(); + + var mockModuleCommands = Pageable.FromPages(new[] + { + Page.FromValues(Array.Empty(), null, mockModuleCommandResponse.Object) + }); + + _ = this.mockEdgeModuleCommands.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockModuleCommands); + + _ = this.mockTableClientFactory.Setup(c => c.GetEdgeModuleCommands()) + .Returns(this.mockEdgeModuleCommands.Object); + _ = this.mockEdgeModelMapper .Setup(x => x.UpdateTableEntity(It.IsAny(), It.IsAny())); @@ -343,6 +398,23 @@ public async Task UpdateEdgeModelShouldUpdateEdgeModelTemplate() ModelId = Guid.NewGuid().ToString() }; + var mockModuleCommandResponse = this.mockRepository.Create(); + + var mockModuleCommands = Pageable.FromPages(new[] + { + Page.FromValues(Array.Empty(), null, mockModuleCommandResponse.Object) + }); + + _ = this.mockEdgeModuleCommands.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockModuleCommands); + + _ = this.mockTableClientFactory.Setup(c => c.GetEdgeModuleCommands()) + .Returns(this.mockEdgeModuleCommands.Object); + var mockResponse = this.mockRepository.Create>(); _ = mockResponse.Setup(c => c.Value).Returns(new TableEntity(Guid.NewGuid().ToString(), Guid.NewGuid().ToString())); @@ -437,6 +509,23 @@ public void WhenUpsertEntityAsyncFailedUpdateEdgeModelShoulThrowInternalServerEr ModelId = Guid.NewGuid().ToString() }; + var mockModuleCommandResponse = this.mockRepository.Create(); + + var mockModuleCommands = Pageable.FromPages(new[] + { + Page.FromValues(Array.Empty(), null, mockModuleCommandResponse.Object) + }); + + _ = this.mockEdgeModuleCommands.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockModuleCommands); + + _ = this.mockTableClientFactory.Setup(c => c.GetEdgeModuleCommands()) + .Returns(this.mockEdgeModuleCommands.Object); + var mockResponse = this.mockRepository.Create>(); _ = mockResponse.Setup(c => c.Value).Returns(new TableEntity(Guid.NewGuid().ToString(), Guid.NewGuid().ToString())); From 13c6163ccf9c75242fe05ac745d08b73975c300c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20TREMBLAY?= Date: Thu, 1 Sep 2022 14:08:07 +0200 Subject: [PATCH 10/12] Add unit tests for the save commands method #1139 --- .../Server/Services/EdgeModelServiceTest.cs | 155 ++++++++++++++++-- .../Server/Entities/EdgeModuleCommand.cs | 3 + .../Server/Services/EdgeModelService.cs | 3 +- 3 files changed, 146 insertions(+), 15 deletions(-) diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs index f4a45974d..fdc6967ab 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs @@ -3,28 +3,29 @@ namespace AzureIoTHub.Portal.Tests.Unit.Server.Services { - using Azure; - using System.Collections.Generic; using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Linq.Expressions; + using System.Threading; + using System.Threading.Tasks; + using Azure; using Azure.Data.Tables; + using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Server.Controllers.V10.LoRaWAN; + using AzureIoTHub.Portal.Server.Entities; + using AzureIoTHub.Portal.Server.Exceptions; using AzureIoTHub.Portal.Server.Factories; using AzureIoTHub.Portal.Server.Managers; using AzureIoTHub.Portal.Server.Mappers; using AzureIoTHub.Portal.Server.Services; - using Moq; - using NUnit.Framework; - using AzureIoTHub.Portal.Models.v10; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - using AzureIoTHub.Portal.Server.Controllers.V10.LoRaWAN; - using System.IO; + using AzureIoTHub.Portal.Shared.Models.v10; using FluentAssertions; - using AzureIoTHub.Portal.Server.Exceptions; + using Microsoft.AspNetCore.Http; using Microsoft.Azure.Devices; - using AzureIoTHub.Portal.Server.Entities; - using System.Linq.Expressions; + using Moq; + using NUnit.Framework; [TestFixture] public class EdgeModelServiceTest @@ -721,6 +722,132 @@ public async Task UpdateEdgeModelAvatarShouldUpdateValue() this.mockRepository.VerifyAll(); } + [Test] + public async Task SaveModuleCommandsShouldUpsertModuleCommandTemplates() + { + // Arrange + var edgeModelService = CreateEdgeModelService(); + + var modules = new List() + { + new IoTEdgeModule + { + ModuleName = "Test", + Commands = new List + { + new IoTEdgeModuleCommand + { + Name = "Command" + } + } + } + }; + + var iotEdgeModel = new IoTEdgeModel + { + ModelId = Guid.NewGuid().ToString(), + EdgeModules = modules, + }; + + var mockModuleCommandResponse = this.mockRepository.Create(); + + var mockModuleCommands = Pageable.FromPages(new[] + { + Page.FromValues(Array.Empty(), null, mockModuleCommandResponse.Object) + }); + + _ = this.mockEdgeModuleCommands.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockModuleCommands); + + _ = this.mockEdgeModuleCommands + .Setup(c => c.UpsertEntity( + It.Is(x => x.RowKey == "Test-Command" && x.PartitionKey == iotEdgeModel.ModelId), + It.IsAny(), + It.IsAny())) + .Returns((Response)null); + + _ = this.mockTableClientFactory.Setup(c => c.GetEdgeModuleCommands()) + .Returns(this.mockEdgeModuleCommands.Object); + + // Act + await edgeModelService.SaveModuleCommands(iotEdgeModel); + + // Assert + this.mockEdgeModuleCommands.Verify(c => c.UpsertEntity(It.IsAny(), TableUpdateMode.Merge, default), Times.Once); + this.mockTableClientFactory.Verify(c => c.GetEdgeModuleCommands(), Times.Exactly(2)); + this.mockRepository.VerifyAll(); + } + + [Test] + public async Task SaveModuleCommandsShouldDeleteModuleCommandTemplates() + { + // Arrange + var edgeModelService = CreateEdgeModelService(); + + var modules = new List() + { + new IoTEdgeModule + { + ModuleName = "Test", + Commands = new List() + } + }; + + var iotEdgeModel = new IoTEdgeModel + { + ModelId = Guid.NewGuid().ToString(), + EdgeModules = modules, + }; + + var mockModuleCommandResponse = this.mockRepository.Create(); + + var mockModuleCommands = Pageable.FromPages(new[] + { + Page.FromValues(new EdgeModuleCommand[1] + { + new EdgeModuleCommand + { + PartitionKey = iotEdgeModel.ModelId, + RowKey = "Test-Command", + Timestamp = DateTime.Now, + Name = "Command", + } + }, null, mockModuleCommandResponse.Object) + }); + + var mockResponse = this.mockRepository.Create(); + + _ = this.mockEdgeModuleCommands.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockModuleCommands); + + _ = this.mockEdgeModuleCommands + .Setup(c => c.DeleteEntityAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(mockResponse.Object); + + _ = this.mockTableClientFactory.Setup(c => c.GetEdgeModuleCommands()) + .Returns(this.mockEdgeModuleCommands.Object); + + // Act + await edgeModelService.SaveModuleCommands(iotEdgeModel); + + // Assert + this.mockEdgeModuleCommands.Verify(c => c.DeleteEntityAsync(It.IsAny(), It.IsAny(), default, default), Times.Once); + this.mockTableClientFactory.Verify(c => c.GetEdgeModuleCommands(), Times.Exactly(2)); + this.mockRepository.VerifyAll(); + } + [Test] public void WhenGetEntityAsyncFailedUpdateEdgeModelAvatarShouldThrowInternalServerErrorException() { diff --git a/src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs b/src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs index c810d9c3f..8129f855d 100644 --- a/src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs +++ b/src/AzureIoTHub.Portal/Server/Entities/EdgeModuleCommand.cs @@ -3,6 +3,9 @@ namespace AzureIoTHub.Portal.Server.Entities { + /// + /// Represents an edge module command. + /// public class EdgeModuleCommand : EntityBase { /// diff --git a/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs b/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs index 62ccbba9c..480e1e635 100644 --- a/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs +++ b/src/AzureIoTHub.Portal/Server/Services/EdgeModelService.cs @@ -318,7 +318,7 @@ private async Task SaveEntity(TableEntity entity, IoTEdgeModel deviceModelObject /// The device model object. /// /// - private async Task SaveModuleCommands(IoTEdgeModel deviceModelObject) + public async Task SaveModuleCommands(IoTEdgeModel deviceModelObject) { IEnumerable moduleCommands = deviceModelObject.EdgeModules .SelectMany(x => x.Commands.Select(cmd => new EdgeModuleCommand @@ -328,6 +328,7 @@ private async Task SaveModuleCommands(IoTEdgeModel deviceModelObject) Timestamp = DateTime.Now, Name = cmd.Name, })).ToArray(); + try { var existingCommands = this.tableClientFactory.GetEdgeModuleCommands() From 31f2d9bd8693a2ff6eb1a0240f798b8b550f35c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20TREMBLAY?= Date: Thu, 1 Sep 2022 15:14:42 +0200 Subject: [PATCH 11/12] Add unit test for the GetEdgeModuleCommands #1139 --- .../Server/Factories/TableClientFactoryTests.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Factories/TableClientFactoryTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Factories/TableClientFactoryTests.cs index 04b085645..e585356fa 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Factories/TableClientFactoryTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Factories/TableClientFactoryTests.cs @@ -24,5 +24,19 @@ public void GetDeviceTemplatesShouldThrowInternalServerErrorExceptionWhenAnIssue // Assert _ = act.Should().Throw(); } + + [Test] + public void GetEdgeModuleCommandsShouldThrowInternalServerErrorExceptionWhenAnIssueOccurs() + { + // Arrange + var connectionString = Guid.NewGuid().ToString(); + var tableClientFactory = new TableClientFactory(connectionString); + + // Act + var act = () => tableClientFactory.GetEdgeModuleCommands(); + + // Assert + _ = act.Should().Throw(); + } } } From b26a8619f12e0dd1ef08155c9b2deb802eb2a761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20TREMBLAY?= Date: Thu, 1 Sep 2022 15:14:58 +0200 Subject: [PATCH 12/12] Fix cancel handle #1139 --- .../Client/Pages/EdgeModels/EdgeModule/ModuleDialog.razor | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/AzureIoTHub.Portal/Client/Pages/EdgeModels/EdgeModule/ModuleDialog.razor b/src/AzureIoTHub.Portal/Client/Pages/EdgeModels/EdgeModule/ModuleDialog.razor index cd8b5e5fb..723aca188 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/EdgeModels/EdgeModule/ModuleDialog.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/EdgeModels/EdgeModule/ModuleDialog.razor @@ -88,6 +88,4 @@ Module.Commands = new List(currentCommands.ToArray()); MudDialog.Close(DialogResult.Ok(true)); } - - void Cancel() => MudDialog.Cancel(); }