From 9c28e803f3e568fd7a90330aabb076106f1eb100 Mon Sep 17 00:00:00 2001 From: ssgueye2 <127868584+ssgueye2@users.noreply.github.com> Date: Fri, 21 Apr 2023 11:56:38 +0200 Subject: [PATCH] 1935 as a developper i want to use amazon s3 to store and expose devices images (#2032) * AWS S3 store & expose device images * s3 storage image + some unit testing * #1935 code + tests * #1935 code + tests * AWS image cache control * removing usersecret --- .../ConfigHandler.cs | 1 + .../AzureIoTHub.Portal.Infrastructure.csproj | 6 +- .../ConfigHandlerBase.cs | 2 + .../DevelopmentConfigHandler.cs | 1 + .../Managers/AwsDeviceModelImageManager.cs | 195 ++++++++ .../ProductionAWSConfigHandler.cs | 1 + .../ProductionAzureConfigHandler.cs | 2 + .../Startup/AWSServiceCollectionExtension.cs | 7 + src/AzureIoTHub.Portal.Server/Startup.cs | 7 + .../DevelopmentConfigHandlerTests.cs | 1 + .../AwsDeviceModelImageManagerTest.cs | 435 ++++++++++++++++++ .../ProductionAWSConfigHandlerTests.cs | 2 + .../ProductionAzureConfigHandlerTests.cs | 1 + 13 files changed, 659 insertions(+), 2 deletions(-) create mode 100644 src/AzureIoTHub.Portal.Infrastructure/Managers/AwsDeviceModelImageManager.cs create mode 100644 src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Managers/AwsDeviceModelImageManagerTest.cs diff --git a/src/AzureIoTHub.Portal.Domain/ConfigHandler.cs b/src/AzureIoTHub.Portal.Domain/ConfigHandler.cs index 6b300e48c..c38f5d5b3 100644 --- a/src/AzureIoTHub.Portal.Domain/ConfigHandler.cs +++ b/src/AzureIoTHub.Portal.Domain/ConfigHandler.cs @@ -79,5 +79,6 @@ public abstract class ConfigHandler public abstract string AWSAccessSecret { get; } public abstract string AWSRegion { get; } public abstract string AWSS3StorageConnectionString { get; } + public abstract string AWSBucketName { get; } } } diff --git a/src/AzureIoTHub.Portal.Infrastructure/AzureIoTHub.Portal.Infrastructure.csproj b/src/AzureIoTHub.Portal.Infrastructure/AzureIoTHub.Portal.Infrastructure.csproj index 415b1e0e2..791b13905 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/AzureIoTHub.Portal.Infrastructure.csproj +++ b/src/AzureIoTHub.Portal.Infrastructure/AzureIoTHub.Portal.Infrastructure.csproj @@ -108,6 +108,7 @@ + @@ -126,8 +127,9 @@ - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs b/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs index 425cb40a2..f5b8f3454 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs @@ -56,5 +56,7 @@ internal abstract class ConfigHandlerBase : ConfigHandler internal const string AWSAccessSecretKey = "AWS:AccessSecret"; internal const string AWSRegionKey = "AWS:Region"; internal const string AWSS3StorageConnectionStringKey = "AWS:S3Storage:ConnectionString"; + internal const string AWSBucketNameKey = "AWS:BucketName"; + } } diff --git a/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs b/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs index 77db9f86e..1581123d9 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs @@ -88,5 +88,6 @@ internal DevelopmentConfigHandler(IConfiguration config) public override string AWSAccessSecret => this.config[AWSAccessSecretKey]!; public override string AWSRegion => this.config[AWSRegionKey]!; public override string AWSS3StorageConnectionString => this.config[AWSS3StorageConnectionStringKey]!; + public override string AWSBucketName => this.config[AWSBucketNameKey]!; } } diff --git a/src/AzureIoTHub.Portal.Infrastructure/Managers/AwsDeviceModelImageManager.cs b/src/AzureIoTHub.Portal.Infrastructure/Managers/AwsDeviceModelImageManager.cs new file mode 100644 index 000000000..663e7c294 --- /dev/null +++ b/src/AzureIoTHub.Portal.Infrastructure/Managers/AwsDeviceModelImageManager.cs @@ -0,0 +1,195 @@ +// 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.Managers +{ + using System; + using System.Threading.Tasks; + using Amazon; + using Amazon.S3; + using Amazon.S3.Model; + using Azure; + using AzureIoTHub.Portal.Application.Managers; + using AzureIoTHub.Portal.Domain; + using AzureIoTHub.Portal.Domain.Exceptions; + using AzureIoTHub.Portal.Domain.Options; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + + public class AwsDeviceModelImageManager : IDeviceModelImageManager + { + private readonly ILogger logger; + private readonly ConfigHandler configHandler; + private readonly IOptions imageOptions; + private readonly IAmazonS3 s3Client; + + public AwsDeviceModelImageManager( + ILogger logger, + ConfigHandler configHandler, + IOptions options, + IAmazonS3 s3Client) + { + this.logger = logger; + this.configHandler = configHandler; + this.imageOptions = options; + this.s3Client = s3Client; + } + + + public async Task ChangeDeviceModelImageAsync(string deviceModelId, Stream stream) + { + this.logger.LogInformation($"Uploading Image to AWS S3 storage"); + + //Portal must be able to upload images to Amazon S3 + var putObjectRequest = new PutObjectRequest + { + BucketName = this.configHandler.AWSBucketName, + Key = deviceModelId, + InputStream = stream, + ContentType = "image/*", + Headers = {CacheControl = $"max-age={this.configHandler.StorageAccountDeviceModelImageMaxAge}, must-revalidate" } + }; + + var putObjectResponse = await this.s3Client.PutObjectAsync(putObjectRequest); + + if (putObjectResponse.HttpStatusCode == System.Net.HttpStatusCode.OK) + { + //Images on S3 are publicly accessible and read-only + var putAclRequest = new PutACLRequest + { + BucketName = this.configHandler.AWSBucketName, + Key = deviceModelId, + CannedACL = S3CannedACL.PublicRead // Set the object's ACL to public read + }; + var putACLResponse = await this.s3Client.PutACLAsync(putAclRequest); + + return putACLResponse.HttpStatusCode == System.Net.HttpStatusCode.OK + ? ComputeImageUrl(deviceModelId) + : throw new InternalServerErrorException("Error by setting the image access to public and read-only"); + } + else + { + throw new InternalServerErrorException("Error by uploading the image in S3 Storage"); + } + + } + + public Uri ComputeImageUri(string deviceModelId) + { + throw new NotImplementedException(); + } + + private string ComputeImageUrl(string deviceModelId) + { + return $"https://{this.configHandler.AWSBucketName}.s3.{RegionEndpoint.GetBySystemName(this.configHandler.AWSRegion)}.amazonaws.com/{deviceModelId}"; + } + public async Task DeleteDeviceModelImageAsync(string deviceModelId) + { + + this.logger.LogInformation($"Deleting image from AWS S3 storage"); + + var deleteImageObject = new DeleteObjectRequest + { + BucketName = this.configHandler.AWSBucketName, + Key = deviceModelId + }; + try + { + _ = await this.s3Client.DeleteObjectAsync(deleteImageObject); + + } + catch (RequestFailedException e) + { + throw new InternalServerErrorException("Unable to delete the image from S3 storage.", e); + } + } + + public async Task SetDefaultImageToModel(string deviceModelId) + { + this.logger.LogInformation($"Uploading Default Image to AWS S3 storage"); + + //Portal must be able to upload images to Amazon S3 + var putObjectRequest = new PutObjectRequest + { + BucketName = this.configHandler.AWSBucketName, + Key = deviceModelId, + FilePath = $"../Resources/{this.imageOptions.Value.DefaultImageName}", + ContentType = "image/*", // image content type + Headers = {CacheControl = $"max-age={this.configHandler.StorageAccountDeviceModelImageMaxAge}, must-revalidate" } + + }; + + var putObjectResponse = await this.s3Client.PutObjectAsync(putObjectRequest); + + if (putObjectResponse.HttpStatusCode == System.Net.HttpStatusCode.OK) + { + //Images on S3 are publicly accessible and read-only + var putAclRequest = new PutACLRequest + { + BucketName = this.configHandler.AWSBucketName, + Key = deviceModelId, + CannedACL = S3CannedACL.PublicRead // Set the object's ACL to public read + }; + var putACLResponse = await this.s3Client.PutACLAsync(putAclRequest); + + return putACLResponse.HttpStatusCode == System.Net.HttpStatusCode.OK + ? ComputeImageUrl(deviceModelId) + : throw new InternalServerErrorException("Error by setting the image access to public and read-only"); + } + else + { + throw new InternalServerErrorException("Error by uploading the image in S3 Storage"); + } + + } + + public async Task InitializeDefaultImageBlob() + { + + this.logger.LogInformation($"Initializing default Image to AWS S3 storage"); + + var putObjectRequest = new PutObjectRequest + { + BucketName = this.configHandler.AWSBucketName, + Key = this.imageOptions.Value.DefaultImageName, + FilePath = $"../Resources/{this.imageOptions.Value.DefaultImageName}", + ContentType = "image/*", // image content type + Headers = {CacheControl = $"max-age={this.configHandler.StorageAccountDeviceModelImageMaxAge}, must-revalidate" } + + }; + + var putObjectResponse = await this.s3Client.PutObjectAsync(putObjectRequest); + + if (putObjectResponse.HttpStatusCode == System.Net.HttpStatusCode.OK) + { + //Images on S3 are publicly accessible and read-only + var putAclRequest = new PutACLRequest + { + BucketName = this.configHandler.AWSBucketName, + Key = this.imageOptions.Value.DefaultImageName, + CannedACL = S3CannedACL.PublicRead // Set the object's ACL to public read + }; + var putACLResponse = await this.s3Client.PutACLAsync(putAclRequest); + + if (putACLResponse.HttpStatusCode != System.Net.HttpStatusCode.OK) + { + throw new InternalServerErrorException("Error by setting the image access to public and read-only"); + } + } + else + { + throw new InternalServerErrorException("Error by uploading the image in S3 Storage"); + } + + } + + public Task SyncImagesCacheControl() + { + /* We don't need an implementation of + this mehod for AWS because new images will processed by the method SetDefaultImageToModel + */ + throw new NotImplementedException(); + + } + } +} diff --git a/src/AzureIoTHub.Portal.Infrastructure/ProductionAWSConfigHandler.cs b/src/AzureIoTHub.Portal.Infrastructure/ProductionAWSConfigHandler.cs index 15382697a..2cd263bc5 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/ProductionAWSConfigHandler.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/ProductionAWSConfigHandler.cs @@ -88,5 +88,6 @@ internal ProductionAWSConfigHandler(IConfiguration config) public override string AWSAccessSecret => this.config[AWSAccessSecretKey]!; public override string AWSRegion => this.config[AWSRegionKey]!; public override string AWSS3StorageConnectionString => this.config[AWSS3StorageConnectionStringKey]!; + public override string AWSBucketName => this.config[AWSBucketNameKey]!; } } diff --git a/src/AzureIoTHub.Portal.Infrastructure/ProductionAzureConfigHandler.cs b/src/AzureIoTHub.Portal.Infrastructure/ProductionAzureConfigHandler.cs index 48b5ed60e..8aba668b6 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/ProductionAzureConfigHandler.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/ProductionAzureConfigHandler.cs @@ -90,5 +90,7 @@ internal ProductionAzureConfigHandler(IConfiguration config) public override string AWSRegion => throw new NotImplementedException(); public override string AWSS3StorageConnectionString => throw new NotImplementedException(); + + public override string AWSBucketName => throw new NotImplementedException(); } } diff --git a/src/AzureIoTHub.Portal.Infrastructure/Startup/AWSServiceCollectionExtension.cs b/src/AzureIoTHub.Portal.Infrastructure/Startup/AWSServiceCollectionExtension.cs index 6574c209f..c50ac9f64 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/Startup/AWSServiceCollectionExtension.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/Startup/AWSServiceCollectionExtension.cs @@ -6,8 +6,11 @@ namespace AzureIoTHub.Portal.Infrastructure.Startup using Amazon; using Amazon.IoT; using Amazon.IotData; + using Amazon.S3; using Amazon.SecretsManager; + using AzureIoTHub.Portal.Application.Managers; using AzureIoTHub.Portal.Domain; + using AzureIoTHub.Portal.Infrastructure.Managers; using Microsoft.Extensions.DependencyInjection; public static class AWSServiceCollectionExtension @@ -34,6 +37,10 @@ private static IServiceCollection ConfigureAWSClient(this IServiceCollection ser _ = services.AddSingleton(() => new AmazonSecretsManagerClient(configuration.AWSAccess, configuration.AWSAccessSecret, RegionEndpoint.GetBySystemName(configuration.AWSRegion))); + _ = services.AddSingleton(() => new AmazonS3Client(configuration.AWSAccess, configuration.AWSAccessSecret, RegionEndpoint.GetBySystemName(configuration.AWSRegion))); + + _ = services.AddTransient(); + return services; } } diff --git a/src/AzureIoTHub.Portal.Server/Startup.cs b/src/AzureIoTHub.Portal.Server/Startup.cs index 7276e7c91..1aa81e693 100644 --- a/src/AzureIoTHub.Portal.Server/Startup.cs +++ b/src/AzureIoTHub.Portal.Server/Startup.cs @@ -448,6 +448,7 @@ public async void Configure(IApplicationBuilder app, IWebHostEnvironment env) await ConfigureAzureAsync(app); break; case CloudProviders.AWS: + await ConfigureAwsAsync(app); break; default: break; @@ -463,6 +464,12 @@ private static async Task ConfigureAzureAsync(IApplicationBuilder app) await EnsureDatabaseCreatedAndUpToDate(app)!; } + private static async Task ConfigureAwsAsync(IApplicationBuilder app) + { + var deviceModelImageManager = app.ApplicationServices.GetService(); + + await deviceModelImageManager?.InitializeDefaultImageBlob()!; + } private static void UseApiExceptionMiddleware(IApplicationBuilder app) { diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs index d7dc11942..dbfea399f 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs @@ -53,6 +53,7 @@ private DevelopmentConfigHandler CreateDevelopmentConfigHandler() [TestCase(ConfigHandlerBase.AWSRegionKey, nameof(ConfigHandlerBase.AWSRegion))] [TestCase(ConfigHandlerBase.AWSS3StorageConnectionStringKey, nameof(ConfigHandlerBase.AWSS3StorageConnectionString))] [TestCase(ConfigHandlerBase.CloudProviderKey, nameof(ConfigHandlerBase.CloudProvider))] + [TestCase(ConfigHandlerBase.AWSBucketNameKey, nameof(ConfigHandlerBase.AWSBucketName))] public void SettingsShouldGetValueFromAppSettings(string configKey, string configPropertyName) { // Arrange diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Managers/AwsDeviceModelImageManagerTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Managers/AwsDeviceModelImageManagerTest.cs new file mode 100644 index 000000000..82a2bc0cb --- /dev/null +++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Managers/AwsDeviceModelImageManagerTest.cs @@ -0,0 +1,435 @@ +// 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.Managers +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Net; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Amazon; + using Amazon.S3; + using Amazon.S3.Model; + using AutoFixture; + using Azure; + using AzureIoTHub.Portal.Application.Managers; + using AzureIoTHub.Portal.Domain; + using AzureIoTHub.Portal.Domain.Exceptions; + using AzureIoTHub.Portal.Domain.Options; + using AzureIoTHub.Portal.Infrastructure.Managers; + using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases; + using FluentAssertions; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + using Moq; + using NUnit.Framework; + + [TestFixture] + public class AwsDeviceModelImageManagerTest : BackendUnitTest + { + private Mock mockConfigHandler; + private Mock> mockDeviceModelImageOptions; + private Mock s3ClientMock; + private Mock putObjectResponse; + private Mock putObjectRequest; + private Mock putACL; + private IDeviceModelImageManager awsDeviceModelImageManager; + + + + public override void Setup() + { + base.Setup(); + + this.mockDeviceModelImageOptions = MockRepository.Create>(); + this.mockConfigHandler = MockRepository.Create(); + + this.s3ClientMock = MockRepository.Create(); + this.putObjectRequest = MockRepository.Create(); + this.putObjectResponse = MockRepository.Create(); + this.putACL = MockRepository.Create(); + + _ = ServiceCollection.AddSingleton(this.mockConfigHandler.Object); + _ = ServiceCollection.AddSingleton(this.s3ClientMock.Object); + _ = ServiceCollection.AddSingleton(this.putObjectRequest.Object); + _ = ServiceCollection.AddSingleton(); + + Services = ServiceCollection.BuildServiceProvider(); + + this.awsDeviceModelImageManager = Services.GetRequiredService(); + } + + /*===========================*** Tests for ChangeDeviceModelImageAsync() **===========================*/ + [Test] + public async Task ChangeDeviceModelImageShouldUploadImageAndReturnAUri() + { + // Arrange + var deviceModelId = Fixture.Create(); + using var imageAsMemoryStream = new MemoryStream(Encoding.UTF8.GetBytes(Fixture.Create())); + var bucketName = Fixture.Create(); + var region = Fixture.Create(); + + _ = this.mockConfigHandler.Setup(handler => handler.AWSRegion).Returns(region); + _ = this.mockConfigHandler.Setup(handler => handler.AWSBucketName).Returns(bucketName); + _ = this.mockConfigHandler.Setup(handler => handler.StorageAccountDeviceModelImageMaxAge).Returns(Fixture.Create()); + + + _ = this.s3ClientMock.Setup(s3 => s3.PutObjectAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new PutObjectResponse + { + HttpStatusCode = HttpStatusCode.OK + }); + + _ = this.s3ClientMock.Setup(s3 => s3.PutACLAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new PutACLResponse + { + HttpStatusCode = HttpStatusCode.OK + }); + + var expectedRetunUrl = $"https://{bucketName}.s3.{RegionEndpoint.GetBySystemName(region)}.amazonaws.com/{deviceModelId}"; + + + // Act + var result = await this.awsDeviceModelImageManager.ChangeDeviceModelImageAsync(deviceModelId, imageAsMemoryStream); + + // Assert + Assert.NotNull(result); + _ = result.Should().Be(expectedRetunUrl); + this.s3ClientMock.VerifyAll(); + } + + [Test] + public void ChangeDeviceModelImageShouldThrowsAnExceptionForPutObjectResStatusCode() + { + // Arrange + var deviceModelId = Fixture.Create(); + using var imageAsMemoryStream = new MemoryStream(Encoding.UTF8.GetBytes(Fixture.Create())); + var bucketName = "invalid bucket Name for example"; + var region = Fixture.Create(); + + _ = this.mockConfigHandler.Setup(handler => handler.AWSRegion).Returns(region); + _ = this.mockConfigHandler.Setup(handler => handler.AWSBucketName).Returns(bucketName); + _ = this.mockConfigHandler.Setup(handler => handler.StorageAccountDeviceModelImageMaxAge).Returns(Fixture.Create()); + + + _ = this.s3ClientMock.Setup(s3 => s3.PutObjectAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new PutObjectResponse + { + HttpStatusCode = HttpStatusCode.BadGateway + }); + + + // Assert + _ = Assert.ThrowsAsync(async () => + { + // Act + _ = await this.awsDeviceModelImageManager.ChangeDeviceModelImageAsync(deviceModelId, imageAsMemoryStream); + + }, "Error by uploading the image in S3 Storage"); + this.s3ClientMock.VerifyAll(); + } + + [Test] + public void ChangeDeviceModelImageShouldThrowsAnExceptionForPutACLStatusCode() + { + // Arrange + var deviceModelId = Fixture.Create(); + using var imageAsMemoryStream = new MemoryStream(Encoding.UTF8.GetBytes(Fixture.Create())); + var bucketName = "invalid bucket Name for example"; + var region = Fixture.Create(); + + _ = this.mockConfigHandler.Setup(handler => handler.AWSRegion).Returns(region); + _ = this.mockConfigHandler.Setup(handler => handler.AWSBucketName).Returns(bucketName); + _ = this.mockConfigHandler.Setup(handler => handler.StorageAccountDeviceModelImageMaxAge).Returns(Fixture.Create()); + + + _ = this.s3ClientMock.Setup(s3 => s3.PutObjectAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new PutObjectResponse + { + HttpStatusCode = HttpStatusCode.OK + }); + _ = this.s3ClientMock.Setup(s3 => s3.PutACLAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new PutACLResponse + { + HttpStatusCode = HttpStatusCode.BadGateway + }); + + // Assert + _ = Assert.ThrowsAsync(async () => + { + // Act + _ = await this.awsDeviceModelImageManager.ChangeDeviceModelImageAsync(deviceModelId, imageAsMemoryStream); + + }, "Error by setting the image access to public and read-only"); + this.s3ClientMock.VerifyAll(); + } + + /*===========================*** Tests for DeleteDeviceModelImageAsync() **===========================*/ + + [Test] + public async Task FailedDeletingDeviceModelImageShouldThrowsAnInternalServerError() + { + // Arrange + var deviceModelId = Fixture.Create(); + var bucketName = Fixture.Create(); + + _ = this.mockConfigHandler.Setup(handler => handler.AWSBucketName).Returns(bucketName); + + _ = this.s3ClientMock.Setup(s3 => s3.DeleteObjectAsync(It.IsAny(), It.IsAny())) + .Throws(new RequestFailedException("")); + + // Act + var act = async () => await this.awsDeviceModelImageManager.DeleteDeviceModelImageAsync(deviceModelId); + + // Assert + _ = await act.Should().ThrowAsync("Unable to delete the image from the blob storage."); + this.s3ClientMock.VerifyAll(); + } + + [Test] + public async Task SucessDeletingDeviceModelImageShouldNotThrowsAnInternalServerError() + { + // Arrange + var deviceModelId = Fixture.Create(); + var bucketName = Fixture.Create(); + + _ = this.mockConfigHandler.Setup(handler => handler.AWSBucketName).Returns(bucketName); + + _ = this.s3ClientMock.Setup(s3 => s3.DeleteObjectAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new DeleteObjectResponse()); + + // Act + await this.awsDeviceModelImageManager.DeleteDeviceModelImageAsync(deviceModelId); + + // Assert + this.s3ClientMock.VerifyAll(); + } + + /*===========================*** Tests for SetDefaultImageToModel() **===========================*/ + + [Test] + public async Task SetDefaultImageToModeShouldUploadImageAndReturnAUri() + { + // Arrange + var deviceModelId = Fixture.Create(); + var bucketName = Fixture.Create(); + var region = Fixture.Create(); + + _ = this.mockConfigHandler.Setup(handler => handler.AWSRegion).Returns(region); + _ = this.mockConfigHandler.Setup(handler => handler.AWSBucketName).Returns(bucketName); + _ = this.mockConfigHandler.Setup(handler => handler.StorageAccountDeviceModelImageMaxAge).Returns(Fixture.Create()); + + + _ = this.s3ClientMock.Setup(s3 => s3.PutObjectAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new PutObjectResponse + { + HttpStatusCode = HttpStatusCode.OK + }); + + _ = this.s3ClientMock.Setup(s3 => s3.PutACLAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new PutACLResponse + { + HttpStatusCode = HttpStatusCode.OK + }); + + var expectedRetunUrl = $"https://{bucketName}.s3.{RegionEndpoint.GetBySystemName(region)}.amazonaws.com/{deviceModelId}"; + + + // Act + var result = await this.awsDeviceModelImageManager.SetDefaultImageToModel(deviceModelId); + + // Assert + Assert.NotNull(result); + _ = result.Should().Be(expectedRetunUrl); + this.s3ClientMock.VerifyAll(); + } + + [Test] + public void SetDefaultImageToModeShouldThrowsAnExceptionForPutObjectResStatusCode() + { + // Arrange + var deviceModelId = Fixture.Create(); + var bucketName = "invalid bucket Name for example"; + var region = Fixture.Create(); + + _ = this.mockConfigHandler.Setup(handler => handler.AWSRegion).Returns(region); + _ = this.mockConfigHandler.Setup(handler => handler.AWSBucketName).Returns(bucketName); + _ = this.mockConfigHandler.Setup(handler => handler.StorageAccountDeviceModelImageMaxAge).Returns(Fixture.Create()); + + + _ = this.s3ClientMock.Setup(s3 => s3.PutObjectAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new PutObjectResponse + { + HttpStatusCode = HttpStatusCode.BadGateway + }); + + + // Assert + _ = Assert.ThrowsAsync(async () => + { + // Act + _ = await this.awsDeviceModelImageManager.SetDefaultImageToModel(deviceModelId); + + }, "Error by uploading the image in S3 Storage"); + this.s3ClientMock.VerifyAll(); + } + + [Test] + public void SetDefaultImageToModelShouldThrowsAnExceptionForPutACLStatusCode() + { + // Arrange + var deviceModelId = Fixture.Create(); + var bucketName = "invalid bucket Name for example"; + var region = Fixture.Create(); + + _ = this.mockConfigHandler.Setup(handler => handler.AWSRegion).Returns(region); + _ = this.mockConfigHandler.Setup(handler => handler.AWSBucketName).Returns(bucketName); + _ = this.mockConfigHandler.Setup(handler => handler.StorageAccountDeviceModelImageMaxAge).Returns(Fixture.Create()); + + + _ = this.s3ClientMock.Setup(s3 => s3.PutObjectAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new PutObjectResponse + { + HttpStatusCode = HttpStatusCode.OK + }); + _ = this.s3ClientMock.Setup(s3 => s3.PutACLAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new PutACLResponse + { + HttpStatusCode = HttpStatusCode.BadGateway + }); + + // Assert + _ = Assert.ThrowsAsync(async () => + { + // Act + _ = await this.awsDeviceModelImageManager.SetDefaultImageToModel(deviceModelId); + + }, "Error by setting the image access to public and read-only"); + this.s3ClientMock.VerifyAll(); + } + + + /*===========================*** Tests for InitializeDefaultImageBlob() **===========================*/ + + [Test] + public async Task InitializeDefaultImageBlobShouldUploadDefaultImage() + { + // Arrange + var bucketName = Fixture.Create(); + + _ = this.mockConfigHandler.Setup(handler => handler.AWSBucketName).Returns(bucketName); + _ = this.mockConfigHandler.Setup(handler => handler.StorageAccountDeviceModelImageMaxAge).Returns(Fixture.Create()); + + _ = this.s3ClientMock.Setup(s3 => s3.PutObjectAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new PutObjectResponse + { + HttpStatusCode = HttpStatusCode.OK + }); + + _ = this.s3ClientMock.Setup(s3 => s3.PutACLAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new PutACLResponse + { + HttpStatusCode = HttpStatusCode.OK + }); + + + // Act + await this.awsDeviceModelImageManager.InitializeDefaultImageBlob(); + + // Assert + this.s3ClientMock.VerifyAll(); + } + + [Test] + public void InitializeDefaultImageBlobShouldThrowsAnExceptionForPutObjectResStatusCode() + { + // Arrange + var bucketName = "invalid bucket Name for example"; + + _ = this.mockConfigHandler.Setup(handler => handler.AWSBucketName).Returns(bucketName); + _ = this.mockConfigHandler.Setup(handler => handler.StorageAccountDeviceModelImageMaxAge).Returns(Fixture.Create()); + + _ = this.s3ClientMock.Setup(s3 => s3.PutObjectAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new PutObjectResponse + { + HttpStatusCode = HttpStatusCode.BadGateway + }); + + + // Assert + _ = Assert.ThrowsAsync(async () => + { + // Act + await this.awsDeviceModelImageManager.InitializeDefaultImageBlob(); + + }, "Error by uploading the image in S3 Storage"); + this.s3ClientMock.VerifyAll(); + } + + [Test] + public void InitializeDefaultImageBlobShouldThrowsAnExceptionForPutACLStatusCode() + { + // Arrange + var bucketName = "invalid bucket Name for example"; + + _ = this.mockConfigHandler.Setup(handler => handler.AWSBucketName).Returns(bucketName); + _ = this.mockConfigHandler.Setup(handler => handler.StorageAccountDeviceModelImageMaxAge).Returns(Fixture.Create()); + + _ = this.s3ClientMock.Setup(s3 => s3.PutObjectAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new PutObjectResponse + { + HttpStatusCode = HttpStatusCode.OK + }); + _ = this.s3ClientMock.Setup(s3 => s3.PutACLAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new PutACLResponse + { + HttpStatusCode = HttpStatusCode.BadGateway + }); + + // Assert + _ = Assert.ThrowsAsync(async () => + { + // Act + await this.awsDeviceModelImageManager.InitializeDefaultImageBlob(); + + }, "Error by setting the image access to public and read-only"); + this.s3ClientMock.VerifyAll(); + } + + /*===========================*** Tests for SyncImagesCacheControl() **===========================*/ + + + [Test] + public void SyncImagesCacheControlShouldThrowsANotImplmentedException() + { + // Arrange + // We just verify that the method throw NotImplmentedExeception() + + // Act + var act = () => this.awsDeviceModelImageManager.SyncImagesCacheControl(); + + // Assert + _ = act.Should().ThrowAsync(); + } + + /*===========================*** Tests for ComputeImageUri() **===========================*/ + + + [Test] + public void ComputeImageUriShouldThrowsANotImplmentedException() + { + // Arrange + var deviceModelId = Fixture.Create(); + + // Assert + _ = Assert.Throws(() => + { + // Act + _ = this.awsDeviceModelImageManager.ComputeImageUri(deviceModelId); + }); + } + } +} diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionAWSConfigHandlerTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionAWSConfigHandlerTests.cs index 567188f06..26cbcafc5 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionAWSConfigHandlerTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionAWSConfigHandlerTests.cs @@ -53,6 +53,8 @@ private ProductionAWSConfigHandler CreateProductionAWSConfigHandler() [TestCase(ConfigHandlerBase.AWSRegionKey, nameof(ConfigHandlerBase.AWSRegion))] [TestCase(ConfigHandlerBase.AWSS3StorageConnectionStringKey, nameof(ConfigHandlerBase.AWSS3StorageConnectionString))] [TestCase(ConfigHandlerBase.CloudProviderKey, nameof(ConfigHandlerBase.CloudProvider))] + [TestCase(ConfigHandlerBase.AWSBucketNameKey, nameof(ConfigHandlerBase.AWSBucketName))] + public void SettingsShouldGetValueFromAppSettings(string configKey, string configPropertyName) { // Arrange diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionAzureConfigHandlerTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionAzureConfigHandlerTests.cs index 047aa1435..cc425bcba 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionAzureConfigHandlerTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionAzureConfigHandlerTests.cs @@ -99,6 +99,7 @@ public void SettingsShouldGetValueFromAppSettings(string configKey, string confi [TestCase(nameof(ConfigHandlerBase.AWSAccessSecret))] [TestCase(nameof(ConfigHandlerBase.AWSRegion))] [TestCase(nameof(ConfigHandlerBase.AWSS3StorageConnectionString))] + [TestCase(nameof(ConfigHandlerBase.AWSBucketName))] public void SettingsShouldThrowError(string configPropertyName) { // Arrange