-
Notifications
You must be signed in to change notification settings - Fork 0
fix(tests): unblock CI by stopping Testcontainers' ES wait from probing HTTPS #23
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
Changes from all commits
86b123f
be2e216
d2a65ee
6ae05bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,10 @@ public class SharedContainerFixture : IAsyncLifetime | |
| Environment.GetEnvironmentVariable("ELASTIC_IMAGE") ?? DefaultElasticsearchImage) | ||
| .WithEnvironment("discovery.type", "single-node") | ||
| .WithEnvironment("xpack.security.enabled", "false") | ||
| // Required so Testcontainers' ElasticsearchConfiguration.TlsEnabled evaluates to false | ||
| // (it AND-s xpack.security.enabled with xpack.security.http.ssl.enabled). Without this, | ||
| // the built-in wait strategy probes HTTPS while ES listens on plain HTTP, and hangs. | ||
| .WithEnvironment("xpack.security.http.ssl.enabled", "false") | ||
| .WithEnvironment("ES_JAVA_OPTS", "-Xms512m -Xmx512m") | ||
| .Build(); | ||
|
|
||
|
|
@@ -188,27 +192,63 @@ public async Task<SearchResponse<T>> WaitForSearchResultsAsync<T>( | |
| TimeSpan? timeout = null, | ||
| TimeSpan? pollInterval = null) | ||
| { | ||
| timeout ??= TimeSpan.FromSeconds(10); | ||
| // 30s overall budget: GitHub-hosted runners are markedly slower than local | ||
| // dev machines and the first SearchAsync after index creation can spend | ||
| // several seconds priming query caches even after Refresh.True returns. | ||
| timeout ??= TimeSpan.FromSeconds(30); | ||
|
Comment on lines
+195
to
+198
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Timeout contract is no longer enforced after loop exit. The method states a 30s overall budget, but the final Proposed fix (preserve “final response” semantics without exceeding budget) public async Task<SearchResponse<T>> WaitForSearchResultsAsync<T>(
Action<SearchRequestDescriptor<T>> configureSearch,
CancellationToken cancellationToken,
TimeSpan? timeout = null,
TimeSpan? pollInterval = null)
{
// 30s overall budget: GitHub-hosted runners are markedly slower than local
// dev machines and the first SearchAsync after index creation can spend
// several seconds priming query caches even after Refresh.True returns.
timeout ??= TimeSpan.FromSeconds(30);
pollInterval ??= TimeSpan.FromMilliseconds(100);
ElasticsearchClient client = Services.GetRequiredService<ElasticsearchClient>();
using CancellationTokenSource overallCts = new(timeout.Value);
using CancellationTokenSource overallLinked =
CancellationTokenSource.CreateLinkedTokenSource(overallCts.Token, cancellationToken);
+ SearchResponse<T>? lastResponse = null;
while (!overallLinked.Token.IsCancellationRequested)
{
try
{
SearchResponse<T> response = await client.SearchAsync<T>(configureSearch, overallLinked.Token);
+ lastResponse = response;
if (response.Documents.Count > 0)
{
return response;
}
}
catch (OperationCanceledException) when (overallLinked.Token.IsCancellationRequested)
{
break;
}
try
{
await Task.Delay(pollInterval.Value, overallLinked.Token);
}
catch (OperationCanceledException)
{
break;
}
}
- // Final attempt with the caller's token only so the assertion sees real
- // "found nothing" data rather than a TaskCanceledException at the wait boundary.
- return await client.SearchAsync<T>(configureSearch, cancellationToken);
+ // Return the most recent real response if available, keeping the overall budget strict.
+ if (lastResponse is not null)
+ {
+ return lastResponse;
+ }
+
+ // If cancellation came from caller, preserve that behavior.
+ cancellationToken.ThrowIfCancellationRequested();
+ throw new TimeoutException("Timed out waiting for Elasticsearch search results.");
}Also applies to: 232-234 🤖 Prompt for AI Agents |
||
| pollInterval ??= TimeSpan.FromMilliseconds(100); | ||
|
|
||
| ElasticsearchClient client = Services.GetRequiredService<ElasticsearchClient>(); | ||
| using CancellationTokenSource cts = new(timeout.Value); | ||
| using CancellationTokenSource linked = | ||
| CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken); | ||
| using CancellationTokenSource overallCts = new(timeout.Value); | ||
| using CancellationTokenSource overallLinked = | ||
| CancellationTokenSource.CreateLinkedTokenSource(overallCts.Token, cancellationToken); | ||
|
Comment on lines
+195
to
+204
|
||
|
|
||
| // Force an index-level refresh up front. SearchIndexService writes documents | ||
| // with Refresh.True (`?refresh=true`), which is supposed to guarantee | ||
| // immediate searchability — but on slow CI disks the per-document refresh | ||
| // is observed to not always propagate before the first SearchAsync. The | ||
| // explicit Indices.RefreshAsync here is defensive and idempotent: locally | ||
| // it's a no-op (everything's already refreshed), on CI it converts an | ||
| // invisible flake into a passing search. | ||
| try | ||
| { | ||
| await client.Indices.RefreshAsync( | ||
| r => r.Indices(client.ElasticsearchClientSettings.DefaultIndex), | ||
| overallLinked.Token); | ||
| } | ||
| catch (OperationCanceledException) when (overallLinked.Token.IsCancellationRequested) | ||
| { | ||
| // Fall through to the final attempt below. | ||
| } | ||
|
|
||
| while (!linked.Token.IsCancellationRequested) | ||
| while (!overallLinked.Token.IsCancellationRequested) | ||
| { | ||
| SearchResponse<T> response = await client.SearchAsync<T>(configureSearch, linked.Token); | ||
| try | ||
| { | ||
| SearchResponse<T> response = await client.SearchAsync<T>(configureSearch, overallLinked.Token); | ||
|
|
||
| if (response.Documents.Count > 0) | ||
| if (response.Documents.Count > 0) | ||
| { | ||
| return response; | ||
| } | ||
| } | ||
| catch (OperationCanceledException) when (overallLinked.Token.IsCancellationRequested) | ||
| { | ||
| return response; | ||
| break; | ||
| } | ||
|
|
||
| await Task.Delay(pollInterval.Value, linked.Token); | ||
| try | ||
| { | ||
| await Task.Delay(pollInterval.Value, overallLinked.Token); | ||
| } | ||
| catch (OperationCanceledException) | ||
| { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| // Final attempt | ||
| // Final attempt with the caller's token only so the assertion sees real | ||
| // "found nothing" data rather than a TaskCanceledException at the wait boundary. | ||
| return await client.SearchAsync<T>(configureSearch, cancellationToken); | ||
|
Comment on lines
+250
to
252
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the 30s budget expires during Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: ANcpLua/Paperless
Length of output: 43
🌐 Web query:
Testcontainers.Elasticsearch 4.11 ElasticsearchConfiguration TlsEnabled implementation💡 Result:
In Testcontainers for.NET (including version 4.11.0), there is no explicit property named TlsEnabled within an ElasticsearchConfiguration class [1][2]. The Testcontainers Elasticsearch module automatically configures the container to use HTTPS by default for modern Elasticsearch versions (8.x and later), which are secure by default [2][3]. Because the container runs with TLS enabled,.NET clients attempting to connect will typically fail unless the client is configured to trust the self-signed certificate generated by the container [2]. To handle this in your.NET application, you should: 1. Create an ElasticsearchClientSettings instance [2]. 2. Set the ServerCertificateValidationCallback delegate to CertificateValidations.AllowAll to bypass certificate validation for your test environment [2]. Example implementation: var clientSettings = new ElasticsearchClientSettings(new Uri(_elasticsearchContainer.GetConnectionString)); clientSettings.ServerCertificateValidationCallback(CertificateValidations.AllowAll); var client = new ElasticsearchClient(clientSettings); If you need to disable security (and thus TLS) for specific testing scenarios, you can typically do so by modifying the environment variables of the container (e.g., setting xpack.security.enabled to false), though this is generally discouraged in favor of proper certificate handling [3][4][5]. Recent updates to the.NET module have improved support for handling these configurations, including returning appropriate HTTP/HTTPS connection strings based on the security state [4].
Citations:
Remove the inaccurate comment about
ElasticsearchConfiguration.TlsEnabledand provide a correct explanation.The comment references
ElasticsearchConfiguration.TlsEnabledand describes an "AND" logic between two environment variables that does not match Testcontainers.Elasticsearch 4.11 implementation.TlsEnabledis not an explicit property that evaluates based on those flags. The environment variables set here control Elasticsearch security configuration directly. Replace the comment with a simpler, accurate explanation:🤖 Prompt for AI Agents