diff --git a/src/AzureIoTHub.Portal.Application/Mappers/AWS/ThingTypeProfile.cs b/src/AzureIoTHub.Portal.Application/Mappers/AWS/ThingTypeProfile.cs index b8035b8ce..42116f07a 100644 --- a/src/AzureIoTHub.Portal.Application/Mappers/AWS/ThingTypeProfile.cs +++ b/src/AzureIoTHub.Portal.Application/Mappers/AWS/ThingTypeProfile.cs @@ -15,6 +15,7 @@ public ThingTypeProfile() .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.ThingTypeID)) .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.ThingTypeName)) .ForMember(dest => dest.Description, opts => opts.MapFrom(src => src.ThingTypeDescription)) + .ForMember(dest => dest.Deprecated, opts => opts.MapFrom(src => src.Deprecated)) .ForMember(dest => dest.ThingTypeSearchableAttributes, opts => opts.MapFrom(src => src.ThingTypeSearchableAttDtos.Select(pair => new ThingTypeSearchableAtt { Name = pair.Name @@ -29,6 +30,7 @@ public ThingTypeProfile() .ForMember(dest => dest.ThingTypeID, opts => opts.MapFrom(src => src.Id)) .ForMember(dest => dest.ThingTypeName, opts => opts.MapFrom(src => src.Name)) .ForMember(dest => dest.ThingTypeDescription, opts => opts.MapFrom(src => src.Description)) + .ForMember(dest => dest.Deprecated, opts => opts.MapFrom(src => src.Deprecated)) .ForMember(dest => dest.Tags, opts => opts.MapFrom(src => src.Tags != null ? src.Tags.ToList() : null)) .ForMember(dest => dest.ThingTypeSearchableAttDtos, opts => opts.MapFrom( src => src.ThingTypeSearchableAttributes != null ? src.ThingTypeSearchableAttributes.ToList() : null)); diff --git a/src/AzureIoTHub.Portal.Application/Mappers/AWS/ThingTypeTagProfile.cs b/src/AzureIoTHub.Portal.Application/Mappers/AWS/ThingTypeTagProfile.cs new file mode 100644 index 000000000..11f96c490 --- /dev/null +++ b/src/AzureIoTHub.Portal.Application/Mappers/AWS/ThingTypeTagProfile.cs @@ -0,0 +1,21 @@ +// 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.Mappers.AWS +{ + using AutoMapper; + using AzureIoTHub.Portal.Domain.Entities.AWS; + using AzureIoTHub.Portal.Models.v10.AWS; + + public class ThingTypeTagProfile : Profile + { + public ThingTypeTagProfile() + { + _ = CreateMap() + .ForMember(dest => dest.Key, opts => opts.MapFrom(src => src.Key)) + .ForMember(dest => dest.Value, opts => opts.MapFrom(src => src.Value)) + .ReverseMap(); + + } + } +} diff --git a/src/AzureIoTHub.Portal.Application/Services/AWS/IThingTypeService.cs b/src/AzureIoTHub.Portal.Application/Services/AWS/IThingTypeService.cs index 60cdc0290..da73696a2 100644 --- a/src/AzureIoTHub.Portal.Application/Services/AWS/IThingTypeService.cs +++ b/src/AzureIoTHub.Portal.Application/Services/AWS/IThingTypeService.cs @@ -4,13 +4,23 @@ namespace AzureIoTHub.Portal.Application.Services.AWS { using AzureIoTHub.Portal.Models.v10.AWS; + using AzureIoTHub.Portal.Shared.Models.v1._0; + using AzureIoTHub.Portal.Shared.Models.v10.Filters; using Microsoft.AspNetCore.Http; public interface IThingTypeService { + //Get All Thing Types + Task> GetThingTypes(DeviceModelFilter deviceModelFilter); + + //Get a thing type + Task GetThingType(string thingTypeId); //Create a thing type Task CreateThingType(ThingTypeDto thingType); - + //Deprecate a thing type + Task DeprecateThingType(string thingTypeId); + //Delete a thing type + Task DeleteThingType(string thingTypeId); Task GetThingTypeAvatar(string thingTypeId); Task UpdateThingTypeAvatar(string thingTypeId, IFormFile file); diff --git a/src/AzureIoTHub.Portal.Client/Components/DeviceModels/DeviceModelSearch.razor b/src/AzureIoTHub.Portal.Client/Components/DeviceModels/DeviceModelSearch.razor index 255ba1d97..2875cb30f 100644 --- a/src/AzureIoTHub.Portal.Client/Components/DeviceModels/DeviceModelSearch.razor +++ b/src/AzureIoTHub.Portal.Client/Components/DeviceModels/DeviceModelSearch.razor @@ -1,8 +1,21 @@ - +@using AzureIoTHub.Portal.Models.v10; + +@inject PortalSettings Portal; + + - + @if (Portal.CloudProvider.Equals("Azure")) + { + + + } + else + { + + + } diff --git a/src/AzureIoTHub.Portal.Client/Pages/DeviceModels/DeleteDeviceModelPage.razor b/src/AzureIoTHub.Portal.Client/Pages/DeviceModels/DeleteDeviceModelPage.razor index 76359f8bb..7a1d77a6e 100644 --- a/src/AzureIoTHub.Portal.Client/Pages/DeviceModels/DeleteDeviceModelPage.razor +++ b/src/AzureIoTHub.Portal.Client/Pages/DeviceModels/DeleteDeviceModelPage.razor @@ -1,9 +1,23 @@ -@inject ISnackbar Snackbar +@using AzureIoTHub.Portal.Models.v10 +@using AzureIoTHub.Portal.Client.Services.AWS + +@inject ISnackbar Snackbar @inject IDeviceModelsClientService DeviceModelsClientService +@inject PortalSettings Portal +@inject IThingTypeClientService ThingTypeClientService -

Delete @deviceModelName ?

+ @if (Portal.CloudProvider.Equals("AWS")) + { +

Delete @thingTypeName ?

+ + } + else + { +

Delete @deviceModelName ?

+ + }

Warning : this cannot be undone.

@@ -21,6 +35,9 @@ [Parameter] public string deviceModelID { get; set; } = default!; [Parameter] public string deviceModelName { get; set; } = default!; + [Parameter] public string thingTypeID { get; set; } = default!; + [Parameter] public string thingTypeName { get; set; } = default!; + void Submit() => MudDialog.Close(DialogResult.Ok(true)); void Cancel() => MudDialog.Cancel(); @@ -32,8 +49,17 @@ { try { - await DeviceModelsClientService.DeleteDeviceModel(deviceModelID); - Snackbar.Add($"Device model {deviceModelName} has been successfully deleted!", Severity.Success); + if (Portal.CloudProvider.Equals("AWS")) + { + await ThingTypeClientService.DeleteThingType(thingTypeID); + Snackbar.Add($"Thing Type {thingTypeName} has been successfully deleted!", Severity.Success); + } + else + { + await DeviceModelsClientService.DeleteDeviceModel(deviceModelID); + Snackbar.Add($"Device model {deviceModelName} has been successfully deleted!", Severity.Success); + } + MudDialog.Close(DialogResult.Ok(true)); } catch (ProblemDetailsException exception) diff --git a/src/AzureIoTHub.Portal.Client/Pages/DeviceModels/DeviceModelDetailPage.razor b/src/AzureIoTHub.Portal.Client/Pages/DeviceModels/DeviceModelDetailPage.razor index 3814afd4c..016fb7b31 100644 --- a/src/AzureIoTHub.Portal.Client/Pages/DeviceModels/DeviceModelDetailPage.razor +++ b/src/AzureIoTHub.Portal.Client/Pages/DeviceModels/DeviceModelDetailPage.razor @@ -1,11 +1,13 @@ @page "/device-models/{ModelID}" @using System.Net.Http.Headers @using AzureIoTHub.Portal.Client.Pages.DeviceModels +@using AzureIoTHub.Portal.Client.Services.AWS @using AzureIoTHub.Portal.Client.Validators @using AzureIoTHub.Portal.Models @using AzureIoTHub.Portal.Models.v10 @using AzureIoTHub.Portal.Models.v10.LoRaWAN @using AzureIoTHub.Portal.Shared.Models +@using AzureIoTHub.Portal.Models.v10.AWS @attribute [Authorize] @inject NavigationManager NavigationManager @@ -13,152 +15,303 @@ @inject IDialogService DialogService @inject IDeviceModelsClientService DeviceModelsClientService @inject ILoRaWanDeviceModelsClientService LoRaWanDeviceModelsClientService +@inject IThingTypeClientService ThingClientService +@inject PortalSettings Portal - Device Model + @if (Portal.CloudProvider.Equals("Azure")) + { + Device Model + + } + else + { + Thing Type + + } - - - - - -
- -
-
- - @if (imageDataUrl != null) +@if (Portal.CloudProvider.Equals("Azure")) +{ + + + + + +
+ +
+
+ + @if (imageDataUrl != null) + { + Delete Picture + } + else + { + +
+ + Delete model + Save Changes + +
+ + + + + + + + Details + + + + + + + + + + + + + + @if (!IsLoRa) + { + + + + Properties + + + Add property + + + @foreach (var item in this.Properties.OrderBy(x => x.Order)) + { + + + + + + + + + + + + + @foreach (DevicePropertyType item in Enum.GetValues(typeof(DevicePropertyType))) + { + @item + } + + + + + Writable + + + Remove + + + } + + + + + + } + + + + + Labels + + + + + + + + + + @if (IsLoRa) { - Delete Picture + + + + } + + +
+
+} +else +{ + + + + + +
+ +
+
+ + @if (imageDataUrl != null) + { + Delete Picture + } + else + { + +
+ + Save Changes + + + Delete + + @if (ThingType.Deprecated) + { + Deprecate + } else { -
-
- - Delete model - Save Changes +
-
- - - - - - - - Details - - - - - - - - - - - - - - @if (!IsLoRa) - { + + + + - - Properties + + Details + + + + + + + + + + + + + + + + + + Tags - Add property - - - @foreach (var item in this.Properties.OrderBy(x => x.Order)) + @if (ThingType.Tags != null) { - - - - - - - - - - - - - @foreach (DevicePropertyType item in Enum.GetValues(typeof(DevicePropertyType))) - { - @item - } - - - - - Writable - - - Remove - - + @foreach (var tag in ThingType.Tags) + { + + + + + + + + + } } + - } - - - - Labels - - - - - - - - - - @if (IsLoRa) - { - - + + + + Searchable Attribute + + + + @if (ThingType.ThingTypeSearchableAttDtos != null) + { + @foreach (var search in ThingType.ThingTypeSearchableAttDtos) + { + + + + + + } + } + + + + + + + + - } - - -
- + + + + +} @code { [CascadingParameter] @@ -219,6 +372,8 @@ ModelId = Guid.NewGuid().ToString() }; + private ThingTypeDto ThingType { get; set; } = new ThingTypeDto(); + // Used to manage the picture private MultipartFormDataContent? content = default!; private string? imageDataUrl = default!; @@ -249,18 +404,27 @@ { isProcessing = true; - if (IsLoRa) + if (Portal.CloudProvider.Equals("AWS")) { - Model = await LoRaWanDeviceModelsClientService.GetDeviceModel(ModelID); - Commands.AddRange(await LoRaWanDeviceModelsClientService.GetDeviceModelCommands(ModelID)); - imageDataUrl = await LoRaWanDeviceModelsClientService.GetAvatarUrl(ModelID); + ThingType = await ThingClientService.GetThingType(ModelID); + imageDataUrl = await ThingClientService.GetAvatarUrl(ThingType.ThingTypeID); } else { - Model = await DeviceModelsClientService.GetDeviceModel(ModelID); - Properties.AddRange(await DeviceModelsClientService.GetDeviceModelModelProperties(ModelID)); - imageDataUrl = await DeviceModelsClientService.GetAvatarUrl(ModelID); + if (IsLoRa) + { + Model = await LoRaWanDeviceModelsClientService.GetDeviceModel(ModelID); + Commands.AddRange(await LoRaWanDeviceModelsClientService.GetDeviceModelCommands(ModelID)); + imageDataUrl = await LoRaWanDeviceModelsClientService.GetAvatarUrl(ModelID); + } + else + { + Model = await DeviceModelsClientService.GetDeviceModel(ModelID); + Properties.AddRange(await DeviceModelsClientService.GetDeviceModelModelProperties(ModelID)); + imageDataUrl = await DeviceModelsClientService.GetAvatarUrl(ModelID); + } } + } catch (ProblemDetailsException exception) { @@ -382,7 +546,32 @@ { isProcessing = false; } - + + } + + private async Task ChangeThingTypeImage() + { + try + { + + if (content is not null) + { + await this.ThingClientService.ChangeAvatar(ThingType.ThingTypeID, content); + } + + Snackbar.Add("Thing Type image successfully updated.", Severity.Success); + + // Go back to the list of devices models + NavigationManager.NavigateTo("device-models"); + } + catch (ProblemDetailsException exception) + { + Error?.ProcessProblemDetails(exception); + } + finally + { + isProcessing = false; + } } private async Task DeleteDeviceModel() @@ -406,4 +595,49 @@ // Go back to the list of devices after the deletion NavigationManager.NavigateTo("device-models"); } + + private async Task DeleteThingType() + { + isProcessing = true; + + var parameters = new DialogParameters + { + {"thingTypeID", ThingType.ThingTypeID}, + {"thingTypeName", ThingType.ThingTypeName} + }; + var result = await DialogService.Show("Confirm Deletion", parameters).Result; + + isProcessing = false; + + if (result.Canceled) + { + return; + } + + // Go back to the list of devices after the deletion + NavigationManager.NavigateTo("device-models"); + } + private async Task Deprecate() + { + isProcessing = true; + + try + { + await ThingClientService.DeprecateThingType(ThingType.ThingTypeID); + Snackbar.Add($"The {ThingType.ThingTypeName} object type has been been successfully deprecated. You have to wait about 5 minutes before you can delete the object type.", Severity.Success); + + // Go back to the list of devices models + NavigationManager.NavigateTo("device-models"); + } + catch (ProblemDetailsException exception) + { + Error?.ProcessProblemDetails(exception); + } + finally + { + isProcessing = false; + } + + } + } diff --git a/src/AzureIoTHub.Portal.Client/Pages/DeviceModels/DeviceModelListPage.razor b/src/AzureIoTHub.Portal.Client/Pages/DeviceModels/DeviceModelListPage.razor index a6f83d755..de0a32ac6 100644 --- a/src/AzureIoTHub.Portal.Client/Pages/DeviceModels/DeviceModelListPage.razor +++ b/src/AzureIoTHub.Portal.Client/Pages/DeviceModels/DeviceModelListPage.razor @@ -1,81 +1,153 @@ @page "/device-models" @using AzureIoTHub.Portal.Models.v10 @using AzureIoTHub.Portal.Shared.Models.v10.Filters; +@using AzureIoTHub.Portal.Client.Services.AWS +@using AzureIoTHub.Portal.Models.v10.AWS + @attribute [Authorize] @inject NavigationManager navigationManager @inject PortalSettings Portal @inject IDialogService dialogService @inject IDeviceModelsClientService DeviceModelsClientService +@inject IThingTypeClientService ThingTypeClientService await Search(args)) /> - - - - - - - - - - Device Models - - - - - - - - - - - Name - Description - Details - Delete - - - - - - - - - @context.Name - - - - - - - - @context.Description - - - - + @if (Portal.CloudProvider.Equals("Azure")) + { + + + + + + + + + + Device Models + + + - - - - + + - - - - - - - No matching records found - - - Loading... - - + + + + Name + Description + Details + Delete + + + + + + + + + @context.Name + + + + + + + + @context.Description + + + + + + + + + + + + + + + + + No matching records found + + + Loading... + + + + } + else + { + + + + + + + + + + Thing Types + + + + + + + + + + + Name + Description + Details + Delete + + + + + + + + + @context.ThingTypeName + + + + + @context.ThingTypeDescription + + + + + + + + + + + + + + + + + No matching records found + + + Loading... + + + + } @@ -85,6 +157,7 @@ public Error Error { get; set; } = default!; private MudTable table = default!; + private MudTable thingTypeTable = default!; private readonly Dictionary pages = new(); @@ -101,6 +174,7 @@ private void AddDeviceModel() { + navigationManager.NavigateTo("/device-models/new"); } @@ -139,6 +213,7 @@ Items = result.Items, TotalItems = result.TotalItems }; + } catch (ProblemDetailsException exception) { @@ -151,6 +226,54 @@ } } + /// + /// Sends a GET request to the ThingTypeController, to retrieve all thing types from the database + /// + /// + private async Task> LoadThingTypes(TableState state) + { + try + { + IsLoading = true; + + string orderBy = default!; + + switch (state.SortDirection) + { + case SortDirection.Ascending: + orderBy = $"{state.SortLabel} asc"; + break; + case SortDirection.Descending: + orderBy = $"{state.SortLabel} desc"; + break; + } + + var result = await ThingTypeClientService.GetThingTypes(new DeviceModelFilter + { + SearchText = this.deviceModelSearchInfo.SearchText!, + PageNumber = state.Page, + PageSize = state.PageSize, + OrderBy = new string[] { orderBy } + }); + + return new TableData + { + Items = result.Items, + TotalItems = result.TotalItems + }; + + } + catch (ProblemDetailsException exception) + { + Error?.ProcessProblemDetails(exception); + return new TableData(); + } + finally + { + IsLoading = false; + } + } + private async Task DeleteDeviceModel(DeviceModelDto deviceModel) { var parameters = new DialogParameters(); @@ -168,10 +291,32 @@ await Search(); } + private async Task DeleteThingType(ThingTypeDto thingType) + { + var parameters = new DialogParameters(); + parameters.Add("thingTypeID", thingType.ThingTypeID); + parameters.Add("thingTypeName", thingType.ThingTypeName); + var result = await dialogService.Show("Confirm Deletion", parameters).Result; + + if (result.Canceled) + { + return; + } + + // Update the list of devices after the deletion + // await LoadDeviceModels(); + await Search(); + } + + private void GoToDetails(DeviceModelDto item) { navigationManager.NavigateTo($"/device-models/{item.ModelId}{((item.SupportLoRaFeatures && Portal.IsLoRaSupported) ? "?isLora=true" : "")}"); } + private void GoToThingTypeDetails(ThingTypeDto thingType) + { + navigationManager.NavigateTo($"/device-models/{thingType.ThingTypeID}"); + } private async Task Search(DeviceModelSearchInfo? deviceModelSearchInfo = null) { @@ -184,11 +329,29 @@ this.deviceModelSearchInfo = deviceModelSearchInfo; } - await table.ReloadServerData(); + if (Portal.CloudProvider.Equals("Azure")) + { + await table.ReloadServerData(); + + } + else + { + await thingTypeTable.ReloadServerData(); + + } } private async void Refresh() { - await table.ReloadServerData(); + if (Portal.CloudProvider.Equals("Azure")) + { + await table.ReloadServerData(); + + } + else + { + await thingTypeTable.ReloadServerData(); + + } } } diff --git a/src/AzureIoTHub.Portal.Client/Services/AWS/IThingTypeClientService.cs b/src/AzureIoTHub.Portal.Client/Services/AWS/IThingTypeClientService.cs index 35bbc234d..3f16b460d 100644 --- a/src/AzureIoTHub.Portal.Client/Services/AWS/IThingTypeClientService.cs +++ b/src/AzureIoTHub.Portal.Client/Services/AWS/IThingTypeClientService.cs @@ -4,11 +4,16 @@ namespace AzureIoTHub.Portal.Client.Services.AWS { using AzureIoTHub.Portal.Models.v10.AWS; + using AzureIoTHub.Portal.Shared.Models.v10.Filters; public interface IThingTypeClientService { - Task CreateThingType(ThingTypeDto thingType); + Task> GetThingTypes(DeviceModelFilter? deviceModelFilter = null); + Task GetThingType(string thingTypeId); + Task CreateThingType(ThingTypeDto thingType); + Task DeprecateThingType(string thingTypeId); + Task DeleteThingType(string thingTypeId); Task GetAvatarUrl(string thingTypeId); Task ChangeAvatar(string thingTypeId, MultipartFormDataContent avatar); diff --git a/src/AzureIoTHub.Portal.Client/Services/AWS/ThingTypeClientService.cs b/src/AzureIoTHub.Portal.Client/Services/AWS/ThingTypeClientService.cs index 9a36037c5..db9934d16 100644 --- a/src/AzureIoTHub.Portal.Client/Services/AWS/ThingTypeClientService.cs +++ b/src/AzureIoTHub.Portal.Client/Services/AWS/ThingTypeClientService.cs @@ -3,26 +3,67 @@ namespace AzureIoTHub.Portal.Client.Services.AWS { + using System.Collections.Generic; + using System.Net.Http; + using System.Net.Http.Json; using System.Threading.Tasks; using AzureIoTHub.Portal.Models.v10.AWS; + using AzureIoTHub.Portal.Shared.Models.v10.Filters; + using Microsoft.AspNetCore.WebUtilities; public class ThingTypeClientService : IThingTypeClientService { private readonly HttpClient http; + private readonly string apiUrlBase = "api/aws/thingtypes"; + public ThingTypeClientService(HttpClient http) { this.http = http; } + public async Task> GetThingTypes(DeviceModelFilter? deviceModelFilter = null) + { + var query = new Dictionary + { + { nameof(DeviceModelFilter.SearchText), deviceModelFilter?.SearchText ?? string.Empty }, +#pragma warning disable CA1305 + { nameof(DeviceModelFilter.PageNumber), deviceModelFilter?.PageNumber.ToString() ?? string.Empty }, + { nameof(DeviceModelFilter.PageSize), deviceModelFilter?.PageSize.ToString() ?? string.Empty }, +#pragma warning restore CA1305 + { nameof(DeviceModelFilter.OrderBy), string.Join("", deviceModelFilter?.OrderBy!) ?? string.Empty } + }; + + var uri = QueryHelpers.AddQueryString(this.apiUrlBase, query); + return await this.http.GetFromJsonAsync>(uri) ?? new PaginationResult(); + } + + public async Task GetThingType(string thingTypeId) + { + return await this.http.GetFromJsonAsync($"api/aws/thingtypes/{thingTypeId}")!; + } + + public async Task CreateThingType(ThingTypeDto thingType) { var response = await this.http.PostAsJsonAsync("api/aws/thingtypes", thingType); - _ = response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } + public async Task DeprecateThingType(string thingTypeId) + { + var result = await this.http.PutAsync($"api/aws/thingtypes/{thingTypeId}", null); + _ = result.EnsureSuccessStatusCode(); + + } + + public async Task DeleteThingType(string thingTypeId) + { + var result = await this.http.DeleteAsync($"api/aws/thingtypes/{thingTypeId}"); + _ = result.EnsureSuccessStatusCode(); + + } public Task GetAvatarUrl(string thingTypeId) { return this.http.GetStringAsync($"api/aws/thingtypes/{thingTypeId}/avatar"); @@ -34,6 +75,5 @@ public async Task ChangeAvatar(string thingTypeId, MultipartFormDataContent avat _ = result.EnsureSuccessStatusCode(); } - } } diff --git a/src/AzureIoTHub.Portal.Domain/Entities/AWS/ThingType.cs b/src/AzureIoTHub.Portal.Domain/Entities/AWS/ThingType.cs index 92b64e511..15a9df276 100644 --- a/src/AzureIoTHub.Portal.Domain/Entities/AWS/ThingType.cs +++ b/src/AzureIoTHub.Portal.Domain/Entities/AWS/ThingType.cs @@ -11,6 +11,7 @@ public class ThingType : EntityBase [Required] public string Name { get; set; } = default!; public string? Description { get; set; } = default!; + public bool Deprecated { get; set; } public ICollection? Tags { get; set; } = default!; public ICollection? ThingTypeSearchableAttributes { get; set; } = default!; } diff --git a/src/AzureIoTHub.Portal.Domain/Repositories/AWS/IThingTypeSearchableAttRepository.cs b/src/AzureIoTHub.Portal.Domain/Repositories/AWS/IThingTypeSearchableAttRepository.cs index d689e8bab..3bfe4f483 100644 --- a/src/AzureIoTHub.Portal.Domain/Repositories/AWS/IThingTypeSearchableAttRepository.cs +++ b/src/AzureIoTHub.Portal.Domain/Repositories/AWS/IThingTypeSearchableAttRepository.cs @@ -5,7 +5,7 @@ namespace AzureIoTHub.Portal.Domain.Repositories.AWS { using AzureIoTHub.Portal.Domain.Entities.AWS; - internal interface IThingTypeSearchableAttRepository : IRepository + public interface IThingTypeSearchableAttRepository : IRepository { } } diff --git a/src/AzureIoTHub.Portal.Domain/Repositories/AWS/IThingTypeTagRepository.cs b/src/AzureIoTHub.Portal.Domain/Repositories/AWS/IThingTypeTagRepository.cs index 4fc67abba..808ef16c9 100644 --- a/src/AzureIoTHub.Portal.Domain/Repositories/AWS/IThingTypeTagRepository.cs +++ b/src/AzureIoTHub.Portal.Domain/Repositories/AWS/IThingTypeTagRepository.cs @@ -5,7 +5,7 @@ namespace AzureIoTHub.Portal.Domain.Repositories.AWS { using AzureIoTHub.Portal.Domain.Entities.AWS; - internal interface IThingTypeTagRepository : IRepository + public interface IThingTypeTagRepository : IRepository { } } diff --git a/src/AzureIoTHub.Portal.Infrastructure/Repositories/ThingTypeRepository.cs b/src/AzureIoTHub.Portal.Infrastructure/Repositories/AWS/ThingTypeRepository.cs similarity index 88% rename from src/AzureIoTHub.Portal.Infrastructure/Repositories/ThingTypeRepository.cs rename to src/AzureIoTHub.Portal.Infrastructure/Repositories/AWS/ThingTypeRepository.cs index f40480a4a..199e7e6e7 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/Repositories/ThingTypeRepository.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/Repositories/AWS/ThingTypeRepository.cs @@ -1,7 +1,7 @@ // 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.Repositories +namespace AzureIoTHub.Portal.Infrastructure.Repositories.AWS { using AzureIoTHub.Portal.Domain.Entities.AWS; using AzureIoTHub.Portal.Domain.Repositories; diff --git a/src/AzureIoTHub.Portal.Infrastructure/Repositories/AWS/ThingTypeSearchableAttributeRepository.cs b/src/AzureIoTHub.Portal.Infrastructure/Repositories/AWS/ThingTypeSearchableAttributeRepository.cs new file mode 100644 index 000000000..7b7238528 --- /dev/null +++ b/src/AzureIoTHub.Portal.Infrastructure/Repositories/AWS/ThingTypeSearchableAttributeRepository.cs @@ -0,0 +1,15 @@ +// 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.Repositories.AWS +{ + using AzureIoTHub.Portal.Domain.Entities.AWS; + using AzureIoTHub.Portal.Domain.Repositories.AWS; + + public class ThingTypeSearchableAttributeRepository : GenericRepository, IThingTypeSearchableAttRepository + { + public ThingTypeSearchableAttributeRepository(PortalDbContext context) : base(context) + { + } + } +} diff --git a/src/AzureIoTHub.Portal.Infrastructure/Repositories/AWS/ThingTypeTagRepository.cs b/src/AzureIoTHub.Portal.Infrastructure/Repositories/AWS/ThingTypeTagRepository.cs new file mode 100644 index 000000000..7d92c247f --- /dev/null +++ b/src/AzureIoTHub.Portal.Infrastructure/Repositories/AWS/ThingTypeTagRepository.cs @@ -0,0 +1,15 @@ +// 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.Repositories.AWS +{ + using AzureIoTHub.Portal.Domain.Entities.AWS; + using AzureIoTHub.Portal.Domain.Repositories.AWS; + + public class ThingTypeTagRepository : GenericRepository, IThingTypeTagRepository + { + public ThingTypeTagRepository(PortalDbContext context) : base(context) + { + } + } +} diff --git a/src/AzureIoTHub.Portal.Infrastructure/Services/AWS/ThingTypeService.cs b/src/AzureIoTHub.Portal.Infrastructure/Services/AWS/ThingTypeService.cs index 8df0f4b9c..5bfb306bb 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/Services/AWS/ThingTypeService.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/Services/AWS/ThingTypeService.cs @@ -3,6 +3,12 @@ namespace AzureIoTHub.Portal.Infrastructure.Services.AWS { + using System; + using System.Linq.Expressions; + using System.Linq; + using System.Linq.Dynamic.Core; + using System.Threading.Tasks; + using Microsoft.EntityFrameworkCore; using Amazon.IoT; using Amazon.IoT.Model; using AutoMapper; @@ -12,12 +18,19 @@ namespace AzureIoTHub.Portal.Infrastructure.Services.AWS using AzureIoTHub.Portal.Domain.Entities.AWS; using AzureIoTHub.Portal.Domain.Exceptions; using AzureIoTHub.Portal.Domain.Repositories; + using AzureIoTHub.Portal.Infrastructure.Repositories; using AzureIoTHub.Portal.Models.v10.AWS; + using AzureIoTHub.Portal.Shared.Models.v1._0; + using AzureIoTHub.Portal.Shared.Models.v10.Filters; using Microsoft.AspNetCore.Http; + using ResourceNotFoundException = Domain.Exceptions.ResourceNotFoundException; + using AzureIoTHub.Portal.Domain.Repositories.AWS; public class ThingTypeService : IThingTypeService { private readonly IThingTypeRepository thingTypeRepository; + private readonly IThingTypeTagRepository thingTypeTagRepository; + private readonly IThingTypeSearchableAttRepository thingTypeSearchableAttributeRepository; private readonly IMapper mapper; private readonly IUnitOfWork unitOfWork; private readonly IAmazonIoT amazonIoTClient; @@ -29,7 +42,9 @@ public ThingTypeService( IThingTypeRepository thingTypeRepository, IMapper mapper, IUnitOfWork unitOfWork, - IDeviceModelImageManager thingTypeImageManager + IDeviceModelImageManager thingTypeImageManager, + IThingTypeTagRepository thingTypeTagRepository, + IThingTypeSearchableAttRepository thingTypeSearchableAttributeRepository ) { @@ -38,8 +53,58 @@ IDeviceModelImageManager thingTypeImageManager this.unitOfWork = unitOfWork; this.amazonIoTClient = amazonIoTClient; this.thingTypeImageManager = thingTypeImageManager; + this.thingTypeTagRepository = thingTypeTagRepository; + this.thingTypeSearchableAttributeRepository = thingTypeSearchableAttributeRepository; } + public async Task> GetThingTypes(DeviceModelFilter deviceModelFilter) + { + var thingTypePredicate = PredicateBuilder.True(); + + if (!string.IsNullOrWhiteSpace(deviceModelFilter.SearchText)) + { + thingTypePredicate = thingTypePredicate.And(thingType => thingType.Name.ToLower().Contains(deviceModelFilter.SearchText.ToLower()) + || thingType.Description.ToLower().Contains(deviceModelFilter.SearchText.ToLower()) + || thingType.Tags.Any( + tag => tag.Key.ToLower().Contains(deviceModelFilter.SearchText.ToLower()) + || tag.Value.ToLower().Contains(deviceModelFilter.SearchText.ToLower())) + || thingType.ThingTypeSearchableAttributes.Any( + attr => attr.Name.ToLower().Contains(deviceModelFilter.SearchText.ToLower()) + + )); + } + + var paginatedThingType = await this.thingTypeRepository.GetPaginatedListAsync(deviceModelFilter.PageNumber, deviceModelFilter.PageSize, deviceModelFilter.OrderBy, thingTypePredicate, includes: new Expression>[] { d => d.ThingTypeSearchableAttributes}); + + var paginatedThingTypeDto = new PaginatedResult + { + Data = paginatedThingType?.Data?.Select(x => this.mapper.Map(x, opts => + { + opts.AfterMap((src, dest) => dest.ImageUrl = this.thingTypeImageManager.ComputeImageUri(x.Id)); + })).ToList(), + TotalCount = paginatedThingType.TotalCount, + CurrentPage = paginatedThingType.CurrentPage, + PageSize = deviceModelFilter.PageSize + }; + + return new PaginatedResult(paginatedThingTypeDto.Data, paginatedThingTypeDto.TotalCount, paginatedThingTypeDto.CurrentPage, paginatedThingType.PageSize); + } + + public async Task GetThingType(string thingTypeId) + { + var getThingType = await this.thingTypeRepository.GetByIdAsync(thingTypeId, d => d.Tags!, d => d.ThingTypeSearchableAttributes!); + if (getThingType == null) + { + throw new ResourceNotFoundException($"The thing type with id {thingTypeId} doesn't exist"); + + } + var getAvatar = this.thingTypeImageManager.ComputeImageUri(thingTypeId); + + var dto = this.mapper.Map(getThingType); + dto.ImageUrl = getAvatar; + + return dto; + } public async Task CreateThingType(ThingTypeDto thingType) { ArgumentNullException.ThrowIfNull(thingType, nameof(thingType)); @@ -70,6 +135,87 @@ private async Task CreateThingTypeInDatabase(ThingTypeDto thingType) return await GetThingType; } + public async Task DeprecateThingType(string thingTypeId) + { + var getThingType = await this.thingTypeRepository.GetByIdAsync(thingTypeId, d => d.Tags!, d => d.ThingTypeSearchableAttributes!); + if (getThingType == null) + { + throw new ResourceNotFoundException($"The thing type with id {thingTypeId} doesn't exist"); + + } + var deprecated = new DeprecateThingTypeRequest() + { + ThingTypeName = getThingType.Name, + UndoDeprecate = false + }; + + var response = await this.amazonIoTClient.DeprecateThingTypeAsync(deprecated); + + if (response.HttpStatusCode != System.Net.HttpStatusCode.OK) + { + throw new InternalServerErrorException("The deprecation of the thing type failed due to an error in the Amazon IoT API."); + } + else + { + getThingType.Deprecated = true; + this.thingTypeRepository.Update(getThingType); + await this.unitOfWork.SaveAsync(); + + return this.mapper.Map(getThingType); + } + } + + public async Task DeleteThingType(string thingTypeId) + { + var getThingType = await this.thingTypeRepository.GetByIdAsync(thingTypeId, d => d.Tags!, d => d.ThingTypeSearchableAttributes!); + if (getThingType == null) + { + throw new ResourceNotFoundException($"The thing type with id {thingTypeId} doesn't exist"); + + } + var deleted = new DeleteThingTypeRequest() + { + ThingTypeName = getThingType.Name + }; + + var response = await this.amazonIoTClient.DeleteThingTypeAsync(deleted); + + if (response.HttpStatusCode != System.Net.HttpStatusCode.OK) + { + throw new InternalServerErrorException("The deletion of the thing type failed due to an error in the Amazon IoT API."); + } + else + { + await DeleteThingTypeInDatabase(getThingType); + } + + } + + private async Task DeleteThingTypeInDatabase(ThingType thingType) + { + if (thingType.Tags != null + && thingType.Tags?.Count != 0) + { + foreach (var tag in thingType.Tags!) + { + this.thingTypeTagRepository.Delete(tag.Id); + + } + } + if (thingType.ThingTypeSearchableAttributes != null + && thingType.ThingTypeSearchableAttributes?.Count != 0) + { + foreach (var search in thingType.ThingTypeSearchableAttributes!) + { + this.thingTypeSearchableAttributeRepository.Delete(search.Id); + + } + } + + this.thingTypeRepository.Delete(thingType.Id); + await this.unitOfWork.SaveAsync(); + _ = this.thingTypeImageManager.DeleteDeviceModelImageAsync(thingType.Id); + } public Task GetThingTypeAvatar(string thingTypeId) { return Task.Run(() => this.thingTypeImageManager.ComputeImageUri(thingTypeId).ToString()); diff --git a/src/AzureIoTHub.Portal.Infrastructure/Startup/AWSServiceCollectionExtension.cs b/src/AzureIoTHub.Portal.Infrastructure/Startup/AWSServiceCollectionExtension.cs index 15dedfcaf..ac78f236e 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/Startup/AWSServiceCollectionExtension.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/Startup/AWSServiceCollectionExtension.cs @@ -11,11 +11,12 @@ namespace AzureIoTHub.Portal.Infrastructure.Startup using AzureIoTHub.Portal.Application.Services.AWS; using AzureIoTHub.Portal.Domain; using AzureIoTHub.Portal.Domain.Repositories; - using AzureIoTHub.Portal.Infrastructure.Repositories; using AzureIoTHub.Portal.Infrastructure.Services.AWS; using AzureIoTHub.Portal.Application.Managers; using AzureIoTHub.Portal.Infrastructure.Managers; using Microsoft.Extensions.DependencyInjection; + using AzureIoTHub.Portal.Domain.Repositories.AWS; + using AzureIoTHub.Portal.Infrastructure.Repositories.AWS; public static class AWSServiceCollectionExtension { @@ -60,6 +61,8 @@ private static IServiceCollection ConfigureAWSServices(this IServiceCollection s private static IServiceCollection ConfigureAWSRepositories(this IServiceCollection services) { _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); return services; } diff --git a/src/AzureIoTHub.Portal.Postgres/Migrations/20230502094509_AWS ThingType initial create.Designer.cs b/src/AzureIoTHub.Portal.Postgres/Migrations/20230511071349_AWS ThingType initial create.Designer.cs similarity index 99% rename from src/AzureIoTHub.Portal.Postgres/Migrations/20230502094509_AWS ThingType initial create.Designer.cs rename to src/AzureIoTHub.Portal.Postgres/Migrations/20230511071349_AWS ThingType initial create.Designer.cs index 924269870..391897b8a 100644 --- a/src/AzureIoTHub.Portal.Postgres/Migrations/20230502094509_AWS ThingType initial create.Designer.cs +++ b/src/AzureIoTHub.Portal.Postgres/Migrations/20230511071349_AWS ThingType initial create.Designer.cs @@ -12,7 +12,7 @@ namespace AzureIoTHub.Portal.Postgres.Migrations { [DbContext(typeof(PortalDbContext))] - [Migration("20230502094509_AWS ThingType initial create")] + [Migration("20230511071349_AWS ThingType initial create")] partial class AWSThingTypeinitialcreate { /// @@ -30,6 +30,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Id") .HasColumnType("text"); + b.Property("Deprecated") + .HasColumnType("boolean"); + b.Property("Description") .HasColumnType("text"); diff --git a/src/AzureIoTHub.Portal.Postgres/Migrations/20230502094509_AWS ThingType initial create.cs b/src/AzureIoTHub.Portal.Postgres/Migrations/20230511071349_AWS ThingType initial create.cs similarity index 96% rename from src/AzureIoTHub.Portal.Postgres/Migrations/20230502094509_AWS ThingType initial create.cs rename to src/AzureIoTHub.Portal.Postgres/Migrations/20230511071349_AWS ThingType initial create.cs index 2ed0619ed..230ebbf16 100644 --- a/src/AzureIoTHub.Portal.Postgres/Migrations/20230502094509_AWS ThingType initial create.cs +++ b/src/AzureIoTHub.Portal.Postgres/Migrations/20230511071349_AWS ThingType initial create.cs @@ -1,14 +1,15 @@ - // Copyright (c) CGI France. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. + #nullable disable + namespace AzureIoTHub.Portal.Postgres.Migrations { using Microsoft.EntityFrameworkCore.Migrations; - /// public partial class AWSThingTypeinitialcreate : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { @@ -18,7 +19,8 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "text", nullable: false), Name = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: true) + Description = table.Column(type: "text", nullable: true), + Deprecated = table.Column(type: "boolean", nullable: false) }, constraints: table => { diff --git a/src/AzureIoTHub.Portal.Postgres/Migrations/PortalDbContextModelSnapshot.cs b/src/AzureIoTHub.Portal.Postgres/Migrations/PortalDbContextModelSnapshot.cs index c33bb01ff..e922132c1 100644 --- a/src/AzureIoTHub.Portal.Postgres/Migrations/PortalDbContextModelSnapshot.cs +++ b/src/AzureIoTHub.Portal.Postgres/Migrations/PortalDbContextModelSnapshot.cs @@ -27,6 +27,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .HasColumnType("text"); + b.Property("Deprecated") + .HasColumnType("boolean"); + b.Property("Description") .HasColumnType("text"); diff --git a/src/AzureIoTHub.Portal.Server/Controllers/v1.0/AWS/ThingTypeController.cs b/src/AzureIoTHub.Portal.Server/Controllers/v1.0/AWS/ThingTypeController.cs index 2efaa0cdc..d668c5eb5 100644 --- a/src/AzureIoTHub.Portal.Server/Controllers/v1.0/AWS/ThingTypeController.cs +++ b/src/AzureIoTHub.Portal.Server/Controllers/v1.0/AWS/ThingTypeController.cs @@ -6,9 +6,11 @@ namespace AzureIoTHub.Portal.Server.Controllers.v1._0.AWS using System.Threading.Tasks; using AzureIoTHub.Portal.Application.Services.AWS; using AzureIoTHub.Portal.Models.v10.AWS; + using AzureIoTHub.Portal.Shared.Models.v10.Filters; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Routing; [Authorize] [ApiController] @@ -24,6 +26,52 @@ public ThingTypeController(IThingTypeService thingTypeService) this.thingTypeService = thingTypeService; } + /// + /// Gets the Thing type list. + /// + /// An array representing the Thing type. + [HttpGet(Name = "GET Thing type list")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetThingTypes([FromQuery] DeviceModelFilter deviceModelFilter) + { + var paginatedThingType = await this.thingTypeService.GetThingTypes(deviceModelFilter); + + var nextPage = string.Empty; + + if (paginatedThingType.HasNextPage) + { + nextPage = Url.RouteUrl(new UrlRouteContext + { + RouteName = "GET Thing Type list", + Values = new + { + deviceModelFilter.SearchText, + deviceModelFilter.PageSize, + pageNumber = deviceModelFilter.PageNumber + 1, + deviceModelFilter.OrderBy + } + }); + } + + return Ok(new PaginationResult + { + Items = paginatedThingType.Data, + TotalItems = paginatedThingType.TotalCount, + NextPage = nextPage + }); + } + + /// + /// Gets a thing type. + /// + /// An array representing the Thing type. + [HttpGet("{id}", Name = "GET A Thing type")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetThingType(string id) + { + return Ok(await thingTypeService.GetThingType(id)); + } + /// /// Creates the Thing type. /// @@ -37,6 +85,31 @@ public async Task> CreateThingTypeAsync(ThingTypeDto thingt return Ok(await this.thingTypeService.CreateThingType(thingtype)); } + /// + /// Deprecate the Thing type. + /// + /// The thing type. + [HttpPut("{id}", Name = "PUT Create AWS Thing type")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> DeprecateThingTypeAsync(string id) + { + return Ok(await this.thingTypeService.DeprecateThingType(id)); + } + + /// + /// Deletes the thing type. + /// + /// The thing type identifier. + [HttpDelete("{id}", Name = "DELETE the thing type")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteThingTypeAsync(string id) + { + await this.thingTypeService.DeleteThingType(id); + return NoContent(); + } + /// /// Gets the thing type avatar. /// diff --git a/src/AzureIoTHub.Portal.Shared/Models/v1.0/AWS/ThingTypeDto.cs b/src/AzureIoTHub.Portal.Shared/Models/v1.0/AWS/ThingTypeDto.cs index 3b76d8cc8..69e502d00 100644 --- a/src/AzureIoTHub.Portal.Shared/Models/v1.0/AWS/ThingTypeDto.cs +++ b/src/AzureIoTHub.Portal.Shared/Models/v1.0/AWS/ThingTypeDto.cs @@ -14,6 +14,7 @@ public class ThingTypeDto [Required(ErrorMessage = "The thing type should have a name.")] public string ThingTypeName { get; set; } public string ThingTypeDescription { get; set; } + public bool Deprecated { get; set; } public List Tags { get; set; } public List ThingTypeSearchableAttDtos { get; set; } public Uri ImageUrl { get; set; } = default!; diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Client/Components/DevicesModels/DeviceModelSearchTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Client/Components/DevicesModels/DeviceModelSearchTests.cs index e839f2a62..ed2b419b1 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Client/Components/DevicesModels/DeviceModelSearchTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Client/Components/DevicesModels/DeviceModelSearchTests.cs @@ -12,6 +12,8 @@ namespace AzureIoTHub.Portal.Tests.Unit.Client.Components.DevicesModels using AzureIoTHub.Portal.Client.Components.DeviceModels; using FluentAssertions; using System.Linq; + using AzureIoTHub.Portal.Models.v10; + using Microsoft.Extensions.DependencyInjection; [TestFixture] public class DeviceModelSearchTests : BlazorUnitTest @@ -22,9 +24,11 @@ public override void Setup() } [Test] - public void SearchDeviceModels_ClickOnSearch_SearchIsFired() + public void SearchDeviceModelsClickOnSearchSearchIsFired() { // Arrange + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "Azure" }); + var searchText = Fixture.Create(); var receivedEvents = new List(); var expectedDeviceModelSearchInfo = new DeviceModelSearchInfo @@ -49,9 +53,71 @@ public void SearchDeviceModels_ClickOnSearch_SearchIsFired() } [Test] - public void SearchDeviceModels_ClickOnReset_SearchTextIsSetToEmptyAndSearchIsFired() + public void SearchDeviceModelsClickOnResetSearchTextIsSetToEmptyAndSearchIsFired() + { + // Arrange + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "Azure" }); + + var searchText = Fixture.Create(); + var receivedEvents = new List(); + var expectedDeviceModelSearchInfo = new DeviceModelSearchInfo + { + SearchText = string.Empty + }; + + var cut = RenderComponent(parameters => parameters.Add(p => p.OnSearch, (searchInfo) => + { + receivedEvents.Add(searchInfo); + })); + + cut.WaitForElement("#searchText").Input(searchText); + + // Act + cut.WaitForElement("#resetSearch").Click(); + + // Assert + cut.WaitForAssertion(() => receivedEvents.Count.Should().Be(1)); + _ = receivedEvents.First().Should().BeEquivalentTo(expectedDeviceModelSearchInfo); + _ = cut.Find("#searchText").TextContent.Should().Be(string.Empty); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + /***=========== Test for AWS ===============***/ + [Test] + public void SearchThingTypeClickOnSearchSearchIsFired() { // Arrange + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "AWS" }); + + var searchText = Fixture.Create(); + var receivedEvents = new List(); + var expectedDeviceModelSearchInfo = new DeviceModelSearchInfo + { + SearchText = searchText + }; + + var cut = RenderComponent(parameters => parameters.Add(p => p.OnSearch, (searchInfo) => + { + receivedEvents.Add(searchInfo); + })); + + cut.WaitForElement("#searchText").Change(searchText); + + // Act + cut.WaitForElement("#searchButton").Click(); + + // Assert + cut.WaitForAssertion(() => receivedEvents.Count.Should().Be(1)); + _ = receivedEvents.First().Should().BeEquivalentTo(expectedDeviceModelSearchInfo); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public void SearchThingTypeClickOnResetSearchTextIsSetToEmptyAndSearchIsFired() + { + // Arrange + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "AWS" }); + var searchText = Fixture.Create(); var receivedEvents = new List(); var expectedDeviceModelSearchInfo = new DeviceModelSearchInfo diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/DevicesModels/DeviceModelDetailsPageTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/DevicesModels/DeviceModelDetailsPageTests.cs index 897708481..e82e625c6 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/DevicesModels/DeviceModelDetailsPageTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/DevicesModels/DeviceModelDetailsPageTests.cs @@ -24,6 +24,9 @@ namespace AzureIoTHub.Portal.Tests.Unit.Client.Pages.DevicesModels using MudBlazor.Services; using NUnit.Framework; using UnitTests.Mocks; + using AzureIoTHub.Portal.Client.Services.AWS; + using AzureIoTHub.Portal.Models.v10.AWS; + using System.Net.Http; [TestFixture] public class DeviceModelDetaislPageTests : BlazorUnitTest @@ -31,6 +34,7 @@ public class DeviceModelDetaislPageTests : BlazorUnitTest private Mock mockDialogService; private Mock mockSnackbarService; private Mock mockDeviceModelsClientService; + private Mock mockThingTypeClientService; private Mock mockLoRaWanDeviceModelsClientService; private readonly string mockModelId = Guid.NewGuid().ToString(); @@ -42,16 +46,19 @@ public override void Setup() this.mockDialogService = MockRepository.Create(); this.mockSnackbarService = MockRepository.Create(); this.mockDeviceModelsClientService = MockRepository.Create(); + this.mockThingTypeClientService = MockRepository.Create(); this.mockLoRaWanDeviceModelsClientService = MockRepository.Create(); _ = Services.AddSingleton(this.mockDialogService.Object); _ = Services.AddSingleton(this.mockSnackbarService.Object); _ = Services.AddSingleton(this.mockDeviceModelsClientService.Object); + _ = Services.AddSingleton(this.mockThingTypeClientService.Object); _ = Services.AddSingleton(this.mockLoRaWanDeviceModelsClientService.Object); Services.Add(new ServiceDescriptor(typeof(IResizeObserver), new MockResizeObserver())); } + /* *============================= Azure tests=======================**/ [Test] public void ClickOnSaveShouldPostDeviceModelData() { @@ -65,6 +72,8 @@ public void ClickOnSaveShouldPostDeviceModelData() PropertyType = DevicePropertyType.Double }).ToArray(); + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "Azure" }); + var expectedModel = SetupMockDeviceModel(properties: expectedProperties); _ = this.mockDeviceModelsClientService.Setup(service => @@ -103,6 +112,8 @@ public void ClickOnSaveShouldProcessProblemDetailsExceptionIfIssueOccursWhenUpda PropertyType = DevicePropertyType.Double }).ToArray(); + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "Azure" }); + var expectedModel = SetupMockDeviceModel(properties: expectedProperties); _ = this.mockDeviceModelsClientService.Setup(service => @@ -135,6 +146,9 @@ public void ClickOnSaveShouldDisplaySnackbarIfValidationError() PropertyType = DevicePropertyType.Double }).ToArray(); + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "Azure" }); + + _ = SetupMockDeviceModel(properties: expectedProperties); _ = this.mockSnackbarService.Setup(c => c.Add(It.IsAny(), Severity.Error, It.IsAny>(), It.IsAny())).Returns((Snackbar)null); @@ -167,6 +181,9 @@ public void ClickOnAddPropertyShouldAddNewProperty() Name = Guid.NewGuid().ToString() }); + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "Azure" }); + + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModelModelProperties(this.mockModelId)) .ReturnsAsync(new List()); @@ -212,6 +229,9 @@ public void ClickOnAddPropertyShouldAddNewProperty() public void ClickOnRemovePropertyShouldRemoveTheProperty() { // Arrange + + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "Azure" }); + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModel(this.mockModelId)) .ReturnsAsync(new DeviceModelDto @@ -260,6 +280,9 @@ public void ClickOnRemovePropertyShouldRemoveTheProperty() public void WhenPresentModelDetailsShouldDisplayProperties() { // Arrange + + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "Azure" }); + var properties = Enumerable.Range(0, 10) .Select(_ => new DeviceProperty { @@ -298,6 +321,7 @@ public void WhenLoraFeatureIsDisabledModelDetailsShouldNotDisplayLoRaWANTab() // Arrange _ = SetupMockDeviceModel(); + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "Azure" }); // Act var cut = RenderComponent @@ -319,6 +343,8 @@ public void WhenLoraFeatureIsEnabledModelDetailsShouldDisplayLoRaWANTab() // Arrange _ = SetupMockLoRaWANDeviceModel(); + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "Azure" }); + // Act var cut = RenderComponent( ComponentParameter.CreateParameter(nameof(DeviceModelDetailPage.ModelID), this.mockModelId), @@ -341,6 +367,9 @@ public void OnInitializedShouldProcessProblemDetailsExceptionWhenIssueOccursOnGe // Arrange _ = SetupMockLoRaWANDeviceModelThrowingException(); + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "Azure" }); + + // Act var cut = RenderComponent( ComponentParameter.CreateParameter(nameof(DeviceModelDetailPage.ModelID), this.mockModelId), @@ -428,6 +457,8 @@ public void ReturnButtonMustNavigateToPreviousPage() // Arrange _ = SetupMockDeviceModel(); + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "Azure" }); + // Act var cut = RenderComponent(ComponentParameter.CreateParameter("ModelId", this.mockModelId )); var returnButton = cut.WaitForElement("#returnButton"); @@ -445,6 +476,8 @@ public void ClickOnDeleteShouldDisplayConfirmationDialogAndReturnIfAborted() // Arrange _ = SetupMockDeviceModel(); + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "Azure" }); + var cut = RenderComponent (ComponentParameter.CreateParameter(nameof(DeviceModelDetailPage.ModelID), this.mockModelId)); @@ -468,6 +501,85 @@ public void ClickOnDeleteShouldDisplayConfirmationDialogAndRedirectIfConfirmed() // Arrange _ = SetupMockDeviceModel(); + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "Azure" }); + + var mockDialogReference = MockRepository.Create(); + _ = mockDialogReference.Setup(c => c.Result).ReturnsAsync(DialogResult.Ok("Ok")); + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference.Object); + + // Act + var cut = RenderComponent + (ComponentParameter.CreateParameter(nameof(DeviceModelDetailPage.ModelID), this.mockModelId)); + + var deleteButton = cut.WaitForElement("#deleteButton"); + deleteButton.Click(); + + // Assert + cut.WaitForAssertion(() => Services.GetRequiredService().Uri.Should().EndWith("/device-models")); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + /* *============================= AWS tests=======================**/ + [Test] + public void ClickOnSaveShouldUpdateImage() + { + // Arrange + var content = new MultipartFormDataContent(); + + var thingTypeDto = new ThingTypeDto + { + ThingTypeID = Guid.NewGuid().ToString(), + ThingTypeName = Guid.NewGuid().ToString(), + ImageUrl = new Uri($"http://fake.local/{this.mockModelId}") + }; + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "AWS" }); + + + _ = this.mockThingTypeClientService.Setup(service => + service.GetThingType(mockModelId)) + .ReturnsAsync(thingTypeDto); + + _ = this.mockThingTypeClientService.Setup(service => + service.GetAvatarUrl(thingTypeDto.ThingTypeID)) + .ReturnsAsync(thingTypeDto.ImageUrl.ToString()); + + _ = this.mockSnackbarService.Setup(c => c.Add(It.IsAny(), Severity.Success, It.IsAny>(), It.IsAny())).Returns((Snackbar)null); + + // Act + var cut = RenderComponent + (ComponentParameter.CreateParameter(nameof(DeviceModelDetailPage.ModelID), this.mockModelId)); + var saveButton = cut.WaitForElement("#saveButton"); + + saveButton.Click(); + + // Assert + cut.WaitForAssertion(() => Services.GetRequiredService().Uri.Should().EndWith("/device-models")); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + + [Test] + public void ClickOnDeleteThingTypeShouldDisplayConfirmationDialogAndRedirectIfConfirmed() + { + // Arrange + var thingTypeDto = new ThingTypeDto + { + ThingTypeID = Guid.NewGuid().ToString(), + ThingTypeName = Guid.NewGuid().ToString(), + ImageUrl = new Uri($"http://fake.local/{this.mockModelId}") + }; + + _ = this.mockThingTypeClientService.Setup(service => + service.GetThingType(mockModelId)) + .ReturnsAsync(thingTypeDto); + + _ = this.mockThingTypeClientService.Setup(service => + service.GetAvatarUrl(thingTypeDto.ThingTypeID)) + .ReturnsAsync(thingTypeDto.ImageUrl.ToString()); + + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "AWS" }); + var mockDialogReference = MockRepository.Create(); _ = mockDialogReference.Setup(c => c.Result).ReturnsAsync(DialogResult.Ok("Ok")); _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) @@ -484,5 +596,45 @@ public void ClickOnDeleteShouldDisplayConfirmationDialogAndRedirectIfConfirmed() cut.WaitForAssertion(() => Services.GetRequiredService().Uri.Should().EndWith("/device-models")); cut.WaitForAssertion(() => MockRepository.VerifyAll()); } + + [Test] + public void ClickOnDeprecateButtonShouldDeprecateThingType() + { + // Arrange + var thingTypeDto = new ThingTypeDto + { + ThingTypeID = Guid.NewGuid().ToString(), + ThingTypeName = Guid.NewGuid().ToString(), + ImageUrl = new Uri($"http://fake.local/{this.mockModelId}"), + Deprecated = false + }; + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "AWS" }); + + _ = this.mockThingTypeClientService.Setup(service => + service.GetThingType(mockModelId)) + .ReturnsAsync(thingTypeDto); + + _ = this.mockThingTypeClientService.Setup(service => + service.GetAvatarUrl(thingTypeDto.ThingTypeID)) + .ReturnsAsync(thingTypeDto.ImageUrl.ToString()); + + _ = this.mockThingTypeClientService.Setup(service => + service.DeprecateThingType(thingTypeDto.ThingTypeID)).Returns(Task.CompletedTask); + + _ = this.mockSnackbarService.Setup(c => c.Add(It.IsAny(), Severity.Success, It.IsAny>(), It.IsAny())).Returns((Snackbar)null); + + // Act + var cut = RenderComponent + (ComponentParameter.CreateParameter(nameof(DeviceModelDetailPage.ModelID), this.mockModelId)); + var saveButton = cut.WaitForElement("#deprecateButton"); + + saveButton.Click(); + + // Assert + cut.WaitForAssertion(() => Services.GetRequiredService().Uri.Should().EndWith("/device-models")); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + } + } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/DevicesModels/DeviceModelListPageTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/DevicesModels/DeviceModelListPageTests.cs index ccc384d46..52afc036c 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/DevicesModels/DeviceModelListPageTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/DevicesModels/DeviceModelListPageTests.cs @@ -22,12 +22,15 @@ namespace AzureIoTHub.Portal.Tests.Unit.Client.Pages.DevicesModels using AzureIoTHub.Portal.Shared.Models.v10.Filters; using System.Collections.Generic; using System.Linq; + using AzureIoTHub.Portal.Client.Services.AWS; + using AzureIoTHub.Portal.Models.v10.AWS; [TestFixture] public class DeviceModelListPageTests : BlazorUnitTest { private Mock mockDialogService; private Mock mockDeviceModelsClientService; + private Mock mockThingTypeClientService; public override void Setup() { @@ -35,11 +38,14 @@ public override void Setup() this.mockDialogService = MockRepository.Create(); this.mockDeviceModelsClientService = MockRepository.Create(); + this.mockThingTypeClientService = MockRepository.Create(); _ = Services.AddSingleton(this.mockDialogService.Object); _ = Services.AddSingleton(this.mockDeviceModelsClientService.Object); + _ = Services.AddSingleton(this.mockThingTypeClientService.Object); } + /***============= Test for Azure ===============***/ [Test] public void WhenLoraFeatureDisableClickToItemShouldRedirectToDeviceDetailsPage() { @@ -49,7 +55,7 @@ public void WhenLoraFeatureDisableClickToItemShouldRedirectToDeviceDetailsPage() _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels(It.IsAny())) .ReturnsAsync(new PaginationResult { Items = new[] { new DeviceModelDto { ModelId = modelId, SupportLoRaFeatures = false } } }); - _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); + _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true, CloudProvider = "Azure" }); var cut = RenderComponent(); cut.WaitForAssertion(() => cut.Markup.Should().NotContain("Loading...")); @@ -71,7 +77,7 @@ public void WhenLoraFeatureEnableClickToItemShouldRedirectToLoRaDeviceDetailsPag _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels(It.IsAny())) .ReturnsAsync(new PaginationResult { Items = new[] { new DeviceModelDto { ModelId = modelId, SupportLoRaFeatures = true } } }); - _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); + _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true, CloudProvider = "Azure" }); var cut = RenderComponent(); cut.WaitForAssertion(() => cut.Markup.Should().NotContain("Loading...")); @@ -97,7 +103,7 @@ public void DeviceModelListPageRendersCorrectly() } }); - _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); + _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true, CloudProvider = "Azure" }); // Act var cut = RenderComponent(); @@ -122,7 +128,7 @@ public void WhenAddNewDeviceModelClickShouldNavigateToNewDeviceModelPage() _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels(It.IsAny())) .ReturnsAsync(new PaginationResult { Items = new[] { new DeviceModelDto { ModelId = deviceId, SupportLoRaFeatures = true } } }); - _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); + _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true, CloudProvider = "Azure" }); // Act var cut = RenderComponent(); @@ -140,7 +146,7 @@ public void LoadDeviceModelsShouldProcessProblemDetailsExceptionWhenIssueOccursO _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels(It.IsAny())) .ThrowsAsync(new ProblemDetailsException(new ProblemDetailsWithExceptionDetails())); - _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); + _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true, CloudProvider = "Azure" }); // Act var cut = RenderComponent(); @@ -159,7 +165,7 @@ public async Task WhenRefreshClickShouldReloadFromApi() _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels(It.IsAny())) .ReturnsAsync(new PaginationResult { Items = new[] { new DeviceModelDto { ModelId = deviceId, SupportLoRaFeatures = true } } }); - _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); + _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true, CloudProvider = "Azure" }); // Act var cut = RenderComponent(); @@ -185,7 +191,7 @@ public void ClickOnDeleteShouldDisplayConfirmationDialogAndReturnIfAborted() _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels(It.IsAny())) .ReturnsAsync(new PaginationResult { Items = new[] { new DeviceModelDto { ModelId = deviceId, SupportLoRaFeatures = true } } }); - _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); + _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true, CloudProvider = "Azure" }); var mockDialogReference = MockRepository.Create(); _ = mockDialogReference.Setup(c => c.Result).ReturnsAsync(DialogResult.Cancel()); @@ -211,7 +217,7 @@ public void ClickOnDeleteShouldDisplayConfirmationDialogAndReloadDeviceModelIfCo _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels(It.IsAny())) .ReturnsAsync(new PaginationResult { Items = new[] { new DeviceModelDto { ModelId = deviceId, SupportLoRaFeatures = true } } }); - _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); + _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true, CloudProvider = "Azure" }); var mockDialogReference = MockRepository.Create(); _ = mockDialogReference.Setup(c => c.Result).ReturnsAsync(DialogResult.Ok("Ok")); @@ -235,7 +241,7 @@ public void ClickOnSearchShouldSearchDeviceModels() // Arrange var expectedSearchText = Fixture.Create(); - _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); + _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true, CloudProvider = "Azure" }); _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels(It.Is(x => string.IsNullOrEmpty(x.SearchText)))).ReturnsAsync(new PaginationResult { @@ -273,7 +279,7 @@ public void ClickOnSearchShouldSearchDeviceModels() public void ClickOnSortLabel() { // Arrange - _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); + _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true, CloudProvider = "Azure" }); _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels(It.IsAny())) .ReturnsAsync(new PaginationResult @@ -305,7 +311,7 @@ public void ClickOnSortLabel() public void SortClickOnSortNameDescDeviceModelsSorted() { // Arrange - _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); + _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true, CloudProvider = "Azure" }); _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels(It.Is(x => string.IsNullOrEmpty(x.OrderBy.First())))) @@ -335,5 +341,130 @@ public void SortClickOnSortNameDescDeviceModelsSorted() // Assert cut.WaitForAssertion(() => MockRepository.VerifyAll()); } + + /***============= Test for AWS ===============***/ + [Test] + public void ThingTypeListPageRendersCorrectly() + { + // Arrange + _ = this.mockThingTypeClientService.Setup(service => service.GetThingTypes(It.IsAny())) + .ReturnsAsync(new PaginationResult + { + Items = new[] { + new ThingTypeDto { ThingTypeID = Guid.NewGuid().ToString() }, + new ThingTypeDto{ ThingTypeID = Guid.NewGuid().ToString() } + } + }); + + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "AWS" }); + + // Act + var cut = RenderComponent(); + var grid = cut.WaitForElement("div.mud-grid"); + + Assert.IsNotNull(cut.Markup); + Assert.IsNotNull(grid.InnerHtml); + cut.WaitForAssertion(() => Assert.AreEqual("Thing Types", cut.Find(".mud-typography-h6").TextContent)); + cut.WaitForAssertion(() => Assert.AreEqual(3, cut.FindAll("tr").Count)); + cut.WaitForAssertion(() => Assert.IsNotNull(cut.Find(".mud-table-container"))); + + // Assert + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public void WhenAddNewThingTypeClickShouldNavigateToNewDeviceModelPage() + { + // Arrange + var thingTypeId = Guid.NewGuid().ToString(); + + _ = this.mockThingTypeClientService.Setup(service => service.GetThingTypes(It.IsAny())) + .ReturnsAsync(new PaginationResult { Items = new[] { new ThingTypeDto { ThingTypeID = thingTypeId } } }); + + _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true, CloudProvider = "AWS" }); + + // Act + var cut = RenderComponent(); + cut.WaitForElement("#addDeviceModelButton").Click(); + cut.WaitForState(() => Services.GetRequiredService().Uri.EndsWith("device-models/new", StringComparison.OrdinalIgnoreCase)); + + // Assert + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public void SortClickOnSortNameThingTypeSorted() + { + // Arrange + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "AWS" }); + + _ = this.mockThingTypeClientService.Setup(service => + service.GetThingTypes(It.Is(x => string.IsNullOrEmpty(x.OrderBy.First())))) + .ReturnsAsync(new PaginationResult + { + Items = new List() + }); + _ = this.mockThingTypeClientService.Setup(service => + service.GetThingTypes(It.Is(x => "Name asc".Equals(x.OrderBy.First())))) + .ReturnsAsync(new PaginationResult + { + Items = new List() + }); + _ = this.mockThingTypeClientService.Setup(service => + service.GetThingTypes(It.Is(x => "Name desc".Equals(x.OrderBy.First())))) + .ReturnsAsync(new PaginationResult + { + Items = new List() + }); + + var cut = RenderComponent(); + + // Act + cut.WaitForElement("#NameLabel").Click(); + cut.WaitForElement("#NameLabel").Click(); + + // Assert + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public void ClickOnSearchShouldSearchThingTypes() + { + // Arrange + var expectedSearchText = Fixture.Create(); + + _ = Services.AddSingleton(new PortalSettings { CloudProvider = "AWS" }); + + _ = this.mockThingTypeClientService.Setup(service => service.GetThingTypes(It.Is(x => string.IsNullOrEmpty(x.SearchText)))).ReturnsAsync(new PaginationResult + { + Items = new List() + { + new(), + new() + } + }); + + _ = this.mockThingTypeClientService.Setup(service => service.GetThingTypes(It.Is(x => expectedSearchText.Equals(x.SearchText)))).ReturnsAsync(new PaginationResult + { + Items = new List() + { + new() + } + }); + + var cut = RenderComponent(); + + cut.WaitForAssertion(() => cut.Markup.Should().NotContain("Loading...")); + cut.WaitForAssertion(() => cut.FindAll("table tbody tr").Count.Should().Be(2)); + cut.WaitForElement("#searchText").Change(expectedSearchText); + + // Act + cut.WaitForElement("#searchButton").Click(); + + // Assert + cut.WaitForAssertion(() => cut.Markup.Should().NotContain("Loading...")); + cut.WaitForAssertion(() => cut.FindAll("table tbody tr").Count.Should().Be(1)); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } } } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Client/Services/AWS/ThingTypeClientServiceTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Client/Services/AWS/ThingTypeClientServiceTests.cs index 806304236..9072d155a 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Client/Services/AWS/ThingTypeClientServiceTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Client/Services/AWS/ThingTypeClientServiceTests.cs @@ -16,6 +16,9 @@ namespace AzureIoTHub.Portal.Tests.Unit.Client.Services.AWS using FluentAssertions; using AzureIoTHub.Portal.Tests.Unit.UnitTests.Helpers; + using AzureIoTHub.Portal.Shared.Models.v10.Filters; + using System.Linq; + [TestFixture] public class ThingTypeClientServiceTests : BlazorUnitTest { @@ -30,6 +33,38 @@ public override void Setup() this.thingTypeClientService = Services.GetRequiredService(); } + [Test] + public async Task GetThingTypesShouldReturnThingTypes() + { + // Arrange + var expectedThingTypes = new PaginationResult() + { + Items = Fixture.Build().CreateMany(3).ToList() + }; + + _ = MockHttpClient.When(HttpMethod.Get, "/api/aws/thingtypes?SearchText=&PageNumber=1&PageSize=10&OrderBy=") + .RespondJson(expectedThingTypes); + + var filter = new DeviceModelFilter + { + SearchText = string.Empty, + PageNumber = 1, + PageSize = 10, + OrderBy = new string[] + { + null + } + }; + + // Act + var result = await this.thingTypeClientService.GetThingTypes(filter); + + // Assert + _ = result.Should().BeEquivalentTo(expectedThingTypes); + MockHttpClient.VerifyNoOutstandingRequest(); + MockHttpClient.VerifyNoOutstandingExpectation(); + } + [Test] public async Task CreateThingTypeShouldCreateThingType() { @@ -42,12 +77,49 @@ public async Task CreateThingTypeShouldCreateThingType() _ = m.Content.Should().BeAssignableTo>(); var body = m.Content as ObjectContent; _ = body.Value.Should().BeEquivalentTo(thingType); + return true; }) .Respond(HttpStatusCode.Created); // Act - _ = await this.thingTypeClientService.CreateThingType(thingType); + var response = await thingTypeClientService.CreateThingType(thingType); + + // Assert + MockHttpClient.VerifyNoOutstandingRequest(); + MockHttpClient.VerifyNoOutstandingExpectation(); + + _ = response.Should().NotBeNull(); + } + + [Test] + public async Task DeprecateAThingTypeShoukdDeprecateThingType() + { + // Arrange + var thingTypeId = Fixture.Create(); + + _ = MockHttpClient.When(HttpMethod.Put, $"/api/aws/thingtypes/{thingTypeId}") + .Respond(HttpStatusCode.OK); + + // Act + await this.thingTypeClientService.DeprecateThingType(thingTypeId); + + // Assert + MockHttpClient.VerifyNoOutstandingRequest(); + MockHttpClient.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task DeleteAThingTypeShoukdDeprecateThingType() + { + // Arrange + var thingTypeId = Fixture.Create(); + + _ = MockHttpClient.When(HttpMethod.Delete, $"/api/aws/thingtypes/{thingTypeId}") + .Respond(HttpStatusCode.OK); + + // Act + await this.thingTypeClientService.DeleteThingType(thingTypeId); // Assert MockHttpClient.VerifyNoOutstandingRequest(); diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/ThingTypeRepositoryTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/AWS/ThingTypeRepositoryTests.cs similarity index 94% rename from src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/ThingTypeRepositoryTests.cs rename to src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/AWS/ThingTypeRepositoryTests.cs index 76d1bba99..ef4e542b6 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/ThingTypeRepositoryTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/AWS/ThingTypeRepositoryTests.cs @@ -1,13 +1,13 @@ // Copyright (c) CGI France. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace AzureIoTHub.Portal.Tests.Unit.Infrastructure.Repositories +namespace AzureIoTHub.Portal.Tests.Unit.Infrastructure.Repositories.AWS { using System.Linq; using System.Threading.Tasks; using AutoFixture; using AzureIoTHub.Portal.Domain.Entities.AWS; - using AzureIoTHub.Portal.Infrastructure.Repositories; + using AzureIoTHub.Portal.Infrastructure.Repositories.AWS; using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases; using FluentAssertions; using NUnit.Framework; diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/AWS/ThingTypeSearchableAttributesRepositoryTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/AWS/ThingTypeSearchableAttributesRepositoryTests.cs new file mode 100644 index 000000000..ca9153841 --- /dev/null +++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/AWS/ThingTypeSearchableAttributesRepositoryTests.cs @@ -0,0 +1,43 @@ +// 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.Infrastructure.Repositories.AWS +{ + using System.Linq; + using System.Threading.Tasks; + using AutoFixture; + using AzureIoTHub.Portal.Domain.Entities.AWS; + using AzureIoTHub.Portal.Infrastructure.Repositories.AWS; + using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases; + using FluentAssertions; + using NUnit.Framework; + + public class ThingTypeSearchableAttributesRepositoryTests : BackendUnitTest + { + private ThingTypeSearchableAttributeRepository thingTypeSearcahbleAttrRepository; + + public override void Setup() + { + base.Setup(); + + this.thingTypeSearcahbleAttrRepository = new ThingTypeSearchableAttributeRepository(DbContext); + } + + [Test] + public async Task GetAllShouldReturnExpectedThingTypelCommands() + { + // Arrange + var expectedThingTypeSearchableAttr = Fixture.CreateMany(5).ToList(); + + await DbContext.AddRangeAsync(expectedThingTypeSearchableAttr); + + _ = await DbContext.SaveChangesAsync(); + + // Act + var result = this.thingTypeSearcahbleAttrRepository.GetAll().ToList(); + + // Assert + _ = result.Should().BeEquivalentTo(expectedThingTypeSearchableAttr); + } + } +} diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/AWS/ThingTypeTagRepositoryTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/AWS/ThingTypeTagRepositoryTests.cs new file mode 100644 index 000000000..5b76b488a --- /dev/null +++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/AWS/ThingTypeTagRepositoryTests.cs @@ -0,0 +1,43 @@ +// 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.Infrastructure.Repositories.AWS +{ + using System.Linq; + using System.Threading.Tasks; + using AutoFixture; + using AzureIoTHub.Portal.Domain.Entities.AWS; + using AzureIoTHub.Portal.Infrastructure.Repositories.AWS; + using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases; + using FluentAssertions; + using NUnit.Framework; + + public class ThingTypeTagRepositoryTests : BackendUnitTest + { + private ThingTypeTagRepository thingTypeTagRepository; + + public override void Setup() + { + base.Setup(); + + this.thingTypeTagRepository = new ThingTypeTagRepository(DbContext); + } + + [Test] + public async Task GetAllShouldReturnExpectedThingTypelCommands() + { + // Arrange + var expectedThingTypeTag = Fixture.CreateMany(5).ToList(); + + await DbContext.AddRangeAsync(expectedThingTypeTag); + + _ = await DbContext.SaveChangesAsync(); + + // Act + var result = this.thingTypeTagRepository.GetAll().ToList(); + + // Assert + _ = result.Should().BeEquivalentTo(expectedThingTypeTag); + } + } +} diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Services/AWS_Tests/ThingTypeServiceTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Services/AWS_Tests/ThingTypeServiceTest.cs index 703b7a52a..e093fbd83 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Services/AWS_Tests/ThingTypeServiceTest.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Services/AWS_Tests/ThingTypeServiceTest.cs @@ -5,6 +5,8 @@ namespace AzureIoTHub.Portal.Tests.Unit.Infrastructure.Services.AWS_Tests { using System; using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -18,8 +20,10 @@ namespace AzureIoTHub.Portal.Tests.Unit.Infrastructure.Services.AWS_Tests using AzureIoTHub.Portal.Domain.Entities.AWS; using AzureIoTHub.Portal.Domain.Exceptions; using AzureIoTHub.Portal.Domain.Repositories; + using AzureIoTHub.Portal.Domain.Repositories.AWS; using AzureIoTHub.Portal.Infrastructure.Services.AWS; using AzureIoTHub.Portal.Models.v10.AWS; + using AzureIoTHub.Portal.Shared.Models.v10.Filters; using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases; using FluentAssertions; using Microsoft.AspNetCore.Http; @@ -33,6 +37,8 @@ public class ThingTypeServiceTest : BackendUnitTest { private Mock mockThingTypeRepository; + private Mock mockThingTypeTagRepository; + private Mock mockThingTypeSearchableAttrRepository; private Mock mockUnitOfWork; private Mock amazonIotClient; private Mock mockDeviceModelImageManager; @@ -45,11 +51,15 @@ public void SetUp() { base.Setup(); this.mockThingTypeRepository = MockRepository.Create(); + this.mockThingTypeTagRepository = MockRepository.Create(); + this.mockThingTypeSearchableAttrRepository = MockRepository.Create(); this.mockUnitOfWork = MockRepository.Create(); this.amazonIotClient = MockRepository.Create(); this.mockDeviceModelImageManager = MockRepository.Create(); _ = ServiceCollection.AddSingleton(this.mockThingTypeRepository.Object); + _ = ServiceCollection.AddSingleton(this.mockThingTypeTagRepository.Object); + _ = ServiceCollection.AddSingleton(this.mockThingTypeSearchableAttrRepository.Object); _ = ServiceCollection.AddSingleton(this.amazonIotClient.Object); _ = ServiceCollection.AddSingleton(this.mockUnitOfWork.Object); _ = ServiceCollection.AddSingleton(this.mockUnitOfWork.Object); @@ -62,6 +72,53 @@ public void SetUp() Mapper = Services.GetRequiredService(); } + [Test] + public async Task GetThingTypesShouldReturnExpectedThingTypes() + { + // Arrange + var expectedThingTypes = Fixture.CreateMany(3).ToList(); + var expectedImageUri = Fixture.Create(); + var expectedThingTypeDto = expectedThingTypes.Select(thingType => + { + var thingTypeDto = Mapper.Map(thingType); + thingTypeDto.ImageUrl = expectedImageUri; + return thingTypeDto; + }).ToList(); + + var filter = new DeviceModelFilter + { + SearchText = Fixture.Create(), + PageNumber = 1, + PageSize = 10, + OrderBy = new string[] + { + null + } + }; + + _ = this.mockThingTypeRepository.Setup(repository => repository.GetPaginatedListAsync(filter.PageNumber, filter.PageSize, filter.OrderBy, It.IsAny>>(), It.IsAny(), It.IsAny>[]>())) + .ReturnsAsync(new Shared.Models.v1._0.PaginatedResult + { + Data = expectedThingTypes, + PageSize = filter.PageSize, + CurrentPage = filter.PageNumber, + TotalCount = 10 + }); + + _ = this.mockDeviceModelImageManager.Setup(manager => manager.ComputeImageUri(It.IsAny())) + .Returns(expectedImageUri); + + // Act + var result = await this.thingTypeService.GetThingTypes(filter); + + // Assert + _ = result.Data.Should().BeEquivalentTo(expectedThingTypeDto); + _ = result.CurrentPage.Should().Be(filter.PageNumber); + _ = result.PageSize.Should().Be(filter.PageSize); + _ = result.TotalCount.Should().Be(10); + MockRepository.VerifyAll(); + } + [Test] public async Task CreateAThingTypeShouldReturnAValue() { @@ -202,6 +259,116 @@ public void CreateThingTypeShouldThrowError500WhenCreateThigFails() MockRepository.VerifyAll(); } + [Test] + public async Task GetAThingTypeShouldReturnAValue() + { + // Arrange + var thingTypeID = Fixture.Create(); + var expectedAvatarUrl = Fixture.Create(); + + var thingType = Fixture.Create(); + + _ = this.mockThingTypeRepository.Setup(repository => repository.GetByIdAsync(thingTypeID, d => d.Tags, d => d.ThingTypeSearchableAttributes)).ReturnsAsync(thingType); + _ = this.mockDeviceModelImageManager.Setup(manager => manager.ComputeImageUri(thingTypeID)) + .Returns(expectedAvatarUrl); + + + //Act + var result = await this.thingTypeService.GetThingType(thingTypeID); + + //Assert + _ = result.Should().NotBeNull(); + Assert.AreEqual(expectedAvatarUrl, result.ImageUrl); + MockRepository.VerifyAll(); + } + + [Test] + public async Task DeprecateAThingTypeShouldReturnAValue() + { + // Arrange + var thingTypeID = Fixture.Create(); + + var thingType = Fixture.Create(); + + _ = this.amazonIotClient.Setup(s3 => s3.DeprecateThingTypeAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new DeprecateThingTypeResponse + { + HttpStatusCode = HttpStatusCode.OK + }); + + _ = this.mockThingTypeRepository.Setup(repository => repository.GetByIdAsync(thingTypeID, d => d.Tags, d => d.ThingTypeSearchableAttributes)).ReturnsAsync(thingType); + _ = this.mockThingTypeRepository.Setup(repository => repository.Update(It.IsAny())); + + _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) + .Returns(Task.CompletedTask); + + + //Act + var result = await this.thingTypeService.DeprecateThingType(thingTypeID); + + //Assert + _ = result.Deprecated.Should().Be(true); + + MockRepository.VerifyAll(); + } + + [Test] + public async Task DeleteAThingTypeShouldReturnAValue() + { + // Arrange + var thingTypeID = Fixture.Create(); + + var thingType = new ThingType + { + Id = thingTypeID, + Name = Fixture.Create(), + Description = Fixture.Create(), + Deprecated = true, + Tags = new List + { + new ThingTypeTag {Key = Fixture.Create(), Value = Fixture.Create()} + }, + ThingTypeSearchableAttributes = new List + { + new ThingTypeSearchableAtt {Name = Fixture.Create()} + } + }; + + + _ = this.amazonIotClient.Setup(s3 => s3.DeleteThingTypeAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new DeleteThingTypeResponse + { + HttpStatusCode = HttpStatusCode.OK + }); + + _ = this.mockThingTypeRepository.Setup(repository => repository.GetByIdAsync(thingTypeID, d => d.Tags, d => d.ThingTypeSearchableAttributes)).ReturnsAsync(thingType); + + foreach (var tag in thingType.Tags) + { + _ = this.mockThingTypeTagRepository.Setup(repository => repository.Delete(tag.Id)); + + } + foreach (var search in thingType.ThingTypeSearchableAttributes) + { + _ = this.mockThingTypeSearchableAttrRepository.Setup(repository => repository.Delete(search.Id)); + + } + + _ = this.mockThingTypeRepository.Setup(repository => repository.Delete(thingType.Id)); + + _ = this.mockDeviceModelImageManager.Setup(manager => manager.DeleteDeviceModelImageAsync(thingTypeID)).Returns(Task.CompletedTask); + + _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) + .Returns(Task.CompletedTask); + + + //Act + await this.thingTypeService.DeleteThingType(thingTypeID); + + //Assert + MockRepository.VerifyAll(); + } + [Test] public async Task GetThingTypeAvatarShouldReturnThingTypeAvatar() { diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/AWS/ThingTypeControllerTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/AWS/ThingTypeControllerTest.cs index 7cbae7af1..1d6674db4 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/AWS/ThingTypeControllerTest.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/AWS/ThingTypeControllerTest.cs @@ -16,6 +16,10 @@ namespace AzureIoTHub.Portal.Tests.Unit.Server.Controllers.v1._0.AWS using FluentAssertions; using AutoFixture; using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases; + using AzureIoTHub.Portal.Shared.Models.v1._0; + using AzureIoTHub.Portal.Shared.Models.v10.Filters; + using Microsoft.AspNetCore.Mvc.Routing; + using System.Linq; [TestFixture] public class ThingTypeControllerTest : BackendUnitTest @@ -37,6 +41,46 @@ public void SetUp() this.mockThingTypeRepository = this.mockRepository.Create(); } + [Test] + public async Task GetListGreaterThan10ShouldReturnAListAndNextPage() + { + // Arrange + var exepectedThingTypes = Fixture.CreateMany(24).ToList(); + var thingTypeController = CreateThingTypeController(); + + var filter = new DeviceModelFilter + { + SearchText = string.Empty, + PageNumber = 0, + PageSize = 10, + OrderBy = new string[] + { + null + } + }; + + _ = this.mockThingTypeService.Setup(service => service.GetThingTypes(filter)) + .ReturnsAsync((DeviceModelFilter filter) => new PaginatedResult + { + Data = exepectedThingTypes.Skip(filter.PageSize * filter.PageNumber).Take(filter.PageSize).ToList(), + TotalCount = exepectedThingTypes.Count + }); + + var locationUrl = "http://location/aws/thingtypes"; + + _ = this.mockUrlHelper + .Setup(x => x.RouteUrl(It.IsAny())) + .Returns(locationUrl); + + // Act + var response = await thingTypeController.GetThingTypes(filter); + + // Assert + _ = ((OkObjectResult)response.Result)?.StatusCode.Should().Be(200); + MockRepository.VerifyAll(); + + } + private ThingTypeController CreateThingTypeController() { return new ThingTypeController(this.mockThingTypeService.Object) @@ -127,5 +171,26 @@ public async Task CreateAThingTypeShouldReturnOK() this.mockRepository.VerifyAll(); } + + + [Test] + public async Task DeprecateAThingTypeShouldReturnOK() + { + // Arrange + var thingTypeController = CreateThingTypeController(); + var thingTypeId = Fixture.Create(); + + _ = this.mockThingTypeService + .Setup(x => x.DeprecateThingType(thingTypeId)).ReturnsAsync(new ThingTypeDto { ThingTypeID = thingTypeId, Deprecated = true }); + + // Act + var response = await thingTypeController.DeprecateThingTypeAsync(thingTypeId); + + // Assert + + _ = ((OkObjectResult)response.Result).StatusCode.Should().Be(200); + + this.mockRepository.VerifyAll(); + } } }