Skip to content

Commit

Permalink
Add Cache Control when uploading devices models images (#1044)
Browse files Browse the repository at this point in the history
* Add unit test on ChangeDeviceModelImageAsync #763

* Add SyncImagesCacheControl #763

* Add unit test on SyncImagesCacheControl #763

* Add StorageAccountDeviceModelImageMaxAge config #763

* Remove unused variable
  • Loading branch information
hocinehacherouf authored Aug 9, 2022
1 parent 8bc2baf commit daca354
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -160,5 +160,15 @@ public void IdeasAuthenticationTokenMustHaveDefaultValue()
// Assert
_ = developmentConfigHandler.IdeasAuthenticationToken.Should().BeEmpty();
}

[Test]
public void StorageAccountDeviceModelImageMaxAgeMustHaveDefaultValue()
{
// Arrange
var developmentConfigHandler = new DevelopmentConfigHandler(new ConfigurationManager());

// Assert
_ = developmentConfigHandler.StorageAccountDeviceModelImageMaxAge.Should().Be(86400);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,90 +5,170 @@ namespace AzureIoTHub.Portal.Server.Tests.Unit.Managers
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
using Azure;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using AzureIoTHub.Portal.Server.Exceptions;
using AzureIoTHub.Portal.Server.Managers;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using NUnit.Framework;

[TestFixture]
public class DeviceModelImageManagerTest
public class DeviceModelImageManagerTest : BackendUnitTest
{
private MockRepository mockRepository;

private Mock<ILogger<DeviceModelImageManager>> mockLogger;
private Mock<BlobServiceClient> mockBlobServiceClient;
private Mock<BlobContainerClient> mockBlobContainerClient;
private Mock<BlobClient> mockBlobClient;
private Mock<ConfigHandler> mockConfigHandler;

private IDeviceModelImageManager deviceModelImageManager;

[SetUp]
public void SetUp()
public override void Setup()
{
this.mockRepository = new MockRepository(MockBehavior.Strict);
base.Setup();

this.mockBlobServiceClient = this.mockRepository.Create<BlobServiceClient>();
this.mockLogger = this.mockRepository.Create<ILogger<DeviceModelImageManager>>();
}
this.mockBlobServiceClient = MockRepository.Create<BlobServiceClient>();
this.mockBlobContainerClient = MockRepository.Create<BlobContainerClient>();
this.mockBlobClient = MockRepository.Create<BlobClient>();
this.mockConfigHandler = MockRepository.Create<ConfigHandler>();

private DeviceModelImageManager CreateManager()
{
var mockBlobContainerClient = new Mock<BlobContainerClient>();
_ = ServiceCollection.AddSingleton(this.mockBlobServiceClient.Object);
_ = ServiceCollection.AddSingleton(this.mockConfigHandler.Object);
_ = ServiceCollection.AddSingleton<IDeviceModelImageManager, DeviceModelImageManager>();

Services = ServiceCollection.BuildServiceProvider();

_ = this.mockBlobServiceClient
.Setup(x => x.GetBlobContainerClient(It.IsAny<string>()))
.Returns(mockBlobContainerClient.Object);
_ = mockBlobContainerClient
.Returns(this.mockBlobContainerClient.Object);

_ = this.mockBlobContainerClient
.Setup(x => x.SetAccessPolicy(It.IsAny<PublicAccessType>(),
It.IsAny<IEnumerable<BlobSignedIdentifier>>(),
It.IsAny<BlobRequestConditions>(),
It.IsAny<CancellationToken>()));
_ = mockBlobContainerClient.Setup(x => x.CreateIfNotExists(It.IsAny<PublicAccessType>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<BlobContainerEncryptionScopeOptions>(),
It.IsAny<CancellationToken>()));

return new DeviceModelImageManager(this.mockLogger.Object, this.mockBlobServiceClient.Object);
It.IsAny<IEnumerable<BlobSignedIdentifier>>(),
It.IsAny<BlobRequestConditions>(),
It.IsAny<CancellationToken>())).Returns(Response.FromValue(BlobsModelFactory.BlobContainerInfo(ETag.All, DateTimeOffset.Now), Mock.Of<Response>()));

_ = this.mockBlobContainerClient.Setup(x => x.CreateIfNotExists(It.IsAny<PublicAccessType>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<BlobContainerEncryptionScopeOptions>(),
It.IsAny<CancellationToken>())).Returns(Response.FromValue(BlobsModelFactory.BlobContainerInfo(ETag.All, DateTimeOffset.Now), Mock.Of<Response>()));

this.deviceModelImageManager = Services.GetRequiredService<IDeviceModelImageManager>();
}

