Skip to content

Commit ab1c8a5

Browse files
committed
Tests
1 parent 36edcda commit ab1c8a5

File tree

9 files changed

+270
-6
lines changed

9 files changed

+270
-6
lines changed

.github/copilot-instructions.md

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Instructions for GitHub and VisualStudio Copilot
2+
### https://github.blog/changelog/2025-01-21-custom-repository-instructions-are-now-available-for-copilot-on-github-com-public-preview/
3+
4+
5+
## General
6+
7+
* Make only high confidence suggestions when reviewing code changes.
8+
* Always use the latest version C#, currently C# 13 features.
9+
* Files must have CRLF line endings.
10+
11+
## Formatting
12+
13+
* Apply code-formatting style defined in `.editorconfig`.
14+
* Prefer file-scoped namespace declarations and single-line using directives.
15+
* Insert a newline before the opening curly brace of any code block (e.g., after `if`, `for`, `while`, `foreach`, `using`, `try`, etc.).
16+
* Ensure that the final return statement of a method is on its own line.
17+
* Use pattern matching and switch expressions wherever possible.
18+
* Use `nameof` instead of string literals when referring to member names.
19+
20+
### Nullable Reference Types
21+
22+
* Declare variables non-nullable, and check for `null` at entry points.
23+
* Always use `is null` or `is not null` instead of `== null` or `!= null`.
24+
* Trust the C# null annotations and don't add null checks when the type system says a value cannot be null.
25+
26+
27+
### Testing
28+
29+
* We use xUnit SDK v3 with Microsoft.Testing.Platform (https://learn.microsoft.com/dotnet/core/testing/microsoft-testing-platform-intro)
30+
* Do not emit "Act", "Arrange" or "Assert" comments.
31+
* We do not use any mocking framework at the moment. Use NSubstitute, if necessary. Never use Moq.
32+
* Use "snake_case" for test method names but keep the original method under test intact.
33+
For example: when adding a test for methond "MethondToTest" instead of "MethondToTest_ShouldReturnSummarisedIssues" use "MethondToTest_should_return_summarised_issues".

playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyHttpTrigger.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ public class MyHttpTrigger(
2222
#endif
2323
EventHubProducerClient eventHubProducerClient,
2424
QueueServiceClient queueServiceClient,
25-
BlobServiceClient blobServiceClient)
25+
BlobServiceClient blobServiceClient,
26+
BlobContainerClient blobContainerClient)
2627
{
2728
[Function("injected-resources")]
2829
public IResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req)
@@ -35,6 +36,7 @@ public IResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] Ht
3536
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected EventHubProducerClient namespace: {eventHubProducerClient.FullyQualifiedNamespace}");
3637
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected QueueServiceClient URI: {queueServiceClient.Uri}");
3738
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected BlobServiceClient URI: {blobServiceClient.Uri}");
39+
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected BlobContainerClient URI: {blobContainerClient.Uri}");
3840
return Results.Text(stringBuilder.ToString());
3941
}
4042
}

src/Components/Aspire.Azure.Storage.Blobs/AssemblyInfo.cs

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Aspire.Azure.Storage.Blobs;
55
using Aspire;
66
using Azure.Storage.Blobs;
7+
using System.Runtime.CompilerServices;
78

