From 90eb04af3a9cb4a3a4ae91398ee8a686c5f7226c Mon Sep 17 00:00:00 2001 From: Stefan Stadlbauer Date: Mon, 23 Mar 2026 10:53:13 +0100 Subject: [PATCH 1/2] Support custom x-ms-range header to support range requests with OpenReadAsync --- .../Controllers/BlobController.cs | 4 + .../E2ETests.cs | 74 +++++++++++++++---- 2 files changed, 63 insertions(+), 15 deletions(-) diff --git a/src/Adliance.AzureBlobSimulator/Controllers/BlobController.cs b/src/Adliance.AzureBlobSimulator/Controllers/BlobController.cs index 4975e39..12ead08 100644 --- a/src/Adliance.AzureBlobSimulator/Controllers/BlobController.cs +++ b/src/Adliance.AzureBlobSimulator/Controllers/BlobController.cs @@ -48,6 +48,10 @@ public IActionResult ReadBlob([FromRoute, ContainerName] string container, [From Response.Headers["ETag"] = Guid.NewGuid().ToString(); Response.Headers["Content-Length"] = fileInfo.Length.ToString(CultureInfo.InvariantCulture); + // support custom ms range header, see https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob?tabs=microsoft-entra-id#request + if (Request.Headers.TryGetValue("x-ms-range", out var requestedRange)) + Request.Headers["Range"] = requestedRange; + var fileStream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read); return File(fileStream, contentType, enableRangeProcessing: true); } diff --git a/test/Adliance.AzureBlobSimulator.Tests/E2ETests.cs b/test/Adliance.AzureBlobSimulator.Tests/E2ETests.cs index d6e390c..95ae06d 100644 --- a/test/Adliance.AzureBlobSimulator.Tests/E2ETests.cs +++ b/test/Adliance.AzureBlobSimulator.Tests/E2ETests.cs @@ -8,17 +8,31 @@ namespace Adliance.AzureBlobSimulator.Tests; public class BlobSimulatorE2ETests : IAsyncLifetime { - private readonly IContainer _simulatorContainer; + private IContainer _simulatorContainer; private string? _connectionString; private readonly string _storagePath = Path.Combine(Path.GetTempPath(), "e2e-storage"); private const string AccountName = "devstoreaccount1"; private const string AccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="; + private const string ContainerName = "demo"; public BlobSimulatorE2ETests() { Directory.CreateDirectory(_storagePath); + } + + public async Task InitializeAsync() + { + var image = new ImageFromDockerfileBuilder() + .WithContextDirectory(CommonDirectoryPath.GetSolutionDirectory().DirectoryPath) + .WithDockerfileDirectory(CommonDirectoryPath.GetSolutionDirectory().DirectoryPath) + .WithDockerfile("dockerfile") + .WithName("azureblobsimulator:e2etest") + .WithReuse(false) + .Build(); - _simulatorContainer = new ContainerBuilder("ghcr.io/adliance/azureblobsimulator:latest") + await image.CreateAsync(); + + _simulatorContainer = new ContainerBuilder(image) .WithBindMount(_storagePath, "/storage") .WithEnvironment("Storage__LocalPath", "/storage") .WithEnvironment("ASPNETCORE_URLS", "http://+:80") @@ -30,10 +44,7 @@ public BlobSimulatorE2ETests() .ForStatusCode(HttpStatusCode.OK)) ) .Build(); - } - public async Task InitializeAsync() - { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); await _simulatorContainer.StartAsync(cts.Token); var logs = await _simulatorContainer.GetLogsAsync(ct: cts.Token); @@ -45,7 +56,7 @@ public async Task InitializeAsync() $"DefaultEndpointsProtocol=http;" + $"AccountName={AccountName};" + $"AccountKey={AccountKey};" + - $"BlobEndpoint=http://localhost:{port}/{AccountName};"; + $"BlobEndpoint=http://localhost:{port}/{ContainerName};"; } public async Task DisposeAsync() @@ -80,26 +91,59 @@ public async Task E2E_UploadAndDownloadBlob_ShouldSucceed() var execResult = await _simulatorContainer.ExecAsync(["uname", "-s"], ct: cts.Token); Assert.Equal("Linux", execResult.Stdout.Trim()); - var accountFolder = Path.Combine(_storagePath, AccountName); - if (!Directory.Exists(accountFolder)) + var containerFolder = Path.Combine(_storagePath, ContainerName); + if (!Directory.Exists(containerFolder)) { - Directory.CreateDirectory(accountFolder); + Directory.CreateDirectory(containerFolder); } - const string containerName = "demo"; - var containerFolder = Path.Combine(accountFolder, containerName); + var container = client.GetBlobContainerClient(ContainerName); + var blob = container.GetBlockBlobClient("file.txt"); + const string txt = "This is a E2E test file which should be uploaded and downloaded by the simulator."; + using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(txt)); + await blob.UploadAsync(stream, cancellationToken: cts.Token); + + var result = await blob.DownloadContentAsync(cts.Token); + Assert.Equal(txt, result.Value.Content.ToString()); + } + + /// + /// Performs a full end-to-end test of the Azure Blob Simulator running in a Linux container: + /// 1. Verifies that the simulator container is running Linux. + /// 2. Ensures a blob container exists or creates it if missing. + /// 3. Uploads a text blob to the container. + /// 4. Downloads the same blob and verifies its content matches the uploaded data. + /// + /// + /// This test validates that the simulator correctly handles fundamental blob storage operations + /// in a Linux environment, including container creation, blob upload, and blob download. + /// It also acts as a sanity check to ensure the test is running in a Linux container, + /// which is required for true Linux E2E testing. + /// + [Fact] + public async Task E2E_UploadAndDownloadBlobAsStream_ShouldSucceed() + { + var client = new BlobServiceClient(_connectionString); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + var execResult = await _simulatorContainer.ExecAsync(["uname", "-s"], ct: cts.Token); + Assert.Equal("Linux", execResult.Stdout.Trim()); + + var containerFolder = Path.Combine(_storagePath, ContainerName); if (!Directory.Exists(containerFolder)) { Directory.CreateDirectory(containerFolder); } - var container = client.GetBlobContainerClient(containerName); + var container = client.GetBlobContainerClient(ContainerName); var blob = container.GetBlockBlobClient("file.txt"); const string txt = "This is a E2E test file which should be uploaded and downloaded by the simulator."; using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(txt)); - await blob.UploadAsync(stream); + await blob.UploadAsync(stream, cancellationToken: cts.Token); - var result = await blob.DownloadContentAsync(); - Assert.Equal(txt, result.Value.Content.ToString()); + var resultStream = await blob.OpenReadAsync(cancellationToken: cts.Token); + using var streamReader = new StreamReader(resultStream); + var result = await streamReader.ReadToEndAsync(cancellationToken: cts.Token); + Assert.Equal(txt, result); } } From b0c08e62d8ee7c05c314c76a34a15592265faa42 Mon Sep 17 00:00:00 2001 From: Daniel Morawetz Date: Tue, 24 Mar 2026 16:23:30 +0100 Subject: [PATCH 2/2] Add E2E test to show SAS authentication --- .../E2ETests.cs | 75 ++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/test/Adliance.AzureBlobSimulator.Tests/E2ETests.cs b/test/Adliance.AzureBlobSimulator.Tests/E2ETests.cs index 95ae06d..ca318ee 100644 --- a/test/Adliance.AzureBlobSimulator.Tests/E2ETests.cs +++ b/test/Adliance.AzureBlobSimulator.Tests/E2ETests.cs @@ -1,14 +1,20 @@ +using System.Globalization; using System.Net; +using Adliance.AzureBlobSimulator.Models; +using Adliance.AzureBlobSimulator.Services; +using Adliance.AzureBlobSimulator.Tests.SharedAccessSignature; +using Azure; using Azure.Storage.Blobs; using Azure.Storage.Blobs.Specialized; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; +using Microsoft.Extensions.Options; namespace Adliance.AzureBlobSimulator.Tests; public class BlobSimulatorE2ETests : IAsyncLifetime { - private IContainer _simulatorContainer; + private IContainer _simulatorContainer = null!; private string? _connectionString; private readonly string _storagePath = Path.Combine(Path.GetTempPath(), "e2e-storage"); private const string AccountName = "devstoreaccount1"; @@ -146,4 +152,71 @@ public async Task E2E_UploadAndDownloadBlobAsStream_ShouldSucceed() var result = await streamReader.ReadToEndAsync(cancellationToken: cts.Token); Assert.Equal(txt, result); } + + /// + /// Performs a full end-to-end test of the Azure Blob Simulator running in a Linux container: + /// 1. Verifies that the simulator container is running Linux. + /// 2. Ensures a blob container exists. + /// 3. Uploads a text blob to the container. + /// 4. Downloads the same blob and verifies its content matches the uploaded data. + /// + /// + /// This test validates that the simulator correctly handles fundamental blob storage operations + /// in a Linux environment, including SAS authentication, blob upload, and blob download. + /// It also acts as a sanity check to ensure the test is running in a Linux container, + /// which is required for true Linux E2E testing. + /// + [Fact] + public async Task E2E_UploadAndDownloadBlobUsingSasAuth_ShouldSucceed() + { + const string blobName = "file.txt"; + + var env = new TestHostEnvironment(); + var options = new OptionsWrapper(new StorageOptions + { + Accounts = [new StorageAccountOptions { Name = AccountName, Key = AccountKey }] + }); + var validator = new SasValidatorService(options, null, env); + var sasHelper = new SasHelper(options, validator); + + // read, list, write + const string sp = "rlw"; + // blob + const string sr = "b"; + // expiry date + var se = DateTimeOffset.UtcNow.AddDays(1).ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture); + var sv = Constants.MsVersion; + + // Generate a signature for our test file we want to upload and download. + // Can't be static, because the expiry date is included in the signature. + var sig = sasHelper.GenerateSignature(sp, null, se, null, sv, sr, canonicalizedResource: $"/blob/{AccountName}/{ContainerName}/{blobName}", accountName: AccountName); + + var credentials = new AzureSasCredential($"sv={sv}&sp={sp}&se={se}&sr={sr}&sig={Uri.EscapeDataString(sig)}"); + + var port = _simulatorContainer.GetMappedPublicPort(80); + + // For SAS authentication the account name is required to be the first segment in the URL for the authentication to work correctly. + var client = new BlobServiceClient(new Uri($"http://localhost:{port}/{AccountName}"), credentials); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + var execResult = await _simulatorContainer.ExecAsync(["uname", "-s"], ct: cts.Token); + Assert.Equal("Linux", execResult.Stdout.Trim()); + + var containerFolder = Path.Combine(_storagePath, ContainerName); + if (!Directory.Exists(containerFolder)) + { + Directory.CreateDirectory(containerFolder); + } + + // The container name is not correctly inserted into the URI when defined in the blob container client. + var container = client.GetBlobContainerClient(""); + // Thus, the container name is prepended manually. Might be related to the used API version, which our simulator might not support. + var blob = container.GetBlockBlobClient($"{ContainerName}/{blobName}"); + const string txt = "This is a E2E test file which should be uploaded and downloaded by the simulator."; + using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(txt)); + await blob.UploadAsync(stream, cancellationToken: cts.Token); + + var result = await blob.DownloadContentAsync(cts.Token); + Assert.Equal(txt, result.Value.Content.ToString()); + } }