Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cache Control when uploading devices models images #1044

Merged
merged 5 commits into from
Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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" });
}
Comment on lines +111 to +116

Check notice

Code scanning / CodeQL

Missed opportunity to use Select

This foreach loop immediately maps its iteration variable to another variable [here](1) - consider mapping the sequence explicitly using '.Select(...)'.
}
}
}
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