From 004a85b9e3d261f6c68d363bf2c38f52756363e5 Mon Sep 17 00:00:00 2001 From: Kevin BEAUGRAND <9513635+kbeaugrand@users.noreply.github.com> Date: Mon, 14 Feb 2022 17:18:36 +0100 Subject: [PATCH] Introduce bultin device models (with builtin commands) (#259) * fix #233 - Add builtin fields on device models and models commands * Update API's descriptions * fix #234 - Update device model forms to add readonly fields for builtins * Add unit tests to device model controller --- .../DeviceModelCommandsControllerTests.cs | 209 +++++++ .../v1.0/DeviceModelsControllerTests.cs | 570 ++++++++++++++++++ .../DeviceModelCommandsManagerTests.cs | 4 +- .../DeviceModels/DeviceModelDetailPage.razor | 22 +- .../DeviceModels/DeviceModelListPage.razor | 2 +- .../v1.0/DeviceModelCommandsController.cs | 106 +++- .../v1.0/DeviceModelsController.cs | 308 ++++++++-- .../Managers/DeviceModelCommandsManager.cs | 16 +- .../Mappers/DeviceModelCommandMapper.cs | 14 +- .../Server/Mappers/DeviceModelMapper.cs | 2 + .../Shared/Models/v1.0/DeviceModel.cs | 53 ++ .../Shared/Models/v1.0/DeviceModelCommand.cs | 26 + 12 files changed, 1238 insertions(+), 94 deletions(-) create mode 100644 src/AzureIoTHub.Portal.Server.Tests/Controllers/v1.0/DeviceModelCommandsControllerTests.cs create mode 100644 src/AzureIoTHub.Portal.Server.Tests/Controllers/v1.0/DeviceModelsControllerTests.cs diff --git a/src/AzureIoTHub.Portal.Server.Tests/Controllers/v1.0/DeviceModelCommandsControllerTests.cs b/src/AzureIoTHub.Portal.Server.Tests/Controllers/v1.0/DeviceModelCommandsControllerTests.cs new file mode 100644 index 000000000..6385a8ad2 --- /dev/null +++ b/src/AzureIoTHub.Portal.Server.Tests/Controllers/v1.0/DeviceModelCommandsControllerTests.cs @@ -0,0 +1,209 @@ +using Azure; +using Azure.Data.Tables; +using AzureIoTHub.Portal.Server.Controllers.V10; +using AzureIoTHub.Portal.Server.Factories; +using AzureIoTHub.Portal.Server.Mappers; +using AzureIoTHub.Portal.Shared.Models.V10; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AzureIoTHub.Portal.Server.Tests.Controllers.V10 +{ + [TestFixture] + public class DeviceModelCommandsControllerTests + { + private MockRepository mockRepository; + + private Mock mockDeviceModelCommandMapper; + private Mock mockTableClientFactory; + private Mock mockDeviceTemplatesTableClient; + private Mock mockCommandsTableClient; + private Mock> mockLogger; + + [SetUp] + public void SetUp() + { + this.mockRepository = new MockRepository(MockBehavior.Strict); + + this.mockLogger = this.mockRepository.Create>(); + this.mockDeviceModelCommandMapper = this.mockRepository.Create(); + this.mockTableClientFactory = this.mockRepository.Create(); + this.mockDeviceTemplatesTableClient = this.mockRepository.Create(); + this.mockCommandsTableClient = this.mockRepository.Create(); + } + + private DeviceModelCommandsController CreateDeviceModelCommandsController() + { + return new DeviceModelCommandsController( + this.mockLogger.Object, + this.mockDeviceModelCommandMapper.Object, + this.mockTableClientFactory.Object); + } + + [Test] + public async Task Post_Should_Create_Command() + { + // Arrange + var deviceModelCommandsController = this.CreateDeviceModelCommandsController(); + var entity = this.SetupMockDeviceModel(); + + DeviceModelCommand command = new DeviceModelCommand + { + Name = Guid.NewGuid().ToString() + }; + + var mockResponse = this.mockRepository.Create(); + + this.mockDeviceModelCommandMapper.Setup(c => c.UpdateTableEntity( + It.Is(x => x.RowKey == command.Name && x.PartitionKey == entity.RowKey), + It.Is(x => x == command))); + + this.mockCommandsTableClient.Setup(c => c.AddEntityAsync( + It.Is(x => x.RowKey == command.Name && x.PartitionKey == entity.RowKey), + It.IsAny())) + .ReturnsAsync(mockResponse.Object); + + this.mockTableClientFactory.Setup(c => c.GetDeviceCommands()) + .Returns(mockCommandsTableClient.Object); + + // Act + var result = await deviceModelCommandsController.Post(entity.RowKey, command); + + // Assert + Assert.IsNotNull(result); + Assert.IsAssignableFrom(result); + + this.mockTableClientFactory.Verify(c => c.GetDeviceTemplates(), Times.Once()); + this.mockDeviceTemplatesTableClient.Verify(c => c.GetEntity( + It.Is(p => p == DeviceModelsController.DefaultPartitionKey), + It.Is(k => k == entity.RowKey), + It.IsAny>(), + It.IsAny()), Times.Once); + + this.mockTableClientFactory.Verify(c => c.GetDeviceCommands(), Times.Once()); + this.mockDeviceModelCommandMapper.VerifyAll(); + this.mockCommandsTableClient.Verify(c => c.AddEntityAsync( + It.Is(x => x.RowKey == command.Name && x.PartitionKey == entity.RowKey), + It.IsAny()), Times.Once()); + } + + [Test] + public async Task When_Model_NotExists_Post_Should_Return_404() + { + // Arrange + var deviceModelCommandsController = this.CreateDeviceModelCommandsController(); + + SetupNotFoundDeviceModel(); + + // Act + var result = await deviceModelCommandsController.Post(Guid.NewGuid().ToString(), new DeviceModelCommand()); + + // Assert + Assert.IsNotNull(result); + Assert.IsAssignableFrom(result); + + this.mockRepository.VerifyAll(); + } + + [Test] + public async Task Delete_Should_Delete_Command() + { + // Arrange + var deviceModelCommandsController = this.CreateDeviceModelCommandsController(); + var entity = this.SetupMockDeviceModel(); + string commandId = Guid.NewGuid().ToString(); + var mockResponse = this.mockRepository.Create(); + + this.mockCommandsTableClient.Setup(c => c.DeleteEntityAsync( + It.Is(x => x == entity.RowKey), + It.Is(x => x == commandId), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(mockResponse.Object); + + this.mockTableClientFactory + .Setup(c => c.GetDeviceCommands()) + .Returns(mockCommandsTableClient.Object); + + // Act + var result = await deviceModelCommandsController.Delete(entity.RowKey, commandId); + + // Assert + Assert.IsNotNull(result); + Assert.IsAssignableFrom(result); + + this.mockTableClientFactory.Verify(c => c.GetDeviceTemplates(), Times.Once()); + this.mockDeviceTemplatesTableClient.Verify(c => c.GetEntity( + It.Is(p => p == DeviceModelsController.DefaultPartitionKey), + It.Is(k => k == entity.RowKey), + It.IsAny>(), + It.IsAny()), Times.Once); + + this.mockCommandsTableClient.Verify(c => c.DeleteEntityAsync( + It.Is(x => x == entity.RowKey), + It.Is(x => x == commandId), + It.IsAny(), + It.IsAny()), Times.Once); + } + + [Test] + public async Task When_Model_NotExists_Delete_Should_Return_404() + { + // Arrange + var deviceModelCommandsController = this.CreateDeviceModelCommandsController(); + + SetupNotFoundDeviceModel(); + + // Act + var result = await deviceModelCommandsController.Delete(Guid.NewGuid().ToString(), Guid.NewGuid().ToString()); + + // Assert + Assert.IsNotNull(result); + Assert.IsAssignableFrom(result); + + this.mockRepository.VerifyAll(); + } + + private TableEntity SetupMockDeviceModel() + { + var mockResponse = this.mockRepository.Create>(); + var modelId = Guid.NewGuid().ToString(); + var entity = new TableEntity(DeviceModelsController.DefaultPartitionKey, modelId); + + mockResponse.Setup(c => c.Value) + .Returns(entity); + + this.mockDeviceTemplatesTableClient.Setup(c => c.GetEntity( + It.Is(p => p == DeviceModelsController.DefaultPartitionKey), + It.Is(k => k == modelId), + It.IsAny>(), + It.IsAny())) + .Returns(mockResponse.Object); + + this.mockTableClientFactory.Setup(c => c.GetDeviceTemplates()) + .Returns(mockDeviceTemplatesTableClient.Object); + + return entity; + } + + private void SetupNotFoundDeviceModel() + { + this.mockDeviceTemplatesTableClient.Setup(c => c.GetEntity( + It.Is(p => p == DeviceModelsController.DefaultPartitionKey), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Throws(new RequestFailedException(StatusCodes.Status404NotFound, "Not Found")); + + this.mockTableClientFactory.Setup(c => c.GetDeviceTemplates()) + .Returns(mockDeviceTemplatesTableClient.Object); + } + } +} diff --git a/src/AzureIoTHub.Portal.Server.Tests/Controllers/v1.0/DeviceModelsControllerTests.cs b/src/AzureIoTHub.Portal.Server.Tests/Controllers/v1.0/DeviceModelsControllerTests.cs new file mode 100644 index 000000000..4ccecffeb --- /dev/null +++ b/src/AzureIoTHub.Portal.Server.Tests/Controllers/v1.0/DeviceModelsControllerTests.cs @@ -0,0 +1,570 @@ +using Azure; +using Azure.Data.Tables; +using AzureIoTHub.Portal.Server.Controllers.V10; +using AzureIoTHub.Portal.Server.Factories; +using AzureIoTHub.Portal.Server.Managers; +using AzureIoTHub.Portal.Server.Mappers; +using AzureIoTHub.Portal.Server.Services; +using AzureIoTHub.Portal.Shared.Models.V10; +using Microsoft.AspNetCore.Http; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using System.IO; +using Microsoft.AspNetCore.Mvc; +using System.Linq.Expressions; +using Microsoft.Azure.Devices.Shared; + +namespace AzureIoTHub.Portal.Server.Tests.Controllers.V10 +{ + [TestFixture] + public class DeviceModelsControllerTests + { + private MockRepository mockRepository; + + private Mock> mockLogger; + private Mock mockDeviceModelImageManager; + private Mock mockDeviceModelCommandMapper; + private Mock mockDeviceModelMapper; + private Mock mockDeviceService; + private Mock mockTableClientFactory; + private Mock mockDeviceTemplatesTableClient; + private Mock mockCommandsTableClient; + + [SetUp] + public void SetUp() + { + this.mockRepository = new MockRepository(MockBehavior.Strict); + + this.mockLogger = this.mockRepository.Create>(); + this.mockDeviceModelImageManager = this.mockRepository.Create(); + this.mockDeviceModelCommandMapper = this.mockRepository.Create(); + this.mockDeviceModelMapper = this.mockRepository.Create(); + this.mockDeviceService = this.mockRepository.Create(); + this.mockTableClientFactory = this.mockRepository.Create(); + this.mockDeviceTemplatesTableClient = this.mockRepository.Create(); + this.mockCommandsTableClient = this.mockRepository.Create(); + } + + private DeviceModelsController CreateDeviceModelsController() + { + var result = new DeviceModelsController( + this.mockLogger.Object, + this.mockDeviceModelImageManager.Object, + this.mockDeviceModelCommandMapper.Object, + this.mockDeviceModelMapper.Object, + this.mockDeviceService.Object, + this.mockTableClientFactory.Object); + + return result; + } + + [Test] + public void GetList_Should_Return_A_List() + { + // Arrange + var deviceModelsController = this.CreateDeviceModelsController(); + var returnedIndex = 10; + + var mockTable = this.mockRepository.Create(); + var mockTableResponse = this.mockRepository.Create>(); + var mockEnumerator = this.mockRepository.Create>(); + mockEnumerator.Setup(x => x.Dispose()).Callback(() => { }); + mockEnumerator.Setup(x => x.MoveNext()).Returns(() => + { + return returnedIndex-- >= 0; + }); + + mockEnumerator.Setup(x => x.Current) + .Returns(new TableEntity(Guid.NewGuid().ToString(), Guid.NewGuid().ToString())); + + mockTableResponse.Setup(x => x.GetEnumerator()) + .Returns(mockEnumerator.Object); + + this.mockDeviceTemplatesTableClient.Setup(c => c.Query(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) + .Returns(mockTableResponse.Object); + + this.mockTableClientFactory.Setup(c => c.GetDeviceTemplates()) + .Returns(mockDeviceTemplatesTableClient.Object); + + this.mockDeviceModelMapper.Setup(c => c.CreateDeviceModel(It.IsAny())) + .Returns((TableEntity entity) => new DeviceModel()); + + // Act + var result = deviceModelsController.Get(); + + // Assert + Assert.IsNotNull(result); + Assert.IsNotEmpty(result); + Assert.AreEqual(10, result.Count()); + + this.mockRepository.VerifyAll(); + } + + [Test] + public void GetItem_Should_Return_A_Value() + { + // Arrange + var deviceModelsController = this.CreateDeviceModelsController(); + + var mockResponse = this.mockRepository.Create>(); + mockResponse.Setup(c => c.Value).Returns(new TableEntity(Guid.NewGuid().ToString(), Guid.NewGuid().ToString())); + + this.mockDeviceTemplatesTableClient.Setup(c => c.GetEntity( + It.Is(p => p == DeviceModelsController.DefaultPartitionKey), + It.Is(k => k == "test"), + It.IsAny>(), + It.IsAny())) + .Returns(mockResponse.Object); + + this.mockTableClientFactory.Setup(c => c.GetDeviceTemplates()) + .Returns(mockDeviceTemplatesTableClient.Object); + + this.mockDeviceModelMapper.Setup(c => c.CreateDeviceModel(It.IsAny())) + .Returns((TableEntity entity) => new DeviceModel()); + + // Act + var result = deviceModelsController.Get("test"); + + // Assert + Assert.IsNotNull(result); + + this.mockRepository.VerifyAll(); + } + + [Test] + public void When_Model_NotExists_GetItem_Should_Return_404() + { + // Arrange + var deviceModelsController = this.CreateDeviceModelsController(); + + SetupNotFoundEntity(); + + // Act + var result = deviceModelsController.Get(Guid.NewGuid().ToString()); + + // Assert + Assert.IsNotNull(result); + Assert.IsAssignableFrom(result); + + this.mockTableClientFactory.Verify(c => c.GetDeviceTemplates(), Times.Once); + this.mockDeviceModelImageManager.VerifyAll(); + } + + [Test] + public void GetAvatar_Should_Return_The_Computed_Model_Avatar_Uri() + { + // Arrange + var deviceModelsController = this.CreateDeviceModelsController(); + var entity = SetupMockEntity(); + + var expectedUrl = $"http://fake.local/{entity.RowKey}"; + + this.mockDeviceModelImageManager.Setup(c => c.ComputeImageUri(It.Is(x => x == entity.RowKey))) + .Returns(expectedUrl); + + // Act + var result = deviceModelsController.GetAvatar(entity.RowKey); + + // Assert + Assert.IsNotNull(result); + Assert.IsAssignableFrom(result); + var okObjectResult = result as OkObjectResult; + + Assert.IsNotNull(okObjectResult); + Assert.AreEqual(200, okObjectResult.StatusCode); + + Assert.IsNotNull(okObjectResult.Value); + Assert.AreEqual(expectedUrl, okObjectResult.Value.ToString()); + + this.mockTableClientFactory.Verify(c => c.GetDeviceTemplates(), Times.Once); + this.mockDeviceModelImageManager.VerifyAll(); + } + + [Test] + public void When_Model_NotExists_GetAvatar_Should_Return_404() + { + // Arrange + var deviceModelsController = this.CreateDeviceModelsController(); + + SetupNotFoundEntity(); + + // Act + var result = deviceModelsController.GetAvatar(Guid.NewGuid().ToString()); + + // Assert + Assert.IsNotNull(result); + Assert.IsAssignableFrom(result); + + this.mockRepository.VerifyAll(); + } + + [Test] + public async Task ChangeAvatar_Should_Change_Model_Image_Stream() + { + // Arrange + var deviceModelsController = this.CreateDeviceModelsController(); + var entity = SetupMockEntity(); + + var mockFile = this.mockRepository.Create(); + var expectedUrl = $"http://fake.local/{entity.RowKey}"; + + using var imageStream = new MemoryStream(); + + mockFile.Setup(c => c.OpenReadStream()) + .Returns(imageStream); + + this.mockDeviceModelImageManager.Setup(c => c.ChangeDeviceModelImageAsync( + It.Is(x => x == entity.RowKey), + It.Is(x => x == imageStream))) + .ReturnsAsync(expectedUrl); + + // Act + var result = await deviceModelsController.ChangeAvatar(entity.RowKey, mockFile.Object); + + // Assert + Assert.IsNotNull(result); + Assert.IsAssignableFrom(result); + var okObjectResult = result as OkObjectResult; + + Assert.IsNotNull(okObjectResult); + Assert.AreEqual(200, okObjectResult.StatusCode); + + Assert.IsNotNull(okObjectResult.Value); + Assert.AreEqual(expectedUrl, okObjectResult.Value.ToString()); + + this.mockTableClientFactory.Verify(c => c.GetDeviceTemplates(), Times.Once); + this.mockDeviceModelImageManager.VerifyAll(); + } + + [Test] + public async Task When_Model_NotExists_ChangeAvatar_Should_Return_404() + { + // Arrange + var deviceModelsController = this.CreateDeviceModelsController(); + + SetupNotFoundEntity(); + + var mockFile = this.mockRepository.Create(); + + // Act + var result = await deviceModelsController.ChangeAvatar(Guid.NewGuid().ToString(), mockFile.Object); + + // Assert + Assert.IsNotNull(result); + Assert.IsAssignableFrom(result); + + this.mockRepository.VerifyAll(); + } + + [Test] + public async Task DeleteAvatar_Should_Remove_Model_Image() + { + // Arrange + var deviceModelsController = this.CreateDeviceModelsController(); + var entity = SetupMockEntity(); + + this.mockDeviceModelImageManager.Setup(c => c.DeleteDeviceModelImageAsync( + It.Is(x => x == entity.RowKey))) + .Returns(Task.CompletedTask); + + // Act + var result = await deviceModelsController.DeleteAvatar(entity.RowKey); + + // Assert + Assert.IsNotNull(result); + Assert.IsAssignableFrom(result); + + this.mockTableClientFactory.Verify(c => c.GetDeviceTemplates(), Times.Once); + this.mockDeviceModelImageManager.VerifyAll(); + } + + [Test] + public async Task When_Model_NotExists_DeleteAvatar_Should_Return_404() + { + // Arrange + var deviceModelsController = this.CreateDeviceModelsController(); + + SetupNotFoundEntity(); + + var mockFile = this.mockRepository.Create(); + + // Act + var result = await deviceModelsController.DeleteAvatar(Guid.NewGuid().ToString()); + + // Assert + Assert.IsNotNull(result); + Assert.IsAssignableFrom(result); + + this.mockRepository.VerifyAll(); + } + + [Test] + public async Task Post_Should_Create_A_New_Entity() + { + // Arrange + var deviceModelsController = this.CreateDeviceModelsController(); + SetupNotFoundEntity(); + + var requestModel = new DeviceModel + { + ModelId = Guid.NewGuid().ToString() + }; + + var mockResponse = this.mockRepository.Create(); + var mockTableResponse = this.mockRepository.Create>(); + var mockEnumerator = this.mockRepository.Create>(); + mockEnumerator.Setup(x => x.Dispose()).Callback(() => { }); + mockEnumerator.Setup(x => x.MoveNext()).Returns(() => false); + mockTableResponse.Setup(x => x.GetEnumerator()).Returns(mockEnumerator.Object); + + mockCommandsTableClient.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockTableResponse.Object); + + this.mockDeviceTemplatesTableClient.Setup(c => c.UpsertEntityAsync( + It.Is(x => x.RowKey == requestModel.ModelId && x.PartitionKey == DeviceModelsController.DefaultPartitionKey), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(mockResponse.Object); + + this.mockDeviceModelMapper.Setup(c => c.UpdateTableEntity( + It.Is(x => x.RowKey == requestModel.ModelId && x.PartitionKey == DeviceModelsController.DefaultPartitionKey), + It.IsAny())); + + this.mockTableClientFactory.Setup(c => c.GetDeviceCommands()) + .Returns(mockCommandsTableClient.Object); + + // Act + var result = await deviceModelsController.Post(requestModel); + + // Assert + Assert.IsNotNull(result); + Assert.IsAssignableFrom(result); + var okObjectResult = result as OkResult; + + Assert.IsNotNull(okObjectResult); + Assert.AreEqual(200, okObjectResult.StatusCode); + + this.mockRepository.VerifyAll(); + } + + [Test] + public async Task When_DeviceModelId_Exists_Post_Should_Return_BadRequest() + { + // Arrange + var deviceModelsController = this.CreateDeviceModelsController(); + var entity = SetupMockEntity(); + + var deviceModel = new DeviceModel + { + ModelId = entity.RowKey + }; + + // Act + var result = await deviceModelsController.Post(deviceModel); + // Assert + Assert.IsNotNull(result); + Assert.IsAssignableFrom(result); + } + + [Test] + public async Task Put_Should_Update_The_Device_Model() + { + // Arrange + var deviceModelsController = this.CreateDeviceModelsController(); + + var deviceModel = SetupMockEntity(); + var mockResponse = this.mockRepository.Create(); + + var requestModel = new DeviceModel + { + ModelId = deviceModel.RowKey + }; + + var mockTableResponse = this.mockRepository.Create>(); + var mockEnumerator = this.mockRepository.Create>(); + mockEnumerator.Setup(x => x.Dispose()).Callback(() => { }); + mockEnumerator.Setup(x => x.MoveNext()).Returns(() => false); + mockTableResponse.Setup(x => x.GetEnumerator()).Returns(mockEnumerator.Object); + + mockCommandsTableClient.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockTableResponse.Object); + + this.mockDeviceTemplatesTableClient.Setup(c => c.UpsertEntityAsync( + It.Is(x => x.RowKey == deviceModel.RowKey && x.PartitionKey == DeviceModelsController.DefaultPartitionKey), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(mockResponse.Object); + + this.mockDeviceModelMapper.Setup(c => c.UpdateTableEntity( + It.Is(x => x.RowKey == deviceModel.RowKey && x.PartitionKey == DeviceModelsController.DefaultPartitionKey), + It.Is(x => x == requestModel))); + + this.mockTableClientFactory.Setup(c => c.GetDeviceTemplates()) + .Returns(mockDeviceTemplatesTableClient.Object); + + this.mockTableClientFactory.Setup(c => c.GetDeviceCommands()) + .Returns(mockCommandsTableClient.Object); + + // Act + var result = await deviceModelsController.Put(requestModel); + + // Assert + Assert.IsNotNull(result); + Assert.IsAssignableFrom(result); + var okObjectResult = result as OkResult; + + Assert.IsNotNull(okObjectResult); + Assert.AreEqual(200, okObjectResult.StatusCode); + + this.mockRepository.VerifyAll(); + } + + [Test] + public async Task When_DeviceModelId_Not_Exists_Put_Should_Return_BadRequest() + { + // Arrange + var deviceModelsController = this.CreateDeviceModelsController(); + SetupNotFoundEntity(); + + var deviceModel = new DeviceModel + { + ModelId = Guid.NewGuid().ToString() + }; + + // Act + var result = await deviceModelsController.Put(deviceModel); + // Assert + Assert.IsNotNull(result); + Assert.IsAssignableFrom(result); + } + + [Test] + public async Task Delete_Should_Remove_The_Entity_Commands_And_Avatar() + { + // Arrange + var deviceModelsController = this.CreateDeviceModelsController(); + string id = Guid.NewGuid().ToString(); + string commandId = Guid.NewGuid().ToString(); + bool returned = false; + + var mockModelResponse = this.mockRepository.Create>(); + mockModelResponse.Setup(c => c.Value) + .Returns(new TableEntity(id, DeviceModelsController.DefaultPartitionKey)); + + var mockResponse = this.mockRepository.Create(); + var mockTableResponse = this.mockRepository.Create>(); + var mockEnumerator = this.mockRepository.Create>(); + mockEnumerator.Setup(x => x.Dispose()).Callback(() => { }); + mockEnumerator.Setup(x => x.MoveNext()).Returns(() => + { + if (returned) + return false; + + returned = true; + return true; + }); + + mockEnumerator.Setup(x => x.Current) + .Returns(new TableEntity(id, commandId)); + + mockTableResponse.Setup(x => x.GetEnumerator()) + .Returns(mockEnumerator.Object); + + this.mockDeviceTemplatesTableClient.Setup(c => c.GetEntity( + It.Is(p => p == DeviceModelsController.DefaultPartitionKey), + It.Is(k => k == id), + It.IsAny>(), + It.IsAny())) + .Returns(mockModelResponse.Object); + + this.mockDeviceTemplatesTableClient.Setup(c => c.DeleteEntityAsync( + It.Is(p => p == DeviceModelsController.DefaultPartitionKey), + It.Is(k => k == id), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(mockResponse.Object); + + this.mockDeviceService.Setup(c => c.GetAllDevice()) + .ReturnsAsync(new List()); + + this.mockDeviceModelImageManager.Setup(c => c.DeleteDeviceModelImageAsync(It.Is(x => x == id))) + .Returns(Task.CompletedTask); + + this.mockCommandsTableClient.Setup(c => c.Query( + It.IsAny>>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(mockTableResponse.Object); + + this.mockCommandsTableClient.Setup(c => c.DeleteEntityAsync( + It.Is(p => p == id), + It.Is(k => k == commandId), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(mockResponse.Object); + + this.mockTableClientFactory.Setup(c => c.GetDeviceTemplates()) + .Returns(mockDeviceTemplatesTableClient.Object); + + this.mockTableClientFactory.Setup(c => c.GetDeviceCommands()) + .Returns(this.mockCommandsTableClient.Object); + + // Act + var result = await deviceModelsController.Delete(id); + + // Assert + this.mockRepository.VerifyAll(); + } + + private TableEntity SetupMockEntity() + { + var mockResponse = this.mockRepository.Create>(); + var modelId = Guid.NewGuid().ToString(); + var entity = new TableEntity(DeviceModelsController.DefaultPartitionKey, modelId); + + mockResponse.Setup(c => c.Value) + .Returns(entity) + .Verifiable(); + + this.mockDeviceTemplatesTableClient.Setup(c => c.GetEntity( + It.Is(p => p == DeviceModelsController.DefaultPartitionKey), + It.Is(k => k == modelId), + It.IsAny>(), + It.IsAny())) + .Returns(mockResponse.Object); + + this.mockTableClientFactory.Setup(c => c.GetDeviceTemplates()) + .Returns(mockDeviceTemplatesTableClient.Object); + + return entity; + } + + private void SetupNotFoundEntity() + { + this.mockDeviceTemplatesTableClient.Setup(c => c.GetEntity( + It.Is(p => p == DeviceModelsController.DefaultPartitionKey), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Throws(new RequestFailedException(StatusCodes.Status404NotFound, "Not Found")); + + this.mockTableClientFactory.Setup(c => c.GetDeviceTemplates()) + .Returns(mockDeviceTemplatesTableClient.Object); + } + } +} diff --git a/src/AzureIoTHub.Portal.Server.Tests/Managers/DeviceModelCommandsManagerTests.cs b/src/AzureIoTHub.Portal.Server.Tests/Managers/DeviceModelCommandsManagerTests.cs index 268fe9ca7..3789f636f 100644 --- a/src/AzureIoTHub.Portal.Server.Tests/Managers/DeviceModelCommandsManagerTests.cs +++ b/src/AzureIoTHub.Portal.Server.Tests/Managers/DeviceModelCommandsManagerTests.cs @@ -2,6 +2,7 @@ using Azure.Data.Tables; using AzureIoTHub.Portal.Server.Factories; using AzureIoTHub.Portal.Server.Managers; +using AzureIoTHub.Portal.Server.Mappers; using AzureIoTHub.Portal.Shared.Models.V10; using AzureIoTHub.Portal.Shared.Models.V10.Device; using Moq; @@ -29,7 +30,8 @@ public void SetUp() private DeviceModelCommandsManager CreateManager() { return new DeviceModelCommandsManager( - this.mockTableClientFactory.Object); + this.mockTableClientFactory.Object, + new DeviceModelCommandMapper()); } [Test] diff --git a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor index 2474fde5a..ef8a81b21 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor @@ -30,12 +30,12 @@ @if (imageDataUrl != null) { - Delete Picture + Delete Picture } else { @@ -52,19 +52,19 @@ - + - + - + - + @@ -87,16 +87,16 @@ - + - + - + - + @@ -107,7 +107,7 @@ - Delete Device + Delete Model Save Changes diff --git a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelListPage.razor b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelListPage.razor index 4527df587..4ff535f45 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelListPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelListPage.razor @@ -64,7 +64,7 @@ - + diff --git a/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DeviceModelCommandsController.cs b/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DeviceModelCommandsController.cs index 4a1b15335..c7c893928 100644 --- a/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DeviceModelCommandsController.cs +++ b/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DeviceModelCommandsController.cs @@ -4,57 +4,129 @@ namespace AzureIoTHub.Portal.Server.Controllers.V10 { using System.Threading.Tasks; + using Azure; using Azure.Data.Tables; using AzureIoTHub.Portal.Server.Factories; using AzureIoTHub.Portal.Server.Mappers; using AzureIoTHub.Portal.Shared.Models.V10; + using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Logging; [ApiController] [ApiVersion("1.0")] - [Route("api/models/{modelId}/commands")] + [Route("api/models/{id}/commands")] [ApiExplorerSettings(GroupName = "Device Models")] public class DeviceModelCommandsController : ControllerBase { + /// + /// The table client factory. + /// private readonly ITableClientFactory tableClientFactory; + + /// + /// The device model command mapper. + /// private readonly IDeviceModelCommandMapper deviceModelCommandMapper; + /// + /// The logger. + /// + private readonly ILogger log; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The device model command mapper. + /// The table client factory. public DeviceModelCommandsController( + ILogger log, IDeviceModelCommandMapper deviceModelCommandMapper, ITableClientFactory tableClientFactory) { + this.log = log; this.tableClientFactory = tableClientFactory; this.deviceModelCommandMapper = deviceModelCommandMapper; } /// - /// Add a command to an Azure DataTable. + /// Creates the specified device model's command. /// - /// Operation status. + /// The model identifier. + /// The command. + /// The action result. [HttpPost] - public async Task Post(string modelId, DeviceModelCommand command) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Post(string id, DeviceModelCommand command) { - TableEntity entity = new TableEntity() + try { - PartitionKey = modelId, - RowKey = command.Name - }; - this.deviceModelCommandMapper.UpdateTableEntity(entity, command); - await this.tableClientFactory - .GetDeviceCommands() - .AddEntityAsync(entity); - return this.Ok("Command successfully added"); + var query = this.tableClientFactory + .GetDeviceTemplates() + .GetEntity(DeviceModelsController.DefaultPartitionKey, id); + + TableEntity entity = new TableEntity() + { + PartitionKey = id, + RowKey = command.Name + }; + + this.deviceModelCommandMapper.UpdateTableEntity(entity, command); + + await this.tableClientFactory + .GetDeviceCommands() + .AddEntityAsync(entity); + + return this.Ok(); + } + catch (RequestFailedException e) + { + if (e.Status == StatusCodes.Status404NotFound) + { + return this.NotFound(); + } + + this.log.LogError(e.Message, e); + + throw; + } } /// - /// Delete a command from an Azure DataTable. + /// Deletes the specified device model's command. /// - /// Operation status. + /// The model identifier. + /// The command identifier. + /// The action result. + /// If the device model's command is deleted. [HttpDelete("{commandId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task Delete(string modelId, string commandId) { - var result = await this.tableClientFactory.GetDeviceCommands().DeleteEntityAsync(modelId, commandId); - return this.StatusCode(result.Status); + try + { + var query = this.tableClientFactory + .GetDeviceTemplates() + .GetEntity(DeviceModelsController.DefaultPartitionKey, modelId); + + var result = await this.tableClientFactory.GetDeviceCommands() + .DeleteEntityAsync(modelId, commandId); + + return this.NoContent(); + } + catch (RequestFailedException e) + { + if (e.Status == StatusCodes.Status404NotFound) + { + return this.NotFound(); + } + + this.log.LogError(e.Message, e); + + throw; + } } } } diff --git a/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DeviceModelsController.cs b/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DeviceModelsController.cs index 5f06777ef..8d7ef18c8 100644 --- a/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DeviceModelsController.cs +++ b/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DeviceModelsController.cs @@ -7,6 +7,7 @@ namespace AzureIoTHub.Portal.Server.Controllers.V10 using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using Azure; using Azure.Data.Tables; using AzureIoTHub.Portal.Server.Factories; using AzureIoTHub.Portal.Server.Helpers; @@ -16,6 +17,7 @@ namespace AzureIoTHub.Portal.Server.Controllers.V10 using AzureIoTHub.Portal.Shared.Models.V10; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Logging; [ApiController] [ApiVersion("1.0")] @@ -23,21 +25,59 @@ namespace AzureIoTHub.Portal.Server.Controllers.V10 [ApiExplorerSettings(GroupName = "Device Models")] public class DeviceModelsController : ControllerBase { - private const string DefaultPartitionKey = "0"; + /// + /// The default partition key. + /// + public const string DefaultPartitionKey = "0"; + /// + /// The logger. + /// + private readonly ILogger log; + + /// + /// The table client factory. + /// private readonly ITableClientFactory tableClientFactory; + + /// + /// The device model mapper. + /// private readonly IDeviceModelMapper deviceModelMapper; + + /// + /// The device model command mapper. + /// private readonly IDeviceModelCommandMapper deviceModelCommandMapper; + + /// + /// The device model image manager. + /// private readonly IDeviceModelImageManager deviceModelImageManager; + + /// + /// The devices service. + /// private readonly IDeviceService devicesService; + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The device model image manager. + /// The device model command mapper. + /// The device model mapper. + /// The devices service. + /// The table client factory. public DeviceModelsController( + ILogger log, IDeviceModelImageManager deviceModelImageManager, IDeviceModelCommandMapper deviceModelCommandMapper, IDeviceModelMapper deviceModelMapper, IDeviceService devicesService, ITableClientFactory tableClientFactory) { + this.log = log; this.deviceModelMapper = deviceModelMapper; this.tableClientFactory = tableClientFactory; this.deviceModelCommandMapper = deviceModelCommandMapper; @@ -46,10 +86,11 @@ public DeviceModelsController( } /// - /// Gets a list of device models from an Azure DataTable. + /// Gets the device models. /// - /// A list of DeviceModel. + /// The list of device models. [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] public IEnumerable Get() { // PartitionKey 0 contains all device models @@ -64,45 +105,162 @@ public IEnumerable Get() } /// - /// Get a specific device model from an Azure DataTable. + /// Gets the specified model identifier. /// - /// A DeviceModel. - [HttpGet("{modelID}")] - public IActionResult Get(string modelID) + /// The model identifier. + /// The corresponding model. + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public IActionResult Get(string id) { - var query = this.tableClientFactory + try + { + var query = this.tableClientFactory .GetDeviceTemplates() - .Query(t => t.RowKey == modelID); + .GetEntity(DefaultPartitionKey, id); - if (!query.Any()) - { - return this.NotFound(); + return this.Ok(this.deviceModelMapper.CreateDeviceModel(query.Value)); } + catch (RequestFailedException e) + { + if (e.Status == StatusCodes.Status404NotFound) + { + return this.NotFound(); + } + + this.log.LogError(e.Message, e); - return this.Ok(this.deviceModelMapper.CreateDeviceModel(query.Single())); + throw; + } } - [HttpGet("{modelID}/avatar")] - public string GetAvatar(string modelID) + /// + /// Gets the avatar. + /// + /// The model identifier. + /// The avatar. + [HttpGet("{id}/avatar")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public IActionResult GetAvatar(string id) { - return this.deviceModelImageManager.ComputeImageUri(modelID); + try + { + var query = this.tableClientFactory + .GetDeviceTemplates() + .GetEntity(DefaultPartitionKey, id); + + return this.Ok(this.deviceModelImageManager.ComputeImageUri(id)); + } + catch (RequestFailedException e) + { + if (e.Status == StatusCodes.Status404NotFound) + { + return this.NotFound(); + } + + this.log.LogError(e.Message, e); + + throw; + } } - [HttpPost("{modelID}/avatar")] - public async Task ChangeAvatar(string modelID, IFormFile file) + /// + /// Changes the avatar. + /// + /// The model identifier. + /// The file. + /// The avatar. + [HttpPost("{id}/avatar")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task ChangeAvatar(string id, IFormFile file) { - return await this.deviceModelImageManager.ChangeDeviceModelImageAsync(modelID, file.OpenReadStream()); + try + { + var query = this.tableClientFactory + .GetDeviceTemplates() + .GetEntity(DefaultPartitionKey, id); + + return this.Ok(await this.deviceModelImageManager.ChangeDeviceModelImageAsync(id, file.OpenReadStream())); + } + catch (RequestFailedException e) + { + if (e.Status == StatusCodes.Status404NotFound) + { + return this.NotFound(); + } + + this.log.LogError(e.Message, e); + + throw; + } } - [HttpDelete("{modelID}/avatar")] - public async Task DeleteAvatar(string modelID) + /// + /// Deletes the avatar. + /// + /// The model identifier. + [HttpDelete("{id}/avatar")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteAvatar(string id) { - await this.deviceModelImageManager.DeleteDeviceModelImageAsync(modelID); + try + { + var query = this.tableClientFactory + .GetDeviceTemplates() + .GetEntity(DefaultPartitionKey, id); + + await this.deviceModelImageManager.DeleteDeviceModelImageAsync(id); + + return this.NoContent(); + } + catch (RequestFailedException e) + { + if (e.Status == StatusCodes.Status404NotFound) + { + return this.NotFound(); + } + + this.log.LogError(e.Message, e); + + throw; + } } + /// + /// Creates the specified device model. + /// + /// The device model. + /// The action result. [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task Post(DeviceModel deviceModel) { + if (!string.IsNullOrEmpty(deviceModel.ModelId)) + { + try + { + var query = this.tableClientFactory + .GetDeviceTemplates() + .GetEntity(DefaultPartitionKey, deviceModel.ModelId); + + return this.BadRequest("Cannot create a device model with an existing id."); + } + catch (RequestFailedException e) + { + if (e.Status != StatusCodes.Status404NotFound) + { + this.log.LogError(e.Message, e); + + throw; + } + } + } + TableEntity entity = new TableEntity() { PartitionKey = DefaultPartitionKey, @@ -114,64 +272,112 @@ public async Task Post(DeviceModel deviceModel) return this.Ok(); } + /// + /// Updates the specified device model. + /// + /// The device model. + /// The action result. [HttpPut] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task Put(DeviceModel deviceModel) { - TableEntity entity = new TableEntity() + if (string.IsNullOrEmpty(deviceModel.ModelId)) { - PartitionKey = DefaultPartitionKey, - RowKey = deviceModel.ModelId - }; + return this.BadRequest("Should provide the model id."); + } + + TableEntity entity; + + try + { + entity = this.tableClientFactory + .GetDeviceTemplates() + .GetEntity(DefaultPartitionKey, deviceModel.ModelId); + } + catch (RequestFailedException e) + { + if (e.Status == StatusCodes.Status404NotFound) + { + return this.NotFound(); + } + + this.log.LogError(e.Message, e); + + throw; + } await this.SaveEntity(entity, deviceModel); return this.Ok(); } - [HttpDelete("{deviceModelID}")] - public async Task Delete(string deviceModelID) + /// + /// Deletes the specified device model. + /// + /// The device model identifier. + /// The action result. + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task Delete(string id) { // we get all devices var deviceList = await this.devicesService.GetAllDevice(); - // we get the device model with a query - var query = this.tableClientFactory - .GetDeviceTemplates() - .Query(t => t.RowKey == deviceModelID); + TableEntity entity = null; - if (!query.Any()) + try { - return this.NotFound(); + entity = this.tableClientFactory + .GetDeviceTemplates() + .GetEntity(DefaultPartitionKey, id); } + catch (RequestFailedException e) + { + if (e.Status == StatusCodes.Status404NotFound) + { + return this.NotFound(); + } - var deviceModel = this.deviceModelMapper.CreateDeviceModel(query.Single()); + this.log.LogError(e.Message, e); - var queryCommand = this.tableClientFactory - .GetDeviceCommands() - .Query(t => t.PartitionKey == deviceModel.ModelId); + throw; + } - if (deviceList.Any(x => DeviceHelper.RetrieveTagValue(x, "modelId") == deviceModel.ModelId)) + if (deviceList.Any(x => DeviceHelper.RetrieveTagValue(x, "modelId") == id)) { - return this.Unauthorized("This model is already in use by a device and cannot be deleted."); + return this.BadRequest("This model is already in use by a device and cannot be deleted."); } - var commands = queryCommand.Select(item => this.deviceModelCommandMapper.GetDeviceModelCommand(item)).ToList(); + var queryCommand = this.tableClientFactory + .GetDeviceCommands() + .Query(t => t.PartitionKey == id) + .ToArray(); - foreach (var item in commands) + foreach (var item in queryCommand) { _ = await this.tableClientFactory - .GetDeviceCommands().DeleteEntityAsync(deviceModelID, item.Name); + .GetDeviceCommands() + .DeleteEntityAsync(id, item.RowKey); } // Image deletion - await this.deviceModelImageManager.DeleteDeviceModelImageAsync(deviceModelID); + await this.deviceModelImageManager.DeleteDeviceModelImageAsync(id); var result = await this.tableClientFactory .GetDeviceTemplates() - .DeleteEntityAsync(DefaultPartitionKey, deviceModelID); + .DeleteEntityAsync(DefaultPartitionKey, id); - return this.StatusCode(result.Status); + return this.NoContent(); } + /// + /// Saves the entity. + /// + /// The entity. + /// The device model object. private async Task SaveEntity(TableEntity entity, DeviceModel deviceModelObject) { this.deviceModelMapper.UpdateTableEntity(entity, deviceModelObject); @@ -181,15 +387,11 @@ await this.tableClientFactory .UpsertEntityAsync(entity); var commandsTable = this.tableClientFactory.GetDeviceCommands(); - var commandsPage = commandsTable.QueryAsync(c => c.PartitionKey == entity.RowKey) - .AsPages(); + var commandsPage = commandsTable.Query(c => c.PartitionKey == entity.RowKey); - await foreach (var page in commandsPage) + foreach (var item in commandsPage.Where(x => !deviceModelObject.Commands.Any(c => c.Name == x.RowKey))) { - foreach (var item in page.Values.Where(x => !deviceModelObject.Commands.Any(c => c.Name == x.RowKey))) - { - await commandsTable.DeleteEntityAsync(item.PartitionKey, item.RowKey); - } + await commandsTable.DeleteEntityAsync(item.PartitionKey, item.RowKey); } foreach (var item in deviceModelObject.Commands) diff --git a/src/AzureIoTHub.Portal/Server/Managers/DeviceModelCommandsManager.cs b/src/AzureIoTHub.Portal/Server/Managers/DeviceModelCommandsManager.cs index 4891acf1e..174a4817a 100644 --- a/src/AzureIoTHub.Portal/Server/Managers/DeviceModelCommandsManager.cs +++ b/src/AzureIoTHub.Portal/Server/Managers/DeviceModelCommandsManager.cs @@ -4,18 +4,22 @@ namespace AzureIoTHub.Portal.Server.Managers { using System.Collections.Generic; + using System.Linq; using Azure.Data.Tables; using AzureIoTHub.Portal.Server.Factories; + using AzureIoTHub.Portal.Server.Mappers; using AzureIoTHub.Portal.Shared.Models.V10; using AzureIoTHub.Portal.Shared.Models.V10.Device; public class DeviceModelCommandsManager : IDeviceModelCommandsManager { private readonly ITableClientFactory tableClientFactory; + private readonly IDeviceModelCommandMapper deviceModelCommandMapper; - public DeviceModelCommandsManager(ITableClientFactory tableClientFactory) + public DeviceModelCommandsManager(ITableClientFactory tableClientFactory, IDeviceModelCommandMapper deviceModelCommandMapper) { this.tableClientFactory = tableClientFactory; + this.deviceModelCommandMapper = deviceModelCommandMapper; } /// @@ -62,15 +66,7 @@ public List RetrieveDeviceModelCommands(string deviceModel) .GetDeviceCommands() .Query(filter: $"PartitionKey eq '{deviceModel}'"); - foreach (TableEntity qEntity in queryResultsFilter) - { - commands.Add(new DeviceModelCommand() - { - Name = qEntity.RowKey, - Frame = qEntity[nameof(DeviceModelCommand.Frame)].ToString(), - Port = int.Parse(qEntity[nameof(DeviceModelCommand.Port)].ToString()) - }); - } + commands.AddRange(queryResultsFilter.Select(this.deviceModelCommandMapper.GetDeviceModelCommand)); return commands; } diff --git a/src/AzureIoTHub.Portal/Server/Mappers/DeviceModelCommandMapper.cs b/src/AzureIoTHub.Portal/Server/Mappers/DeviceModelCommandMapper.cs index 73582f07d..0230c41c0 100644 --- a/src/AzureIoTHub.Portal/Server/Mappers/DeviceModelCommandMapper.cs +++ b/src/AzureIoTHub.Portal/Server/Mappers/DeviceModelCommandMapper.cs @@ -8,20 +8,32 @@ namespace AzureIoTHub.Portal.Server.Mappers public class DeviceModelCommandMapper : IDeviceModelCommandMapper { + /// + /// Gets the device model command. + /// + /// The entity. + /// The device model comamnd. public DeviceModelCommand GetDeviceModelCommand(TableEntity entity) { return new DeviceModelCommand { Name = entity.RowKey, Frame = entity[nameof(DeviceModelCommand.Frame)].ToString(), - Port = int.Parse(entity[nameof(DeviceModelCommand.Port)].ToString()) + Port = int.Parse(entity[nameof(DeviceModelCommand.Port)].ToString()), + IsBuiltin = bool.Parse(entity[nameof(DeviceModelCommand.IsBuiltin)]?.ToString() ?? "false"), }; } + /// + /// Updates the table entity. + /// + /// The command entity. + /// The element. public void UpdateTableEntity(TableEntity commandEntity, DeviceModelCommand element) { commandEntity[nameof(DeviceModelCommand.Frame)] = element.Frame; commandEntity[nameof(DeviceModelCommand.Port)] = element.Port; + commandEntity[nameof(DeviceModelCommand.IsBuiltin)] = element.IsBuiltin; } } } diff --git a/src/AzureIoTHub.Portal/Server/Mappers/DeviceModelMapper.cs b/src/AzureIoTHub.Portal/Server/Mappers/DeviceModelMapper.cs index d828154b4..121796e16 100644 --- a/src/AzureIoTHub.Portal/Server/Mappers/DeviceModelMapper.cs +++ b/src/AzureIoTHub.Portal/Server/Mappers/DeviceModelMapper.cs @@ -23,6 +23,7 @@ public DeviceModel CreateDeviceModel(TableEntity entity) return new DeviceModel { ModelId = entity.RowKey, + IsBuiltin = bool.Parse(entity[nameof(DeviceModel.IsBuiltin)]?.ToString() ?? "false"), ImageUrl = this.deviceModelImageManager.ComputeImageUri(entity.RowKey).ToString(), Name = entity[nameof(DeviceModel.Name)]?.ToString(), Description = entity[nameof(DeviceModel.Description)]?.ToString(), @@ -38,6 +39,7 @@ public void UpdateTableEntity(TableEntity entity, DeviceModel model) entity[nameof(DeviceModel.Description)] = model.Description; entity[nameof(DeviceModel.AppEUI)] = model.AppEUI; entity[nameof(DeviceModel.SensorDecoderURL)] = model.SensorDecoderURL; + entity[nameof(DeviceModel.IsBuiltin)] = model.IsBuiltin; } } } diff --git a/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceModel.cs b/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceModel.cs index a6b63c22a..216baab4f 100644 --- a/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceModel.cs +++ b/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceModel.cs @@ -8,23 +8,76 @@ namespace AzureIoTHub.Portal.Shared.Models.V10 public class DeviceModel { + /// + /// Gets or sets the model identifier. + /// + /// + /// The model identifier. + /// public string ModelId { get; set; } + /// + /// Gets or sets the image URL. + /// + /// + /// The image URL. + /// public string ImageUrl { get; set; } + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// [Required(ErrorMessage = "The device model name is required.")] public string Name { get; set; } + /// + /// Gets or sets the description. + /// + /// + /// The description. + /// public string Description { get; set; } + /// + /// Gets or sets the application eui. + /// + /// + /// The application eui. + /// [Required(ErrorMessage = "The OTAA App EUI is required.")] public string AppEUI { get; set; } + /// + /// Gets or sets the sensor decoder URL. + /// + /// + /// The sensor decoder URL. + /// public string SensorDecoderURL { get; set; } + /// + /// Gets or sets a value indicating whether this instance is builtin. + /// + /// + /// true if this instance is builtin; otherwise, false. + /// + public bool IsBuiltin { get; set; } + + /// + /// Gets or sets the commands. + /// + /// + /// The commands. + /// [ValidateComplexType] public List Commands { get; set; } + /// + /// Initializes a new instance of the class. + /// public DeviceModel() { this.Commands = new List(); diff --git a/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceModelCommand.cs b/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceModelCommand.cs index 507570858..0b840dfda 100644 --- a/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceModelCommand.cs +++ b/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceModelCommand.cs @@ -7,14 +7,40 @@ namespace AzureIoTHub.Portal.Shared.Models.V10 public class DeviceModelCommand { + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// [Required(ErrorMessage = "The command name is required.")] public string Name { get; set; } + /// + /// Gets or sets the frame. + /// + /// + /// The frame. + /// [Required(ErrorMessage = "The frame is required.")] public string Frame { get; set; } + /// + /// Gets or sets the port. + /// + /// + /// The port. + /// [Required(ErrorMessage = "The port number is required.")] [Range(1, 223, ErrorMessage = "The port number should be between 1 and 223.")] public int Port { get; set; } = 1; + + /// + /// Gets or sets a value indicating whether this instance is builtin. + /// + /// + /// true if this instance is builtin; otherwise, false. + /// + public bool IsBuiltin { get; set; } } }