Skip to content
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
4 changes: 4 additions & 0 deletions src/Adliance.AzureBlobSimulator/Controllers/BlobController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
145 changes: 131 additions & 14 deletions test/Adliance.AzureBlobSimulator.Tests/E2ETests.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,44 @@
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 readonly IContainer _simulatorContainer;
private IContainer _simulatorContainer = null!;
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")
Expand All @@ -30,10 +50,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);
Expand All @@ -45,7 +62,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()
Expand Down Expand Up @@ -80,26 +97,126 @@ 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());
}

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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 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);
}

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[Fact]
public async Task E2E_UploadAndDownloadBlobUsingSasAuth_ShouldSucceed()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hami88 Hier der besprochene E2E test für die SAS Authentifizierung.

{
const string blobName = "file.txt";

var env = new TestHostEnvironment();
var options = new OptionsWrapper<StorageOptions>(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();
var result = await blob.DownloadContentAsync(cts.Token);
Assert.Equal(txt, result.Value.Content.ToString());
}
}
Loading