89
[assembly: ConfigurationSchema("Aspire:Azure:Storage:Blobs", typeof(AzureStorageBlobsSettings))]
910
[assembly: ConfigurationSchema("Aspire:Azure:Storage:Blobs:ClientOptions", typeof(BlobClientOptions), exclusionPaths: ["Default"])]
@@ -12,3 +13,5 @@
1213
"Azure",
1314
"Azure.Core",
1415
"Azure.Identity")]
16+
17+
[assembly: InternalsVisibleTo("Aspire.Azure.Storage.Blobs.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001004b86c4cb78549b34bab61a3b1800e23bfeb5b3ec390074041536a7e3cbd97f5f04cf0f857155a8928eaa29ebfd11cfbbad3ba70efea7bda3226c6a8d370a4cd303f714486b6ebc225985a638471e6ef571cc92a4613c00b8fa65d61ccee0cbe5f36330c9a01f4183559f1bef24cc2917c6d913e3a541333a1d05d9bed22b38cb")]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Configuration;
5+
using Aspire.Azure.Common;
6+
using Aspire.Azure.Storage.Blobs;
7+
using Xunit;
8+
9+
namespace Aspire.Hosting.Azure.Tests;
10+
11+
public class AzureBlobStorageContainerSettingsTests
12+
{
13+
[Theory]
14+
[InlineData(null)]
15+
[InlineData("")]
16+
[InlineData(";")]
17+
[InlineData("Endpoint=https://example.blob.core.windows.net;")]
18+
[InlineData("ContainerName=my-container;")]
19+
[InlineData("Endpoint=https://example.blob.core.windows.net;ExtraParam=value;")]
20+
public void ParseConnectionString_invalid_input(string? connectionString)
21+
{
22+
var settings = new AzureBlobStorageContainerSettings();
23+
24+
((IConnectionStringSettings)settings).ParseConnectionString(connectionString);
25+
26+
Assert.Null(settings.ConnectionString);
27+
Assert.Null(settings.BlobContainerName);
28+
}
29+
30+
[Fact]
31+
public void ParseConnectionString_invalid_input_results_in_AE()
32+
{
33+
var settings = new AzureBlobStorageContainerSettings();
34+
string connectionString = "InvalidConnectionString";
35+
36+
Assert.Throws<ArgumentException>(() => ((IConnectionStringSettings)settings).ParseConnectionString(connectionString));
37+
}
38+
39+
[Theory]
40+
[InlineData("Endpoint=https://example.blob.core.windows.net;ContainerName=my-container")]
41+
[InlineData("Endpoint=https://example.blob.core.windows.net;ContainerName=my-container;ExtraParam=value")]
42+
[InlineData("endpoint=https://example.blob.core.windows.net;containername=my-container")]
43+
[InlineData("ENDPOINT=https://example.blob.core.windows.net;CONTAINERNAME=my-container")]
44+
public void ParseConnectionString_valid_input(string connectionString)
45+
{
46+
var settings = new AzureBlobStorageContainerSettings();
47+
48+
((IConnectionStringSettings)settings).ParseConnectionString(connectionString);
49+
50+
Assert.Equal("https://example.blob.core.windows.net", settings.ConnectionString);
51+
Assert.Equal("my-container", settings.BlobContainerName);
52+
}
53+
}

tests/Aspire.Azure.Storage.Blobs.Tests/ConformanceTests.cs

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public class ConformanceTests : ConformanceTests<BlobServiceClient, AzureStorage
2424

2525
protected override string ActivitySourceName => "Azure.Storage.Blobs.BlobContainerClient";
2626

27+
// AzureStorageBlobsSettings subclassed by AzureBlobStorageContainerSettings
28+
protected override bool CheckOptionClassSealed => false;
29+
2730
protected override string[] RequiredLogCategories => new string[]
2831
{
2932
"Azure.Core",

tests/Aspire.Hosting.Azure.Tests/AzureStorageEmulatorFunctionalTests.cs

+46-1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,50 @@ public async Task VerifyWaitForOnAzureStorageEmulatorForBlobsBlocksDependentReso
6262
await app.StopAsync();
6363
}
6464

65+
[Fact]
66+
[RequiresDocker]
67+
public async Task VerifyWaitForOnAzureStorageEmulatorForBlobContainersBlocksDependentResources()
68+
{
69+
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
70+
using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
71+
72+
var healthCheckTcs = new TaskCompletionSource<HealthCheckResult>();
73+
builder.Services.AddHealthChecks().AddAsyncCheck("blocking_check", () =>
74+
{
75+
return healthCheckTcs.Task;
76+
});
77+
78+
var storage = builder.AddAzureStorage("resource")
79+
.RunAsEmulator()
80+
.WithHealthCheck("blocking_check");
81+
82+
var blobs = storage.AddBlobs("blobs");
83+
var blobContainer = blobs.AddBlobContainer("testblobcontainer");
84+
85+
var dependentResource = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
86+
.WaitFor(blobContainer);
87+
88+
using var app = builder.Build();
89+
90+
var pendingStart = app.StartAsync(cts.Token);
91+
92+
var rns = app.Services.GetRequiredService<ResourceNotificationService>();
93+
94+
await rns.WaitForResourceAsync(storage.Resource.Name, KnownResourceStates.Running, cts.Token);
95+
96+
await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, cts.Token);
97+
98+
healthCheckTcs.SetResult(HealthCheckResult.Healthy());
99+
100+
await rns.WaitForResourceHealthyAsync(blobContainer.Resource.Name, cts.Token);
101+
102+
await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token);
103+
104+
await pendingStart;
105+
106+
await app.StopAsync();
107+
}
108+
65109
[Fact]
66110
[RequiresDocker]
67111
public async Task VerifyAzureStorageEmulatorResource()
@@ -91,7 +135,7 @@ public async Task VerifyAzureStorageEmulatorResource()
91135