[Test]
public void WhenDeleteAsyncFAiletDeleteDeviceModelImageAsyncShouldThrowAnInternalServerErrorException()
public async Task ChangeDeviceModelImageShouldUploadImageAndReturnItsUri()
{
// Arrange
var mockDeviceModelImageManager = this.CreateManager();
var deviceModelId = Fixture.Create<string>();
var expectedImageUri = Fixture.Create<Uri>();
using var imageAsMemoryStream = new MemoryStream(Encoding.UTF8.GetBytes(Fixture.Create<string>()));

_ = this.mockBlobServiceClient
.Setup(x => x.GetBlobContainerClient(It.IsAny<string>()))
.Returns(this.mockBlobContainerClient.Object);

_ = this.mockBlobContainerClient
.Setup(x => x.GetBlobClient(deviceModelId))
.Returns(this.mockBlobClient.Object);

_ = this.mockBlobClient
.Setup(client =>
client.SetHttpHeadersAsync(It.IsAny<BlobHttpHeaders>(), It.IsAny<BlobRequestConditions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Response.FromValue(BlobsModelFactory.BlobInfo(ETag.All, DateTimeOffset.Now), Mock.Of<Response>()));

_ = this.mockBlobClient
.Setup(client => client.UploadAsync(It.IsAny<Stream>(), true, It.IsAny<CancellationToken>()))
.ReturnsAsync(Response.FromValue(
BlobsModelFactory.BlobContentInfo(ETag.All, DateTimeOffset.Now, Array.Empty<byte>(), string.Empty,
1L), Mock.Of<Response>()));

_ = this.mockBlobClient
.Setup(client => client.Uri)
.Returns(expectedImageUri);

var mockBlobContainerClient = new Mock<BlobContainerClient>();
var mockBlobClient = new Mock<BlobClient>();
_ = this.mockConfigHandler.Setup(handler => handler.StorageAccountDeviceModelImageMaxAge).Returns(3600);

_ = this.mockLogger
.Setup(x => x.Log(
It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(), It.IsAny<Func<It.IsAnyType, Exception, string>>()));
// Act
var result = await this.deviceModelImageManager.ChangeDeviceModelImageAsync(deviceModelId, imageAsMemoryStream);

// Assert
_ = result.Should().Be(expectedImageUri.ToString());
MockRepository.VerifyAll();
}

[Test]
public async Task WhenDeleteAsyncFailedDeleteDeviceModelImageAsyncShouldThrowAnInternalServerErrorException()
{
// Arrange
var deviceModelId = Fixture.Create<string>();
var imageUri = Fixture.Create<Uri>();

_ = this.mockBlobServiceClient
.Setup(x => x.GetBlobContainerClient(It.IsAny<string>()))
.Returns(mockBlobContainerClient.Object);
.Returns(this.mockBlobContainerClient.Object);

_ = this.mockBlobContainerClient
.Setup(x => x.GetBlobClient(deviceModelId))
.Returns(this.mockBlobClient.Object);

_ = mockBlobContainerClient
.Setup(x => x.GetBlobClient(It.IsAny<string>()))
.Returns(mockBlobClient.Object);
_ = this.mockBlobClient
.Setup(client => client.Uri)
.Returns(imageUri);

_ = mockBlobClient
_ = this.mockBlobClient
.Setup(x => x.DeleteIfExistsAsync(It.IsAny<DeleteSnapshotsOption>(), It.IsAny<BlobRequestConditions>(), It.IsAny<CancellationToken>()))
.Throws(new RequestFailedException(""));

// Act
var act = async () => await mockDeviceModelImageManager.DeleteDeviceModelImageAsync("test");
var act = async () => await this.deviceModelImageManager.DeleteDeviceModelImageAsync(deviceModelId);

// Assert
_ = act.Should().ThrowAsync<InternalServerErrorException>();
_ = await act.Should().ThrowAsync<InternalServerErrorException>();
MockRepository.VerifyAll();
}

[Test]
public async Task SyncImagesCacheControlShouldUpdateBlobsCacheControls()
{
// Arrange
var deviceModelId = Fixture.Create<string>();

this.mockRepository.VerifyAll();
var blob = BlobsModelFactory.BlobItem(name:deviceModelId);
var blobsPage = Page<BlobItem>.FromValues(new[] { blob }, default, new Mock<Response>().Object);
var blobsPageable = AsyncPageable<BlobItem>.FromPages(new[] { blobsPage });

_ = this.mockBlobServiceClient
.Setup(x => x.GetBlobContainerClient(It.IsAny<string>()))
.Returns(this.mockBlobContainerClient.Object);

_ = this.mockBlobContainerClient
.Setup(x => x.GetBlobsAsync(It.IsAny<BlobTraits>(), It.IsAny<BlobStates>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(blobsPageable);

_ = this.mockBlobContainerClient
.Setup(x => x.GetBlobClient(deviceModelId))
.Returns(this.mockBlobClient.Object);

_ = this.mockBlobClient
.Setup(client =>
client.SetHttpHeadersAsync(It.IsAny<BlobHttpHeaders>(), It.IsAny<BlobRequestConditions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Response.FromValue(BlobsModelFactory.BlobInfo(ETag.All, DateTimeOffset.Now), Mock.Of<Response>()));

_ = this.mockConfigHandler.Setup(handler => handler.StorageAccountDeviceModelImageMaxAge).Returns(3600);

// Act
await this.deviceModelImageManager.SyncImagesCacheControl();

// Assert
MockRepository.VerifyAll();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,5 +184,15 @@ public void IdeasAuthenticationTokenMustHaveDefaultValue()
// Assert
_ = productionConfigHandler.IdeasAuthenticationToken.Should().BeEmpty();
}

[Test]
public void StorageAccountDeviceModelImageMaxAgeMustHaveDefaultValue()
{
// Arrange
var productionConfigHandler = new ProductionConfigHandler(new ConfigurationManager());

// Assert
_ = productionConfigHandler.StorageAccountDeviceModelImageMaxAge.Should().Be(86400);
}
}
}
3 changes: 3 additions & 0 deletions src/AzureIoTHub.Portal/Server/ConfigHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public abstract class ConfigHandler
internal const string IsLoRaFeatureEnabledKey = "LoRaFeature:Enabled";

internal const string StorageAccountConnectionStringKey = "StorageAccount:ConnectionString";
internal const string StorageAccountDeviceModelImageMaxAgeKey = "StorageAccount:DeviceModel:Image:MaxAgeInSeconds";

internal const string LoRaKeyManagementUrlKey = "LoRaKeyManagement:Url";
internal const string LoRaKeyManagementCodeKey = "LoRaKeyManagement:Code";
Expand Down Expand Up @@ -67,6 +68,8 @@ internal static ConfigHandler Create(IWebHostEnvironment env, IConfiguration con

internal abstract string StorageAccountConnectionString { get; }

internal abstract int StorageAccountDeviceModelImageMaxAge { get; }

internal abstract bool UseSecurityHeaders { get; }

internal abstract string OIDCScope { get; }
Expand Down
2 changes: 2 additions & 0 deletions src/AzureIoTHub.Portal/Server/DevelopmentConfigHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ internal DevelopmentConfigHandler(IConfiguration config)

internal override string StorageAccountConnectionString => this.config[StorageAccountConnectionStringKey];

internal override int StorageAccountDeviceModelImageMaxAge => this.config.GetValue(StorageAccountDeviceModelImageMaxAgeKey, 86400);

internal override bool UseSecurityHeaders => this.config.GetValue(UseSecurityHeadersKey, true);

internal override string OIDCScope => this.config[OIDCScopeKey];
Expand Down
20 changes: 18 additions & 2 deletions src/AzureIoTHub.Portal/Server/Managers/DeviceModelImageManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ public class DeviceModelImageManager : IDeviceModelImageManager

private readonly BlobServiceClient blobService;
private readonly ILogger<DeviceModelImageManager> logger;
private readonly ConfigHandler configHandler;

public DeviceModelImageManager(ILogger<DeviceModelImageManager> logger, BlobServiceClient blobService)
public DeviceModelImageManager(ILogger<DeviceModelImageManager> logger, BlobServiceClient blobService, ConfigHandler configHandler)
{
this.logger = logger;
this.blobService = blobService;
this.configHandler = configHandler;

var blobClient = this.blobService.GetBlobContainerClient(ImageContainerName);

Expand All @@ -39,9 +41,11 @@ public async Task<string> ChangeDeviceModelImageAsync(string deviceModelId, Stre

var blobClient = blobContainer.GetBlobClient(deviceModelId);

_ = await blobClient.SetHttpHeadersAsync(new BlobHttpHeaders { CacheControl = $"max-age={this.configHandler.StorageAccountDeviceModelImageMaxAge}, must-revalidate" });

this.logger.LogInformation($"Uploading to Blob storage as blob:\n\t {blobClient.Uri}\n");

_ = await blobClient.UploadAsync(stream, overwrite: true);
_ = await blobClient.UploadAsync(stream, true);

return blobClient.Uri.ToString();
}
Expand Down Expand Up @@ -99,5 +103,17 @@ public async Task InitializeDefaultImageBlob()

_ = await blobClient.UploadAsync(defaultImageStream, overwrite: true);
}

public async Task SyncImagesCacheControl()
{
var container = this.blobService.GetBlobContainerClient(ImageContainerName);

await foreach (var blob in container.GetBlobsAsync())
{
var blobClient = container.GetBlobClient(blob.Name);

_ = await blobClient.SetHttpHeadersAsync(new BlobHttpHeaders { CacheControl = $"max-age={this.configHandler.StorageAccountDeviceModelImageMaxAge}, must-revalidate" });
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ public interface IDeviceModelImageManager
Uri ComputeImageUri(string deviceModelId);

Task InitializeDefaultImageBlob();

Task SyncImagesCacheControl();
}
}
2 changes: 2 additions & 0 deletions src/AzureIoTHub.Portal/Server/ProductionConfigHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ internal ProductionConfigHandler(IConfiguration config)

internal override string StorageAccountConnectionString => this.config.GetConnectionString(StorageAccountConnectionStringKey);

internal override int StorageAccountDeviceModelImageMaxAge => this.config.GetValue(StorageAccountDeviceModelImageMaxAgeKey, 86400);

internal override bool UseSecurityHeaders => this.config.GetValue(UseSecurityHeadersKey, true);

internal override string OIDCScope => this.config[OIDCScopeKey];
Expand Down
8 changes: 5 additions & 3 deletions src/AzureIoTHub.Portal/Server/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -359,9 +359,11 @@ public async void Configure(IApplicationBuilder app, IWebHostEnvironment env)
});
});

await app?.ApplicationServices
.GetService<IDeviceModelImageManager>()
.InitializeDefaultImageBlob();

var deviceModelImageManager = app.ApplicationServices.GetService<IDeviceModelImageManager>();

await deviceModelImageManager?.InitializeDefaultImageBlob()!;
await deviceModelImageManager?.SyncImagesCacheControl()!;
}

private static void UseApiExceptionMiddleware(IApplicationBuilder app)
Expand Down

0 comments on commit daca354

Please sign in to comment.