Skip to content

Commit 2faab94

Browse files
paulirwinclaude
andauthored
Add xUnit integration tests for Azure Search Emulator (#18)
* Add xUnit integration tests for Azure Search Emulator - Create AzureSearchEmulator.IntegrationTests project with Testcontainers - Implement 8 comprehensive integration tests covering: - Index creation and deletion - Document indexing and retrieval - Search functionality with filtering, sorting, and paging - Use product/e-commerce domain with realistic test data - Add DebugClient project for local debugging/diagnostics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix project guid and add to Release build * PR feedback --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d55edde commit 2faab94

18 files changed

Lines changed: 782 additions & 22 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,4 +397,5 @@ FodyWeavers.xsd
397397

398398
# End of https://www.toptal.com/developers/gitignore/api/visualstudio
399399

400-
!**/aspnetapp.pfx
400+
!**/aspnetapp.pfx
401+
.DS_Store
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<IsTestProject>true</IsTestProject>
8+
<WarningsAsErrors>nullable</WarningsAsErrors>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Azure.Search.Documents" Version="11.7.0" />
13+
<PackageReference Include="Testcontainers" Version="4.8.1" />
14+
<PackageReference Include="xunit" Version="2.9.3" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
16+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
17+
<PrivateAssets>all</PrivateAssets>
18+
</PackageReference>
19+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<ProjectReference Include="..\AzureSearchEmulator\AzureSearchEmulator.csproj" />
24+
</ItemGroup>
25+
26+
</Project>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using Azure;
2+
using Azure.Core.Pipeline;
3+
using Azure.Search.Documents;
4+
using Azure.Search.Documents.Indexes;
5+
using DotNet.Testcontainers.Builders;
6+
using DotNet.Testcontainers.Containers;
7+
using Xunit;
8+
9+
namespace AzureSearchEmulator.IntegrationTests;
10+
11+
/// <summary>
12+
/// Factory for creating and managing an Azure Search Emulator container using Testcontainers.
13+
/// The container runs the emulator built from the Dockerfile in the repository root.
14+
/// </summary>
15+
public class EmulatorFactory : IAsyncLifetime
16+
{
17+
private readonly int _httpsPort = Random.Shared.Next(5000, 60000);
18+
19+
private IContainer? _container;
20+
21+
/// <summary>
22+
/// Gets the HTTPS endpoint URI for the running emulator container.
23+
/// </summary>
24+
private Uri Endpoint { get; set; } = null!;
25+
26+
/// <summary>
27+
/// Starts the emulator container.
28+
/// Must be called before using the SearchIndexClient or SearchClient.
29+
/// </summary>
30+
public async Task InitializeAsync()
31+
{
32+
var image = new ImageFromDockerfileBuilder()
33+
.WithDockerfileDirectory(CommonDirectoryPath.GetProjectDirectory(), "..")
34+
.WithCleanUp(true)
35+
.Build();
36+
37+
_container = new ContainerBuilder()
38+
.WithImage(image)
39+
.WithPortBinding(_httpsPort, _httpsPort)
40+
.WithEnvironment("ASPNETCORE_URLS", $"https://+:{_httpsPort}")
41+
.WithEnvironment("ASPNETCORE_HTTPS_PORT", _httpsPort.ToString())
42+
.WithEnvironment("ASPNETCORE_Kestrel__Certificates__Default__Password", "password")
43+
.WithEnvironment("ASPNETCORE_Kestrel__Certificates__Default__Path", "/app/aspnetapp.pfx")
44+
.WithWaitStrategy(Wait.ForUnixContainer().UntilExternalTcpPortIsAvailable(_httpsPort)) // NOTE: we cannot use HTTP wait strategy due to self-signed cert
45+
.Build();
46+
47+
await image.CreateAsync();
48+
await _container.StartAsync();
49+
50+
// Get the mapped HTTPS port
51+
var mappedPort = _container.GetMappedPublicPort(_httpsPort);
52+
Endpoint = new Uri($"https://localhost:{mappedPort}");
53+
}
54+
55+
/// <summary>
56+
/// Gets a key credential for use with the Azure Search SDK (any key works for the emulator).
57+
/// </summary>
58+
private static AzureKeyCredential Credential { get; } = new("test-key");
59+
60+
/// <summary>
61+
/// Creates a SearchIndexClient configured for testing against the emulator.
62+
/// </summary>
63+
public SearchIndexClient CreateSearchIndexClient()
64+
{
65+
var handler = new HttpClientHandler();
66+
// Allow untrusted certificates for testing. This is safe in test environments only, not for production use.
67+
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
68+
69+
var options = new SearchClientOptions
70+
{
71+
Transport = new HttpClientTransport(handler),
72+
Retry = { MaxRetries = 1 } // Reduce retries to speed up tests
73+
};
74+
75+
return new SearchIndexClient(Endpoint, Credential, options);
76+
}
77+
78+
/// <summary>
79+
/// Creates a SearchClient configured for testing against the emulator.
80+
/// </summary>
81+
public SearchClient CreateSearchClient(string indexName)
82+
{
83+
var handler = new HttpClientHandler();
84+
// Allow untrusted certificates for testing. This is safe in test environments only, not for production use.
85+
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
86+
87+
var options = new SearchClientOptions
88+
{
89+
Transport = new HttpClientTransport(handler),
90+
Retry = { MaxRetries = 1 } // Reduce retries to speed up tests
91+
};
92+
93+
return new SearchClient(Endpoint, indexName, Credential, options);
94+
}
95+
96+
/// <summary>
97+
/// Stops and disposes the emulator container.
98+
/// </summary>
99+
public async Task DisposeAsync()
100+
{
101+
if (_container != null)
102+
{
103+
await _container.StopAsync();
104+
await _container.DisposeAsync();
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)