92136
[Fact]
93137
[RequiresDocker]
94-
public async Task VerifyAzureStorageEmulatorBlobContainer()
138+
public async Task VerifyAzureStorageEmulator_blobcontainer_auto_created()
95139
{
96140
using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
97141
var storage = builder.AddAzureStorage("storage").RunAsEmulator();
@@ -115,6 +159,7 @@ public async Task VerifyAzureStorageEmulatorBlobContainer()
115159
var blobContainerClient = serviceClient.GetBlobContainerClient("testblobcontainer");
116160

117161
var exists = await blobContainerClient.ExistsAsync();
162+
Assert.True(exists, "Blob container should exist after starting the application.");
118163

119164
var blobNameAndContent = Guid.NewGuid().ToString();
120165
var response = await blobContainerClient.UploadBlobAsync(blobNameAndContent, new BinaryData(blobNameAndContent));

tests/Aspire.Hosting.Azure.Tests/AzureStorageExtensionsTests.cs

+107-2
Original file line numberDiff line numberDiff line change
@@ -164,20 +164,125 @@ public async Task AddAzureStorage_RunAsEmulator_SetSkipApiVersionCheck()
164164
Assert.Contains("--skipApiVersionCheck", args);
165165
}
166166

167+
[Fact]
168+
public async Task AddBlobs_ConnectionString_resolved_expected_RunAsEmulator()
169+
{
170+
const string expected = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;";
171+
172+
using var builder = TestDistributedApplicationBuilder.Create();
173+
174+
var storage = builder.AddAzureStorage("storage").RunAsEmulator(e =>
175+
{
176+
e.WithEndpoint("blob", e => e.AllocatedEndpoint = new(e, "localhost", 10000));
177+
e.WithEndpoint("queue", e => e.AllocatedEndpoint = new(e, "localhost", 10001));
178+
e.WithEndpoint("table", e => e.AllocatedEndpoint = new(e, "localhost", 10002));
179+
});
180+
181+
Assert.True(storage.Resource.IsContainer());
182+
183+
var blobs = storage.AddBlobs("blob");
184+
185+
Assert.Equal(expected, await ((IResourceWithConnectionString)blobs.Resource).GetConnectionStringAsync());
186+
}
187+
188+
[Fact]
189+
public async Task AddBlobs_ConnectionString_resolved_expected()
190+
{
191+
const string blobsConnectionString = "https://myblob";
192+
193+
using var builder = TestDistributedApplicationBuilder.Create();
194+
195+
var storagesku = builder.AddParameter("storagesku");
196+
var storage = builder.AddAzureStorage("storage");
197+
storage.Resource.Outputs["blobEndpoint"] = blobsConnectionString;
198+
199+
var blobs = storage.AddBlobs("blob");
200+
201+
Assert.Equal(blobsConnectionString, await ((IResourceWithConnectionString)blobs.Resource).GetConnectionStringAsync());
202+
}
203+
204+
[Fact]
205+
public void AddBlobs_ConnectionString_unresolved_expected()
206+
{
207+
using var builder = TestDistributedApplicationBuilder.Create();
208+
209+
var storage = builder.AddAzureStorage("storage");
210+
var blobs = storage.AddBlobs("blob");
211+
212+
Assert.Equal("{storage.outputs.blobEndpoint}", blobs.Resource.ConnectionStringExpression.ValueExpression);
213+
}
214+
215+
[Fact]
216+
public async Task AddBlobContainer_ConnectionString_resolved_expected_RunAsEmulator()
217+
{
218+
const string blobContainerName = "my-blob-container";
219+
220+
using var builder = TestDistributedApplicationBuilder.Create();
221+
222+
var storage = builder.AddAzureStorage("storage").RunAsEmulator(e =>
223+
{
224+
e.WithEndpoint("blob", e => e.AllocatedEndpoint = new(e, "localhost", 10000));
225+
e.WithEndpoint("queue", e => e.AllocatedEndpoint = new(e, "localhost", 10001));
226+
e.WithEndpoint("table", e => e.AllocatedEndpoint = new(e, "localhost", 10002));
227+
});
228+
229+
Assert.True(storage.Resource.IsContainer());
230+
231+
var blobs = storage.AddBlobs("blob");
232+
var blobContainer = blobs.AddBlobContainer(name: "myContainer", blobContainerName);
233+
234+
string? blobConntionString = await ((IResourceWithConnectionString)blobs.Resource).GetConnectionStringAsync();
235+
string expected = $"Endpoint=\"{blobConntionString}\";ContainerName={blobContainerName};";
236+
237+
Assert.Equal(expected, await ((IResourceWithConnectionString)blobContainer.Resource).GetConnectionStringAsync());
238+
}
239+
240+
[Fact]
241+
public async Task AddBlobContainer_ConnectionString_resolved_expected()
242+
{
243+
const string blobContainerName = "my-blob-container";
244+
245+
using var builder = TestDistributedApplicationBuilder.Create();
246+
247+
var storagesku = builder.AddParameter("storagesku");
248+
var storage = builder.AddAzureStorage("storage");
249+
storage.Resource.Outputs["blobEndpoint"] = "https://myblob";
250+
251+
var blobs = storage.AddBlobs("blob");
252+
var blobContainer = blobs.AddBlobContainer(name: "myContainer", blobContainerName);
253+
254+
string? blobsConnectionString = await ((IResourceWithConnectionString)blobs.Resource).GetConnectionStringAsync();
255+
string expected = $"Endpoint=\"{blobsConnectionString}\";ContainerName={blobContainerName};";
256+
257+
Assert.Equal(expected, await ((IResourceWithConnectionString)blobContainer.Resource).GetConnectionStringAsync());
258+
}
259+
260+
[Fact]
261+
public void AddBlobContainer_ConnectionString_unresolved_expected()
262+
{
263+
using var builder = TestDistributedApplicationBuilder.Create();
264+
265+
var storage = builder.AddAzureStorage("storage");
266+
var blobs = storage.AddBlobs("blob");
267+
var blobContainer = blobs.AddBlobContainer(name: "myContainer");
268+
269+
Assert.Equal("Endpoint=\"{storage.outputs.blobEndpoint}\";ContainerName=myContainer;", blobContainer.Resource.ConnectionStringExpression.ValueExpression);
270+
}
271+
167272
[Fact]
168273
public async Task ResourceNamesBicepValid()
169274
{
170275
using var builder = TestDistributedApplicationBuilder.Create();
171276
var storage = builder.AddAzureStorage("storage");
172277

173278
var blobs = storage.AddBlobs("myblobs");
174-
var blob = blobs.AddBlobContainer("myContainer", "my-blob-container");
279+
var blob = blobs.AddBlobContainer(name: "myContainer", blobContainerName: "my-blob-container");
175280
var queues = storage.AddQueues("myqueues");
176281
var tables = storage.AddTables("mytables");
177282

178283
var manifest = await AzureManifestUtils.GetManifestWithBicep(storage.Resource);
179284

180285
await Verifier.Verify(manifest.BicepText, extension: "bicep")
181-
.UseDirectory("Snapshots");
286+
.UseHelixAwareDirectory("Snapshots");
182287
}
183288
}

tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.ResourceNamesBicepValid.verified.bicep

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = {
2626
parent: storage
2727
}
2828

29-
resource my_blob_container 'Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01' = {
29+
resource myContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01' = {
3030
name: 'my-blob-container'
3131
parent: blobs
3232
}

tests/Aspire.Playground.Tests/ProjectSpecificTests.cs

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Text.RegularExpressions;
45
using Aspire.Hosting;
56
using Aspire.Hosting.Tests.Utils;
67
using Aspire.TestUtilities;
@@ -76,7 +77,26 @@ await WaitForAllTextAsync(app,
7677
timeoutSecs: 160);
7778

7879
// Assert that HTTP triggers work correctly
79-
await AppHostTests.CreateHttpClientWithResilience(app, "funcapp").GetAsync("/api/injected-resources");
80+
var response = await AppHostTests.CreateHttpClientWithResilience(app, "funcapp").GetAsync("/api/injected-resources");
81+
82+
// The output contains multiple text lines.
83+
// There are some URLs which contain port number, but the port numbers may vary, so we replace those for test validation.
84+
var output = await response.Content.ReadAsStringAsync();
85+
output = Regex.Replace(output, pattern: @"(?<=http:\/\/127\.0\.0\.1:)\d+", replacement: "*");
86+
87+
_testOutput.WriteLine($"[DEBUG] Response:\r\n{output}");
88+
var expectedStrings = new string[]
89+
{
90+
"Aspire-injected EventHubProducerClient namespace: localhost",
91+
"Aspire-injected QueueServiceClient URI: http://127.0.0.1:*/devstoreaccount1",
92+
"Aspire-injected BlobServiceClient URI: http://127.0.0.1:*/devstoreaccount1",
93+
"Aspire-injected BlobContainerClient URI: http://127.0.0.1:*/devstoreaccount1/myblobcontainer"
94+
};
95+
foreach (string s in expectedStrings)
96+
{
97+
Assert.Contains(s, output);
98+
}
99+
80100
await WaitForAllTextAsync(app,
81101
[
82102
"Executed 'Functions.injected-resources'"

0 commit comments

Comments
 (0)