From 190deef25becd336552512b4f60a7c24a4b669aa Mon Sep 17 00:00:00 2001 From: hocine hacherouf Date: Sun, 4 Jun 2023 19:45:17 +0200 Subject: [PATCH] Feature: Improve edge models for AWS (#2142) * Add edge module id to store greengrass public components * Add GetPublicEdgeModules * Add dialogs for aws json component + public components * Update create and edit edge model pages to handle aws greengrass components * Update GetPublicEdgeModules to load all public greengrass components + fix loading public component * Update aws greengrass dialogs * Fi edge model create/edit pages to handle public and private edge modules (aws components) * Remove dead code from AwsConfigService * Fix class AwsConfigService after rebase * Fix #2145 - Move Savechanges at last step of CRUD in edge model service * Fix duplication issue after selecting public components * Fix unit tests * Fix deployment model synchronization * Fix Edgedevice load and update * Add unit tests on GetPublicEdgeModules * Fix codeql warning on CreateEdgeModelShouldThrowInternalServerErrorExceptionIfDbUpdateExceptionOccurs * Rename AwsGreengrassPublicComponents to AwsGreengrassPublicComponentsDialog * Add unit tests on AwsGreengrassComponentDialog and AwsGreengrassPublicComponentsDialog * Add UT GetPublicEdgeModules_GetPublicEdgeModules_EdgeModulesReturned * Add UT GetPublicEdgeModules_GetPublicEdgeModules_EdgeModulesReturned on EdgeModelService * Add UT GetPublicEdgeModules_GetPublicEdgeModules_EdgeModulesReturned on EdgeModelClientService * Add unit tests on EdgeModelDetailPage and CreateEdgeModelsPage on add edge module and public edge modules * Fix codeql warning --------- Co-authored-by: Kevin BEAUGRAND --- .../Services/IConfigService.cs | 12 +- .../Services/IEdgeModelService.cs | 62 +- .../AwsGreengrassComponentDialog.razor | 114 ++ .../AwsGreengrassPublicComponentsDialog.razor | 53 + .../Enums/Context.cs | 11 + .../EdgeDevices/EdgeDeviceDetailPage.razor | 3 +- .../EdgeModels/CreateEdgeModelsPage.razor | 196 ++- .../EdgeModels/EdgeModelDetailPage.razor | 192 ++- .../Services/EdgeModelClientService.cs | 7 +- .../Services/IEdgeModelClientService.cs | 4 +- src/AzureIoTHub.Portal.Client/_Imports.razor | 1 + .../Jobs/AWS/SyncGreenGrassDeploymentsJob.cs | 5 +- .../Services/AWS/AWSEdgeDevicesService.cs | 10 +- .../Services/AWS/AwsConfigService.cs | 815 +++++----- .../Services/AwsExternalDeviceService.cs | 23 +- .../Services/EdgeModelService.cs | 23 +- .../Controllers/v1.0/EdgeModelsController.cs | 12 + .../Services/ConfigService.cs | 719 ++++----- .../Services/DeviceConfigurationsService.cs | 2 +- .../Services/DeviceModelService.cs | 2 +- .../Models/v1.0/IoTEdgeModule.cs | 7 +- .../AwsGreengrassComponentDialogTests.cs | 148 ++ ...wsGreengrassPublicComponentsDialogTests.cs | 161 ++ .../EdgeModels/CreateEdgeModelsPageTest.cs | 61 +- .../EdgeModels/EdgeModelDetailPageTest.cs | 60 +- .../Services/EdgeModelClientServiceTest.cs | 18 + .../Services/AWS_Tests/AwsConfigTests.cs | 113 +- .../v1.0/EdgeModelsControllerTest.cs | 41 + .../Server/Services/ConfigServiceTests.cs | 26 +- .../DeviceConfigurationsServiceTest.cs | 16 +- .../Services/DeviceModelServiceTests.cs | 4 +- .../Server/Services/EdgeModelServiceTest.cs | 1319 ++++++++--------- 32 files changed, 2479 insertions(+), 1761 deletions(-) create mode 100644 src/AzureIoTHub.Portal.Client/Dialogs/EdgeModels/AwsGreengrassComponentDialog.razor create mode 100644 src/AzureIoTHub.Portal.Client/Dialogs/EdgeModels/AwsGreengrassPublicComponentsDialog.razor create mode 100644 src/AzureIoTHub.Portal.Client/Enums/Context.cs create mode 100644 src/AzureIoTHub.Portal.Tests.Unit/Client/Dialogs/EdgeModels/AwsGreengrassComponentDialogTests.cs create mode 100644 src/AzureIoTHub.Portal.Tests.Unit/Client/Dialogs/EdgeModels/AwsGreengrassPublicComponentsDialogTests.cs diff --git a/src/AzureIoTHub.Portal.Application/Services/IConfigService.cs b/src/AzureIoTHub.Portal.Application/Services/IConfigService.cs index 047f797db..239cf2ab6 100644 --- a/src/AzureIoTHub.Portal.Application/Services/IConfigService.cs +++ b/src/AzureIoTHub.Portal.Application/Services/IConfigService.cs @@ -4,7 +4,7 @@ namespace AzureIoTHub.Portal.Application.Services { using System.Collections.Generic; - using System.Threading.Tasks; + using System.Threading.Tasks; using AzureIoTHub.Portal.Models.v10; using AzureIoTHub.Portal.Shared.Models.v10; using Microsoft.Azure.Devices; @@ -15,13 +15,13 @@ public interface IConfigService Task> GetDevicesConfigurations(); - Task RollOutDeviceModelConfiguration(string modelId, Dictionary desiredProperties); + Task RollOutDeviceModelConfiguration(string modelId, Dictionary desiredProperties); Task DeleteDeviceModelConfigurationByConfigurationNamePrefix(string configurationNamePrefix); - Task RollOutEdgeModelConfiguration(IoTEdgeModel edgeModel); + Task RollOutEdgeModelConfiguration(IoTEdgeModel edgeModel); - Task RollOutDeviceConfiguration(string modelId, Dictionary desiredProperties, string configurationId, Dictionary targetTags, int priority = 0); + Task RollOutDeviceConfiguration(string modelId, Dictionary desiredProperties, string configurationId, Dictionary targetTags, int priority = 0); Task GetConfigItem(string id); @@ -33,6 +33,8 @@ public interface IConfigService Task> GetModelSystemModule(string modelId); - Task> GetConfigRouteList(string modelId); + Task> GetConfigRouteList(string modelId); + + Task> GetPublicEdgeModules(); } } diff --git a/src/AzureIoTHub.Portal.Application/Services/IEdgeModelService.cs b/src/AzureIoTHub.Portal.Application/Services/IEdgeModelService.cs index 579ded29a..8e430ea07 100644 --- a/src/AzureIoTHub.Portal.Application/Services/IEdgeModelService.cs +++ b/src/AzureIoTHub.Portal.Application/Services/IEdgeModelService.cs @@ -1,31 +1,31 @@ -// 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.Application.Services -{ - using System.Collections.Generic; - using System.Threading.Tasks; - using AzureIoTHub.Portal.Models.v10; - using AzureIoTHub.Portal.Shared.Models.v10.Filters; - using Microsoft.AspNetCore.Http; - - public interface IEdgeModelService - { - Task> GetEdgeModels(EdgeModelFilter edgeModelFilter); - - Task GetEdgeModel(string modelId); - - Task CreateEdgeModel(IoTEdgeModel edgeModel); - Task UpdateEdgeModel(IoTEdgeModel edgeModel); - - Task DeleteEdgeModel(string edgeModelId); - - Task GetEdgeModelAvatar(string edgeModelId); - - Task UpdateEdgeModelAvatar(string edgeModelId, IFormFile file); - - Task DeleteEdgeModelAvatar(string edgeModelId); - - Task SaveModuleCommands(IoTEdgeModel deviceModelObject); - } -} +// 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.Application.Services +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Shared.Models.v10.Filters; + using Microsoft.AspNetCore.Http; + + public interface IEdgeModelService + { + Task> GetEdgeModels(EdgeModelFilter edgeModelFilter); + + Task GetEdgeModel(string modelId); + + Task CreateEdgeModel(IoTEdgeModel edgeModel); + Task UpdateEdgeModel(IoTEdgeModel edgeModel); + + Task DeleteEdgeModel(string edgeModelId); + + Task GetEdgeModelAvatar(string edgeModelId); + + Task UpdateEdgeModelAvatar(string edgeModelId, IFormFile file); + + Task DeleteEdgeModelAvatar(string edgeModelId); + + Task> GetPublicEdgeModules(); + } +} diff --git a/src/AzureIoTHub.Portal.Client/Dialogs/EdgeModels/AwsGreengrassComponentDialog.razor b/src/AzureIoTHub.Portal.Client/Dialogs/EdgeModels/AwsGreengrassComponentDialog.razor new file mode 100644 index 000000000..333a97277 --- /dev/null +++ b/src/AzureIoTHub.Portal.Client/Dialogs/EdgeModels/AwsGreengrassComponentDialog.razor @@ -0,0 +1,114 @@ +@using AzureIoTHub.Portal.Models.v10; +@using System.Text.Json; + + + + + + + + + + + Cancel + Submit + + +@code { + [CascadingParameter] + MudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public IoTEdgeModule Module { get; set; } = default!; + + [Parameter] + public List EdgeModules { get; set; } = default!; + + [Parameter] + public Context Context { get; set; } = default!; + + private bool formIsValid; + private MudForm form = default!; + + private string currentModuleName = default!; + private string currentModuleVersion = default!; + private string jsonRecipe = default!; + + protected override void OnInitialized() + { + if (Context == Context.Edit) + { + jsonRecipe = Module.ContainerCreateOptions; + } + } + + private IEnumerable ValidateJsonRecipe(string json) + { + var jsonProperties = JsonSerializer.Deserialize>(json) ?? new Dictionary(); + + if (!jsonProperties.ContainsKey("ComponentName")) + { + currentModuleName = string.Empty; + yield return "ComponentName is missing"; + } + else if (string.IsNullOrEmpty(jsonProperties["ComponentName"].ToString())) + { + currentModuleName = string.Empty; + yield return "ComponentName is empty"; + } + else + { + if (Context == Context.Create && EdgeModules.Any(m => m.ModuleName.Equals(jsonProperties["ComponentName"].ToString()))) + { + yield return $"Component {jsonProperties["ComponentName"].ToString()} is already used"; + } + currentModuleName = jsonProperties["ComponentName"].ToString(); + } + + if (!jsonProperties.ContainsKey("ComponentVersion")) + { + currentModuleVersion = string.Empty; + yield return "ComponentVersion is missing"; + } + else if (string.IsNullOrEmpty(jsonProperties["ComponentVersion"].ToString())) + { + currentModuleVersion = string.Empty; + yield return "ComponentVersion is empty"; + } + else + { + currentModuleVersion = jsonProperties["ComponentVersion"].ToString(); + } + } + + void Cancel() => MudDialog.Cancel(); + + public async Task Submit() + { + await form.Validate(); + if (!form.IsValid) return; + + if(Context == Context.Create) + { + EdgeModules.Add(new IoTEdgeModule + { + ModuleName = currentModuleName, + Version = currentModuleVersion, + ImageURI = "example.com", + ContainerCreateOptions = jsonRecipe + }); + } + else + { + Module.ModuleName = currentModuleName; + Module.Version = currentModuleVersion; + // ImageURI is required, but not used for Greengrass components + Module.ImageURI = "example.com"; + Module.ContainerCreateOptions = jsonRecipe; + } + + MudDialog.Close(DialogResult.Ok(true)); + } + +} diff --git a/src/AzureIoTHub.Portal.Client/Dialogs/EdgeModels/AwsGreengrassPublicComponentsDialog.razor b/src/AzureIoTHub.Portal.Client/Dialogs/EdgeModels/AwsGreengrassPublicComponentsDialog.razor new file mode 100644 index 000000000..3602a2103 --- /dev/null +++ b/src/AzureIoTHub.Portal.Client/Dialogs/EdgeModels/AwsGreengrassPublicComponentsDialog.razor @@ -0,0 +1,53 @@ +@using AzureIoTHub.Portal.Models.v10; + +@inject IEdgeModelClientService EdgeModelClientService + + + + + + Name + Version + + + @context.ModuleName + @context.Version + + + + + + + + Cancel + Submit + + +@code { + [CascadingParameter] + MudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public List EdgeModules { get; set; } = default!; + + private List publicComponents = new(); + private HashSet selectedPublicComponents = new HashSet(); + + protected async override Task OnInitializedAsync() + { + publicComponents = await EdgeModelClientService.GetPublicEdgeModules(); + selectedPublicComponents = new HashSet(publicComponents.Where(m => EdgeModules.Any(e => m.Id.Equals(e.Id)))); + } + + private void Cancel() => MudDialog.Cancel(); + + private void Submit() + { + EdgeModules.RemoveAll(e => !string.IsNullOrEmpty(e.Id)); + EdgeModules.AddRange(selectedPublicComponents.ToList()); + + MudDialog.Close(DialogResult.Ok(true)); + } + +} diff --git a/src/AzureIoTHub.Portal.Client/Enums/Context.cs b/src/AzureIoTHub.Portal.Client/Enums/Context.cs new file mode 100644 index 000000000..d14595620 --- /dev/null +++ b/src/AzureIoTHub.Portal.Client/Enums/Context.cs @@ -0,0 +1,11 @@ +// 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.Client.Enums +{ + public enum Context + { + Create, + Edit + } +} diff --git a/src/AzureIoTHub.Portal.Client/Pages/EdgeDevices/EdgeDeviceDetailPage.razor b/src/AzureIoTHub.Portal.Client/Pages/EdgeDevices/EdgeDeviceDetailPage.razor index a062740f5..9df76706e 100644 --- a/src/AzureIoTHub.Portal.Client/Pages/EdgeDevices/EdgeDeviceDetailPage.razor +++ b/src/AzureIoTHub.Portal.Client/Pages/EdgeDevices/EdgeDeviceDetailPage.razor @@ -119,7 +119,8 @@ Label="Device name" Variant="Variant.Outlined" For="@(()=> edgeDevice.DeviceName)" - Required="true" /> + Required="true" + ReadOnly=@(Portal.CloudProvider == CloudProviders.AWS)/> diff --git a/src/AzureIoTHub.Portal.Client/Pages/EdgeModels/CreateEdgeModelsPage.razor b/src/AzureIoTHub.Portal.Client/Pages/EdgeModels/CreateEdgeModelsPage.razor index a1fe9a548..89d2c1ab3 100644 --- a/src/AzureIoTHub.Portal.Client/Pages/EdgeModels/CreateEdgeModelsPage.razor +++ b/src/AzureIoTHub.Portal.Client/Pages/EdgeModels/CreateEdgeModelsPage.razor @@ -106,51 +106,48 @@ - } - - - - Modules - - - - - - - - - - - Module name - Image URI - See detail - Delete - - - - - - - - - - Detail - - - - - - - Add new module - - - - - - - - @if (Portal.CloudProvider.Equals(CloudProviders.Azure)) - { + + + + Modules + + + + + + + + + + + Module name + Image URI + See detail + Delete + + + + + + + + + + Detail + + + + + + + Add new module + + + + + + + @@ -198,6 +195,47 @@ + } + @if (Portal.CloudProvider.Equals(CloudProviders.AWS)) + { + + + + Modules + + + + + + + + + + Module name + See detail + Delete + + + + @moduleContext.ModuleName + + + Detail + + + + + + + Add new module + Add public modules + + + + + + + } @@ -219,7 +257,7 @@ -@code { +@code { [CascadingParameter] public Error Error { get; set; } = default!; @@ -269,28 +307,58 @@ } private void AddModule() - { - edgeModules.Add(new IoTEdgeModule()); - } - + { + edgeModules.Add(new IoTEdgeModule()); + } + + private async Task ShowAddEdgePublicModulesDialog() + { + var parameters = new DialogParameters(); + parameters.Add("EdgeModules", edgeModules); + + DialogOptions options = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true }; + + await DialogService.Show(string.Empty, parameters, options).Result; + + await InvokeAsync(StateHasChanged); + } + private async Task ShowAddEdgeModuleDialog(IoTEdgeModule module) { var parameters = new DialogParameters(); - parameters.Add("module", module); - - DialogOptions options = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true }; - - if (!string.IsNullOrWhiteSpace(module.ModuleName)) - { - var result = await DialogService.Show(module.ModuleName, parameters, options).Result; - - if (result.Canceled) - { - return; - } - } - } + DialogOptions options = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true }; + + if (Portal.CloudProvider.Equals(CloudProviders.Azure)) + { + if (!string.IsNullOrWhiteSpace(module.ModuleName)) + { + + parameters.Add("module", module); + await DialogService.Show(module.ModuleName, parameters, options).Result; + } + } + else + { + parameters.Add("Context", Context.Edit); + parameters.Add("Module", module); + + await DialogService.Show(module.ModuleName, parameters, options).Result; + } + } + + private async Task ShowAddNewEdgeModuleDialog() + { + var parameters = new DialogParameters(); + + DialogOptions options = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true }; + + parameters.Add("Context", Context.Create); + parameters.Add("EdgeModules", edgeModules); + + await DialogService.Show(string.Empty, parameters, options).Result; + } + private async Task ShowSystemModuleDetail(EdgeModelSystemModule systemModule) { var parameters = new DialogParameters(); diff --git a/src/AzureIoTHub.Portal.Client/Pages/EdgeModels/EdgeModelDetailPage.razor b/src/AzureIoTHub.Portal.Client/Pages/EdgeModels/EdgeModelDetailPage.razor index 1a5759fed..7a30e343c 100644 --- a/src/AzureIoTHub.Portal.Client/Pages/EdgeModels/EdgeModelDetailPage.razor +++ b/src/AzureIoTHub.Portal.Client/Pages/EdgeModels/EdgeModelDetailPage.razor @@ -112,52 +112,49 @@ - } - - - - Modules - - - - - - - - - - - Module name - Image URI - See detail - Delete - - - - - - - - - - Detail - - - - - - - Add new module - - - - - - - - @if (Portal.CloudProvider.Equals(CloudProviders.Azure)) - { + + + + Modules + + + + + + + + + + + Module name + Image URI + See detail + Delete + + + + + + + + + + Detail + + + + + + + Add new module + + + + + + + @@ -206,7 +203,47 @@ } - + @if (Portal.CloudProvider.Equals(CloudProviders.AWS)) + { + + + + Modules + + + + + + + + + + Module name + See detail + Delete + + + + @moduleContext.ModuleName + + + Detail + + + + + + + Add new module + Add public modules + + + + + + + + } @@ -360,25 +397,54 @@ private void AddModule() { - EdgeModel.EdgeModules.Add(new IoTEdgeModule()); - } - + EdgeModel.EdgeModules.Add(new IoTEdgeModule()); + } + + private async Task ShowAddNewEdgeModuleDialog() + { + var parameters = new DialogParameters(); + + DialogOptions options = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true }; + + parameters.Add("Context", Context.Create); + parameters.Add("EdgeModules", EdgeModel.EdgeModules); + + await DialogService.Show(string.Empty, parameters, options).Result; + } + + private async Task ShowAddEdgePublicModulesDialog() + { + var parameters = new DialogParameters(); + parameters.Add("EdgeModules", EdgeModel.EdgeModules); + + DialogOptions options = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true }; + + await DialogService.Show(string.Empty, parameters, options).Result; + + await InvokeAsync(StateHasChanged); + } + private async Task ShowEditEdgeModuleDialog(IoTEdgeModule module) { - var parameters = new DialogParameters(); - parameters.Add("module", module); - - DialogOptions options = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true }; - - if (!string.IsNullOrWhiteSpace(module.ModuleName)) - { - var result = await DialogService.Show(module.ModuleName, parameters, options).Result; - - if (result.Canceled) - { - return; - } - } + var parameters = new DialogParameters(); + parameters.Add("module", module); + + DialogOptions options = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true }; + + if (Portal.CloudProvider.Equals(CloudProviders.Azure)) + { + if (!string.IsNullOrWhiteSpace(module.ModuleName)) + { + await DialogService.Show(module.ModuleName, parameters, options).Result; + } + } + else + { + parameters.Add("Context", Context.Edit); + parameters.Add("Module", module); + + await DialogService.Show(module.ModuleName, parameters, options).Result; + } } private void DeleteModule(IoTEdgeModule module) diff --git a/src/AzureIoTHub.Portal.Client/Services/EdgeModelClientService.cs b/src/AzureIoTHub.Portal.Client/Services/EdgeModelClientService.cs index 289a6f8cb..de38c0820 100644 --- a/src/AzureIoTHub.Portal.Client/Services/EdgeModelClientService.cs +++ b/src/AzureIoTHub.Portal.Client/Services/EdgeModelClientService.cs @@ -66,6 +66,11 @@ public Task ChangeAvatar(string id, MultipartFormDataContent content) public Task DeleteAvatar(string id) { return this.http.DeleteAsync($"{this.apiUrlBase}/{id}/avatar"); - } + } + + public async Task> GetPublicEdgeModules() + { + return await this.http.GetFromJsonAsync>($"{this.apiUrlBase}/public-modules") ?? new List(); + } } } diff --git a/src/AzureIoTHub.Portal.Client/Services/IEdgeModelClientService.cs b/src/AzureIoTHub.Portal.Client/Services/IEdgeModelClientService.cs index ab48d3c24..a29f9502f 100644 --- a/src/AzureIoTHub.Portal.Client/Services/IEdgeModelClientService.cs +++ b/src/AzureIoTHub.Portal.Client/Services/IEdgeModelClientService.cs @@ -25,6 +25,8 @@ public interface IEdgeModelClientService Task ChangeAvatar(string id, MultipartFormDataContent content); - Task DeleteAvatar(string id); + Task DeleteAvatar(string id); + + Task> GetPublicEdgeModules(); } } diff --git a/src/AzureIoTHub.Portal.Client/_Imports.razor b/src/AzureIoTHub.Portal.Client/_Imports.razor index 3ff9ce7b0..5c18999e1 100644 --- a/src/AzureIoTHub.Portal.Client/_Imports.razor +++ b/src/AzureIoTHub.Portal.Client/_Imports.razor @@ -27,5 +27,6 @@ @using AzureIoTHub.Portal.Client.Components.Devices.LoRaWAN @using AzureIoTHub.Portal.Client.Components.EdgeDevices @using AzureIoTHub.Portal.Client.Components.EdgeModels +@using AzureIoTHub.Portal.Client.Dialogs.EdgeModels @using MudBlazor @using ChartJs.Blazor; diff --git a/src/AzureIoTHub.Portal.Infrastructure/Jobs/AWS/SyncGreenGrassDeploymentsJob.cs b/src/AzureIoTHub.Portal.Infrastructure/Jobs/AWS/SyncGreenGrassDeploymentsJob.cs index bb27e5bea..c591877fe 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/Jobs/AWS/SyncGreenGrassDeploymentsJob.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/Jobs/AWS/SyncGreenGrassDeploymentsJob.cs @@ -115,9 +115,8 @@ private async Task> GetAllGreenGrassDeployments() private async Task CreateNonExisitingGreenGrassDeployment(IoTEdgeModel iotEdgeModel) { - var iotEdgeModels = (await this.edgeDeviceModelRepository.GetAllAsync()) - .Where(edge => edge.ExternalIdentifier!.Equals(iotEdgeModel.ExternalIdentifier, StringComparison.Ordinal)).ToList(); + .Where(edge => string.Equals(edge.ExternalIdentifier, iotEdgeModel.ExternalIdentifier, StringComparison.Ordinal)).ToList(); if (iotEdgeModels.Count == 0) { @@ -138,7 +137,7 @@ private async Task DeleteGreenGrassDeployments(List edgeModels) { //Get All Deployments that are not in AWS var deploymentToDelete = (await this.edgeDeviceModelRepository.GetAllAsync()) - .Where(edge => !edgeModels.Any(edgeModel => edge.ExternalIdentifier!.Equals(edgeModel.ExternalIdentifier, StringComparison.Ordinal))) + .Where(edge => !edgeModels.Any(edgeModel => string.Equals(edge.ExternalIdentifier, edgeModel.ExternalIdentifier, StringComparison.Ordinal))) .ToList(); foreach (var edgeModel in deploymentToDelete) diff --git a/src/AzureIoTHub.Portal.Infrastructure/Services/AWS/AWSEdgeDevicesService.cs b/src/AzureIoTHub.Portal.Infrastructure/Services/AWS/AWSEdgeDevicesService.cs index 673dc99f0..0ce7a2aae 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/Services/AWS/AWSEdgeDevicesService.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/Services/AWS/AWSEdgeDevicesService.cs @@ -96,9 +96,8 @@ public async Task UpdateEdgeDevice(IoTEdgeDevice edgeDevice) { ArgumentNullException.ThrowIfNull(edgeDevice, nameof(edgeDevice)); - _ = await this.awsExternalDevicesService.GetDevice(edgeDevice.DeviceId); + _ = await this.awsExternalDevicesService.GetDevice(edgeDevice.DeviceName); - // TODO var result = await UpdateEdgeDeviceInDatabase(edgeDevice); await this.unitOfWork.SaveAsync(); @@ -131,8 +130,11 @@ public async Task GetEdgeDevice(string edgeDeviceId) var deviceDto = await base.GetEdgeDevice(edgeDeviceId); deviceDto.LastDeployment = await this.externalDeviceService.RetrieveLastConfiguration(deviceDto); - deviceDto.RuntimeResponse = deviceDto.LastDeployment.Status; - deviceDto.Modules = await this.configService.GetConfigModuleList(deviceDto.ModelId); + deviceDto.RuntimeResponse = deviceDto.LastDeployment?.Status; + + var model = await this.deviceModelRepository.GetByIdAsync(deviceDto.ModelId); + + deviceDto.Modules = await this.configService.GetConfigModuleList(model.ExternalIdentifier!); deviceDto.NbDevices = await this.awsExternalDevicesService.GetEdgeDeviceNbDevices(deviceDto); deviceDto.NbModules = deviceDto.Modules.Count; diff --git a/src/AzureIoTHub.Portal.Infrastructure/Services/AWS/AwsConfigService.cs b/src/AzureIoTHub.Portal.Infrastructure/Services/AWS/AwsConfigService.cs index c9fa19af8..9f5f6237a 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/Services/AWS/AwsConfigService.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/Services/AWS/AwsConfigService.cs @@ -1,470 +1,345 @@ -// 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.Infrastructure.Services.AWS -{ - using System.Collections.Generic; - using System.Net; - using System.Text; - using System.Threading.Tasks; - using Amazon.GreengrassV2; - using Amazon.GreengrassV2.Model; - using Amazon.IoT; - using Amazon.IoT.Model; - using AutoMapper; - using AzureIoTHub.Portal.Application.Services; - using AzureIoTHub.Portal.Domain; - using AzureIoTHub.Portal.Domain.Exceptions; - using AzureIoTHub.Portal.Domain.Repositories; - using AzureIoTHub.Portal.Models.v10; - using AzureIoTHub.Portal.Shared.Models.v10; - using Newtonsoft.Json.Linq; - using Configuration = Microsoft.Azure.Devices.Configuration; - - public class AwsConfigService : IConfigService - { - private readonly IAmazonGreengrassV2 greengras; - private readonly IAmazonIoT iotClient; - - private readonly IUnitOfWork unitOfWork; - private readonly IEdgeDeviceModelRepository edgeModelRepository; - private readonly ConfigHandler config; - private readonly IMapper mapper; - - public AwsConfigService( - IAmazonGreengrassV2 greengras, - IAmazonIoT iot, - IMapper mapper, - IUnitOfWork unitOfWork, - IEdgeDeviceModelRepository edgeModelRepository, - ConfigHandler config) - { - this.greengras = greengras; - this.iotClient = iot; - this.mapper = mapper; - this.unitOfWork = unitOfWork; - this.edgeModelRepository = edgeModelRepository; - this.config = config; - } - - public async Task RollOutEdgeModelConfiguration(IoTEdgeModel edgeModel) - { - - var createDeploymentRequest = new CreateDeploymentRequest - { - DeploymentName = edgeModel?.Name, - Components = await CreateGreenGrassComponents(edgeModel!), - TargetArn = await GetThingGroupArn(edgeModel!) - }; - - var createDeploymentResponse = await this.greengras.CreateDeploymentAsync(createDeploymentRequest); - - if (createDeploymentResponse.HttpStatusCode != HttpStatusCode.Created) - { - throw new InternalServerErrorException("The deployment creation failed due to an error in the Amazon IoT API."); - - } - else - { - var edgeModelEntity = await this.edgeModelRepository.GetByIdAsync(edgeModel?.ModelId!); - if (edgeModelEntity == null) - { - throw new Domain.Exceptions.ResourceNotFoundException($"The edge model with id {edgeModel?.ModelId} not found"); - - } - else - { - edgeModel!.ExternalIdentifier = createDeploymentResponse.DeploymentId; - - _ = this.mapper.Map(edgeModel, edgeModelEntity); - - this.edgeModelRepository.Update(edgeModelEntity); - await this.unitOfWork.SaveAsync(); - } - - } - } - - private async Task GetThingGroupArn(IoTEdgeModel edgeModel) - { - await CreateThingTypeIfNotExists(edgeModel!.Name); - - var dynamicThingGroup = new DescribeThingGroupRequest - { - ThingGroupName = edgeModel?.Name - }; - - try - { - var existingThingGroupResponse = await this.iotClient.DescribeThingGroupAsync(dynamicThingGroup); - - return existingThingGroupResponse.ThingGroupArn; - } - catch (Amazon.IoT.Model.ResourceNotFoundException) - { - var createThingGroupResponse = await this.iotClient.CreateDynamicThingGroupAsync(new CreateDynamicThingGroupRequest - { - ThingGroupName = edgeModel!.Name, - QueryString = $"thingTypeName: {edgeModel!.Name}" - }); - - return createThingGroupResponse.ThingGroupArn; - } - } - - private async Task CreateThingTypeIfNotExists(string thingTypeName) - { - var existingThingType = new DescribeThingTypeRequest - { - ThingTypeName = thingTypeName - }; - - try - { - _ = await this.iotClient.DescribeThingTypeAsync(existingThingType); - } - catch (Amazon.IoT.Model.ResourceNotFoundException) - { - _ = await this.iotClient.CreateThingTypeAsync(new CreateThingTypeRequest - { - ThingTypeName = thingTypeName, - Tags = new List - { - new Tag - { - Key = "iotEdge", - Value = "True" - } - } - }); - } - } - - private async Task> CreateGreenGrassComponents(IoTEdgeModel edgeModel) - { - var listcomponentName = new Dictionary(); - foreach (var component in edgeModel.EdgeModules) - { - try - { - _ = await this.greengras.DescribeComponentAsync(new DescribeComponentRequest - { - Arn = $"arn:aws:greengrass:{config.AWSRegion}:{config.AWSAccountId}:components:{component.ModuleName}:versions:{component.Version}" - }); - listcomponentName.Add(component.ModuleName, new ComponentDeploymentSpecification { ComponentVersion = component.Version }); - - } - catch (Amazon.GreengrassV2.Model.ResourceNotFoundException) - { - var recipeJson = JsonCreateComponent(component); - var recipeBytes = Encoding.UTF8.GetBytes(recipeJson.ToString()); - var recipeStream = new MemoryStream(recipeBytes); - - var componentVersion = new CreateComponentVersionRequest - { - InlineRecipe = recipeStream - }; - var response = await greengras.CreateComponentVersionAsync(componentVersion); - if (response.HttpStatusCode != HttpStatusCode.Created) - { - throw new InternalServerErrorException("The component creation failed due to an error in the Amazon IoT API."); - - } - listcomponentName.Add(component.ModuleName, new ComponentDeploymentSpecification { ComponentVersion = component.Version }); - } - } - - - return listcomponentName; - } - - private static JObject JsonCreateComponent(IoTEdgeModule component) - { - - var environmentVariableObject = new JObject(); - - foreach (var env in component.EnvironmentVariables) - { - environmentVariableObject.Add(new JProperty(env.Name, env.Value)); - } - - var recipeJson =new JObject( - new JProperty("RecipeFormatVersion", "2020-01-25"), - new JProperty("ComponentName", component.ModuleName), - new JProperty("ComponentVersion", component.Version), - new JProperty("ComponentPublisher", "IotHub"), - new JProperty("ComponentDependencies", - new JObject( - new JProperty("aws.greengrass.DockerApplicationManager", - new JObject(new JProperty("VersionRequirement", "~2.0.0"))), - new JProperty("aws.greengrass.TokenExchangeService", - new JObject(new JProperty("VersionRequirement", "~2.0.0"))) - ) - ), - new JProperty("Manifests", - new JArray( - new JObject( - new JProperty("Platform", - new JObject(new JProperty("os", "linux"))), - new JProperty("Lifecycle", - new JObject(new JProperty("Run", $"docker run {component.ImageURI}"), - new JProperty("Environment",environmentVariableObject))), - new JProperty("Artifacts", - new JArray( - new JObject(new JProperty("URI", $"docker:{component.ImageURI}")) - ) - ) - ) - ) - ) - ); - - return recipeJson; - } - - //AWS Not implemented methods - - public Task> GetIoTEdgeConfigurations() - { - throw new NotImplementedException(); - } - - public Task> GetDevicesConfigurations() - { - throw new NotImplementedException(); - } - - public Task RollOutDeviceModelConfiguration(string modelId, Dictionary desiredProperties) - { - throw new NotImplementedException(); - } - - public Task DeleteDeviceModelConfigurationByConfigurationNamePrefix(string configurationNamePrefix) - { - throw new NotImplementedException(); - } - - public Task RollOutDeviceConfiguration(string modelId, Dictionary desiredProperties, string configurationId, Dictionary targetTags, int priority = 0) - { - throw new NotImplementedException(); - } - - public Task GetConfigItem(string id) - { - throw new NotImplementedException(); - } - - public async Task DeleteConfiguration(string modelId) - { - var modules = await GetConfigModuleList(modelId); - foreach (var module in modules) - { - var deletedComponentResponse = await this.greengras.DeleteComponentAsync(new DeleteComponentRequest - { - Arn = $"arn:aws:greengrass:{config.AWSRegion}:{config.AWSAccountId}:components:{module.ModuleName}:versions:{module.Version}" - }); - - if (deletedComponentResponse.HttpStatusCode != HttpStatusCode.NoContent) - { - throw new InternalServerErrorException("The deletion of the component failed due to an error in the Amazon IoT API."); - - } - } - - var cancelDeploymentResponse = await this.greengras.CancelDeploymentAsync(new CancelDeploymentRequest - { - DeploymentId = modelId - }); - if (cancelDeploymentResponse.HttpStatusCode != HttpStatusCode.OK) - { - throw new InternalServerErrorException("The cancellation of the deployment failed due to an error in the Amazon IoT API."); - - } - else - { - var deleteDeploymentResponse = await this.greengras.DeleteDeploymentAsync(new DeleteDeploymentRequest - { - DeploymentId = modelId - }); - - if (deleteDeploymentResponse.HttpStatusCode != HttpStatusCode.NoContent) - { - throw new InternalServerErrorException("The deletion of the deployment failed due to an error in the Amazon IoT API."); - } - } - - } - - public Task GetFailedDeploymentsCount() - { - throw new NotImplementedException(); - } - - public async Task> GetConfigModuleList(string modelId) - { - - var moduleList = new List(); - - var getDeployement = new GetDeploymentRequest - { - DeploymentId = modelId, - }; - try - { - var response = await this.greengras.GetDeploymentAsync(getDeployement); - - foreach (var compoenent in response.Components) - { - try - { - var responseComponent = await this.greengras.GetComponentAsync(new GetComponentRequest - { - Arn = $"arn:aws:greengrass:{config.AWSRegion}:{config.AWSAccountId}:components:{compoenent.Key}:versions:{compoenent.Value.ComponentVersion}", - RecipeOutputFormat = RecipeOutputFormat.JSON - }); - - // Read the Recipe which is in JSON Format - using var reader = new StreamReader(responseComponent.Recipe); - var recipeJsonString = reader.ReadToEnd(); - - // Extract the imageUri from the 'Run' JSON object - var uriImage = retreiveImageUri("Lifecycle", "Run", recipeJsonString); - // Extract the environment Variables from the 'Environment' JSON object - var env = retreiveEnvVariableAttr("Lifecycle", "Environment", recipeJsonString); - - var iotEdgeModule = new IoTEdgeModule - { - ModuleName = compoenent.Key, - ImageURI = uriImage, - EnvironmentVariables = env, - Version = compoenent.Value.ComponentVersion - }; - - moduleList.Add(iotEdgeModule); - } - catch (Amazon.GreengrassV2.Model.ResourceNotFoundException) - { - throw new InternalServerErrorException($"The component {compoenent.Key} is not found or may not be a docker component or may be a public component"); - - } - - - } - return moduleList; - } - catch (Amazon.GreengrassV2.Model.ResourceNotFoundException) - { - throw new InternalServerErrorException("The deployment is not found"); - - } - } - - private static string retreiveImageUri(string parent, string child, string recipeJsonString) - { - var uriImage = ""; - // Parse the string as a JSON object - var recipeJsonObject = JObject.Parse(recipeJsonString); - - // Extract the "Manifests" array - var jArray = recipeJsonObject["Manifests"] as JArray; - var manifests = jArray; - - if (manifests != null && manifests.Count > 0) - { - // Get the first manifest in the array - var firstManifest = manifests[0] as JObject; - - // Extract the "Lifecycle" object - var jObject = firstManifest?[parent] as JObject; - var lifecycle = jObject; - - if (lifecycle != null) - { - // Extract the value of "Run" - var runObject = lifecycle[child] as JObject; - var runValueObject = runObject; - if (runValueObject != null) - { - var runValue = runValueObject.ToString(); - // Search the index of the 1st whitespace - var firstSpaceIndex = runValue.IndexOf(' ', StringComparison.Ordinal); - - if (firstSpaceIndex != -1) - { - // // Search the index of the 2nd whitespace - var secondSpaceIndex = runValue.IndexOf(' ', firstSpaceIndex + 1); - - if (secondSpaceIndex != -1) - { - // Extract the URI iamge - uriImage = runValue[(secondSpaceIndex + 1)..]; - } - - } - } - } - } - - return uriImage; - } - - private static List retreiveEnvVariableAttr(string parent, string child, string recipeJsonString) - { - - // Parse the string as a JSON object - var recipeJsonObject = JObject.Parse(recipeJsonString); - - var environmentVariables = new List(); - - // Extract the "Manifests" array - var jArray = recipeJsonObject["Manifests"] as JArray; - var manifests = jArray; - - if (manifests != null && manifests.Count > 0) - { - // Get the first manifest in the array - var firstManifest = manifests[0] as JObject; - - // Extract the "Lifecycle" object - var jObject = firstManifest?[parent] as JObject; - var lifecycle = jObject; - - if (lifecycle != null) - { - // Extract the value of "Environment" - var envObject = lifecycle[child] as JObject; - var env = envObject; - - if (env != null) - { - // Convert Environment JSON Object as a dictionnary - var keyValuePairs = env!.ToObject>(); - - foreach (var kvp in keyValuePairs!) - { - var iotEnvVariable = new IoTEdgeModuleEnvironmentVariable - { - Name = kvp.Key, - Value = kvp.Value - }; - - environmentVariables.Add(iotEnvVariable); - } - } - } - } - return environmentVariables; - } - - public Task> GetModelSystemModule(string modelId) - { - throw new NotImplementedException(); - } - - public Task> GetConfigRouteList(string modelId) - { - throw new NotImplementedException(); - } - - } -} +// 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.Infrastructure.Services.AWS +{ + using System.Collections.Generic; + using System.Net; + using System.Text; + using System.Threading.Tasks; + using Amazon.GreengrassV2; + using Amazon.GreengrassV2.Model; + using Amazon.IoT; + using Amazon.IoT.Model; + using AutoMapper; + using AzureIoTHub.Portal.Application.Services; + using AzureIoTHub.Portal.Domain; + using AzureIoTHub.Portal.Domain.Exceptions; + using AzureIoTHub.Portal.Domain.Repositories; + using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Shared.Models.v10; + using Configuration = Microsoft.Azure.Devices.Configuration; + + public class AwsConfigService : IConfigService + { + private readonly IAmazonGreengrassV2 greengrass; + private readonly IAmazonIoT iotClient; + + private readonly IUnitOfWork unitOfWork; + private readonly IEdgeDeviceModelRepository edgeModelRepository; + private readonly ConfigHandler config; + private readonly IMapper mapper; + + public AwsConfigService( + IAmazonGreengrassV2 greengrass, + IAmazonIoT iot, + IMapper mapper, + IUnitOfWork unitOfWork, + IEdgeDeviceModelRepository edgeModelRepository, + ConfigHandler config) + { + this.greengrass = greengrass; + this.iotClient = iot; + this.mapper = mapper; + this.unitOfWork = unitOfWork; + this.edgeModelRepository = edgeModelRepository; + this.config = config; + } + + public async Task RollOutEdgeModelConfiguration(IoTEdgeModel edgeModel) + { + + var createDeploymentRequest = new CreateDeploymentRequest + { + DeploymentName = edgeModel?.Name, + Components = await CreateGreenGrassComponents(edgeModel!), + TargetArn = await GetThingGroupArn(edgeModel!) + }; + + var createDeploymentResponse = await this.greengrass.CreateDeploymentAsync(createDeploymentRequest); + + if (createDeploymentResponse.HttpStatusCode != HttpStatusCode.Created) + { + throw new InternalServerErrorException("The deployment creation failed due to an error in the Amazon IoT API."); + } + + return createDeploymentResponse.DeploymentId; + } + + private async Task GetThingGroupArn(IoTEdgeModel edgeModel) + { + await CreateThingTypeIfNotExists(edgeModel!.Name); + + var dynamicThingGroup = new DescribeThingGroupRequest + { + ThingGroupName = edgeModel?.Name + }; + + try + { + var existingThingGroupResponse = await this.iotClient.DescribeThingGroupAsync(dynamicThingGroup); + + return existingThingGroupResponse.ThingGroupArn; + } + catch (Amazon.IoT.Model.ResourceNotFoundException) + { + var createThingGroupResponse = await this.iotClient.CreateDynamicThingGroupAsync(new CreateDynamicThingGroupRequest + { + ThingGroupName = edgeModel!.Name, + QueryString = $"thingTypeName: {edgeModel!.Name}" + }); + + return createThingGroupResponse.ThingGroupArn; + } + } + + private async Task CreateThingTypeIfNotExists(string thingTypeName) + { + var existingThingType = new DescribeThingTypeRequest + { + ThingTypeName = thingTypeName + }; + + try + { + _ = await this.iotClient.DescribeThingTypeAsync(existingThingType); + } + catch (Amazon.IoT.Model.ResourceNotFoundException) + { + _ = await this.iotClient.CreateThingTypeAsync(new CreateThingTypeRequest + { + ThingTypeName = thingTypeName, + Tags = new List + { + new Tag + { + Key = "iotEdge", + Value = "True" + } + } + }); + } + } + + private async Task> CreateGreenGrassComponents(IoTEdgeModel edgeModel) + { + var components = new Dictionary(); + foreach (var component in edgeModel.EdgeModules) + { + try + { + var componentArn = !string.IsNullOrEmpty(component.Id) ? + $"{component.Id}:versions:{component.Version}" : // Public greengrass component + $"arn:aws:greengrass:{config.AWSRegion}:{config.AWSAccountId}:components:{component.ModuleName}:versions:{component.Version}"; // + + _ = await this.greengrass.DescribeComponentAsync(new DescribeComponentRequest + { + Arn = componentArn + }); + components.Add(component.ModuleName, new ComponentDeploymentSpecification { ComponentVersion = component.Version }); + + } + catch (Amazon.GreengrassV2.Model.ResourceNotFoundException) + { + var componentVersion = new CreateComponentVersionRequest + { + InlineRecipe = new MemoryStream(Encoding.UTF8.GetBytes(component.ContainerCreateOptions)) + }; + var response = await greengrass.CreateComponentVersionAsync(componentVersion); + if (response.HttpStatusCode != HttpStatusCode.Created) + { + throw new InternalServerErrorException("The component creation failed due to an error in the Amazon IoT API."); + + } + components.Add(component.ModuleName, new ComponentDeploymentSpecification { ComponentVersion = component.Version }); + } + } + + return components; + } + + //AWS Not implemented methods + + public Task> GetIoTEdgeConfigurations() + { + throw new NotImplementedException(); + } + + public Task> GetDevicesConfigurations() + { + throw new NotImplementedException(); + } + + public Task RollOutDeviceModelConfiguration(string modelId, Dictionary desiredProperties) + { + throw new NotImplementedException(); + } + + public Task DeleteDeviceModelConfigurationByConfigurationNamePrefix(string configurationNamePrefix) + { + throw new NotImplementedException(); + } + + public Task RollOutDeviceConfiguration(string modelId, Dictionary desiredProperties, string configurationId, Dictionary targetTags, int priority = 0) + { + throw new NotImplementedException(); + } + + public Task GetConfigItem(string id) + { + throw new NotImplementedException(); + } + + public async Task DeleteConfiguration(string modelId) + { + var modules = await GetConfigModuleList(modelId); + + foreach (var module in modules.Where(c => string.IsNullOrEmpty(c.Id))) + { + var deletedComponentResponse = await this.greengrass.DeleteComponentAsync(new DeleteComponentRequest + { + Arn = $"arn:aws:greengrass:{config.AWSRegion}:{config.AWSAccountId}:components:{module.ModuleName}:versions:{module.Version}" + }); + + if (deletedComponentResponse.HttpStatusCode != HttpStatusCode.NoContent) + { + throw new InternalServerErrorException("The deletion of the component failed due to an error in the Amazon IoT API."); + } + } + + var cancelDeploymentResponse = await this.greengrass.CancelDeploymentAsync(new CancelDeploymentRequest + { + DeploymentId = modelId + }); + + if (cancelDeploymentResponse.HttpStatusCode != HttpStatusCode.OK) + { + throw new InternalServerErrorException("The cancellation of the deployment failed due to an error in the Amazon IoT API."); + + } + else + { + var deleteDeploymentResponse = await this.greengrass.DeleteDeploymentAsync(new DeleteDeploymentRequest + { + DeploymentId = modelId + }); + + if (deleteDeploymentResponse.HttpStatusCode != HttpStatusCode.NoContent) + { + throw new InternalServerErrorException("The deletion of the deployment failed due to an error in the Amazon IoT API."); + } + } + } + + public Task GetFailedDeploymentsCount() + { + throw new NotImplementedException(); + } + + public async Task> GetConfigModuleList(string modelId) + { + var moduleList = new List(); + + var getDeployement = new GetDeploymentRequest + { + DeploymentId = modelId, + }; + try + { + var response = await this.greengrass.GetDeploymentAsync(getDeployement); + + foreach (var compoenent in response.Components) + { + var componentId = string.Empty; + var jsonRecipe = string.Empty; + + try + { + var responseComponent = await this.greengrass.GetComponentAsync(new GetComponentRequest + { + Arn = $"arn:aws:greengrass:{config.AWSRegion}:{config.AWSAccountId}:components:{compoenent.Key}:versions:{compoenent.Value.ComponentVersion}", + RecipeOutputFormat = RecipeOutputFormat.JSON + }); + + // Read the Recipe which is in JSON Format + using var reader = new StreamReader(responseComponent.Recipe); + jsonRecipe = reader.ReadToEnd(); + } + catch (Amazon.GreengrassV2.Model.ResourceNotFoundException) + { + // If the component is not found, we assume it is a public component + componentId = $"arn:aws:greengrass:{config.AWSRegion}:aws:components:{compoenent.Key}"; + + var responseComponent = await this.greengrass.GetComponentAsync(new GetComponentRequest + { + Arn = $"arn:aws:greengrass:{config.AWSRegion}:aws:components:{compoenent.Key}:versions:{compoenent.Value.ComponentVersion}", + RecipeOutputFormat = RecipeOutputFormat.JSON + }); + + using var reader = new StreamReader(responseComponent.Recipe); + jsonRecipe = reader.ReadToEnd(); + } + + var iotEdgeModule = new IoTEdgeModule + { + Id = componentId, + ModuleName = compoenent.Key, + Version = compoenent.Value.ComponentVersion, + ContainerCreateOptions = jsonRecipe, + // ImageURI is required, but not used for Greengrass components + ImageURI = "example.com" + }; + + moduleList.Add(iotEdgeModule); + + } + return moduleList; + } + catch (Amazon.GreengrassV2.Model.ResourceNotFoundException) + { + throw new InternalServerErrorException("The deployment is not found"); + + } + } + + public Task> GetModelSystemModule(string modelId) + { + throw new NotImplementedException(); + } + + public Task> GetConfigRouteList(string modelId) + { + throw new NotImplementedException(); + } + + public async Task> GetPublicEdgeModules() + { + var publicComponents = new List(); + + var nextToken = string.Empty; + + do + { + var response = await this.greengrass.ListComponentsAsync(new ListComponentsRequest + { + Scope = ComponentVisibilityScope.PUBLIC, + NextToken = nextToken + }); + + publicComponents.AddRange(response.Components); + + nextToken = response.NextToken; + } + while (!string.IsNullOrEmpty(nextToken)); + + return publicComponents.Select(c => new IoTEdgeModule + { + Id = c.Arn, + ModuleName = c.ComponentName, + Version = c.LatestVersion.ComponentVersion, + // ImageURI is required, but not used for Greengrass components + ImageURI = "example.com" + }); + } + } +} diff --git a/src/AzureIoTHub.Portal.Infrastructure/Services/AwsExternalDeviceService.cs b/src/AzureIoTHub.Portal.Infrastructure/Services/AwsExternalDeviceService.cs index 5c49acedc..314a30e7e 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/Services/AwsExternalDeviceService.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/Services/AwsExternalDeviceService.cs @@ -252,17 +252,24 @@ public async Task GetDeviceCredentials(string deviceName) public async Task RetrieveLastConfiguration(IoTEdgeDevice ioTEdgeDevice) { - var coreDevice = await this.greengrass.GetCoreDeviceAsync(new GetCoreDeviceRequest + try { - CoreDeviceThingName = ioTEdgeDevice.DeviceName - }); + var coreDevice = await this.greengrass.GetCoreDeviceAsync(new GetCoreDeviceRequest + { + CoreDeviceThingName = ioTEdgeDevice.DeviceName + }); - return new ConfigItem + return new ConfigItem + { + Name = coreDevice.CoreDeviceThingName, + DateCreation = coreDevice.LastStatusUpdateTimestamp, + Status = coreDevice.Status + }; + } + catch (Amazon.GreengrassV2.Model.ResourceNotFoundException) { - Name = coreDevice.CoreDeviceThingName, - DateCreation = coreDevice.LastStatusUpdateTimestamp, - Status = coreDevice.Status - }; + return null!; + } } public Task UpdateDevice(Device device) diff --git a/src/AzureIoTHub.Portal.Infrastructure/Services/EdgeModelService.cs b/src/AzureIoTHub.Portal.Infrastructure/Services/EdgeModelService.cs index ea68eda6a..427f1ae63 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/Services/EdgeModelService.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/Services/EdgeModelService.cs @@ -104,11 +104,11 @@ public async Task> GetEdgeModels(EdgeModelFilt public async Task CreateEdgeModel(IoTEdgeModel edgeModel) { var edgeModelEntity = await this.edgeModelRepository.GetByIdAsync(edgeModel?.ModelId); + if (edgeModelEntity == null) { edgeModelEntity = this.mapper.Map(edgeModel); await this.edgeModelRepository.InsertAsync(edgeModelEntity); - await this.unitOfWork.SaveAsync(); } else { @@ -123,8 +123,9 @@ public async Task CreateEdgeModel(IoTEdgeModel edgeModel) await SaveModuleCommands(edgeModel); } - await this.configService.RollOutEdgeModelConfiguration(edgeModel); - + edgeModelEntity.ExternalIdentifier = await this.configService.RollOutEdgeModelConfiguration(edgeModel); + + await this.unitOfWork.SaveAsync(); } /// @@ -133,9 +134,8 @@ public async Task CreateEdgeModel(IoTEdgeModel edgeModel) /// The device model object. /// /// - public async Task SaveModuleCommands(IoTEdgeModel deviceModelObject) + private async Task SaveModuleCommands(IoTEdgeModel deviceModelObject) { - IEnumerable moduleCommands = deviceModelObject.EdgeModules .SelectMany(x => x.Commands.Select(cmd => new IoTEdgeModuleCommand { @@ -146,6 +146,7 @@ public async Task SaveModuleCommands(IoTEdgeModel deviceModelObject) })).ToArray(); var existingCommands = this.commandRepository.GetAll().Where(x => x.EdgeDeviceModelId == deviceModelObject.ModelId).ToList(); + foreach (var command in existingCommands) { this.commandRepository.Delete(command.Id); @@ -155,7 +156,6 @@ public async Task SaveModuleCommands(IoTEdgeModel deviceModelObject) { await this.commandRepository.InsertAsync(this.mapper.Map(cmd)); } - await this.unitOfWork.SaveAsync(); } /// @@ -262,12 +262,13 @@ public async Task UpdateEdgeModel(IoTEdgeModel edgeModel) _ = this.mapper.Map(edgeModel, edgeModelEntity); this.edgeModelRepository.Update(edgeModelEntity); - await this.unitOfWork.SaveAsync(); await SaveModuleCommands(edgeModel); } - await this.configService.RollOutEdgeModelConfiguration(edgeModel); + edgeModel.ExternalIdentifier = await this.configService.RollOutEdgeModelConfiguration(edgeModel); + + await this.unitOfWork.SaveAsync(); } /// @@ -343,7 +344,11 @@ public Task UpdateEdgeModelAvatar(string edgeModelId, IFormFile file) public Task DeleteEdgeModelAvatar(string edgeModelId) { return Task.Run(() => this.deviceModelImageManager.DeleteDeviceModelImageAsync(edgeModelId)); + } + + public Task> GetPublicEdgeModules() + { + return this.configService.GetPublicEdgeModules(); } - } } diff --git a/src/AzureIoTHub.Portal.Server/Controllers/v1.0/EdgeModelsController.cs b/src/AzureIoTHub.Portal.Server/Controllers/v1.0/EdgeModelsController.cs index c57b7b995..22778508f 100644 --- a/src/AzureIoTHub.Portal.Server/Controllers/v1.0/EdgeModelsController.cs +++ b/src/AzureIoTHub.Portal.Server/Controllers/v1.0/EdgeModelsController.cs @@ -108,5 +108,17 @@ public virtual async Task DeleteAvatar(string edgeModelId) await this.edgeModelService.DeleteEdgeModelAvatar(edgeModelId); return NoContent(); } + + /// + /// Get public edge modules + /// + /// Public edge modules + [HttpGet("public-modules", Name = "GET edge public modules")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public virtual async Task>> GetPublicEdgeModules() + { + return Ok(await this.edgeModelService.GetPublicEdgeModules()); + } } } diff --git a/src/AzureIoTHub.Portal.Server/Services/ConfigService.cs b/src/AzureIoTHub.Portal.Server/Services/ConfigService.cs index 95dbb2708..14a6a66d9 100644 --- a/src/AzureIoTHub.Portal.Server/Services/ConfigService.cs +++ b/src/AzureIoTHub.Portal.Server/Services/ConfigService.cs @@ -1,354 +1,365 @@ -// 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.Services -{ - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Linq; - using System.Text; - using System.Threading.Tasks; - using Azure; - using AzureIoTHub.Portal.Application.Helpers; - using AzureIoTHub.Portal.Application.Services; - using AzureIoTHub.Portal.Crosscutting.Extensions; - using AzureIoTHub.Portal.Domain.Exceptions; - using AzureIoTHub.Portal.Models.v10; - using AzureIoTHub.Portal.Shared.Models.v10; - using Microsoft.Azure.Devices; - using Microsoft.Azure.Devices.Common.Extensions; - using Newtonsoft.Json.Linq; - - public class ConfigService : IConfigService - { - private readonly RegistryManager registryManager; - - public ConfigService( - RegistryManager registry) - { - this.registryManager = registry; - } - - public async Task> GetIoTEdgeConfigurations() - { - try - { - var configurations = await this.registryManager.GetConfigurationsAsync(0); - return configurations.Where(c => c.Content.ModulesContent.Count > 0); - } - catch (RequestFailedException ex) - { - throw new InternalServerErrorException("Unable to get IOT Edge configurations", ex); - } - } - - public async Task> GetDevicesConfigurations() - { - try - { - var configurations = await this.registryManager.GetConfigurationsAsync(0); - - return configurations - .Where(c => c.Priority > 0 && c.Content.ModulesContent.Count == 0); - } - catch (RequestFailedException e) - { - throw new InternalServerErrorException("Unable to get devices configurations", e); - } - } - - public async Task GetConfigItem(string id) - { - try - { - return await this.registryManager.GetConfigurationAsync(id); - } - catch (RequestFailedException ex) - { - throw new InternalServerErrorException($"Unable to get the configuration for id {id}", ex); - } - } - - public async Task> GetConfigModuleList(string modelId) - { - var configList = await GetIoTEdgeConfigurations(); - - var config = configList.FirstOrDefault((x) => x.Id.StartsWith(modelId, StringComparison.Ordinal)); - - if (config == null) - { - throw new InternalServerErrorException("Config does not exist."); - } - - var moduleList = new List(); - - // Details of every modules are stored within the EdgeAgent module data - if (config.Content.ModulesContent != null - && config.Content.ModulesContent.TryGetValue("$edgeAgent", out var edgeAgentModule) - && edgeAgentModule.TryGetValue("properties.desired", out var edgeAgentDesiredProperties)) - { - // Converts the object to a JObject to access its properties more easily - if (edgeAgentDesiredProperties is not JObject modObject) - { - throw new InvalidOperationException($"Could not parse properties.desired for the configuration id {config.Id}"); - } - - // Adds regular modules to the list of modules - if (modObject.TryGetValue("modules", out var modules)) - { - foreach (var newModule in modules.Values().Select(module => ConfigHelper.CreateGatewayModule(config, module))) - { - newModule.ModuleIdentityTwinSettings = ConfigHelper.CreateModuleTwinSettings(config.Content.ModulesContent, newModule.ModuleName); - moduleList.Add(newModule); - } - } - } - - return moduleList; - } - - public async Task> GetModelSystemModule(string modelId) - { - var configList = await GetIoTEdgeConfigurations(); - - var config = configList.FirstOrDefault((x) => x.Id.StartsWith(modelId, StringComparison.Ordinal)); - - if (config == null) - { - throw new InternalServerErrorException("Config does not exist."); - } - - var moduleList = new List(); - - // Details of every modules are stored within the EdgeAgent module data - if (config.Content.ModulesContent != null - && config.Content.ModulesContent.TryGetValue("$edgeAgent", out var edgeAgentModule) - && edgeAgentModule.TryGetValue("properties.desired", out var edgeAgentDesiredProperties)) - { - // Converts the object to a JObject to access its properties more easily - if (edgeAgentDesiredProperties is not JObject modObject) - { - throw new InvalidOperationException($"Could not parse properties.desired for the configuration id {config.Id}"); - } - - // Adds regular modules to the list of modules - if (modObject.TryGetValue("systemModules", out var modules)) - { - foreach (var newModule in modules.Values().Select(module => ConfigHelper.CreateGatewayModule(config, module))) - { - moduleList.Add(new EdgeModelSystemModule(newModule.ModuleName) - { - ImageUri = newModule.ImageURI, - EnvironmentVariables = newModule.EnvironmentVariables, - ContainerCreateOptions = newModule.ContainerCreateOptions, - }); - } - } - } - - return moduleList; - } - - public async Task> GetConfigRouteList(string modelId) - { - var configList = await GetIoTEdgeConfigurations(); - - var config = configList.FirstOrDefault((x) => x.Id.StartsWith(modelId, StringComparison.Ordinal)); - - if (config == null) - { - throw new InternalServerErrorException("Config does not exist."); - } - - var routeList = new List(); - - // Details of routes are stored within the EdgeHub properties.desired - if (config.Content.ModulesContent != null - && config.Content.ModulesContent.TryGetValue("$edgeHub", out var edgeHubModule) - && edgeHubModule.TryGetValue("properties.desired", out var edgeHubDesiredProperties)) - { - // - if (edgeHubDesiredProperties is not JObject modObject) - { - throw new InvalidOperationException($"Could not parse properties.desired for the configuration id {config.Id}"); - } - - // - if (modObject.TryGetValue("routes", out var routes)) - { - foreach (var newRoute in routes.Values().Select(route => ConfigHelper.CreateIoTEdgeRouteFromJProperty(route))) - { - routeList.Add(newRoute); - } - } - } - - return routeList; - - } - - public async Task DeleteConfiguration(string configId) - { - try - { - await this.registryManager.RemoveConfigurationAsync(configId); - } - catch (RequestFailedException e) - { - throw new InternalServerErrorException($"Unable to delete the configuration for id {configId}", e); - } - } - - public async Task RollOutDeviceModelConfiguration(string modelId, Dictionary desiredProperties) - { -#pragma warning disable CA1308 // Normalize strings to uppercase - var configurationNamePrefix = modelId?.Trim() - .ToLowerInvariant() - .Replace(" ", "-", StringComparison.OrdinalIgnoreCase); -#pragma warning restore CA1308 // Normalize strings to uppercase - - await DeleteDeviceModelConfigurationByConfigurationNamePrefix(configurationNamePrefix); - - var newConfiguration = new Configuration($"{configurationNamePrefix}-{DateTime.UtcNow.Ticks}"); - - newConfiguration.Labels.Add("created-by", "Azure IoT hub Portal"); - newConfiguration.TargetCondition = $"tags.modelId = '{modelId}'"; - newConfiguration.Content.DeviceContent = desiredProperties; - - _ = await this.registryManager.AddConfigurationAsync(newConfiguration); - } - - public async Task DeleteDeviceModelConfigurationByConfigurationNamePrefix(string configurationNamePrefix) - { - var configurations = await this.registryManager.GetConfigurationsAsync(0); - - foreach (var item in configurations) - { - if (!item.Id.StartsWith(configurationNamePrefix, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - await this.registryManager.RemoveConfigurationAsync(item.Id); - } - } - - public async Task RollOutEdgeModelConfiguration(IoTEdgeModel edgeModel) - { - var configurations = await this.registryManager.GetConfigurationsAsync(0); - - var configurationNamePrefix = edgeModel.ModelId?.Trim() - .ToLowerInvariant() - .Replace(" ", "-", StringComparison.OrdinalIgnoreCase); - - var newConfiguration = new Configuration($"{configurationNamePrefix}-{DateTime.UtcNow.Ticks}"); - newConfiguration.Labels.Add("created-by", "Azure IoT hub Portal"); - newConfiguration.TargetCondition = $"tags.modelId = '{edgeModel.ModelId}'"; - newConfiguration.Priority = 10; - - newConfiguration.Content.ModulesContent = ConfigHelper.GenerateModulesContent(edgeModel); - - try - { - _ = await this.registryManager.AddConfigurationAsync(newConfiguration); - } - catch (RequestFailedException e) - { - throw new InternalServerErrorException("Unable to create configuration.", e); - } - - foreach (var item in configurations) - { - if (!item.Id.StartsWith(configurationNamePrefix, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - await this.registryManager.RemoveConfigurationAsync(item.Id); - } - } - - public async Task RollOutDeviceConfiguration( - string modelId, - Dictionary desiredProperties, - string configurationId, - Dictionary targetTags, - int priority = 0) - { - IEnumerable configurations; - - try - { - configurations = await this.registryManager.GetConfigurationsAsync(0); - } - catch (RequestFailedException e) - { - throw new InternalServerErrorException("Unable to get configurations", e); - } - - var configurationNamePrefix = configurationId.Trim().ToLowerInvariant().RemoveDiacritics(); - - foreach (var item in configurations) - { - if (!item.Id.StartsWith(configurationNamePrefix, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - try - { - await this.registryManager.RemoveConfigurationAsync(item.Id); - } - catch (RequestFailedException e) - { - throw new InternalServerErrorException($"Unable to remove configuration {item.Id}", e); - } - } - - var newConfiguration = new Configuration($"{configurationNamePrefix}-{DateTime.UtcNow.Ticks}"); - - newConfiguration.Labels.Add("created-by", "Azure IoT hub Portal"); - newConfiguration.Labels.Add("configuration-id", configurationId); - - var culture = CultureInfo.CreateSpecificCulture("en-En"); - var targetCondition = new StringBuilder(); - - foreach (var item in targetTags) - { - _ = targetCondition.AppendFormat(culture, " and tags.{0}", item.Key); - _ = targetCondition.AppendFormat(culture, " = '{0}'", item.Value); - } - - newConfiguration.TargetCondition = $"tags.modelId = '{modelId}'" + targetCondition; - newConfiguration.Content.DeviceContent = desiredProperties; - newConfiguration.Priority = priority; - - try - { - _ = await this.registryManager.AddConfigurationAsync(newConfiguration); - } - catch (RequestFailedException e) - { - throw new InternalServerErrorException($"Unable to add configuration {newConfiguration.Id}", e); - } - } - - public async Task GetFailedDeploymentsCount() - { - try - { - var configurations = await this.registryManager.GetConfigurationsAsync(0); - - var failedDeploymentsCount= configurations.Where(c => c.Content.ModulesContent.Count > 0) - .Sum(c => c.SystemMetrics.Results.GetValueOrDefault("reportedFailedCount", 0)); - - return Convert.ToInt32(failedDeploymentsCount); - } - catch (RequestFailedException ex) - { - throw new InternalServerErrorException("Unable to get failed deployments count", ex); - } - } - } -} +// 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.Services +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using Azure; + using AzureIoTHub.Portal.Application.Helpers; + using AzureIoTHub.Portal.Application.Services; + using AzureIoTHub.Portal.Crosscutting.Extensions; + using AzureIoTHub.Portal.Domain.Exceptions; + using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Shared.Models.v10; + using Microsoft.Azure.Devices; + using Microsoft.Azure.Devices.Common.Extensions; + using Newtonsoft.Json.Linq; + + public class ConfigService : IConfigService + { + private readonly RegistryManager registryManager; + + public ConfigService( + RegistryManager registry) + { + this.registryManager = registry; + } + + public async Task> GetIoTEdgeConfigurations() + { + try + { + var configurations = await this.registryManager.GetConfigurationsAsync(0); + return configurations.Where(c => c.Content.ModulesContent.Count > 0); + } + catch (RequestFailedException ex) + { + throw new InternalServerErrorException("Unable to get IOT Edge configurations", ex); + } + } + + public async Task> GetDevicesConfigurations() + { + try + { + var configurations = await this.registryManager.GetConfigurationsAsync(0); + + return configurations + .Where(c => c.Priority > 0 && c.Content.ModulesContent.Count == 0); + } + catch (RequestFailedException e) + { + throw new InternalServerErrorException("Unable to get devices configurations", e); + } + } + + public async Task GetConfigItem(string id) + { + try + { + return await this.registryManager.GetConfigurationAsync(id); + } + catch (RequestFailedException ex) + { + throw new InternalServerErrorException($"Unable to get the configuration for id {id}", ex); + } + } + + public async Task> GetConfigModuleList(string modelId) + { + var configList = await GetIoTEdgeConfigurations(); + + var config = configList.FirstOrDefault((x) => x.Id.StartsWith(modelId, StringComparison.Ordinal)); + + if (config == null) + { + throw new InternalServerErrorException("Config does not exist."); + } + + var moduleList = new List(); + + // Details of every modules are stored within the EdgeAgent module data + if (config.Content.ModulesContent != null + && config.Content.ModulesContent.TryGetValue("$edgeAgent", out var edgeAgentModule) + && edgeAgentModule.TryGetValue("properties.desired", out var edgeAgentDesiredProperties)) + { + // Converts the object to a JObject to access its properties more easily + if (edgeAgentDesiredProperties is not JObject modObject) + { + throw new InvalidOperationException($"Could not parse properties.desired for the configuration id {config.Id}"); + } + + // Adds regular modules to the list of modules + if (modObject.TryGetValue("modules", out var modules)) + { + foreach (var newModule in modules.Values().Select(module => ConfigHelper.CreateGatewayModule(config, module))) + { + newModule.ModuleIdentityTwinSettings = ConfigHelper.CreateModuleTwinSettings(config.Content.ModulesContent, newModule.ModuleName); + moduleList.Add(newModule); + } + } + } + + return moduleList; + } + + public async Task> GetModelSystemModule(string modelId) + { + var configList = await GetIoTEdgeConfigurations(); + + var config = configList.FirstOrDefault((x) => x.Id.StartsWith(modelId, StringComparison.Ordinal)); + + if (config == null) + { + throw new InternalServerErrorException("Config does not exist."); + } + + var moduleList = new List(); + + // Details of every modules are stored within the EdgeAgent module data + if (config.Content.ModulesContent != null + && config.Content.ModulesContent.TryGetValue("$edgeAgent", out var edgeAgentModule) + && edgeAgentModule.TryGetValue("properties.desired", out var edgeAgentDesiredProperties)) + { + // Converts the object to a JObject to access its properties more easily + if (edgeAgentDesiredProperties is not JObject modObject) + { + throw new InvalidOperationException($"Could not parse properties.desired for the configuration id {config.Id}"); + } + + // Adds regular modules to the list of modules + if (modObject.TryGetValue("systemModules", out var modules)) + { + foreach (var newModule in modules.Values().Select(module => ConfigHelper.CreateGatewayModule(config, module))) + { + moduleList.Add(new EdgeModelSystemModule(newModule.ModuleName) + { + ImageUri = newModule.ImageURI, + EnvironmentVariables = newModule.EnvironmentVariables, + ContainerCreateOptions = newModule.ContainerCreateOptions, + }); + } + } + } + + return moduleList; + } + + public async Task> GetConfigRouteList(string modelId) + { + var configList = await GetIoTEdgeConfigurations(); + + var config = configList.FirstOrDefault((x) => x.Id.StartsWith(modelId, StringComparison.Ordinal)); + + if (config == null) + { + throw new InternalServerErrorException("Config does not exist."); + } + + var routeList = new List(); + + // Details of routes are stored within the EdgeHub properties.desired + if (config.Content.ModulesContent != null + && config.Content.ModulesContent.TryGetValue("$edgeHub", out var edgeHubModule) + && edgeHubModule.TryGetValue("properties.desired", out var edgeHubDesiredProperties)) + { + // + if (edgeHubDesiredProperties is not JObject modObject) + { + throw new InvalidOperationException($"Could not parse properties.desired for the configuration id {config.Id}"); + } + + // + if (modObject.TryGetValue("routes", out var routes)) + { + foreach (var newRoute in routes.Values().Select(route => ConfigHelper.CreateIoTEdgeRouteFromJProperty(route))) + { + routeList.Add(newRoute); + } + } + } + + return routeList; + + } + + public async Task DeleteConfiguration(string configId) + { + try + { + await this.registryManager.RemoveConfigurationAsync(configId); + } + catch (RequestFailedException e) + { + throw new InternalServerErrorException($"Unable to delete the configuration for id {configId}", e); + } + } + + public async Task RollOutDeviceModelConfiguration(string modelId, Dictionary desiredProperties) + { +#pragma warning disable CA1308 // Normalize strings to uppercase + var configurationNamePrefix = modelId?.Trim() + .ToLowerInvariant() + .Replace(" ", "-", StringComparison.OrdinalIgnoreCase); +#pragma warning restore CA1308 // Normalize strings to uppercase + + await DeleteDeviceModelConfigurationByConfigurationNamePrefix(configurationNamePrefix); + + var newConfiguration = new Configuration($"{configurationNamePrefix}-{DateTime.UtcNow.Ticks}"); + + newConfiguration.Labels.Add("created-by", "Azure IoT hub Portal"); + newConfiguration.TargetCondition = $"tags.modelId = '{modelId}'"; + newConfiguration.Content.DeviceContent = desiredProperties; + + _ = await this.registryManager.AddConfigurationAsync(newConfiguration); + + return Guid.NewGuid().ToString(); + } + + public async Task DeleteDeviceModelConfigurationByConfigurationNamePrefix(string configurationNamePrefix) + { + var configurations = await this.registryManager.GetConfigurationsAsync(0); + + foreach (var item in configurations) + { + if (!item.Id.StartsWith(configurationNamePrefix, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + await this.registryManager.RemoveConfigurationAsync(item.Id); + } + } + + public async Task RollOutEdgeModelConfiguration(IoTEdgeModel edgeModel) + { + var configurations = await this.registryManager.GetConfigurationsAsync(0); + + var configurationNamePrefix = edgeModel.ModelId?.Trim() + .ToLowerInvariant() + .Replace(" ", "-", StringComparison.OrdinalIgnoreCase); + + var newConfiguration = new Configuration($"{configurationNamePrefix}-{DateTime.UtcNow.Ticks}"); + newConfiguration.Labels.Add("created-by", "Azure IoT hub Portal"); + newConfiguration.TargetCondition = $"tags.modelId = '{edgeModel.ModelId}'"; + newConfiguration.Priority = 10; + + newConfiguration.Content.ModulesContent = ConfigHelper.GenerateModulesContent(edgeModel); + + try + { + _ = await this.registryManager.AddConfigurationAsync(newConfiguration); + } + catch (RequestFailedException e) + { + throw new InternalServerErrorException("Unable to create configuration.", e); + } + + foreach (var item in configurations) + { + if (!item.Id.StartsWith(configurationNamePrefix, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + await this.registryManager.RemoveConfigurationAsync(item.Id); + } + + return Guid.NewGuid().ToString(); + } + + public async Task RollOutDeviceConfiguration( + string modelId, + Dictionary desiredProperties, + string configurationId, + Dictionary targetTags, + int priority = 0) + { + IEnumerable configurations; + + try + { + configurations = await this.registryManager.GetConfigurationsAsync(0); + } + catch (RequestFailedException e) + { + throw new InternalServerErrorException("Unable to get configurations", e); + } + + var configurationNamePrefix = configurationId.Trim().ToLowerInvariant().RemoveDiacritics(); + + foreach (var item in configurations) + { + if (!item.Id.StartsWith(configurationNamePrefix, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + try + { + await this.registryManager.RemoveConfigurationAsync(item.Id); + } + catch (RequestFailedException e) + { + throw new InternalServerErrorException($"Unable to remove configuration {item.Id}", e); + } + } + + var newConfiguration = new Configuration($"{configurationNamePrefix}-{DateTime.UtcNow.Ticks}"); + + newConfiguration.Labels.Add("created-by", "Azure IoT hub Portal"); + newConfiguration.Labels.Add("configuration-id", configurationId); + + var culture = CultureInfo.CreateSpecificCulture("en-En"); + var targetCondition = new StringBuilder(); + + foreach (var item in targetTags) + { + _ = targetCondition.AppendFormat(culture, " and tags.{0}", item.Key); + _ = targetCondition.AppendFormat(culture, " = '{0}'", item.Value); + } + + newConfiguration.TargetCondition = $"tags.modelId = '{modelId}'" + targetCondition; + newConfiguration.Content.DeviceContent = desiredProperties; + newConfiguration.Priority = priority; + + try + { + _ = await this.registryManager.AddConfigurationAsync(newConfiguration); + } + catch (RequestFailedException e) + { + throw new InternalServerErrorException($"Unable to add configuration {newConfiguration.Id}", e); + } + + return Guid.NewGuid().ToString(); + } + + public async Task GetFailedDeploymentsCount() + { + try + { + var configurations = await this.registryManager.GetConfigurationsAsync(0); + + var failedDeploymentsCount= configurations.Where(c => c.Content.ModulesContent.Count > 0) + .Sum(c => c.SystemMetrics.Results.GetValueOrDefault("reportedFailedCount", 0)); + + return Convert.ToInt32(failedDeploymentsCount); + } + catch (RequestFailedException ex) + { + throw new InternalServerErrorException("Unable to get failed deployments count", ex); + } + } + + public Task> GetPublicEdgeModules() + { + return Task.FromResult>(Array.Empty()); + } + } +} diff --git a/src/AzureIoTHub.Portal.Server/Services/DeviceConfigurationsService.cs b/src/AzureIoTHub.Portal.Server/Services/DeviceConfigurationsService.cs index fc1f7410a..5cbf5c93f 100644 --- a/src/AzureIoTHub.Portal.Server/Services/DeviceConfigurationsService.cs +++ b/src/AzureIoTHub.Portal.Server/Services/DeviceConfigurationsService.cs @@ -116,7 +116,7 @@ private async Task CreateOrUpdateConfiguration(DeviceConfig deviceConfig) desiredProperties.Add($"properties.desired.{item.Key}", propertyValue); } - await this.configService.RollOutDeviceConfiguration(deviceConfig.ModelId, desiredProperties, deviceConfig.ConfigurationId, deviceConfig.Tags, 100); + _ = await this.configService.RollOutDeviceConfiguration(deviceConfig.ModelId, desiredProperties, deviceConfig.ConfigurationId, deviceConfig.Tags, 100); } } } diff --git a/src/AzureIoTHub.Portal.Server/Services/DeviceModelService.cs b/src/AzureIoTHub.Portal.Server/Services/DeviceModelService.cs index 7583dc5c2..8023b9dff 100644 --- a/src/AzureIoTHub.Portal.Server/Services/DeviceModelService.cs +++ b/src/AzureIoTHub.Portal.Server/Services/DeviceModelService.cs @@ -205,7 +205,7 @@ private async Task CreateDeviceModelConfiguration(TModel deviceModel) _ = await this.deviceRegistryProvider.CreateEnrollmentGroupFromModelAsync(deviceModel.ModelId, deviceModel.Name, deviceModelTwin); - await this.configService.RollOutDeviceModelConfiguration(deviceModel.ModelId, desiredProperties); + _ = await this.configService.RollOutDeviceModelConfiguration(deviceModel.ModelId, desiredProperties); } } } diff --git a/src/AzureIoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule.cs b/src/AzureIoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule.cs index 643b710f5..4979faa26 100644 --- a/src/AzureIoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule.cs +++ b/src/AzureIoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule.cs @@ -11,7 +11,12 @@ namespace AzureIoTHub.Portal.Models.v10 /// IoT Edge module. /// public class IoTEdgeModule - { + { + /// + /// The module name, only used for AWS IoT Greengrass. + /// + public string Id { get; set; } = default!; + /// /// The module name. /// diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Client/Dialogs/EdgeModels/AwsGreengrassComponentDialogTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Client/Dialogs/EdgeModels/AwsGreengrassComponentDialogTests.cs new file mode 100644 index 000000000..0204fc82c --- /dev/null +++ b/src/AzureIoTHub.Portal.Tests.Unit/Client/Dialogs/EdgeModels/AwsGreengrassComponentDialogTests.cs @@ -0,0 +1,148 @@ +// 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.Tests.Unit.Client.Dialogs.EdgeModels +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using AzureIoTHub.Portal.Client.Dialogs.EdgeModels; + using AzureIoTHub.Portal.Client.Enums; + using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases; + using Bunit; + using FluentAssertions; + using Moq; + using MudBlazor; + using NUnit.Framework; + + [TestFixture] + public class AwsGreengrassComponentDialogTests : BlazorUnitTest + { + public override void Setup() + { + base.Setup(); + } + + [Test] + public async Task AwsGreengrassComponentDialog_ClickOnCancel_DialogCanceled() + { + // Arrange + var edgeModules = Array.Empty().ToList(); + + var cut = RenderComponent(); + var service = Services.GetService() as DialogService; + + var parameters = new DialogParameters + { + { "Context", Context.Create }, + { "EdgeModules", edgeModules } + }; + + IDialogReference dialogReference = null; + await cut.InvokeAsync(() => dialogReference = service?.Show(string.Empty, parameters)); + + // Act + cut.Find("#greengrass-component-cancel").Click(); + + var result = await dialogReference.Result; + + // Assert + _ = result.Canceled.Should().BeTrue(); + _ = edgeModules.Should().BeEmpty(); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public async Task AwsGreengrassComponentDialog_CreateAWSComponentAndSubmit_EdgeModuleAdded() + { + // Arrange + var edgeModules = Array.Empty().ToList(); + + var inputJsonRecipe = /*lang=json*/ @" +{ + ""ComponentName"": ""com.example.DDboxAdvantech"", + ""ComponentVersion"": ""1.0.0"" +} +"; + + var cut = RenderComponent(); + var service = Services.GetService() as DialogService; + + var parameters = new DialogParameters + { + { "Context", Context.Create }, + { "EdgeModules", edgeModules } + }; + + IDialogReference dialogReference = null; + await cut.InvokeAsync(() => dialogReference = service?.Show(string.Empty, parameters)); + + // Act + cut.WaitForElement("#greengrass-component-recipe-json").Change(inputJsonRecipe); + cut.Find("#greengrass-component-submit").Click(); + + var result = await dialogReference.Result; + + // Assert + _ = result.Canceled.Should().BeFalse(); + _ = edgeModules.Count.Should().Be(1); + _ = edgeModules.First().ModuleName.Should().Be("com.example.DDboxAdvantech"); + _ = edgeModules.First().Version.Should().Be("1.0.0"); + _ = edgeModules.First().ContainerCreateOptions.Should().Be(inputJsonRecipe); + _ = edgeModules.First().ImageURI.Should().Be("example.com"); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public async Task AwsGreengrassComponentDialog_EditAWSComponentAndSubmit_EdgeModuleUpdated() + { + // Arrange + var existingJsonRecipe = /*lang=json*/ @" +{ + ""ComponentName"": ""com.example.DDboxAdvantech"", + ""ComponentVersion"": ""1.0.0"" +} +"; + var edgeModule = new IoTEdgeModule + { + ModuleName = "com.example.DDboxAdvantech", + Version = "1.0.0", + ContainerCreateOptions = existingJsonRecipe + }; + + var newJsonRecipe = /*lang=json*/ @" +{ + ""ComponentName"": ""com.example.DDboxAdvantech"", + ""ComponentVersion"": ""2.0.0"" +} +"; + + var cut = RenderComponent(); + var service = Services.GetService() as DialogService; + + var parameters = new DialogParameters + { + { "Context", Context.Edit }, + { "Module", edgeModule } + }; + + IDialogReference dialogReference = null; + await cut.InvokeAsync(() => dialogReference = service?.Show(string.Empty, parameters)); + + // Act + cut.WaitForElement("#greengrass-component-recipe-json").Change(newJsonRecipe); + cut.Find("#greengrass-component-submit").Click(); + + var result = await dialogReference.Result; + + // Assert + _ = result.Canceled.Should().BeFalse(); + _ = edgeModule.ModuleName.Should().Be("com.example.DDboxAdvantech"); + _ = edgeModule.Version.Should().Be("2.0.0"); + _ = edgeModule.ContainerCreateOptions.Should().Be(newJsonRecipe); + _ = edgeModule.ImageURI.Should().Be("example.com"); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + } +} diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Client/Dialogs/EdgeModels/AwsGreengrassPublicComponentsDialogTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Client/Dialogs/EdgeModels/AwsGreengrassPublicComponentsDialogTests.cs new file mode 100644 index 000000000..684b75ed7 --- /dev/null +++ b/src/AzureIoTHub.Portal.Tests.Unit/Client/Dialogs/EdgeModels/AwsGreengrassPublicComponentsDialogTests.cs @@ -0,0 +1,161 @@ +// 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.Tests.Unit.Client.Dialogs.EdgeModels +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using AutoFixture; + using AzureIoTHub.Portal.Client.Dialogs.EdgeModels; + using AzureIoTHub.Portal.Client.Services; + using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases; + using Bunit; + using FluentAssertions; + using Microsoft.Extensions.DependencyInjection; + using Moq; + using MudBlazor; + using NUnit.Framework; + + [TestFixture] + public class AwsGreengrassPublicComponentsDialogTests : BlazorUnitTest + { + private Mock mockEdgeModelClientService; + + public override void Setup() + { + base.Setup(); + + this.mockEdgeModelClientService = MockRepository.Create(); + + _ = Services.AddSingleton(this.mockEdgeModelClientService.Object); + } + + [Test] + public async Task AwsGreengrassPublicComponentsDialog_AfterOnInitializedAsync_PublicEdgeComponentsAreLoaded() + { + // Arrange + var edgeModules = Array.Empty().ToList(); + var publicEdgeComponents = Fixture.CreateMany(10).ToList(); + + _ = this.mockEdgeModelClientService.Setup(s => s.GetPublicEdgeModules()) + .ReturnsAsync(publicEdgeComponents); + + var cut = RenderComponent(); + var service = Services.GetService() as DialogService; + + var parameters = new DialogParameters + { + {"EdgeModules", edgeModules} + }; + + // Act + await cut.InvokeAsync(() => service?.Show(string.Empty, parameters)); + cut.WaitForAssertion(() => cut.FindAll("table tbody tr").Count.Should().Be(10)); + + // Assert + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public async Task AwsGreengrassPublicComponentsDialog_ClickOnCancel_DialogCanceled() + { + // Arrange + var edgeModules = Array.Empty().ToList(); + var publicEdgeComponents = Fixture.CreateMany(10).ToList(); + + _ = this.mockEdgeModelClientService.Setup(s => s.GetPublicEdgeModules()) + .ReturnsAsync(publicEdgeComponents); + + var cut = RenderComponent(); + var service = Services.GetService() as DialogService; + + var parameters = new DialogParameters + { + {"EdgeModules", edgeModules} + }; + + IDialogReference dialogReference = null; + await cut.InvokeAsync(() => dialogReference = service?.Show(string.Empty, parameters)); + cut.WaitForAssertion(() => cut.FindAll("table tbody tr").Count.Should().Be(10)); + + // Act + cut.Find("#greengrass-public-components-cancel").Click(); + + var result = await dialogReference.Result; + + // Assert + _ = result.Canceled.Should().BeTrue(); + _ = edgeModules.Should().BeEmpty(); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public async Task AwsGreengrassPublicComponentsDialog_ClickOnSubmitWithoutSelectingPublicComponents_EdgeModulesNotUpdated() + { + // Arrange + var edgeModules = Array.Empty().ToList(); + var publicEdgeComponents = Fixture.CreateMany(10).ToList(); + + _ = this.mockEdgeModelClientService.Setup(s => s.GetPublicEdgeModules()) + .ReturnsAsync(publicEdgeComponents); + + var cut = RenderComponent(); + var service = Services.GetService() as DialogService; + + var parameters = new DialogParameters + { + {"EdgeModules", edgeModules} + }; + + IDialogReference dialogReference = null; + await cut.InvokeAsync(() => dialogReference = service?.Show(string.Empty, parameters)); + cut.WaitForAssertion(() => cut.FindAll("table tbody tr").Count.Should().Be(10)); + + // Act + cut.Find("#greengrass-public-components-submit").Click(); + + var result = await dialogReference.Result; + + // Assert + _ = result.Canceled.Should().BeFalse(); + _ = edgeModules.Should().BeEmpty(); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public async Task AwsGreengrassPublicComponentsDialog_ClickOnSubmitAfterSelectingPublicComponent_EdgeModulesNotUpdated() + { + // Arrange + var edgeModules = Array.Empty().ToList(); + var publicEdgeComponents = Fixture.CreateMany(10).ToList(); + + _ = this.mockEdgeModelClientService.Setup(s => s.GetPublicEdgeModules()) + .ReturnsAsync(publicEdgeComponents); + + var cut = RenderComponent(); + var service = Services.GetService() as DialogService; + + var parameters = new DialogParameters + { + {"EdgeModules", edgeModules} + }; + + IDialogReference dialogReference = null; + await cut.InvokeAsync(() => dialogReference = service?.Show(string.Empty, parameters)); + cut.WaitForAssertion(() => cut.FindAll("table tbody tr").Count.Should().Be(10)); + + // Act + cut.WaitForAssertion(() => cut.Find("table tbody tr").Click()); + cut.Find("#greengrass-public-components-submit").Click(); + + var result = await dialogReference.Result; + + // Assert + _ = result.Canceled.Should().BeFalse(); + _ = edgeModules.Count.Should().Be(1); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + } +} diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/EdgeModels/CreateEdgeModelsPageTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/EdgeModels/CreateEdgeModelsPageTest.cs index 001b26a42..0a918ec59 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/EdgeModels/CreateEdgeModelsPageTest.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/EdgeModels/CreateEdgeModelsPageTest.cs @@ -4,7 +4,8 @@ namespace AzureIoTHub.Portal.Tests.Unit.Client.Pages.EdgeModels { using System; - using System.Threading.Tasks; + using System.Threading.Tasks; + using AzureIoTHub.Portal.Client.Dialogs.EdgeModels; using AzureIoTHub.Portal.Client.Exceptions; using AzureIoTHub.Portal.Client.Models; using AzureIoTHub.Portal.Client.Pages.EdgeModels; @@ -378,5 +379,63 @@ public void ClickOnDeleteRouteShouldRemoveRouteFromEdgeModelData() cut.WaitForAssertion(() => Assert.AreEqual(0, cut.FindAll(".deleteRouteButton").Count)); cut.WaitForAssertion(() => MockRepository.VerifyAll()); } + + [Test] + public void CreateEdgeModelsPage_ClickOnAddEdgeModule_ShowAwsGreengrassComponentDialog() + { + // Arrange + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "AWS" }); + + var edgeModel = new IoTEdgeModel() + { + Name = Guid.NewGuid().ToString(), + }; + + var mockDialogReference = MockRepository.Create(); + _ = mockDialogReference.Setup(c => c.Result).ReturnsAsync(DialogResult.Ok("Ok")); + + _ = this.mockDialogService + .Setup(c => c.Show(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(mockDialogReference.Object); + + var cut = RenderComponent(); + + cut.WaitForElement($"#{nameof(IoTEdgeModel.Name)}").Change(edgeModel.Name); + + // Act + cut.WaitForElement("#add-edge-module").Click(); + + // Assert + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public void CreateEdgeModelsPage_ClickOnAddPublicEdgeModules_ShowAwsGreengrassPublicComponentsDialog() + { + // Arrange + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "AWS" }); + + var edgeModel = new IoTEdgeModel() + { + Name = Guid.NewGuid().ToString(), + }; + + var mockDialogReference = MockRepository.Create(); + _ = mockDialogReference.Setup(c => c.Result).ReturnsAsync(DialogResult.Ok("Ok")); + + _ = this.mockDialogService + .Setup(c => c.Show(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(mockDialogReference.Object); + + var cut = RenderComponent(); + + cut.WaitForElement($"#{nameof(IoTEdgeModel.Name)}").Change(edgeModel.Name); + + // Act + cut.WaitForElement("#add-public-edge-modules").Click(); + + // Assert + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } } } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/EdgeModels/EdgeModelDetailPageTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/EdgeModels/EdgeModelDetailPageTest.cs index 0fe160f80..ad4f67a47 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/EdgeModels/EdgeModelDetailPageTest.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/EdgeModels/EdgeModelDetailPageTest.cs @@ -5,7 +5,8 @@ namespace AzureIoTHub.Portal.Tests.Unit.Client.Pages.EdgeModels { using System; using System.Collections.Generic; - using System.Threading.Tasks; + using System.Threading.Tasks; + using AzureIoTHub.Portal.Client.Dialogs.EdgeModels; using AzureIoTHub.Portal.Client.Exceptions; using AzureIoTHub.Portal.Client.Models; using AzureIoTHub.Portal.Client.Pages.EdgeModels; @@ -31,8 +32,6 @@ public class EdgeModelDetailPageTest : BlazorUnitTest private readonly string mockEdgeModleId = Guid.NewGuid().ToString(); - private FakeNavigationManager mockNavigationManager; - public override void Setup() { base.Setup(); @@ -45,9 +44,6 @@ public override void Setup() _ = Services.AddSingleton(this.mockDialogService.Object); _ = Services.AddSingleton(this.mockSnackbarService.Object); _ = Services.AddSingleton(new PortalSettings { CloudProvider = "Azure" }); - - - this.mockNavigationManager = Services.GetRequiredService(); } [Test] @@ -63,7 +59,7 @@ public void ClickOnReturnButtonMustNavigateToPreviousPage() cut.WaitForElement("#returnButton").Click(); // Assert - cut.WaitForAssertion(() => this.mockNavigationManager.Uri.Should().EndWith("/edge/models")); + cut.WaitForAssertion(() => Services.GetRequiredService().Uri.Should().EndWith("/edge/models")); cut.WaitForAssertion(() => MockRepository.VerifyAll()); } @@ -225,7 +221,7 @@ public void ClickOnDeleteEdgeModelButtonShouldShowDeleteDialogAndRedirectIfOk() deleteModelBtn.Click(); // Assert - cut.WaitForAssertion(() => this.mockNavigationManager.Uri.Should().EndWith("/edge/models")); + cut.WaitForAssertion(() => Services.GetRequiredService().Uri.Should().EndWith("/edge/models")); cut.WaitForAssertion(() => MockRepository.VerifyAll()); } @@ -457,5 +453,53 @@ public void ClickOnDeleteRouteShouldRemoveRouteFromEdgeModelData() cut.WaitForAssertion(() => Assert.AreEqual(0, cut.FindAll(".deleteRouteButton").Count)); cut.WaitForAssertion(() => MockRepository.VerifyAll()); } + + [Test] + public void EdgeModelDetailPage_ClickOnAddEdgeModule_ShowAwsGreengrassComponentDialog() + { + // Arrange + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "AWS" }); + + _ = SetupLoadEdgeModel(); + + var mockDialogReference = MockRepository.Create(); + _ = mockDialogReference.Setup(c => c.Result).ReturnsAsync(DialogResult.Ok("Ok")); + + _ = this.mockDialogService + .Setup(c => c.Show(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(mockDialogReference.Object); + + var cut = RenderComponent(ComponentParameter.CreateParameter("ModelID", this.mockEdgeModleId)); + + // Act + cut.WaitForElement("#add-edge-module").Click(); + + // Assert + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public void EdgeModelDetailPage_ClickOnAddPublicEdgeModules_ShowAwsGreengrassPublicComponentsDialog() + { + // Arrange + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "AWS" }); + + _ = SetupLoadEdgeModel(); + + var mockDialogReference = MockRepository.Create(); + _ = mockDialogReference.Setup(c => c.Result).ReturnsAsync(DialogResult.Ok("Ok")); + + _ = this.mockDialogService + .Setup(c => c.Show(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(mockDialogReference.Object); + + var cut = RenderComponent(ComponentParameter.CreateParameter("ModelID", this.mockEdgeModleId)); + + // Act + cut.WaitForElement("#add-public-edge-modules").Click(); + + // Assert + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } } } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Client/Services/EdgeModelClientServiceTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Client/Services/EdgeModelClientServiceTest.cs index 80c42e484..2e4019ae8 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Client/Services/EdgeModelClientServiceTest.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Client/Services/EdgeModelClientServiceTest.cs @@ -200,5 +200,23 @@ public async Task DeleteAvatarPropertiesShouldChangeAvatar() MockHttpClient.VerifyNoOutstandingRequest(); MockHttpClient.VerifyNoOutstandingExpectation(); } + + [Test] + public async Task GetPublicEdgeModules_GetPublicEdgeModules_EdgeModulesReturned() + { + // Arrange + var expectedModels = Fixture.Build().CreateMany(3).ToList(); + + _ = MockHttpClient.When(HttpMethod.Get, "/api/edge/models/public-modules") + .RespondJson(expectedModels); + + // Act + var result = await this.edgeModelClientService.GetPublicEdgeModules(); + + // Assert + _ = result.Should().BeEquivalentTo(expectedModels); + MockHttpClient.VerifyNoOutstandingRequest(); + MockHttpClient.VerifyNoOutstandingExpectation(); + } } } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Services/AWS_Tests/AwsConfigTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Services/AWS_Tests/AwsConfigTests.cs index 2fa4c0b8a..d3aef3a2f 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Services/AWS_Tests/AwsConfigTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Services/AWS_Tests/AwsConfigTests.cs @@ -26,8 +26,11 @@ namespace AzureIoTHub.Portal.Tests.Unit.Infrastructure.Services.AWS_Tests using System.Collections.Generic; using System.IO; using System.Text; - using Newtonsoft.Json.Linq; - + using Newtonsoft.Json.Linq; + using System.Linq; + using FluentAssertions; + using System; + [TestFixture] public class AwsConfigTests : BackendUnitTest { @@ -70,12 +73,17 @@ public void SetUp() [Test] public async Task CreateDeploymentWithComponentsAndExistingThingGroupAndThingTypeShouldCreateTheDeployment() - { - //Act + { + // Arrange + var deploymentId = Fixture.Create(); + _ = this.mockConfigHandler.Setup(handler => handler.AWSRegion).Returns("eu-west-1"); _ = this.mockConfigHandler.Setup(handler => handler.AWSAccountId).Returns("00000000"); - var edge = Fixture.Create(); + var edge = Fixture.Create(); + // Simulate a custom/private component + edge.EdgeModules.First().Id = string.Empty; + var edgeDeviceModelEntity = Mapper.Map(edge); _ = this.mockIotClient.Setup(s3 => s3.DescribeThingGroupAsync(It.IsAny(), It.IsAny())) @@ -90,7 +98,8 @@ public async Task CreateDeploymentWithComponentsAndExistingThingGroupAndThingTyp _ = this.mockGreengrasClient.Setup(s3 => s3.CreateDeploymentAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new CreateDeploymentResponse { - HttpStatusCode = HttpStatusCode.Created + HttpStatusCode = HttpStatusCode.Created, + DeploymentId = deploymentId }); _ = this.mockGreengrasClient.Setup(s3 => s3.DescribeComponentAsync(It.IsAny(), It.IsAny())) @@ -102,29 +111,26 @@ public async Task CreateDeploymentWithComponentsAndExistingThingGroupAndThingTyp HttpStatusCode = HttpStatusCode.Created }); - _ = this.mockEdgeModelRepository.Setup(x => x.GetByIdAsync(It.IsAny())) - .ReturnsAsync(edgeDeviceModelEntity); - - _ = this.mockEdgeModelRepository.Setup(repository => repository.Update(It.IsAny())); - _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) - .Returns(Task.CompletedTask); - - //Arrange - await this.awsConfigService.RollOutEdgeModelConfiguration(edge); + // Act + var result = _ = await this.awsConfigService.RollOutEdgeModelConfiguration(edge); - //Assert + // Assert + Assert.AreEqual(deploymentId, result); MockRepository.VerifyAll(); - } [Test] public async Task CreateDeploymentWithExistingComponentsAndExistingThingGroupAndThingTypeShouldCreateTheDeployment() { - //Act + // Arrange + var deploymentId = Fixture.Create(); _ = this.mockConfigHandler.Setup(handler => handler.AWSRegion).Returns("eu-west-1"); _ = this.mockConfigHandler.Setup(handler => handler.AWSAccountId).Returns("00000000"); - var edge = Fixture.Create(); + var edge = Fixture.Create(); + // Simulate a custom/private component + edge.EdgeModules.First().Id = string.Empty; + var edgeDeviceModelEntity = Mapper.Map(edge); _ = this.mockIotClient.Setup(s3 => s3.DescribeThingGroupAsync(It.IsAny(), It.IsAny())) @@ -139,7 +145,8 @@ public async Task CreateDeploymentWithExistingComponentsAndExistingThingGroupAnd _ = this.mockGreengrasClient.Setup(s3 => s3.CreateDeploymentAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new CreateDeploymentResponse { - HttpStatusCode = HttpStatusCode.Created + HttpStatusCode = HttpStatusCode.Created, + DeploymentId = deploymentId }); _ = this.mockGreengrasClient.Setup(s3 => s3.DescribeComponentAsync(It.IsAny(), It.IsAny())) @@ -148,29 +155,25 @@ public async Task CreateDeploymentWithExistingComponentsAndExistingThingGroupAnd HttpStatusCode = HttpStatusCode.OK }); - _ = this.mockEdgeModelRepository.Setup(x => x.GetByIdAsync(It.IsAny())) - .ReturnsAsync(edgeDeviceModelEntity); - - _ = this.mockEdgeModelRepository.Setup(repository => repository.Update(It.IsAny())); - _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) - .Returns(Task.CompletedTask); - - //Arrange - await this.awsConfigService.RollOutEdgeModelConfiguration(edge); + // Act + var result = await this.awsConfigService.RollOutEdgeModelConfiguration(edge); - //Assert + // Assert + Assert.AreEqual(deploymentId, result); MockRepository.VerifyAll(); - } [Test] public async Task CreateDeploymentWithNonExistingComponentsAndNonExistingThingGroupAndThingTypeShouldCreateThingGroupAndThingTypeAndTheDeployment() { - //Act + // Arrange _ = this.mockConfigHandler.Setup(handler => handler.AWSRegion).Returns("eu-west-1"); _ = this.mockConfigHandler.Setup(handler => handler.AWSAccountId).Returns("00000000"); - var edge = Fixture.Create(); + var edge = Fixture.Create(); + // Simulate a custom/private component + edge.EdgeModules.First().Id = string.Empty; + var edgeDeviceModelEntity = Mapper.Map(edge); _ = this.mockIotClient.Setup(s3 => s3.DescribeThingGroupAsync(It.IsAny(), It.IsAny())) @@ -199,19 +202,11 @@ public async Task CreateDeploymentWithNonExistingComponentsAndNonExistingThingGr HttpStatusCode = HttpStatusCode.Created }); - _ = this.mockEdgeModelRepository.Setup(x => x.GetByIdAsync(It.IsAny())) - .ReturnsAsync(edgeDeviceModelEntity); + // Act + _ = await this.awsConfigService.RollOutEdgeModelConfiguration(edge); - _ = this.mockEdgeModelRepository.Setup(repository => repository.Update(It.IsAny())); - _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) - .Returns(Task.CompletedTask); - - //Arrange - await this.awsConfigService.RollOutEdgeModelConfiguration(edge); - - //Assert + // Assert MockRepository.VerifyAll(); - } [Test] @@ -303,5 +298,35 @@ public async Task DeleteDeploymentShouldDeleteTheDeploymentVersionAndAllItsCompo MockRepository.VerifyAll(); } + + [Test] + public async Task GetPublicEdgeModules_AwsContext_EmptyListIsReturned() + { + //Arrange + var components = Fixture.CreateMany(2).ToList(); + + var expectedPublicEdgeModules = components.Select(c => new IoTEdgeModule + { + Id = c.Arn, + ModuleName = c.ComponentName, + Version = c.LatestVersion.ComponentVersion, + ImageURI = "example.com" + }).ToList(); + + _ = this.mockGreengrasClient.Setup(s3 => s3.ListComponentsAsync(It.Is(a => a.Scope == ComponentVisibilityScope.PUBLIC), It.IsAny())) + .ReturnsAsync(new ListComponentsResponse + { + Components = components, + NextToken = string.Empty, + }); + + // Act + var publicEdgeModules = await this.awsConfigService.GetPublicEdgeModules(); + + //Assert + _ = publicEdgeModules.Should().BeEquivalentTo(expectedPublicEdgeModules); + MockRepository.VerifyAll(); + + } } } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/EdgeModelsControllerTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/EdgeModelsControllerTest.cs index 78a82ea42..6690184d8 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/EdgeModelsControllerTest.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/EdgeModelsControllerTest.cs @@ -256,5 +256,46 @@ public async Task DeleteAvatarShouldDeleteAvatar() this.mockRepository.VerifyAll(); } + [Test] + public async Task GetPublicEdgeModules_GetPublicEdgeModules_EdgeModulesReturned() + { + // Arrange + var edgeModelController = CreateController(); + var publicEdgeModules = new List + { + new IoTEdgeModule{ + Id = Guid.NewGuid().ToString() + } + }; + + _ = this.mockEdgeModelService + .Setup(x => x.GetPublicEdgeModules()) + .ReturnsAsync(publicEdgeModules); + + // Act + var response = await edgeModelController.GetPublicEdgeModules(); + + // Assert + Assert.IsNotNull(response); + Assert.IsAssignableFrom(response.Result); + + if (response.Result is OkObjectResult okResponse) + { + Assert.AreEqual(200, okResponse.StatusCode); + + Assert.IsNotNull(okResponse.Value); + if (okResponse.Value is List result) + { + Assert.AreEqual(1, result.Count); + } + } + else + { + Assert.Fail("Cannot inspect the result."); + } + + this.mockRepository.VerifyAll(); + } + } } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/ConfigServiceTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/ConfigServiceTests.cs index d599d7477..8044c6535 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/ConfigServiceTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/ConfigServiceTests.cs @@ -186,7 +186,7 @@ public async Task RolloutDeviceModelConfigurationStateUnderTestExpectedBehavior( .ReturnsAsync((Configuration conf) => conf); // Act - await configsServices.RollOutDeviceModelConfiguration(modelId, desiredProperties); + _ = await configsServices.RollOutDeviceModelConfiguration(modelId, desiredProperties); // Assert Assert.IsNotNull(newConfiguration); @@ -231,7 +231,7 @@ public async Task WhenConfigurationExistsRolloutDeviceModelConfigurationShouldRe .Returns(Task.CompletedTask); // Act - await configsServices.RollOutDeviceModelConfiguration(deviceType, desirectProperties); + _ = await configsServices.RollOutDeviceModelConfiguration(deviceType, desirectProperties); // Assert this.mockRegistryManager.Verify(c => c.GetConfigurationsAsync(It.IsAny()), Times.Once()); @@ -301,7 +301,7 @@ public async Task RolloutDeviceConfigurationStateUnderTestExpectedBehavior() .ReturnsAsync((Configuration conf) => conf); // Act - await configsServices.RollOutDeviceConfiguration(modelId, desiredProperties, configurationName, targetTags); + _ = await configsServices.RollOutDeviceConfiguration(modelId, desiredProperties, configurationName, targetTags); // Assert Assert.IsNotNull(newConfiguration); @@ -438,7 +438,7 @@ public async Task WhenConfigurationExistsRolloutDeviceConfigurationShouldRemoveI .Returns(Task.CompletedTask); // Act - await configsServices.RollOutDeviceConfiguration(modelId, desiredProperties, configurationName, targetTags); + _ = await configsServices.RollOutDeviceConfiguration(modelId, desiredProperties, configurationName, targetTags); // Assert this.mockRegistryManager.Verify(c => c.GetConfigurationsAsync(It.IsAny()), Times.Once()); @@ -885,7 +885,7 @@ public async Task RollOutEdgeModelConfigurationShouldCreateNewConfiguration(stri .ReturnsAsync((Configuration conf) => conf); // Act - await configsServices.RollOutEdgeModelConfiguration(edgeModel); + _ = await configsServices.RollOutEdgeModelConfiguration(edgeModel); // Assert Assert.IsNotNull(newConfiguration); @@ -929,7 +929,7 @@ public async Task WhenConfigurationExistsRollOutEdgeModelConfigurationShouldRemo .Returns(Task.CompletedTask); // Act - await configsServices.RollOutEdgeModelConfiguration(edgeModel); + _ = await configsServices.RollOutEdgeModelConfiguration(edgeModel); // Assert this.mockRegistryManager.Verify(c => c.GetConfigurationsAsync(It.IsAny()), Times.Once()); @@ -989,5 +989,19 @@ public async Task WhenConfigurationExistsDeleteDeviceModelConfigurationByConfigu this.mockRegistryManager.Verify(c => c.GetConfigurationsAsync(It.IsAny()), Times.Once()); this.mockRegistryManager.Verify(c => c.RemoveConfigurationAsync(It.IsAny()), Times.Once()); } + + [Test] + public async Task GetPublicEdgeModules_AzureContext_EmptyListIsReturned() + { + // Arrange + var configsServices = CreateConfigsServices(); + + //Arrange + var publicEdgeModules = await configsServices.GetPublicEdgeModules(); + + //Assert + _ = publicEdgeModules.Should().BeEquivalentTo(Array.Empty()); + this.mockRepository.VerifyAll(); + } } } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/DeviceConfigurationsServiceTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/DeviceConfigurationsServiceTest.cs index 8313abed5..900a290c3 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/DeviceConfigurationsServiceTest.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/DeviceConfigurationsServiceTest.cs @@ -7,11 +7,13 @@ namespace AzureIoTHub.Portal.Tests.Unit.Server.Services using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using AutoFixture; using Azure; using AzureIoTHub.Portal.Application.Services; using AzureIoTHub.Portal.Domain.Entities; using AzureIoTHub.Portal.Domain.Exceptions; using AzureIoTHub.Portal.Server.Services; + using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases; using FluentAssertions; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Azure.Devices; @@ -22,7 +24,7 @@ namespace AzureIoTHub.Portal.Tests.Unit.Server.Services using Configuration = Microsoft.Azure.Devices.Configuration; [TestFixture] - public class DeviceConfigurationsServiceTest + public class DeviceConfigurationsServiceTest : BackendUnitTest { private MockRepository mockRepository; @@ -170,7 +172,7 @@ public async Task CreateConfigStateUnderTestExpectedBehavior() It.Is(x => x == deviceConfig.ConfigurationId), It.IsAny>(), It.Is(x => x == 100))) - .Returns(Task.CompletedTask); + .Returns(Task.FromResult(Fixture.Create())); _ = this.mockDeviceModelPropertiesService.Setup(c => c.GetModelProperties(deviceConfig.ModelId)) .ReturnsAsync(new[] @@ -230,7 +232,7 @@ public async Task UpdateConfigStateUnderTestExpectedBehavior() It.Is(x => x == deviceConfig.ConfigurationId), It.IsAny>(), It.Is(x => x == 100))) - .Returns(Task.CompletedTask); + .Returns(Task.FromResult(Fixture.Create())); _ = this.mockDeviceModelPropertiesService.Setup(c => c.GetModelProperties(deviceConfig.ModelId)) .ReturnsAsync(new[] @@ -363,7 +365,7 @@ public async Task UpdateConfigShouldUpdatePropertyInValueType(DevicePropertyType It.Is(x => x == deviceConfig.ConfigurationId), It.IsAny>(), It.Is(x => x == 100))) - .Returns(Task.CompletedTask) + .Returns(Task.FromResult(Fixture.Create())) .Callback((string _, Dictionary properties, string _, Dictionary _, int _) => requestedProperties = properties); @@ -422,7 +424,7 @@ public async Task CreateConfigShouldUpdatePropertyInValueType(DevicePropertyType It.Is(x => x == deviceConfig.ConfigurationId), It.IsAny>(), It.Is(x => x == 100))) - .Returns(Task.CompletedTask) + .Returns(Task.FromResult(Fixture.Create())) .Callback((string _, Dictionary properties, string _, Dictionary _, int _) => requestedProperties = properties); @@ -472,7 +474,7 @@ public async Task WhenPropertyNotPresentInModelUpdateConfigShouldNotUpdateThePro It.Is(x => x == deviceConfig.ConfigurationId), It.IsAny>(), It.Is(x => x == 100))) - .Returns(Task.CompletedTask) + .Returns(Task.FromResult(Fixture.Create())) .Callback((string _, Dictionary properties, string _, Dictionary _, int _) => requestedProperties = properties); @@ -523,7 +525,7 @@ public async Task WhenPropertyNotPresentInModelCreateConfigShouldNotUpdateThePro It.Is(x => x == deviceConfig.ConfigurationId), It.IsAny>(), It.Is(x => x == 100))) - .Returns(Task.CompletedTask) + .Returns(Task.FromResult(Fixture.Create())) .Callback((string _, Dictionary properties, string _, Dictionary _, int _) => requestedProperties = properties); diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/DeviceModelServiceTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/DeviceModelServiceTests.cs index 7bfc8a023..f9c91fcb8 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/DeviceModelServiceTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/DeviceModelServiceTests.cs @@ -188,7 +188,7 @@ public async Task CreateDeviceModelShouldCreateDeviceModel() _ = this.mockConfigService.Setup(service => service.RollOutDeviceModelConfiguration(deviceModelDto.ModelId, It.IsAny>())) - .Returns(Task.CompletedTask); + .Returns(Task.FromResult(Fixture.Create())); _ = this.mockDeviceModelImageManager.Setup(manager => manager.SetDefaultImageToModel(deviceModelDto.ModelId)) @@ -257,7 +257,7 @@ public async Task UpdateDeviceModelShouldUpdateDeviceModel() _ = this.mockConfigService.Setup(service => service.RollOutDeviceModelConfiguration(deviceModelDto.ModelId, It.IsAny>())) - .Returns(Task.CompletedTask); + .Returns(Task.FromResult(Fixture.Create())); // Act await this.deviceModelService.UpdateDeviceModel(deviceModelDto); diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs index a02deb186..1e6aeb8b8 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeModelServiceTest.cs @@ -1,676 +1,643 @@ -// 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.Tests.Unit.Server.Services -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Linq.Expressions; - using System.Threading.Tasks; - using AutoFixture; - using AutoMapper; - using AzureIoTHub.Portal.Application.Managers; - using AzureIoTHub.Portal.Application.Services; - using AzureIoTHub.Portal.Domain; - using AzureIoTHub.Portal.Domain.Entities; - using AzureIoTHub.Portal.Domain.Exceptions; - using AzureIoTHub.Portal.Domain.Repositories; - using AzureIoTHub.Portal.Models.v10; - using AzureIoTHub.Portal.Shared.Models.v10.Filters; - using AzureIoTHub.Portal.Shared.Models.v10; - using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases; - using FluentAssertions; - using Microsoft.AspNetCore.Http; - using Microsoft.Azure.Devices; - using Microsoft.EntityFrameworkCore; - using Microsoft.Extensions.DependencyInjection; - using Moq; - using NUnit.Framework; - using AzureIoTHub.Portal.Infrastructure.Services; - - [TestFixture] - public class EdgeModelServiceTest : BackendUnitTest - { - private Mock mockUnitOfWork; - private Mock mockEdgeDeviceModelRepository; - private Mock mockEdgeDeviceModelCommandRepository; - private Mock mockConfigService; - private Mock mockDeviceModelImageManager; - private Mock mockLabelRepository; - private Mock mockConfigHandler; - - private IEdgeModelService edgeDeviceModelService; - - [SetUp] - public void SetUp() - { - base.Setup(); - - this.mockUnitOfWork = MockRepository.Create(); - this.mockEdgeDeviceModelRepository = MockRepository.Create(); - - this.mockEdgeDeviceModelCommandRepository = MockRepository.Create(); - this.mockConfigService = MockRepository.Create(); - this.mockDeviceModelImageManager = MockRepository.Create(); - this.mockLabelRepository = MockRepository.Create(); - this.mockConfigHandler = MockRepository.Create(); - - _ = ServiceCollection.AddSingleton(this.mockUnitOfWork.Object); - _ = ServiceCollection.AddSingleton(this.mockEdgeDeviceModelRepository.Object); - _ = ServiceCollection.AddSingleton(this.mockEdgeDeviceModelCommandRepository.Object); - _ = ServiceCollection.AddSingleton(this.mockConfigService.Object); - _ = ServiceCollection.AddSingleton(this.mockDeviceModelImageManager.Object); - _ = ServiceCollection.AddSingleton(this.mockLabelRepository.Object); - _ = ServiceCollection.AddSingleton(); - _ = ServiceCollection.AddSingleton(this.mockConfigHandler.Object); - - Services = ServiceCollection.BuildServiceProvider(); - - this.edgeDeviceModelService = Services.GetRequiredService(); - Mapper = Services.GetRequiredService(); - - } - - [Test] - public async Task GetEdgeModelsShouldReturnAList() - { - // Arrange - var expectedEdgeDeviceModels = Fixture.CreateMany(3).ToList(); - var expectedIoTEdgeDeviceModelListItems = expectedEdgeDeviceModels.Select(model => Mapper.Map(model)).ToList(); - var expectedImageUri = Fixture.Create(); - - foreach (var item in expectedIoTEdgeDeviceModelListItems) - { - item.ImageUrl = expectedImageUri; - } - - var edgeModelFilter = new EdgeModelFilter - { - Keyword = Guid.NewGuid().ToString() - }; - - _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.GetAllAsync(It.IsAny>>(), default, new Expression>[] { d => d.Labels })) - .ReturnsAsync(expectedEdgeDeviceModels); - - _ = this.mockDeviceModelImageManager.Setup(c => c.ComputeImageUri(It.IsAny())) - .Returns(expectedImageUri); - - // Act - var result = await this.edgeDeviceModelService.GetEdgeModels(edgeModelFilter); - - - // Assert - _ = result.Should().BeEquivalentTo(expectedIoTEdgeDeviceModelListItems); - MockRepository.VerifyAll(); - } - - [Test] - public async Task GetEdgeModelShouldReturnValueAsync() - { - // Arrange - _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("Azure"); - - var expectedModules = Fixture.CreateMany(2).ToList(); - var expectedRoutes = Fixture.CreateMany(2).ToList(); - var expectedSysModule = Fixture.CreateMany(2).ToList(); - var expectedImageUri = Fixture.Create(); - - var expectedEdgeDeviceModel = new IoTEdgeModel() - { - ModelId = Guid.NewGuid().ToString(), - Name = Guid.NewGuid().ToString(), - ImageUrl = expectedImageUri, - Description = Guid.NewGuid().ToString(), - EdgeModules = expectedModules, - EdgeRoutes = expectedRoutes, - SystemModules = expectedSysModule - }; - - var expectedCommands = Fixture.CreateMany(5).Select(command => - { - command.EdgeDeviceModelId = expectedEdgeDeviceModel.ModelId; - command.ModuleName = expectedModules[0].ModuleName; - return command; - }) .ToList(); - - expectedCommands.Add(new EdgeDeviceModelCommand - { - EdgeDeviceModelId = expectedEdgeDeviceModel.ModelId, - Id = Guid.NewGuid().ToString(), - Name = Guid.NewGuid().ToString(), - ModuleName = Guid.NewGuid().ToString() - }); - - var expectedEdgeDeviceModelEntity = Mapper.Map(expectedEdgeDeviceModel); - - _ = this.mockDeviceModelImageManager.Setup(c => c.ComputeImageUri(It.IsAny())) - .Returns(expectedImageUri); - - _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny(), d => d.Labels)) - .ReturnsAsync(expectedEdgeDeviceModelEntity); - - _ = this.mockConfigService.Setup(x => x.GetConfigModuleList(It.IsAny())) - .ReturnsAsync(expectedModules); - - _ = this.mockConfigService.Setup(x => x.GetModelSystemModule(It.IsAny())) - .ReturnsAsync(expectedSysModule); - - _ = this.mockConfigService.Setup(x => x.GetConfigRouteList(It.IsAny())) - .ReturnsAsync(expectedRoutes); - - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.GetAll()) - .Returns(expectedCommands); - - // Act - var result = await this.edgeDeviceModelService.GetEdgeModel(expectedEdgeDeviceModel.ModelId); - - // Assert - Assert.IsNotNull(result); - _ = result.Should().BeEquivalentTo(expectedEdgeDeviceModel); - } - - [Test] - public async Task GetEdgeModelForAwsShouldReturnValueAsync() - { - // Arrange - _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("AWS"); - - var expectedModules = Fixture.CreateMany(2).ToList(); - - var expectedImageUri = Fixture.Create(); - - var expectedEdgeDeviceModel = new IoTEdgeModel() - { - ModelId = Guid.NewGuid().ToString(), - Name = Guid.NewGuid().ToString(), - ImageUrl = expectedImageUri, - Description = Guid.NewGuid().ToString(), - EdgeModules = expectedModules - }; - - var expectedCommands = Fixture.CreateMany(5).Select(command => - { - command.EdgeDeviceModelId = expectedEdgeDeviceModel.ModelId; - command.ModuleName = expectedModules[0].ModuleName; - return command; - }) .ToList(); - - expectedCommands.Add(new EdgeDeviceModelCommand - { - EdgeDeviceModelId = expectedEdgeDeviceModel.ModelId, - Id = Guid.NewGuid().ToString(), - Name = Guid.NewGuid().ToString(), - ModuleName = Guid.NewGuid().ToString() - }); - - var expectedEdgeDeviceModelEntity = Mapper.Map(expectedEdgeDeviceModel); - - _ = this.mockDeviceModelImageManager.Setup(c => c.ComputeImageUri(It.IsAny())) - .Returns(expectedImageUri); - - _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny(), d => d.Labels)) - .ReturnsAsync(expectedEdgeDeviceModelEntity); - - _ = this.mockConfigService.Setup(x => x.GetConfigModuleList(It.IsAny())) - .ReturnsAsync(expectedModules); - - // Act - var result = await this.edgeDeviceModelService.GetEdgeModel(expectedEdgeDeviceModel.ModelId); - - // Assert - Assert.IsNotNull(result); - _ = result.Should().BeEquivalentTo(expectedEdgeDeviceModel); - } - - [Test] - public void GetEdgeModelShouldThrowResourceNotFoundExceptionIfModelDoesNotExist() - { - - // Arrange - _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny(), m => m.Labels)) - .ReturnsAsync(value: null); - - // Act - var result = async () => await this.edgeDeviceModelService.GetEdgeModel("test"); - - // Assert - _ = result.Should().ThrowAsync(); - } - - [Test] - public async Task CreateEdgeModelForAzureShouldCreateEdgeModel() - { - // Arrange - - var edgeDeviceModel = Fixture.Create(); - var expectedImageUri = Fixture.Create(); - - _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny())) - .ReturnsAsync((EdgeDeviceModel)null); - _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.InsertAsync(It.IsAny())) - .Returns(Task.CompletedTask); - _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) - .Returns(Task.CompletedTask); - _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("Azure"); - - var expectedCommands = Fixture.CreateMany(5).Select(command => - { - command.EdgeDeviceModelId = edgeDeviceModel.ModelId; - return command; - }) .ToList(); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.GetAll()) - .Returns(expectedCommands); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.Delete(It.IsAny())); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.InsertAsync(It.IsAny())) - .Returns(Task.CompletedTask); - - _ = this.mockConfigService.Setup(x => x.RollOutEdgeModelConfiguration(It.IsAny())) - .Returns(Task.CompletedTask); - - _ = this.mockDeviceModelImageManager.Setup(manager => - manager.SetDefaultImageToModel(It.IsAny())) - .ReturnsAsync(expectedImageUri.ToString()); - - // Act - await this.edgeDeviceModelService.CreateEdgeModel(edgeDeviceModel); - - // Assert - MockRepository.VerifyAll(); - } - - [Test] - public async Task CreateEdgeModelForAwsShouldCreateEdgeModel() - { - // Arrange - - var edgeDeviceModel = Fixture.Create(); - var expectedImageUri = Fixture.Create(); - - _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny())) - .ReturnsAsync((EdgeDeviceModel)null); - _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.InsertAsync(It.IsAny())) - .Returns(Task.CompletedTask); - _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) - .Returns(Task.CompletedTask); - _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("AWS"); - - - _ = this.mockConfigService.Setup(x => x.RollOutEdgeModelConfiguration(It.IsAny())) - .Returns(Task.CompletedTask); - - _ = this.mockDeviceModelImageManager.Setup(manager => - manager.SetDefaultImageToModel(It.IsAny())) - .ReturnsAsync(expectedImageUri.ToString()); - - // Act - await this.edgeDeviceModelService.CreateEdgeModel(edgeDeviceModel); - - // Assert - MockRepository.VerifyAll(); - } - - [Test] - public void CreateEdgeModelShouldThrowResourceAlreadyExistsExceptionIfModelAlreadyExists() - { - // Arrange - var edgeDeviceModel = Fixture.Create(); - var edgeDeviceModelEntity = Mapper.Map(edgeDeviceModel); - - _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny())) - .ReturnsAsync(edgeDeviceModelEntity); - - // Act - var result = async () => await this.edgeDeviceModelService.CreateEdgeModel(edgeDeviceModel); - - // Assert - _ = result.Should().ThrowAsync(); - } - - [Test] - public void CreateEdgeModelShouldThrowInternalServerErrorExceptionIfDbUpdateExceptionOccurs() - { - // Arrange - var edgeDeviceModel = Fixture.Create(); - - _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny())) - .ReturnsAsync((EdgeDeviceModel)null); - _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.InsertAsync(It.IsAny())) - .Returns(Task.CompletedTask); - _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) - .Throws(new DbUpdateException()); - - // Act - var result = async () => await this.edgeDeviceModelService.CreateEdgeModel(edgeDeviceModel); - - // Assert - _ = result.Should().ThrowAsync(); - } - - [Test] - public async Task UpdateEdgeModelShouldUpdateEdgeModel() - { - // Arrange - _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("Azure"); - - var edgeDeviceModel = Fixture.Create(); - var edgeDeviceModelEntity = Mapper.Map(edgeDeviceModel); - - _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny(), d => d.Labels)) - .ReturnsAsync(edgeDeviceModelEntity); - - this.mockLabelRepository.Setup(repository => repository.Delete(It.IsAny())) - .Verifiable(); - - _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.Update(It.IsAny())); - _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) - .Returns(Task.CompletedTask); - - var expectedCommands = Fixture.CreateMany(5).Select(command => - { - command.EdgeDeviceModelId = edgeDeviceModel.ModelId; - return command; - }) .ToList(); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.GetAll()) - .Returns(expectedCommands); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.Delete(It.IsAny())); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.InsertAsync(It.IsAny())) - .Returns(Task.CompletedTask); - - _ = this.mockConfigService.Setup(x => x.RollOutEdgeModelConfiguration(It.IsAny())) - .Returns(Task.CompletedTask); - - // Act - await this.edgeDeviceModelService.UpdateEdgeModel(edgeDeviceModel); - - // Assert - MockRepository.VerifyAll(); - } - - - [Test] - public async Task UpdateEdgeModelForAWSShouldUpdateEdgeModel() - { - // Arrange - _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("AWS"); - - var edgeDeviceModel = Fixture.Create(); - var edgeDeviceModelEntity = Mapper.Map(edgeDeviceModel); - - _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny(), d => d.Labels)) - .ReturnsAsync(edgeDeviceModelEntity); - - this.mockLabelRepository.Setup(repository => repository.Delete(It.IsAny())) - .Verifiable(); - - _ = this.mockConfigService.Setup(x => x.RollOutEdgeModelConfiguration(It.IsAny())) - .Returns(Task.CompletedTask); - - // Act - await this.edgeDeviceModelService.UpdateEdgeModel(edgeDeviceModel); - - // Assert - MockRepository.VerifyAll(); - } - - [Test] - public void UpdateEdgeModelShouldThrowResourceNotFoundExceptionIfModelDoesNotExist() - { - // Arrange - var edgeDeviceModel = Fixture.Create(); - - _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny(), m => m.Labels)) - .ReturnsAsync(value: null); - - // Act - var result = async () => await this.edgeDeviceModelService.UpdateEdgeModel(edgeDeviceModel); - - // Assert - _ = result.Should().ThrowAsync(); - } - - [Test] - public void UpdateEdgeModelShouldThrowInternalServerErrorExceptionIfDbUpdateExceptionOccurs() - { - // Arrange - var edgeDeviceModel = Fixture.Create(); - var edgeDeviceModelEntity = Mapper.Map(edgeDeviceModel); - - _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny())) - .ReturnsAsync(edgeDeviceModelEntity); - _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.Update(It.IsAny())); - _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) - .Throws(new DbUpdateException()); - - // Act - var result = async () => await this.edgeDeviceModelService.UpdateEdgeModel(edgeDeviceModel); - - // Assert - _ = result.Should().ThrowAsync(); - } - - [Test] - public async Task DeleteEdgeModelShouldDeleteEdgeModel() - { - // Arrange - _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("Azure"); - - var edgeDeviceModel = Fixture.Create(); - var edgeDeviceModelEntity = Mapper.Map(edgeDeviceModel); - - _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.GetByIdAsync(edgeDeviceModelEntity.Id, d => d.Labels)) - .ReturnsAsync(edgeDeviceModelEntity); - - this.mockLabelRepository.Setup(repository => repository.Delete(It.IsAny())) - .Verifiable(); - - _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.Delete(It.IsAny())); - _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) - .Returns(Task.CompletedTask); - - var expectedCommands = Fixture.CreateMany(5).Select(command => - { - command.EdgeDeviceModelId = edgeDeviceModel.ModelId; - return command; - }) .ToList(); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.GetAll()) - .Returns(expectedCommands); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.Delete(It.IsAny())); - - var configurations = new List() - { - new Configuration(edgeDeviceModel.ModelId) - }; - _ = this.mockConfigService.Setup(x => x.GetIoTEdgeConfigurations()) - .ReturnsAsync(configurations); - _ = this.mockConfigService.Setup(x => x.DeleteConfiguration(It.IsAny())) - .Returns(Task.CompletedTask); - - // Act - await this.edgeDeviceModelService.DeleteEdgeModel(edgeDeviceModel.ModelId); - - // Assert - MockRepository.VerifyAll(); - } - - [Test] - public async Task DeleteEdgeModelForAwsShouldDeleteEdgeModel() - { - // Arrange - _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("AWS"); - - var edgeDeviceModel = Fixture.Create(); - var edgeDeviceModelEntity = Mapper.Map(edgeDeviceModel); - - _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.GetByIdAsync(edgeDeviceModelEntity.Id, d => d.Labels)) - .ReturnsAsync(edgeDeviceModelEntity); - - this.mockLabelRepository.Setup(repository => repository.Delete(It.IsAny())) - .Verifiable(); - - _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.Delete(It.IsAny())); - _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) - .Returns(Task.CompletedTask); - - _ = this.mockConfigService.Setup(x => x.DeleteConfiguration(It.IsAny())) - .Returns(Task.CompletedTask); - - // Act - await this.edgeDeviceModelService.DeleteEdgeModel(edgeDeviceModel.ModelId); - - // Assert - MockRepository.VerifyAll(); - } - - [Test] - public async Task DeleteEdgeModel_ModelDoesntExist_NothingIsDone() - { - // Arrange - var edgeModelId = Fixture.Create(); - - _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.GetByIdAsync(edgeModelId, d => d.Labels)) - .ReturnsAsync(value: null); - - // Act - await this.edgeDeviceModelService.DeleteEdgeModel(edgeModelId); - - // Assert - MockRepository.VerifyAll(); - } - - [Test] - public void DeleteEdgeModelShouldThrowInternalServerErrorExceptionIfDbUpdateExceptionOccurs() - { - // Arrange - var edgeDeviceModel = Fixture.Create(); - - _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.Delete(It.IsAny())); - _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) - .Throws(new DbUpdateException()); - - var expectedCommands = Fixture.CreateMany(5).Select(command => - { - command.EdgeDeviceModelId = edgeDeviceModel.ModelId; - return command; - }) .ToList(); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.GetAll()) - .Returns(expectedCommands); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.Delete(It.IsAny())); - - // Act - var result = async () => await this.edgeDeviceModelService.DeleteEdgeModel(edgeDeviceModel.ModelId); - - // Assert - _ = result.Should().ThrowAsync(); - } - - [Test] - public async Task GetEdgeDeviceModelAvatarShouldReturnEdgeDeviceModelAvatar() - { - // Arrange - var expectedImageUri = Fixture.Create(); - _ = this.mockDeviceModelImageManager.Setup(c => c.ComputeImageUri(It.IsAny())) - .Returns(expectedImageUri); - - // Act - var result = await this.edgeDeviceModelService.GetEdgeModelAvatar(Guid.NewGuid().ToString()); - - // Assert - _ = result.Should().Be(expectedImageUri.ToString()); - MockRepository.VerifyAll(); - } - - [Test] - public async Task UpdateEdgeDeviceModelAvatarShouldUpdateEdgeDeviceModelAvatar() - { - // Arrange - var expectedImageUri = Fixture.Create(); - - var mockFormFile = MockRepository.Create(); - - _ = this.mockDeviceModelImageManager.Setup(manager => - manager.ChangeDeviceModelImageAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(expectedImageUri.ToString()); - - _ = mockFormFile.Setup(file => file.OpenReadStream()) - .Returns(Stream.Null); - - // Act - var result = await this.edgeDeviceModelService.UpdateEdgeModelAvatar(Guid.NewGuid().ToString(), mockFormFile.Object); - - // Assert - _ = result.Should().Be(expectedImageUri.ToString()); - MockRepository.VerifyAll(); - } - - [Test] - public async Task DeleteEdgeDeviceModelAvatarShouldDeleteEdgeDeviceModelAvatar() - { - // Arrange - _ = this.mockDeviceModelImageManager - .Setup(manager => manager.DeleteDeviceModelImageAsync(It.IsAny())) - .Returns(Task.CompletedTask); - - // Act - await this.edgeDeviceModelService.DeleteEdgeModelAvatar(Guid.NewGuid().ToString()); - - // Assert - MockRepository.VerifyAll(); - } - - [Test] - public async Task SaveModuleCommandsShouldUpdateDatabase() - { - // Arrange - var edgeDeviceModel = Fixture.Create(); - - var expectedCommands = Fixture.CreateMany(5).Select(command => - { - command.EdgeDeviceModelId = edgeDeviceModel.ModelId; - return command; - }) .ToList(); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.GetAll()) - .Returns(expectedCommands); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.Delete(It.IsAny())); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.InsertAsync(It.IsAny())) - .Returns(Task.CompletedTask); - - _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) - .Returns(Task.CompletedTask); - - // Act - await this.edgeDeviceModelService.SaveModuleCommands(edgeDeviceModel); - - // Assert - MockRepository.VerifyAll(); - } - - [Test] - public void SaveModuleCommandsShouldThrowInternalServerErrorExceptionIfDbUpdateExceptionOccurs() - { - // Arrange - var edgeDeviceModel = Fixture.Create(); - - var expectedCommands = Fixture.CreateMany(5).Select(command => - { - command.EdgeDeviceModelId = edgeDeviceModel.ModelId; - return command; - }) .ToList(); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.GetAll()) - .Returns(expectedCommands); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.Delete(It.IsAny())); - _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.InsertAsync(It.IsAny())) - .Returns(Task.CompletedTask); - - _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) - .Throws(new DbUpdateException()); - - // Act - var result = async () => await this.edgeDeviceModelService.SaveModuleCommands(edgeDeviceModel); - - // Assert - _ = result.Should().ThrowAsync(); - } - } -} +// 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.Tests.Unit.Server.Services +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Linq.Expressions; + using System.Threading.Tasks; + using AutoFixture; + using AutoMapper; + using AzureIoTHub.Portal.Application.Managers; + using AzureIoTHub.Portal.Application.Services; + using AzureIoTHub.Portal.Domain; + using AzureIoTHub.Portal.Domain.Entities; + using AzureIoTHub.Portal.Domain.Exceptions; + using AzureIoTHub.Portal.Domain.Repositories; + using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Shared.Models.v10.Filters; + using AzureIoTHub.Portal.Shared.Models.v10; + using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases; + using FluentAssertions; + using Microsoft.AspNetCore.Http; + using Microsoft.Azure.Devices; + using Microsoft.EntityFrameworkCore; + using Microsoft.Extensions.DependencyInjection; + using Moq; + using NUnit.Framework; + using AzureIoTHub.Portal.Infrastructure.Services; + + [TestFixture] + public class EdgeModelServiceTest : BackendUnitTest + { + private Mock mockUnitOfWork; + private Mock mockEdgeDeviceModelRepository; + private Mock mockEdgeDeviceModelCommandRepository; + private Mock mockConfigService; + private Mock mockDeviceModelImageManager; + private Mock mockLabelRepository; + private Mock mockConfigHandler; + + private IEdgeModelService edgeDeviceModelService; + + [SetUp] + public void SetUp() + { + base.Setup(); + + this.mockUnitOfWork = MockRepository.Create(); + this.mockEdgeDeviceModelRepository = MockRepository.Create(); + + this.mockEdgeDeviceModelCommandRepository = MockRepository.Create(); + this.mockConfigService = MockRepository.Create(); + this.mockDeviceModelImageManager = MockRepository.Create(); + this.mockLabelRepository = MockRepository.Create(); + this.mockConfigHandler = MockRepository.Create(); + + _ = ServiceCollection.AddSingleton(this.mockUnitOfWork.Object); + _ = ServiceCollection.AddSingleton(this.mockEdgeDeviceModelRepository.Object); + _ = ServiceCollection.AddSingleton(this.mockEdgeDeviceModelCommandRepository.Object); + _ = ServiceCollection.AddSingleton(this.mockConfigService.Object); + _ = ServiceCollection.AddSingleton(this.mockDeviceModelImageManager.Object); + _ = ServiceCollection.AddSingleton(this.mockLabelRepository.Object); + _ = ServiceCollection.AddSingleton(); + _ = ServiceCollection.AddSingleton(this.mockConfigHandler.Object); + + Services = ServiceCollection.BuildServiceProvider(); + + this.edgeDeviceModelService = Services.GetRequiredService(); + Mapper = Services.GetRequiredService(); + + } + + [Test] + public async Task GetEdgeModelsShouldReturnAList() + { + // Arrange + var expectedEdgeDeviceModels = Fixture.CreateMany(3).ToList(); + var expectedIoTEdgeDeviceModelListItems = expectedEdgeDeviceModels.Select(model => Mapper.Map(model)).ToList(); + var expectedImageUri = Fixture.Create(); + + foreach (var item in expectedIoTEdgeDeviceModelListItems) + { + item.ImageUrl = expectedImageUri; + } + + var edgeModelFilter = new EdgeModelFilter + { + Keyword = Guid.NewGuid().ToString() + }; + + _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.GetAllAsync(It.IsAny>>(), default, new Expression>[] { d => d.Labels })) + .ReturnsAsync(expectedEdgeDeviceModels); + + _ = this.mockDeviceModelImageManager.Setup(c => c.ComputeImageUri(It.IsAny())) + .Returns(expectedImageUri); + + // Act + var result = await this.edgeDeviceModelService.GetEdgeModels(edgeModelFilter); + + + // Assert + _ = result.Should().BeEquivalentTo(expectedIoTEdgeDeviceModelListItems); + MockRepository.VerifyAll(); + } + + [Test] + public async Task GetEdgeModelShouldReturnValueAsync() + { + // Arrange + _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("Azure"); + + var expectedModules = Fixture.CreateMany(2).ToList(); + var expectedRoutes = Fixture.CreateMany(2).ToList(); + var expectedSysModule = Fixture.CreateMany(2).ToList(); + var expectedImageUri = Fixture.Create(); + + var expectedEdgeDeviceModel = new IoTEdgeModel() + { + ModelId = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString(), + ImageUrl = expectedImageUri, + Description = Guid.NewGuid().ToString(), + EdgeModules = expectedModules, + EdgeRoutes = expectedRoutes, + SystemModules = expectedSysModule + }; + + var expectedCommands = Fixture.CreateMany(5).Select(command => + { + command.EdgeDeviceModelId = expectedEdgeDeviceModel.ModelId; + command.ModuleName = expectedModules[0].ModuleName; + return command; + }) .ToList(); + + expectedCommands.Add(new EdgeDeviceModelCommand + { + EdgeDeviceModelId = expectedEdgeDeviceModel.ModelId, + Id = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString(), + ModuleName = Guid.NewGuid().ToString() + }); + + var expectedEdgeDeviceModelEntity = Mapper.Map(expectedEdgeDeviceModel); + + _ = this.mockDeviceModelImageManager.Setup(c => c.ComputeImageUri(It.IsAny())) + .Returns(expectedImageUri); + + _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny(), d => d.Labels)) + .ReturnsAsync(expectedEdgeDeviceModelEntity); + + _ = this.mockConfigService.Setup(x => x.GetConfigModuleList(It.IsAny())) + .ReturnsAsync(expectedModules); + + _ = this.mockConfigService.Setup(x => x.GetModelSystemModule(It.IsAny())) + .ReturnsAsync(expectedSysModule); + + _ = this.mockConfigService.Setup(x => x.GetConfigRouteList(It.IsAny())) + .ReturnsAsync(expectedRoutes); + + _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.GetAll()) + .Returns(expectedCommands); + + // Act + var result = await this.edgeDeviceModelService.GetEdgeModel(expectedEdgeDeviceModel.ModelId); + + // Assert + Assert.IsNotNull(result); + _ = result.Should().BeEquivalentTo(expectedEdgeDeviceModel); + } + + [Test] + public async Task GetEdgeModelForAwsShouldReturnValueAsync() + { + // Arrange + _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("AWS"); + + var expectedModules = Fixture.CreateMany(2).ToList(); + + var expectedImageUri = Fixture.Create(); + + var expectedEdgeDeviceModel = new IoTEdgeModel() + { + ModelId = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString(), + ImageUrl = expectedImageUri, + Description = Guid.NewGuid().ToString(), + EdgeModules = expectedModules + }; + + var expectedCommands = Fixture.CreateMany(5).Select(command => + { + command.EdgeDeviceModelId = expectedEdgeDeviceModel.ModelId; + command.ModuleName = expectedModules[0].ModuleName; + return command; + }) .ToList(); + + expectedCommands.Add(new EdgeDeviceModelCommand + { + EdgeDeviceModelId = expectedEdgeDeviceModel.ModelId, + Id = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString(), + ModuleName = Guid.NewGuid().ToString() + }); + + var expectedEdgeDeviceModelEntity = Mapper.Map(expectedEdgeDeviceModel); + + _ = this.mockDeviceModelImageManager.Setup(c => c.ComputeImageUri(It.IsAny())) + .Returns(expectedImageUri); + + _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny(), d => d.Labels)) + .ReturnsAsync(expectedEdgeDeviceModelEntity); + + _ = this.mockConfigService.Setup(x => x.GetConfigModuleList(It.IsAny())) + .ReturnsAsync(expectedModules); + + // Act + var result = await this.edgeDeviceModelService.GetEdgeModel(expectedEdgeDeviceModel.ModelId); + + // Assert + Assert.IsNotNull(result); + _ = result.Should().BeEquivalentTo(expectedEdgeDeviceModel); + } + + [Test] + public void GetEdgeModelShouldThrowResourceNotFoundExceptionIfModelDoesNotExist() + { + + // Arrange + _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny(), m => m.Labels)) + .ReturnsAsync(value: null); + + // Act + var result = async () => await this.edgeDeviceModelService.GetEdgeModel("test"); + + // Assert + _ = result.Should().ThrowAsync(); + } + + [Test] + public async Task CreateEdgeModelForAzureShouldCreateEdgeModel() + { + // Arrange + + var edgeDeviceModel = Fixture.Create(); + var expectedImageUri = Fixture.Create(); + + _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny())) + .ReturnsAsync((EdgeDeviceModel)null); + _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.InsertAsync(It.IsAny())) + .Returns(Task.CompletedTask); + _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) + .Returns(Task.CompletedTask); + _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("Azure"); + + var expectedCommands = Fixture.CreateMany(5).Select(command => + { + command.EdgeDeviceModelId = edgeDeviceModel.ModelId; + return command; + }) .ToList(); + _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.GetAll()) + .Returns(expectedCommands); + _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.Delete(It.IsAny())); + _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.InsertAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + _ = this.mockConfigService.Setup(x => x.RollOutEdgeModelConfiguration(It.IsAny())) + .Returns(Task.FromResult(Fixture.Create())); + + _ = this.mockDeviceModelImageManager.Setup(manager => + manager.SetDefaultImageToModel(It.IsAny())) + .ReturnsAsync(expectedImageUri.ToString()); + + // Act + await this.edgeDeviceModelService.CreateEdgeModel(edgeDeviceModel); + + // Assert + MockRepository.VerifyAll(); + } + + [Test] + public async Task CreateEdgeModelForAwsShouldCreateEdgeModel() + { + // Arrange + + var edgeDeviceModel = Fixture.Create(); + var expectedImageUri = Fixture.Create(); + + _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny())) + .ReturnsAsync((EdgeDeviceModel)null); + _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.InsertAsync(It.IsAny())) + .Returns(Task.CompletedTask); + _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) + .Returns(Task.CompletedTask); + _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("AWS"); + + + _ = this.mockConfigService.Setup(x => x.RollOutEdgeModelConfiguration(It.IsAny())) + .Returns(Task.FromResult(Fixture.Create())); + + _ = this.mockDeviceModelImageManager.Setup(manager => + manager.SetDefaultImageToModel(It.IsAny())) + .ReturnsAsync(expectedImageUri.ToString()); + + // Act + await this.edgeDeviceModelService.CreateEdgeModel(edgeDeviceModel); + + // Assert + MockRepository.VerifyAll(); + } + + [Test] + public void CreateEdgeModelShouldThrowResourceAlreadyExistsExceptionIfModelAlreadyExists() + { + // Arrange + var edgeDeviceModel = Fixture.Create(); + var edgeDeviceModelEntity = Mapper.Map(edgeDeviceModel); + + _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny())) + .ReturnsAsync(edgeDeviceModelEntity); + + // Act + var result = async () => await this.edgeDeviceModelService.CreateEdgeModel(edgeDeviceModel); + + // Assert + _ = result.Should().ThrowAsync(); + } + + [Test] + public void CreateEdgeModelShouldThrowInternalServerErrorExceptionIfDbUpdateExceptionOccurs() + { + // Arrange + var edgeDeviceModel = Fixture.Create(); + + _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny())) + .ReturnsAsync(value: null); + _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.InsertAsync(It.IsAny())) + .Returns(Task.CompletedTask); + _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) + .Throws(new DbUpdateException()); + + // Act + var result = async () => await this.edgeDeviceModelService.CreateEdgeModel(edgeDeviceModel); + + // Assert + _ = result.Should().ThrowAsync(); + } + + [Test] + public async Task UpdateEdgeModelShouldUpdateEdgeModel() + { + // Arrange + _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("Azure"); + + var edgeDeviceModel = Fixture.Create(); + var edgeDeviceModelEntity = Mapper.Map(edgeDeviceModel); + + _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny(), d => d.Labels)) + .ReturnsAsync(edgeDeviceModelEntity); + + this.mockLabelRepository.Setup(repository => repository.Delete(It.IsAny())) + .Verifiable(); + + _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.Update(It.IsAny())); + _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) + .Returns(Task.CompletedTask); + + var expectedCommands = Fixture.CreateMany(5).Select(command => + { + command.EdgeDeviceModelId = edgeDeviceModel.ModelId; + return command; + }) .ToList(); + _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.GetAll()) + .Returns(expectedCommands); + _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.Delete(It.IsAny())); + _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.InsertAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + _ = this.mockConfigService.Setup(x => x.RollOutEdgeModelConfiguration(It.IsAny())) + .Returns(Task.FromResult(Fixture.Create())); + + // Act + await this.edgeDeviceModelService.UpdateEdgeModel(edgeDeviceModel); + + // Assert + MockRepository.VerifyAll(); + } + + + [Test] + public async Task UpdateEdgeModelForAWSShouldUpdateEdgeModel() + { + // Arrange + _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("AWS"); + + var edgeDeviceModel = Fixture.Create(); + var edgeDeviceModelEntity = Mapper.Map(edgeDeviceModel); + + _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny(), d => d.Labels)) + .ReturnsAsync(edgeDeviceModelEntity); + + this.mockLabelRepository.Setup(repository => repository.Delete(It.IsAny())) + .Verifiable(); + + _ = this.mockConfigService.Setup(x => x.RollOutEdgeModelConfiguration(It.IsAny())) + .Returns(Task.FromResult(Fixture.Create())); + + _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) + .Returns(Task.CompletedTask); + + // Act + await this.edgeDeviceModelService.UpdateEdgeModel(edgeDeviceModel); + + // Assert + MockRepository.VerifyAll(); + } + + [Test] + public void UpdateEdgeModelShouldThrowResourceNotFoundExceptionIfModelDoesNotExist() + { + // Arrange + var edgeDeviceModel = Fixture.Create(); + + _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny(), m => m.Labels)) + .ReturnsAsync(value: null); + + // Act + var result = async () => await this.edgeDeviceModelService.UpdateEdgeModel(edgeDeviceModel); + + // Assert + _ = result.Should().ThrowAsync(); + } + + [Test] + public void UpdateEdgeModelShouldThrowInternalServerErrorExceptionIfDbUpdateExceptionOccurs() + { + // Arrange + var edgeDeviceModel = Fixture.Create(); + var edgeDeviceModelEntity = Mapper.Map(edgeDeviceModel); + + _ = this.mockEdgeDeviceModelRepository.Setup(x => x.GetByIdAsync(It.IsAny())) + .ReturnsAsync(edgeDeviceModelEntity); + _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.Update(It.IsAny())); + _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) + .Throws(new DbUpdateException()); + + // Act + var result = async () => await this.edgeDeviceModelService.UpdateEdgeModel(edgeDeviceModel); + + // Assert + _ = result.Should().ThrowAsync(); + } + + [Test] + public async Task DeleteEdgeModelShouldDeleteEdgeModel() + { + // Arrange + _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("Azure"); + + var edgeDeviceModel = Fixture.Create(); + var edgeDeviceModelEntity = Mapper.Map(edgeDeviceModel); + + _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.GetByIdAsync(edgeDeviceModelEntity.Id, d => d.Labels)) + .ReturnsAsync(edgeDeviceModelEntity); + + this.mockLabelRepository.Setup(repository => repository.Delete(It.IsAny())) + .Verifiable(); + + _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.Delete(It.IsAny())); + _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) + .Returns(Task.CompletedTask); + + var expectedCommands = Fixture.CreateMany(5).Select(command => + { + command.EdgeDeviceModelId = edgeDeviceModel.ModelId; + return command; + }) .ToList(); + _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.GetAll()) + .Returns(expectedCommands); + _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.Delete(It.IsAny())); + + var configurations = new List() + { + new Configuration(edgeDeviceModel.ModelId) + }; + _ = this.mockConfigService.Setup(x => x.GetIoTEdgeConfigurations()) + .ReturnsAsync(configurations); + _ = this.mockConfigService.Setup(x => x.DeleteConfiguration(It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + await this.edgeDeviceModelService.DeleteEdgeModel(edgeDeviceModel.ModelId); + + // Assert + MockRepository.VerifyAll(); + } + + [Test] + public async Task DeleteEdgeModelForAwsShouldDeleteEdgeModel() + { + // Arrange + _ = this.mockConfigHandler.Setup(handler => handler.CloudProvider).Returns("AWS"); + + var edgeDeviceModel = Fixture.Create(); + var edgeDeviceModelEntity = Mapper.Map(edgeDeviceModel); + + _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.GetByIdAsync(edgeDeviceModelEntity.Id, d => d.Labels)) + .ReturnsAsync(edgeDeviceModelEntity); + + this.mockLabelRepository.Setup(repository => repository.Delete(It.IsAny())) + .Verifiable(); + + _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.Delete(It.IsAny())); + _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) + .Returns(Task.CompletedTask); + + _ = this.mockConfigService.Setup(x => x.DeleteConfiguration(It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + await this.edgeDeviceModelService.DeleteEdgeModel(edgeDeviceModel.ModelId); + + // Assert + MockRepository.VerifyAll(); + } + + [Test] + public async Task DeleteEdgeModel_ModelDoesntExist_NothingIsDone() + { + // Arrange + var edgeModelId = Fixture.Create(); + + _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.GetByIdAsync(edgeModelId, d => d.Labels)) + .ReturnsAsync(value: null); + + // Act + await this.edgeDeviceModelService.DeleteEdgeModel(edgeModelId); + + // Assert + MockRepository.VerifyAll(); + } + + [Test] + public void DeleteEdgeModelShouldThrowInternalServerErrorExceptionIfDbUpdateExceptionOccurs() + { + // Arrange + var edgeDeviceModel = Fixture.Create(); + + _ = this.mockEdgeDeviceModelRepository.Setup(repository => repository.Delete(It.IsAny())); + _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) + .Throws(new DbUpdateException()); + + var expectedCommands = Fixture.CreateMany(5).Select(command => + { + command.EdgeDeviceModelId = edgeDeviceModel.ModelId; + return command; + }) .ToList(); + _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.GetAll()) + .Returns(expectedCommands); + _ = this.mockEdgeDeviceModelCommandRepository.Setup(x => x.Delete(It.IsAny())); + + // Act + var result = async () => await this.edgeDeviceModelService.DeleteEdgeModel(edgeDeviceModel.ModelId); + + // Assert + _ = result.Should().ThrowAsync(); + } + + [Test] + public async Task GetEdgeDeviceModelAvatarShouldReturnEdgeDeviceModelAvatar() + { + // Arrange + var expectedImageUri = Fixture.Create(); + _ = this.mockDeviceModelImageManager.Setup(c => c.ComputeImageUri(It.IsAny())) + .Returns(expectedImageUri); + + // Act + var result = await this.edgeDeviceModelService.GetEdgeModelAvatar(Guid.NewGuid().ToString()); + + // Assert + _ = result.Should().Be(expectedImageUri.ToString()); + MockRepository.VerifyAll(); + } + + [Test] + public async Task UpdateEdgeDeviceModelAvatarShouldUpdateEdgeDeviceModelAvatar() + { + // Arrange + var expectedImageUri = Fixture.Create(); + + var mockFormFile = MockRepository.Create(); + + _ = this.mockDeviceModelImageManager.Setup(manager => + manager.ChangeDeviceModelImageAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedImageUri.ToString()); + + _ = mockFormFile.Setup(file => file.OpenReadStream()) + .Returns(Stream.Null); + + // Act + var result = await this.edgeDeviceModelService.UpdateEdgeModelAvatar(Guid.NewGuid().ToString(), mockFormFile.Object); + + // Assert + _ = result.Should().Be(expectedImageUri.ToString()); + MockRepository.VerifyAll(); + } + + [Test] + public async Task DeleteEdgeDeviceModelAvatarShouldDeleteEdgeDeviceModelAvatar() + { + // Arrange + _ = this.mockDeviceModelImageManager + .Setup(manager => manager.DeleteDeviceModelImageAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + await this.edgeDeviceModelService.DeleteEdgeModelAvatar(Guid.NewGuid().ToString()); + + // Assert + MockRepository.VerifyAll(); + } + + [Test] + public async Task GetPublicEdgeModules_GetPublicEdgeModules_EdgeModulesReturned() + { + // Arrange + var edgeModules = Fixture.CreateMany(10).ToList(); + + _ = this.mockConfigService + .Setup(s => s.GetPublicEdgeModules()) + .ReturnsAsync(edgeModules); + + // Act + var result = await this.edgeDeviceModelService.GetPublicEdgeModules(); + + // Assert + _ = result.Should().BeEquivalentTo(edgeModules); + MockRepository.VerifyAll(); + } + } +}