From f4e306acd68ce58e745fd7294238158d0450d652 Mon Sep 17 00:00:00 2001 From: salim ben ahben <40862545+Sben65@users.noreply.github.com> Date: Mon, 28 Mar 2022 17:16:01 +0200 Subject: [PATCH] Issue#369 add health probe for sub services (#503) * add healthcheck endpoint * fix #369 * add new test --- .../Services/DeviceTagServiceTests.cs | 2 +- .../IoTHubHealthCheckTest.cs | 153 ++++++++++++++++++ .../LoRaManagementKeyFacadeHealthCheckTest.cs | 86 ++++++++++ ...rovisioningServiceClientHealthCheckTest.cs | 112 +++++++++++++ .../StorageAccountHealthCheckTest.cs | 126 +++++++++++++++ .../TableStorageHealthCheckTest.cs | 134 +++++++++++++++ .../Server/Factories/ITableClientFactory.cs | 4 +- .../Server/Factories/TableClientFactory.cs | 5 + .../DeviceProvisioningServiceManager.cs | 6 + .../IDeviceProvisioningServiceManager.cs | 3 + .../Managers/ILoraDeviceMethodManager.cs | 2 + .../Managers/LoraDeviceMethodManager.cs | 6 + .../ServicesHealthCheck/IoTHubHealthCheck.cs | 47 ++++++ .../LoRaManagementKeyFacadeHealthCheck.cs | 37 +++++ .../ProvisioningServiceClientHealthCheck.cs | 62 +++++++ .../StorageAccountHealthCheck.cs | 49 ++++++ .../TableStorageHealthCheck.cs | 49 ++++++ src/AzureIoTHub.Portal/Server/Startup.cs | 8 +- .../Wrappers/IProvisioningServiceClient.cs | 7 +- .../ProvisioningServiceClientWrapper.cs | 8 +- 20 files changed, 900 insertions(+), 6 deletions(-) create mode 100644 src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/IoTHubHealthCheckTest.cs create mode 100644 src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/LoRaManagementKeyFacadeHealthCheckTest.cs create mode 100644 src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/ProvisioningServiceClientHealthCheckTest.cs create mode 100644 src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/StorageAccountHealthCheckTest.cs create mode 100644 src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/TableStorageHealthCheckTest.cs create mode 100644 src/AzureIoTHub.Portal/Server/ServicesHealthCheck/IoTHubHealthCheck.cs create mode 100644 src/AzureIoTHub.Portal/Server/ServicesHealthCheck/LoRaManagementKeyFacadeHealthCheck.cs create mode 100644 src/AzureIoTHub.Portal/Server/ServicesHealthCheck/ProvisioningServiceClientHealthCheck.cs create mode 100644 src/AzureIoTHub.Portal/Server/ServicesHealthCheck/StorageAccountHealthCheck.cs create mode 100644 src/AzureIoTHub.Portal/Server/ServicesHealthCheck/TableStorageHealthCheck.cs diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Services/DeviceTagServiceTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Services/DeviceTagServiceTests.cs index 6981c0a20..871c698b9 100644 --- a/src/AzureIoTHub.Portal.Server.Tests.Unit/Services/DeviceTagServiceTests.cs +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Services/DeviceTagServiceTests.cs @@ -70,7 +70,7 @@ public async Task UpdateShouldCreateNewEntity() It.IsAny(), It.IsAny>(), It.IsAny())) - .Returns(Pageable.FromPages(new[] { + .Returns(Pageable.FromPages(new[] { Page.FromValues(new[] { new TableEntity(DeviceTagService.DefaultPartitionKey,tag.Name) diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/IoTHubHealthCheckTest.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/IoTHubHealthCheckTest.cs new file mode 100644 index 000000000..2dc323f47 --- /dev/null +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/IoTHubHealthCheckTest.cs @@ -0,0 +1,153 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.Tests.Unit.ServicesHealthCheck +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using AzureIoTHub.Portal.Server.ServicesHealthCheck; + using Microsoft.Azure.Devices; + using Microsoft.Extensions.Diagnostics.HealthChecks; + using Moq; + using NUnit.Framework; + + [TestFixture] + public class IoTHubHealthCheckTest + { + private MockRepository mockRepository; + + private Mock mockRegistryManager; + private Mock mockServiceClient; + + [SetUp] + public void SetUp() + { + this.mockRepository = new MockRepository(MockBehavior.Strict); + + this.mockRegistryManager = this.mockRepository.Create(); + this.mockServiceClient = this.mockRepository.Create(); + } + + private IoTHubHealthCheck CreateHealthCheck() + { + return new IoTHubHealthCheck(this.mockRegistryManager.Object, this.mockServiceClient.Object); + } + + [Test] + public async Task CheckHealthAsyncStateUnderTestExpectedBehavior() + { + // Arrange + var healthService = CreateHealthCheck(); + + var mockServiceStat = this.mockRepository.Create(); + var mockQuery = this.mockRepository.Create(); + + var healthCheckContext = new HealthCheckContext(); + var token = new CancellationToken(canceled:false); + + _ = mockQuery.Setup(c => c.GetNextAsJsonAsync()) + .ReturnsAsync(new string[] + { + /*lang=json*/ + "{ $0: 2}" + }); + + _ = this.mockServiceClient + .Setup(c => c.GetServiceStatisticsAsync(It.IsAny())) + .ReturnsAsync(mockServiceStat.Object); + + _ = this.mockRegistryManager + .Setup(c => c.CreateQuery(It.Is(x => x == "SELECT count() FROM devices"))) + .Returns(mockQuery.Object); + + // Act + var result = await healthService.CheckHealthAsync(healthCheckContext, token); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(HealthStatus.Healthy, result.Status); + } + + [Test] + public async Task CheckHealthAsyncThrowExceptionReturnUnHealthy() + { + // Arrange + var healthService = CreateHealthCheck(); + + var mockServiceStat = this.mockRepository.Create(); + var mockQuery = this.mockRepository.Create(); + + var healthRegistration = new HealthCheckRegistration(Guid.NewGuid().ToString(), healthService, HealthStatus.Unhealthy, new List()); + + var healthCheckContext = new HealthCheckContext() + { + Registration = healthRegistration + }; + var token = new CancellationToken(canceled:false); + + _ = mockQuery.Setup(c => c.GetNextAsJsonAsync()) + .ReturnsAsync(new string[] + { + /*lang=json*/ + "{ $0: 2}" + }); + + _ = this.mockServiceClient + .Setup(c => c.GetServiceStatisticsAsync(It.IsAny())) + .Throws(exception: new SystemException("test")); + + _ = this.mockRegistryManager + .Setup(c => c.CreateQuery(It.Is(x => x == "SELECT count() FROM devices"))) + .Returns(mockQuery.Object); + + // Act + var result = await healthService.CheckHealthAsync(healthCheckContext, token); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(HealthStatus.Unhealthy, result.Status); + } + + [Test] + public async Task CheckHealthAsyncWithNullQueryReturnUnHealthy() + { + // Arrange + var healthService = CreateHealthCheck(); + + var mockServiceStat = this.mockRepository.Create(); + var mockQuery = this.mockRepository.Create(); + + var healthRegistration = new HealthCheckRegistration(Guid.NewGuid().ToString(), healthService, HealthStatus.Unhealthy, new List()); + + var healthCheckContext = new HealthCheckContext() + { + Registration = healthRegistration + }; + var token = new CancellationToken(canceled:false); + + _ = mockQuery.Setup(c => c.GetNextAsJsonAsync()) + .ReturnsAsync(new string[] + { + /*lang=json*/ + "{ $0: 2}" + }); + + _ = this.mockServiceClient + .Setup(c => c.GetServiceStatisticsAsync(It.IsAny())) + .ReturnsAsync(mockServiceStat.Object); + + _ = this.mockRegistryManager + .Setup(c => c.CreateQuery(It.Is(x => x == "SELECT count() FROM devices"))) + .Returns(value: null); + + // Act + var result = await healthService.CheckHealthAsync(healthCheckContext, token); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(HealthStatus.Unhealthy, result.Status); + } + } +} diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/LoRaManagementKeyFacadeHealthCheckTest.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/LoRaManagementKeyFacadeHealthCheckTest.cs new file mode 100644 index 000000000..1a22bb282 --- /dev/null +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/LoRaManagementKeyFacadeHealthCheckTest.cs @@ -0,0 +1,86 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.Tests.Unit.ServicesHealthCheck +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + using AzureIoTHub.Portal.Server.Managers; + using AzureIoTHub.Portal.Server.ServicesHealthCheck; + using Microsoft.Extensions.Diagnostics.HealthChecks; + using Moq; + using NUnit.Framework; + + [TestFixture] + public class LoRaManagementKeyFacadeHealthCheckTest + { + private MockRepository mockRepository; + private Mock mockoraDeviceMethodManager; + + [SetUp] + public void SetUp() + { + this.mockRepository = new MockRepository(MockBehavior.Strict); + + this.mockoraDeviceMethodManager = this.mockRepository.Create(); + } + + private LoRaManagementKeyFacadeHealthCheck CreateService() + { + return new LoRaManagementKeyFacadeHealthCheck(this.mockoraDeviceMethodManager.Object); + } + + [Test] + public async Task CheckHealthAsyncStateUnderTestReturnHealthy() + { + // Arrange + var healthService = CreateService(); + + var healthCheckContext = new HealthCheckContext(); + var token = new CancellationToken(canceled:false); + + var mockHttpMessage = this.mockRepository.Create(System.Net.HttpStatusCode.OK); + + _ = this.mockoraDeviceMethodManager + .Setup(c => c.CheckAzureFunctionReturn(It.IsAny())) + .ReturnsAsync(mockHttpMessage.Object); + + // Act + var result = await healthService.CheckHealthAsync(healthCheckContext, token); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(HealthStatus.Healthy, result.Status); + } + + [Test] + public async Task CheckHealthAsyncStateUnderTestReturnUnhealthy() + { + // Arrange + var healthService = CreateService(); + + var healthRegistration = new HealthCheckRegistration(Guid.NewGuid().ToString(), healthService, HealthStatus.Unhealthy, new List()); + var healthCheckContext = new HealthCheckContext() + { + Registration = healthRegistration + }; + var token = new CancellationToken(canceled:false); + + var mockHttpMessage = this.mockRepository.Create(System.Net.HttpStatusCode.BadRequest); + + _ = this.mockoraDeviceMethodManager + .Setup(c => c.CheckAzureFunctionReturn(It.IsAny())) + .ReturnsAsync(mockHttpMessage.Object); + + // Act + var result = await healthService.CheckHealthAsync(healthCheckContext, token); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(HealthStatus.Unhealthy, result.Status); + } + } +} diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/ProvisioningServiceClientHealthCheckTest.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/ProvisioningServiceClientHealthCheckTest.cs new file mode 100644 index 000000000..e42d48c94 --- /dev/null +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/ProvisioningServiceClientHealthCheckTest.cs @@ -0,0 +1,112 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.Tests.Unit.ServicesHealthCheck +{ + using System; + using System.Collections.Generic; + using System.Security.Cryptography; + using System.Threading; + using System.Threading.Tasks; + using AzureIoTHub.Portal.Server.ServicesHealthCheck; + using AzureIoTHub.Portal.Server.Wrappers; + using Microsoft.Azure.Devices.Provisioning.Service; + using Microsoft.Extensions.Diagnostics.HealthChecks; + using Moq; + using NUnit.Framework; + + [TestFixture] + public class ProvisioningServiceClientHealthCheckTest + { + private MockRepository mockRepository; + private Mock mockDps; + + [SetUp] + public void SetUp() + { + this.mockRepository = new MockRepository(MockBehavior.Strict); + + this.mockDps = this.mockRepository.Create(); + } + + private ProvisioningServiceClientHealthCheck CreateHealthService() + { + return new ProvisioningServiceClientHealthCheck(this.mockDps.Object); + } + + //[Test] + //public async Task CheckHealthAsyncStateUnderTestReturnHealthy() + //{ + // // Arrange + // var healthService = CreateHealthService(); + + // var healthCheckContext = new HealthCheckContext(); + // var token = new CancellationToken(canceled:false); + + // var attestation = new SymmetricKeyAttestation(GenerateKey(), GenerateKey()); + // var enrollmentGroup = new EnrollmentGroup("enrollmentId", attestation); + + // _ = this.mockDps + // .Setup(c => c.CreateOrUpdateEnrollmentGroupAsync(It.IsAny())) + // .ReturnsAsync(enrollmentGroup); + + // _ = this.mockDps + // .Setup(c => c.GetEnrollmentGroupAsync(It.IsAny())) + // .ReturnsAsync(enrollmentGroup); + + // _ = this.mockDps + // .Setup(c => c.DeleteEnrollmentGroupAsync(It.IsAny(), It.IsAny())); + + // // Act + // var result = await healthService.CheckHealthAsync(healthCheckContext, token); + + // // Assert + // Assert.IsNotNull(result); + // Assert.AreEqual(HealthStatus.Healthy, result.Status); + //} + + [Test] + public async Task CheckHealthAsyncStateUnderTestReturnUnhealthy() + { + // Arrange + var healthService = CreateHealthService(); + + var healthRegistration = new HealthCheckRegistration(Guid.NewGuid().ToString(), healthService, HealthStatus.Unhealthy, new List()); + var healthCheckContext = new HealthCheckContext() + { + Registration = healthRegistration + }; + var token = new CancellationToken(canceled:false); + + var attestation = new SymmetricKeyAttestation(GenerateKey(), GenerateKey()); + var enrollmentGroup = new EnrollmentGroup("enrollmentId", attestation); + + _ = this.mockDps + .Setup(c => c.CreateOrUpdateEnrollmentGroupAsync(It.IsAny())) + .Throws(new Exception()); + + _ = this.mockDps + .Setup(c => c.GetEnrollmentGroupAsync(It.IsAny())) + .ReturnsAsync(enrollmentGroup); + + _ = this.mockDps + .Setup(c => c.DeleteEnrollmentGroupAsync(It.IsAny(), It.IsAny())); + + // Act + var result = await healthService.CheckHealthAsync(healthCheckContext, token); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(HealthStatus.Unhealthy, result.Status); + } + + private static string GenerateKey() + { + const int length = 48; + var rnd = RandomNumberGenerator.GetBytes(length); + + return Convert.ToBase64String(rnd); + } + + } +} diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/StorageAccountHealthCheckTest.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/StorageAccountHealthCheckTest.cs new file mode 100644 index 000000000..3bc24814b --- /dev/null +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/StorageAccountHealthCheckTest.cs @@ -0,0 +1,126 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.Tests.Unit.ServicesHealthCheck +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Azure.Storage.Blobs; + using Azure.Storage.Blobs.Models; + using AzureIoTHub.Portal.Server.ServicesHealthCheck; + using Microsoft.Extensions.Diagnostics.HealthChecks; + using Moq; + using NUnit.Framework; + + [TestFixture] + public class StorageAccountHealthCheckTest + { + private MockRepository mockRepository; + private Mock mockBlobServiceClient; + private Mock mockConfigHandler; + + [SetUp] + public void SetUp() + { + this.mockRepository = new MockRepository(MockBehavior.Strict); + + this.mockBlobServiceClient = this.mockRepository.Create(); + this.mockConfigHandler = this.mockRepository.Create(); + } + + private StorageAccountHealthCheck CreateHealthService() + { + return new StorageAccountHealthCheck(this.mockBlobServiceClient.Object, this.mockConfigHandler.Object); + } + + [Test] + public async Task CheckHealthAsyncContainerExistReturnHealthy() + { + // Arrange + var healthService = CreateHealthService(); + var healthRegistration = new HealthCheckRegistration(Guid.NewGuid().ToString(), healthService, HealthStatus.Healthy, new List()); + + var healthCheckContext = new HealthCheckContext() + { + Registration = healthRegistration + }; + var token = new CancellationToken(canceled:false); + + var blobContainerClient = this.mockRepository.Create(); + var responseContainerExist = this.mockRepository.Create>(); + var responseGetPropertiesAsync = this.mockRepository.Create>(); + + _ = this.mockConfigHandler.SetupGet(c => c.StorageAccountBlobContainerName).Returns(Guid.NewGuid().ToString()); + + _ = responseContainerExist + .SetupGet(c => c.Value) + .Returns(true); + + _ = this.mockBlobServiceClient + .Setup(c => c.GetBlobContainerClient(It.IsAny())) + .Returns(blobContainerClient.Object); + + _ = blobContainerClient + .Setup(c => c.ExistsAsync(It.IsAny())) + .ReturnsAsync(responseContainerExist.Object); + + _ = blobContainerClient + .Setup(c => c.GetPropertiesAsync(null, It.IsAny())) + .ReturnsAsync(responseGetPropertiesAsync.Object); + + // Act + var result = await healthService.CheckHealthAsync(healthCheckContext, token); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(HealthStatus.Healthy, result.Status); + } + + [Test] + public async Task CheckHealthAsyncContainerDoesNotExistReturnUnHealthy() + { + // Arrange + var healthService = CreateHealthService(); + + var healthRegistration = new HealthCheckRegistration(Guid.NewGuid().ToString(), healthService, HealthStatus.Unhealthy, new List()); + + var healthCheckContext = new HealthCheckContext() + { + Registration = healthRegistration + }; + + var token = new CancellationToken(canceled:false); + + var blobContainerClient = this.mockRepository.Create(); + var responseContainerExist = this.mockRepository.Create>(); + var responseGetPropertiesAsync = this.mockRepository.Create>(); + + _ = this.mockConfigHandler.SetupGet(c => c.StorageAccountBlobContainerName).Returns(Guid.NewGuid().ToString()); + + _ = responseContainerExist + .SetupGet(c => c.Value) + .Returns(false); + + _ = this.mockBlobServiceClient + .Setup(c => c.GetBlobContainerClient(It.IsAny())) + .Returns(blobContainerClient.Object); + + _ = blobContainerClient + .Setup(c => c.ExistsAsync(It.IsAny())) + .ReturnsAsync(responseContainerExist.Object); + + _ = blobContainerClient + .Setup(c => c.GetPropertiesAsync(null, It.IsAny())) + .ReturnsAsync(responseGetPropertiesAsync.Object); + + // Act + var result = await healthService.CheckHealthAsync(healthCheckContext, token); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(HealthStatus.Unhealthy, result.Status); + } + } +} diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/TableStorageHealthCheckTest.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/TableStorageHealthCheckTest.cs new file mode 100644 index 000000000..56045a8de --- /dev/null +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/ServicesHealthCheck/TableStorageHealthCheckTest.cs @@ -0,0 +1,134 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.Tests.Unit.ServicesHealthCheck +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Azure; + using Azure.Data.Tables; + using Azure.Data.Tables.Models; + using AzureIoTHub.Portal.Server.Factories; + using AzureIoTHub.Portal.Server.ServicesHealthCheck; + using Microsoft.Extensions.Diagnostics.HealthChecks; + using Moq; + using NUnit.Framework; + + [TestFixture] + public class TableStorageHealthCheckTest + { + private MockRepository mockRepository; + private Mock mockTableClientFactory; + + [SetUp] + public void SetUp() + { + this.mockRepository = new MockRepository(MockBehavior.Strict); + + this.mockTableClientFactory = this.mockRepository.Create(); + } + + private TableStorageHealthCheck CreateHealthService() + { + return new TableStorageHealthCheck(this.mockTableClientFactory.Object); + } + + [Test] + public async Task CheckHealthAsyncStateUnderReturnHealthy() + { + // Arrange + var healthService = CreateHealthService(); + + var healthCheckContext = new HealthCheckContext(); + var token = new CancellationToken(); + + var mockTable = this.mockRepository.Create(); + + var responseCreateIfNotExist = this.mockRepository.Create>(); + var responseAddentity = this.mockRepository.Create(); + + _ = this.mockTableClientFactory + .Setup(c => c.GetTemplatesHealthCheck()) + .Returns(mockTable.Object); + + _ = mockTable + .Setup(c => c.CreateIfNotExistsAsync(It.IsAny())) + .ReturnsAsync(responseCreateIfNotExist.Object); + + _ = mockTable + .Setup(c => c.AddEntityAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(responseAddentity.Object); + + _ = mockTable + .Setup(c => c.DeleteEntityAsync( + It.Is(_ => true), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(responseAddentity.Object); + + _ = mockTable + .Setup(c => c.DeleteAsync(It.IsAny())) + .ReturnsAsync(responseAddentity.Object); + + // Act + var result = await healthService.CheckHealthAsync(healthCheckContext, token); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(HealthStatus.Healthy, result.Status); + } + + [Test] + public async Task CheckHealthAsyncStateUnderReturnUnhealthy() + { + // Arrange + var healthService = CreateHealthService(); + + var healthRegistration = new HealthCheckRegistration(Guid.NewGuid().ToString(), healthService, HealthStatus.Unhealthy, new List()); + var healthCheckContext = new HealthCheckContext() + { + Registration = healthRegistration + }; + var token = new CancellationToken(); + + var mockTable = this.mockRepository.Create(); + + var responseCreateIfNotExist = this.mockRepository.Create>(); + var mockResponse = this.mockRepository.Create(); + + _ = this.mockTableClientFactory + .Setup(c => c.GetTemplatesHealthCheck()) + .Returns(mockTable.Object); + + _ = mockTable + .Setup(c => c.CreateIfNotExistsAsync(It.IsAny())) + .Throws(new Exception()); + + _ = mockTable + .Setup(c => c.AddEntityAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockResponse.Object); + + _ = mockTable + .Setup(c => c.DeleteEntityAsync( + It.Is(_ => true), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(mockResponse.Object); + + _ = mockTable + .Setup(c => c.DeleteAsync(It.IsAny())) + .ReturnsAsync(mockResponse.Object); + + // Act + var result = await healthService.CheckHealthAsync(healthCheckContext, token); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(HealthStatus.Unhealthy, result.Status); + } + } +} diff --git a/src/AzureIoTHub.Portal/Server/Factories/ITableClientFactory.cs b/src/AzureIoTHub.Portal/Server/Factories/ITableClientFactory.cs index c80822eaf..f025fc552 100644 --- a/src/AzureIoTHub.Portal/Server/Factories/ITableClientFactory.cs +++ b/src/AzureIoTHub.Portal/Server/Factories/ITableClientFactory.cs @@ -1,4 +1,4 @@ -// Copyright (c) CGI France. All rights reserved. +// Copyright (c) CGI France. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace AzureIoTHub.Portal.Server.Factories @@ -19,5 +19,7 @@ public interface ITableClientFactory TableClient GetDeviceTemplateProperties(); TableClient GetDeviceTagSettings(); + + public TableClient GetTemplatesHealthCheck(); } } diff --git a/src/AzureIoTHub.Portal/Server/Factories/TableClientFactory.cs b/src/AzureIoTHub.Portal/Server/Factories/TableClientFactory.cs index 882aabe44..3e16178c0 100644 --- a/src/AzureIoTHub.Portal/Server/Factories/TableClientFactory.cs +++ b/src/AzureIoTHub.Portal/Server/Factories/TableClientFactory.cs @@ -42,5 +42,10 @@ public TableClient GetDeviceTemplateProperties() { return CreateClient(ITableClientFactory.DeviceTemplatePropertiesTableName); } + + public TableClient GetTemplatesHealthCheck() + { + return CreateClient("tableHealthCheck"); + } } } diff --git a/src/AzureIoTHub.Portal/Server/Managers/DeviceProvisioningServiceManager.cs b/src/AzureIoTHub.Portal/Server/Managers/DeviceProvisioningServiceManager.cs index fed184af7..acb749b49 100644 --- a/src/AzureIoTHub.Portal/Server/Managers/DeviceProvisioningServiceManager.cs +++ b/src/AzureIoTHub.Portal/Server/Managers/DeviceProvisioningServiceManager.cs @@ -12,6 +12,7 @@ namespace AzureIoTHub.Portal.Server.Managers using AzureIoTHub.Portal.Models.v10; using Microsoft.Azure.Devices.Provisioning.Service; using Microsoft.Azure.Devices.Shared; + using System.Threading; public class DeviceProvisioningServiceManager : IDeviceProvisioningServiceManager { @@ -74,6 +75,11 @@ private async Task CreateNewEnrollmentGroup(string name, bool i return await this.dps.CreateOrUpdateEnrollmentGroupAsync(enrollmentGroup); } + public async Task DeleteEnrollmentGroupAsync(EnrollmentGroup enrollmentGroup, CancellationToken cancellationToken = default) + { + await this.dps.DeleteEnrollmentGroupAsync(enrollmentGroup, cancellationToken); + } + /// /// this function get the attestation mechanism of the DPS. /// diff --git a/src/AzureIoTHub.Portal/Server/Managers/IDeviceProvisioningServiceManager.cs b/src/AzureIoTHub.Portal/Server/Managers/IDeviceProvisioningServiceManager.cs index aadc9495a..2037dc048 100644 --- a/src/AzureIoTHub.Portal/Server/Managers/IDeviceProvisioningServiceManager.cs +++ b/src/AzureIoTHub.Portal/Server/Managers/IDeviceProvisioningServiceManager.cs @@ -3,6 +3,7 @@ namespace AzureIoTHub.Portal.Server.Managers { + using System.Threading; using System.Threading.Tasks; using AzureIoTHub.Portal.Models.v10; using Microsoft.Azure.Devices.Provisioning.Service; @@ -38,5 +39,7 @@ public interface IDeviceProvisioningServiceManager /// The device identifier. /// The device type. Task GetEnrollmentCredentialsAsync(string deviceId, string deviceType); + + Task DeleteEnrollmentGroupAsync(EnrollmentGroup enrollmentGroup, CancellationToken cancellationToken); } } diff --git a/src/AzureIoTHub.Portal/Server/Managers/ILoraDeviceMethodManager.cs b/src/AzureIoTHub.Portal/Server/Managers/ILoraDeviceMethodManager.cs index ff505439c..0768bbd40 100644 --- a/src/AzureIoTHub.Portal/Server/Managers/ILoraDeviceMethodManager.cs +++ b/src/AzureIoTHub.Portal/Server/Managers/ILoraDeviceMethodManager.cs @@ -4,11 +4,13 @@ namespace AzureIoTHub.Portal.Server.Managers { using System.Net.Http; + using System.Threading; using System.Threading.Tasks; using AzureIoTHub.Portal.Models.v10.LoRaWAN; public interface ILoraDeviceMethodManager { Task ExecuteLoRaDeviceMessage(string deviceId, DeviceModelCommand command); + Task CheckAzureFunctionReturn(CancellationToken cancellationToken); } } diff --git a/src/AzureIoTHub.Portal/Server/Managers/LoraDeviceMethodManager.cs b/src/AzureIoTHub.Portal/Server/Managers/LoraDeviceMethodManager.cs index d2a3c6bab..a3d1eeac9 100644 --- a/src/AzureIoTHub.Portal/Server/Managers/LoraDeviceMethodManager.cs +++ b/src/AzureIoTHub.Portal/Server/Managers/LoraDeviceMethodManager.cs @@ -8,6 +8,7 @@ namespace AzureIoTHub.Portal.Server.Managers using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text; + using System.Threading; using System.Threading.Tasks; using AzureIoTHub.Portal.Models.v10.LoRaWAN; @@ -35,5 +36,10 @@ public async Task ExecuteLoRaDeviceMessage(string deviceId, return await this.httpClient.PostAsync(new Uri($"api/cloudtodevicemessage/{deviceId}"), commandContent); } + + public async Task CheckAzureFunctionReturn(CancellationToken cancellationToken) + { + return await this.httpClient.GetAsync("", cancellationToken); + } } } diff --git a/src/AzureIoTHub.Portal/Server/ServicesHealthCheck/IoTHubHealthCheck.cs b/src/AzureIoTHub.Portal/Server/ServicesHealthCheck/IoTHubHealthCheck.cs new file mode 100644 index 000000000..5a18859d4 --- /dev/null +++ b/src/AzureIoTHub.Portal/Server/ServicesHealthCheck/IoTHubHealthCheck.cs @@ -0,0 +1,47 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.ServicesHealthCheck +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Azure.Devices; + using Microsoft.Extensions.Diagnostics.HealthChecks; + + public class IoTHubHealthCheck : IHealthCheck + { + private readonly RegistryManager registryManager; + private readonly ServiceClient serviceClient; + + public IoTHubHealthCheck( + RegistryManager registryManager, + ServiceClient serviceClient) + { + this.registryManager = registryManager; + this.serviceClient = serviceClient; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + _ = await this.serviceClient.GetServiceStatisticsAsync(cancellationToken); + + var query = this.registryManager.CreateQuery("SELECT count() FROM devices"); + + if (query == null) + { + return new HealthCheckResult(context.Registration.FailureStatus, description: "Something went wrong when the registry manager executed the query."); + } + _ = await query.GetNextAsJsonAsync(); + + return HealthCheckResult.Healthy(); + } + catch (Exception ex) + { + return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); + } + } + } +} diff --git a/src/AzureIoTHub.Portal/Server/ServicesHealthCheck/LoRaManagementKeyFacadeHealthCheck.cs b/src/AzureIoTHub.Portal/Server/ServicesHealthCheck/LoRaManagementKeyFacadeHealthCheck.cs new file mode 100644 index 000000000..67f646725 --- /dev/null +++ b/src/AzureIoTHub.Portal/Server/ServicesHealthCheck/LoRaManagementKeyFacadeHealthCheck.cs @@ -0,0 +1,37 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.ServicesHealthCheck +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using AzureIoTHub.Portal.Server.Managers; + using Microsoft.Extensions.Diagnostics.HealthChecks; + + public class LoRaManagementKeyFacadeHealthCheck : IHealthCheck + { + private readonly ILoraDeviceMethodManager loraDeviceMethodManager; + + public LoRaManagementKeyFacadeHealthCheck(ILoraDeviceMethodManager loraDeviceMethodManager) + { + this.loraDeviceMethodManager = loraDeviceMethodManager; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + var result = await this.loraDeviceMethodManager.CheckAzureFunctionReturn(cancellationToken); + + _ = result.EnsureSuccessStatusCode(); + + return HealthCheckResult.Healthy(); + } + catch (Exception ex) + { + return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); + } + } + } +} diff --git a/src/AzureIoTHub.Portal/Server/ServicesHealthCheck/ProvisioningServiceClientHealthCheck.cs b/src/AzureIoTHub.Portal/Server/ServicesHealthCheck/ProvisioningServiceClientHealthCheck.cs new file mode 100644 index 000000000..d092a2d40 --- /dev/null +++ b/src/AzureIoTHub.Portal/Server/ServicesHealthCheck/ProvisioningServiceClientHealthCheck.cs @@ -0,0 +1,62 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.ServicesHealthCheck +{ + using System; + using System.Security.Cryptography; + using System.Threading; + using System.Threading.Tasks; + using AzureIoTHub.Portal.Server.Wrappers; + using Microsoft.Azure.Devices.Provisioning.Service; + using Microsoft.Extensions.Diagnostics.HealthChecks; + + public class ProvisioningServiceClientHealthCheck : IHealthCheck + { + private readonly IProvisioningServiceClient provisioningServiceClient; + + public ProvisioningServiceClientHealthCheck(IProvisioningServiceClient provisioningServiceClient) + { + this.provisioningServiceClient = provisioningServiceClient; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + var enrollemntId = "enrollmentId"; + var attestation = new SymmetricKeyAttestation(GenerateKey(), GenerateKey()); + var enrollmentGroup = new EnrollmentGroup(enrollemntId, attestation); + + await ExecuteDPSWriteCheckAsync(enrollmentGroup); + await ExecuteDPSReadCheck(enrollmentGroup); + + await this.provisioningServiceClient.DeleteEnrollmentGroupAsync(enrollmentGroup, cancellationToken); + + return HealthCheckResult.Healthy(); + } + catch (Exception ex) + { + return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); + } + } + + private async Task ExecuteDPSWriteCheckAsync(EnrollmentGroup enrollmentGroup) + { + _ = await this.provisioningServiceClient.CreateOrUpdateEnrollmentGroupAsync(enrollmentGroup); + } + + private async Task ExecuteDPSReadCheck(EnrollmentGroup enrollmentGroup) + { + _ = await this.provisioningServiceClient.GetEnrollmentGroupAsync(enrollmentGroup.EnrollmentGroupId); + } + + private static string GenerateKey() + { + const int length = 48; + var rnd = RandomNumberGenerator.GetBytes(length); + + return Convert.ToBase64String(rnd); + } + } +} diff --git a/src/AzureIoTHub.Portal/Server/ServicesHealthCheck/StorageAccountHealthCheck.cs b/src/AzureIoTHub.Portal/Server/ServicesHealthCheck/StorageAccountHealthCheck.cs new file mode 100644 index 000000000..c92624495 --- /dev/null +++ b/src/AzureIoTHub.Portal/Server/ServicesHealthCheck/StorageAccountHealthCheck.cs @@ -0,0 +1,49 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.ServicesHealthCheck +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Azure.Storage.Blobs; + using Microsoft.Extensions.Diagnostics.HealthChecks; + + public class StorageAccountHealthCheck : IHealthCheck + { + private readonly BlobServiceClient blobServiceClient; + private readonly ConfigHandler configHandler; + + public StorageAccountHealthCheck(BlobServiceClient blobServiceClient, + ConfigHandler configHandler) + { + this.blobServiceClient = blobServiceClient; + this.configHandler = configHandler; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + + if (this.configHandler.StorageAccountBlobContainerName != null) + { + var containerClient = this.blobServiceClient.GetBlobContainerClient(this.configHandler.StorageAccountBlobContainerName); + + if (!await containerClient.ExistsAsync(cancellationToken)) + { + return new HealthCheckResult(context.Registration.FailureStatus, description: $"Container '{this.configHandler.StorageAccountBlobContainerName}' not exists"); + } + + _ = await containerClient.GetPropertiesAsync(cancellationToken: cancellationToken); + } + + return HealthCheckResult.Healthy(); + } + catch (Exception ex) + { + return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); + } + } + } +} diff --git a/src/AzureIoTHub.Portal/Server/ServicesHealthCheck/TableStorageHealthCheck.cs b/src/AzureIoTHub.Portal/Server/ServicesHealthCheck/TableStorageHealthCheck.cs new file mode 100644 index 000000000..6987332f2 --- /dev/null +++ b/src/AzureIoTHub.Portal/Server/ServicesHealthCheck/TableStorageHealthCheck.cs @@ -0,0 +1,49 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.ServicesHealthCheck +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Azure.Data.Tables; + using AzureIoTHub.Portal.Server.Factories; + using Microsoft.Extensions.Diagnostics.HealthChecks; + + public class TableStorageHealthCheck : IHealthCheck + { + private readonly ITableClientFactory tableClientFactory; + + public TableStorageHealthCheck(ITableClientFactory tableClientFactory) + { + this.tableClientFactory = tableClientFactory; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + var tableClient = this.tableClientFactory.GetTemplatesHealthCheck(); + + _ = await tableClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken); + var partitionKey = "0"; + var rowKey = "1"; + var entity = new TableEntity(partitionKey,rowKey) + { + {"key","value" } + }; + + _ = await tableClient.AddEntityAsync(entity, cancellationToken: cancellationToken); + _ = await tableClient.DeleteEntityAsync(partitionKey, rowKey, cancellationToken: cancellationToken); + _ = await tableClient.DeleteAsync(cancellationToken: cancellationToken); + + return HealthCheckResult.Healthy(); + } + catch (Exception ex) + { + return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); + } + } + + } +} diff --git a/src/AzureIoTHub.Portal/Server/Startup.cs b/src/AzureIoTHub.Portal/Server/Startup.cs index 2c4cc614b..98ab57476 100644 --- a/src/AzureIoTHub.Portal/Server/Startup.cs +++ b/src/AzureIoTHub.Portal/Server/Startup.cs @@ -18,6 +18,7 @@ namespace AzureIoTHub.Portal.Server using AzureIoTHub.Portal.Server.Managers; using AzureIoTHub.Portal.Server.Mappers; using AzureIoTHub.Portal.Server.Services; + using AzureIoTHub.Portal.Server.ServicesHealthCheck; using AzureIoTHub.Portal.Server.Wrappers; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -202,7 +203,12 @@ Specify the authorization token got from your IDP as a header. var mapper = mapperConfig.CreateMapper(); _ = services.AddSingleton(mapper); - _ = services.AddHealthChecks(); + _ = services.AddHealthChecks() + .AddCheck("iothubHealth") + .AddCheck("storageAccountHealth") + .AddCheck("tableStorageHealth") + .AddCheck("dpsHealth") + .AddCheck("loraManagementFacadeHealth"); } /// diff --git a/src/AzureIoTHub.Portal/Server/Wrappers/IProvisioningServiceClient.cs b/src/AzureIoTHub.Portal/Server/Wrappers/IProvisioningServiceClient.cs index 2b257287c..2f4bab320 100644 --- a/src/AzureIoTHub.Portal/Server/Wrappers/IProvisioningServiceClient.cs +++ b/src/AzureIoTHub.Portal/Server/Wrappers/IProvisioningServiceClient.cs @@ -1,8 +1,9 @@ -// Copyright (c) CGI France. All rights reserved. +// Copyright (c) CGI France. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace AzureIoTHub.Portal.Server.Wrappers { + using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Devices.Provisioning.Service; @@ -13,5 +14,7 @@ public interface IProvisioningServiceClient Task GetEnrollmentGroupAsync(string enrollmentGroupId); Task GetEnrollmentGroupAttestationAsync(string v); + + Task DeleteEnrollmentGroupAsync(EnrollmentGroup enrollmentGroup, CancellationToken cancellationToken); } -} \ No newline at end of file +} diff --git a/src/AzureIoTHub.Portal/Server/Wrappers/ProvisioningServiceClientWrapper.cs b/src/AzureIoTHub.Portal/Server/Wrappers/ProvisioningServiceClientWrapper.cs index 43654bd7e..3eb0eaccb 100644 --- a/src/AzureIoTHub.Portal/Server/Wrappers/ProvisioningServiceClientWrapper.cs +++ b/src/AzureIoTHub.Portal/Server/Wrappers/ProvisioningServiceClientWrapper.cs @@ -1,9 +1,10 @@ -// Copyright (c) CGI France. All rights reserved. +// Copyright (c) CGI France. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace AzureIoTHub.Portal.Server.Wrappers { using System.Net.Http; + using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Devices.Provisioning.Service; @@ -44,5 +45,10 @@ public async Task GetEnrollmentGroupAttestationAsync(stri throw new HttpRequestException(e.ErrorMessage, e, e.StatusCode); } } + + public async Task DeleteEnrollmentGroupAsync(EnrollmentGroup enrollmentGroup, CancellationToken cancellationToken = default) + { + await this.provisioningServiceClient.DeleteEnrollmentGroupAsync(enrollmentGroup, cancellationToken); + } } }