diff --git a/src/Couchbase.Analytics/Async/QueryHandle.cs b/src/Couchbase.Analytics/Async/QueryHandle.cs index 5e4f14b..46f1142 100644 --- a/src/Couchbase.Analytics/Async/QueryHandle.cs +++ b/src/Couchbase.Analytics/Async/QueryHandle.cs @@ -22,7 +22,7 @@ using System.Text.Json; using Couchbase.AnalyticsClient.Internal; using Couchbase.AnalyticsClient.Options; -using Couchbase.AnalyticsClient.Results; +using Couchbase.AnalyticsClient.Query; namespace Couchbase.AnalyticsClient.Async; @@ -34,6 +34,10 @@ public class QueryHandle { private readonly IAnalyticsService _analyticsService; + internal string? Status { get; } + + internal AsyncQueryMetrics? Metrics { get; } + /// /// The query handle string used to poll for the result handle. /// @@ -44,11 +48,22 @@ public class QueryHandle /// public string RequestId { get; } - internal QueryHandle(string handle, string requestId, IAnalyticsService analyticsService) + internal QueryHandle(string handle, string requestId, JsonElement root, IAnalyticsService analyticsService) { Handle = handle ?? throw new ArgumentNullException(nameof(handle)); RequestId = requestId ?? throw new ArgumentNullException(nameof(requestId)); _analyticsService = analyticsService ?? throw new ArgumentNullException(nameof(analyticsService)); + + if (root.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) + { + throw new ArgumentException("The JSON response element must not be empty or null.", nameof(root)); + } + + Status = root.TryGetProperty("status", out var statusProp) ? statusProp.GetString() : null; + if (root.TryGetProperty("metrics", out var metricsElement)) + { + Metrics = JsonSerializer.Deserialize(metricsElement.GetRawText()); + } } /// @@ -93,4 +108,12 @@ public Task CancelAsync(Func options, Cancellation cancelOptions = options.Invoke(cancelOptions); return CancelAsync(cancelOptions, cancellationToken); } + + /// + public override string ToString() + { + var elapsed = Metrics?.ElapsedTime?.TotalMilliseconds; + var metricsStr = elapsed.HasValue ? $"{elapsed}ms elapsed" : "none"; + return $"QueryHandle [RequestId={RequestId}, Handle={Handle}, Status={Status ?? "unknown"}, Metrics={{{metricsStr}}}]"; + } } diff --git a/src/Couchbase.Analytics/Async/QueryResultHandle.cs b/src/Couchbase.Analytics/Async/QueryResultHandle.cs index 308fea4..def5e30 100644 --- a/src/Couchbase.Analytics/Async/QueryResultHandle.cs +++ b/src/Couchbase.Analytics/Async/QueryResultHandle.cs @@ -19,8 +19,10 @@ * ************************************************************/ #endregion +using System.Text.Json; using Couchbase.AnalyticsClient.Internal; using Couchbase.AnalyticsClient.Options; +using Couchbase.AnalyticsClient.Query; using Couchbase.AnalyticsClient.Results; namespace Couchbase.AnalyticsClient.Async; @@ -33,16 +35,38 @@ public class QueryResultHandle private readonly string _handlePath; private readonly IAnalyticsService _analyticsService; + internal string? Status { get; } + + internal AsyncQueryMetrics? Metrics { get; } + + internal int? ResultCount { get; } + /// /// The request ID assigned by the server when the query was submitted. /// public string RequestId { get; } - internal QueryResultHandle(string handlePath, string requestId, IAnalyticsService analyticsService) + internal QueryResultHandle(string handlePath, string requestId, JsonElement root, IAnalyticsService analyticsService) { _handlePath = handlePath ?? throw new ArgumentNullException(nameof(handlePath)); RequestId = requestId ?? throw new ArgumentNullException(nameof(requestId)); _analyticsService = analyticsService ?? throw new ArgumentNullException(nameof(analyticsService)); + + if (root.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) + { + throw new ArgumentException("The JSON response element must not be empty or null.", nameof(root)); + } + + Status = root.TryGetProperty("status", out var statusProp) ? statusProp.GetString() : null; + if (root.TryGetProperty("metrics", out var metricsElement)) + { + Metrics = JsonSerializer.Deserialize(metricsElement.GetRawText()); + } + + if (root.TryGetProperty("resultCount", out var resultCountProp) && resultCountProp.TryGetInt32(out var resultCount)) + { + ResultCount = resultCount; + } } /// @@ -87,4 +111,14 @@ public Task DiscardResultsAsync(Func + public override string ToString() + { + var elapsed = Metrics?.ElapsedTime?.TotalMilliseconds; + var metricsStr = elapsed.HasValue ? $"{elapsed}ms elapsed" : "none"; + var countStr = ResultCount.HasValue ? $", ResultCount={ResultCount}" : ""; + + return $"QueryResultHandle [RequestId={RequestId}, Status={Status ?? "unknown"}{countStr}, Metrics={{{metricsStr}}}]"; + } } diff --git a/src/Couchbase.Analytics/Cluster.cs b/src/Couchbase.Analytics/Cluster.cs index 6477974..e659b27 100644 --- a/src/Couchbase.Analytics/Cluster.cs +++ b/src/Couchbase.Analytics/Cluster.cs @@ -30,7 +30,7 @@ namespace Couchbase.AnalyticsClient; -public class Cluster : IDisposable +public partial class Cluster : IDisposable { private volatile ICredential _credential; private readonly ClusterOptions _clusterOptions; @@ -52,6 +52,8 @@ private Cluster(ICredential credential, ClusterOptions clusterOptions) _logger = _serviceProvider.GetRequiredService>(); _analyticsService = new LazyService(_serviceProvider); + + LogClusterCreated(_logger, clusterOptions.ConnectionString); } /// @@ -164,6 +166,7 @@ public void UpdateCredential(ICredential newCredential) throw new InvalidOperationException( $"Cannot change credential type from {current.GetType().Name} to {newCredential.GetType().Name}."); _credential = newCredential; + LogCredentialUpdated(_logger, current.GetType().Name); } public Task ExecuteQueryAsync(string statement, Func options, CancellationToken cancellationToken = default) @@ -222,5 +225,19 @@ public void Dispose() { disposableProvider.Dispose(); } + LogClusterDisposed(_logger); } + + #region Logging + + [LoggerMessage(1, LogLevel.Information, "Analytics Cluster initialized for connection: {ConnectionString}")] + private static partial void LogClusterCreated(ILogger logger, string connectionString); + + [LoggerMessage(2, LogLevel.Information, "Analytics Cluster credentials dynamically updated (Type: {CredentialType})")] + private static partial void LogCredentialUpdated(ILogger logger, string credentialType); + + [LoggerMessage(3, LogLevel.Information, "Analytics Cluster disposed. Releasing managed resources.")] + private static partial void LogClusterDisposed(ILogger logger); + + #endregion } diff --git a/src/Couchbase.Analytics/Exceptions/QueryNotFoundException.cs b/src/Couchbase.Analytics/Exceptions/QueryNotFoundException.cs index 40f4720..d692ca0 100644 --- a/src/Couchbase.Analytics/Exceptions/QueryNotFoundException.cs +++ b/src/Couchbase.Analytics/Exceptions/QueryNotFoundException.cs @@ -33,5 +33,5 @@ public QueryNotFoundException(string message) : base(message) { } public QueryNotFoundException(string message, Exception innerException) : base(message, innerException) { } - internal QueryNotFoundException(string message, Exception innerException, Couchbase.AnalyticsClient.Internal.Retry.ErrorContext errorContext) : base(message, innerException, errorContext) { } + internal QueryNotFoundException(string message, Exception innerException, Internal.Retry.ErrorContext errorContext) : base(message, innerException, errorContext) { } } diff --git a/src/Couchbase.Analytics/HTTP/CertificateCredential.cs b/src/Couchbase.Analytics/HTTP/CertificateCredential.cs index 84a7f9f..a74a617 100644 --- a/src/Couchbase.Analytics/HTTP/CertificateCredential.cs +++ b/src/Couchbase.Analytics/HTTP/CertificateCredential.cs @@ -90,13 +90,9 @@ public static CertificateCredential FromPkcs12(string path, string? password = n public static CertificateCredential FromPem(string certPath, string keyPath) => new(X509Certificate2.CreateFromPemFile(certPath, keyPath)); - /// - /// Excludes sensitive certificate details from the record's ToString output. - /// Only Subject and Thumbprint are included. - /// private bool PrintMembers(StringBuilder builder) { - builder.Append($"{nameof(Certificate.Subject)} = {Certificate.Subject}, {nameof(Certificate.Thumbprint)} = {Certificate.Thumbprint}"); + builder.Append($"{nameof(Certificate)} = [Subject = {Certificate.Subject}, Thumbprint = {Certificate.Thumbprint}]"); return true; } } diff --git a/src/Couchbase.Analytics/HTTP/Credential.cs b/src/Couchbase.Analytics/HTTP/Credential.cs index 44ba533..7f37142 100644 --- a/src/Couchbase.Analytics/HTTP/Credential.cs +++ b/src/Couchbase.Analytics/HTTP/Credential.cs @@ -50,7 +50,7 @@ public static Credential Create(string username, string password) /// Excludes from the record's ToString output /// to prevent leaking encoded credentials into logs. /// - protected virtual bool PrintMembers(System.Text.StringBuilder builder) + protected virtual bool PrintMembers(StringBuilder builder) { builder.Append($"{nameof(Username)} = {Username}"); return true; diff --git a/src/Couchbase.Analytics/HTTP/JwtCredential.cs b/src/Couchbase.Analytics/HTTP/JwtCredential.cs index cb98470..3b45158 100644 --- a/src/Couchbase.Analytics/HTTP/JwtCredential.cs +++ b/src/Couchbase.Analytics/HTTP/JwtCredential.cs @@ -20,6 +20,7 @@ #endregion using System.Net.Http.Headers; +using System.Text; namespace Couchbase.AnalyticsClient.HTTP; @@ -43,11 +44,7 @@ public static JwtCredential Create(string token) return new(token); } - /// - /// Excludes the full token and from the record's - /// ToString output to prevent leaking credentials into logs. - /// - private bool PrintMembers(System.Text.StringBuilder builder) + private bool PrintMembers(StringBuilder builder) { builder.Append($"{nameof(Token)} = <{Token.Length} chars>"); return true; diff --git a/src/Couchbase.Analytics/Internal/AnalyticsService.cs b/src/Couchbase.Analytics/Internal/AnalyticsService.cs index 48f8dbd..6fd1786 100644 --- a/src/Couchbase.Analytics/Internal/AnalyticsService.cs +++ b/src/Couchbase.Analytics/Internal/AnalyticsService.cs @@ -146,7 +146,7 @@ private async Task ExecuteWithRetryAsync(string statement, QueryOp } try { - LogQueryAttemptStarting(_logger, attempt + 1, options.ClientContextId, _redactor.UserData(statement), stopwatch.Elapsed.TotalMilliseconds); + LogQueryAttemptStarting(_logger, attempt + 1, options.ClientContextId, options.QueryContext?.ToString() ?? "", _redactor.UserData(statement), stopwatch.Elapsed.TotalMilliseconds); var result = await ExecuteQueryAsync(content, httpClient, options.AsStreaming, deserializer, errorContext, cancellationToken).ConfigureAwait(false); @@ -172,7 +172,7 @@ private async Task ExecuteWithRetryAsync(string statement, QueryOp } catch (HttpRequestException httpRequestException) { - LogQueryAttemptFailed(_logger, httpRequestException, attempt + 1, options.ClientContextId, _redactor.UserData(statement), + LogQueryAttemptFailed(_logger, httpRequestException, attempt + 1, options.ClientContextId, options.QueryContext?.ToString() ?? "", _redactor.UserData(statement), httpRequestException.Message, stopwatch.Elapsed.TotalMilliseconds); // "No successful connection(s)" is retryable @@ -238,7 +238,7 @@ public async Task StartQueryAsync(string statement, StartQueryOptio try { - LogAsyncStartQueryAttempt(_logger, attempt + 1, _redactor.SystemData(Uri), options.ClientContextId, _redactor.UserData(statement), stopwatch.Elapsed.TotalMilliseconds); + LogAsyncStartQueryAttempt(_logger, attempt + 1, _redactor.SystemData(Uri), options.ClientContextId, options.QueryContext?.ToString() ?? "", _redactor.UserData(statement), stopwatch.Elapsed.TotalMilliseconds); var request = new HttpRequestMessage(HttpMethod.Post, Uri) { Content = content }; var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken) @@ -289,11 +289,11 @@ public async Task StartQueryAsync(string statement, StartQueryOptio } LogAsyncStartQuerySucceeded(_logger, options.ClientContextId, _redactor.SystemData(handlePath), _redactor.SystemData(requestId), (int)response.StatusCode); - return new QueryHandle(handlePath, requestId, this); + return new QueryHandle(handlePath, requestId, root, this); } catch (HttpRequestException httpRequestException) { - LogAsyncStartQueryFailed(_logger, httpRequestException, attempt + 1, options.ClientContextId, _redactor.UserData(statement), httpRequestException.Message); + LogAsyncStartQueryFailed(_logger, httpRequestException, attempt + 1, options.ClientContextId, options.QueryContext?.ToString() ?? "", _redactor.UserData(statement), httpRequestException.Message); if (httpRequestException.InnerException is AggregateException aggregateEx) { @@ -333,7 +333,7 @@ public async Task StartQueryAsync(string statement, StartQueryOptio var statusUri = Uri.TryCreate(handle.Handle, UriKind.Absolute, out var absUri) && (absUri.Scheme == Uri.UriSchemeHttp || absUri.Scheme == Uri.UriSchemeHttps) ? absUri : new Uri(_baseUri, handle.Handle); - + var request = new HttpRequestMessage(HttpMethod.Get, statusUri); LogFetchResultHandleRequest(_logger, _redactor.SystemData(statusUri), _redactor.SystemData(handle.Handle)); @@ -390,19 +390,19 @@ public async Task StartQueryAsync(string statement, StartQueryOptio if (!string.Equals(status, "success", StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(status, "stopped", StringComparison.OrdinalIgnoreCase) || + if (string.Equals(status, "stopped", StringComparison.OrdinalIgnoreCase) || string.Equals(status, "aborted", StringComparison.OrdinalIgnoreCase) || string.Equals(status, "closed", StringComparison.OrdinalIgnoreCase)) { throw new QueryNotFoundException($"Query has been discarded or canceled (status: {status})."); } - + if (string.Equals(status, "timeout", StringComparison.OrdinalIgnoreCase)) { throw new AnalyticsTimeoutException("The query evaluation timed out on the server."); } - - if (string.Equals(status, "fatal", StringComparison.OrdinalIgnoreCase) || + + if (string.Equals(status, "fatal", StringComparison.OrdinalIgnoreCase) || string.Equals(status, "failed", StringComparison.OrdinalIgnoreCase) || string.Equals(status, "errors", StringComparison.OrdinalIgnoreCase)) { @@ -414,7 +414,7 @@ public async Task StartQueryAsync(string statement, StartQueryOptio } throw new AnalyticsException($"Query execution failed on the server (status: {status})."); } - + throw new AnalyticsException($"Query status fetch failed with unrecognized status: {status}"); } @@ -424,7 +424,7 @@ public async Task StartQueryAsync(string statement, StartQueryOptio throw new InvalidOperationException("Query status indicates success but no result handle was provided by the server."); } - return new QueryResultHandle(resultHandle, handle.RequestId, this); + return new QueryResultHandle(resultHandle, handle.RequestId, root, this); } catch (TaskCanceledException taskCanceledEx) { @@ -441,7 +441,7 @@ public async Task FetchResultsAsync(string requestId, string handl var resultUri = Uri.TryCreate(handlePath, UriKind.Absolute, out var absUri) && (absUri.Scheme == Uri.UriSchemeHttp || absUri.Scheme == Uri.UriSchemeHttps) ? absUri : new Uri(_baseUri, handlePath); - + var request = new HttpRequestMessage(HttpMethod.Get, resultUri); LogFetchResultsRequest(_logger, _redactor.SystemData(resultUri), _redactor.SystemData(handlePath)); @@ -489,7 +489,7 @@ public async Task DiscardResultsAsync(string requestId, string handlePath, Disca var resultUri = Uri.TryCreate(handlePath, UriKind.Absolute, out var absUri) && (absUri.Scheme == Uri.UriSchemeHttp || absUri.Scheme == Uri.UriSchemeHttps) ? absUri : new Uri(_baseUri, handlePath); - + var request = new HttpRequestMessage(HttpMethod.Delete, resultUri); LogDiscardResultsRequest(_logger, _redactor.SystemData(resultUri), _redactor.SystemData(handlePath)); @@ -579,8 +579,8 @@ private static Exception ThrowTooManyRetries(ErrorContext errorContext) #region Logging - [LoggerMessage(1, LogLevel.Debug, "Analytics query attempt {Attempt} starting for {ClientContextId}: {Statement} (elapsed: {Elapsed}ms)")] - private static partial void LogQueryAttemptStarting(ILogger logger, int attempt, string? clientContextId, Redacted statement, double elapsed); + [LoggerMessage(1, LogLevel.Debug, "Analytics query attempt {Attempt} starting for {ClientContextId} on context [{QueryContext}]: {Statement} (elapsed: {Elapsed}ms)")] + private static partial void LogQueryAttemptStarting(ILogger logger, int attempt, string? clientContextId, string queryContext, Redacted statement, double elapsed); [LoggerMessage(2, LogLevel.Debug, "Received retriable server errors for ClientContextId {ClientContextId}, retrying...")] private static partial void LogRetriableServerErrors(ILogger logger, string? clientContextId); @@ -588,14 +588,14 @@ private static Exception ThrowTooManyRetries(ErrorContext errorContext) [LoggerMessage(3, LogLevel.Debug, "HttpRequestException is not retriable, failing immediately")] private static partial void LogNonRetriableHttpException(ILogger logger); - [LoggerMessage(4, LogLevel.Debug, "Analytics query attempt {Attempt} for ClientContextId {ClientContextId}: {Statement} failed: {Error} (elapsed: {Elapsed}ms)")] - private static partial void LogQueryAttemptFailed(ILogger logger, Exception ex, int attempt, string? clientContextId, Redacted statement, string error, double elapsed); + [LoggerMessage(4, LogLevel.Debug, "Analytics query attempt {Attempt} for ClientContextId {ClientContextId} on context [{QueryContext}]: {Statement} failed: {Error} (elapsed: {Elapsed}ms)")] + private static partial void LogQueryAttemptFailed(ILogger logger, Exception ex, int attempt, string? clientContextId, string queryContext, Redacted statement, string error, double elapsed); - [LoggerMessage(5, LogLevel.Debug, "Async StartQuery attempt {Attempt} sending POST to {Uri} for {ClientContextId}: {Statement} (elapsed: {Elapsed}ms)")] - private static partial void LogAsyncStartQueryAttempt(ILogger logger, int attempt, Redacted uri, string? clientContextId, Redacted statement, double elapsed); + [LoggerMessage(5, LogLevel.Debug, "Async StartQuery attempt {Attempt} sending POST to {Uri} for {ClientContextId} on context [{QueryContext}]: {Statement} (elapsed: {Elapsed}ms)")] + private static partial void LogAsyncStartQueryAttempt(ILogger logger, int attempt, Redacted uri, string? clientContextId, string queryContext, Redacted statement, double elapsed); - [LoggerMessage(6, LogLevel.Debug, "Async StartQuery attempt {Attempt} for {ClientContextId}: {Statement} failed: {Error}")] - private static partial void LogAsyncStartQueryFailed(ILogger logger, Exception ex, int attempt, string? clientContextId, Redacted statement, string error); + [LoggerMessage(6, LogLevel.Debug, "Async StartQuery attempt {Attempt} for {ClientContextId} on context [{QueryContext}]: {Statement} failed: {Error}")] + private static partial void LogAsyncStartQueryFailed(ILogger logger, Exception ex, int attempt, string? clientContextId, string queryContext, Redacted statement, string error); [LoggerMessage(7, LogLevel.Debug, "DiscardResults returned 404 for handle {Handle} — already discarded or canceled.")] private static partial void LogDiscardResults404(ILogger logger, Redacted handle); diff --git a/src/Couchbase.Analytics/Internal/DI/CouchbaseServiceProvider.cs b/src/Couchbase.Analytics/Internal/DI/CouchbaseServiceProvider.cs index 8db04a5..5a12d8b 100644 --- a/src/Couchbase.Analytics/Internal/DI/CouchbaseServiceProvider.cs +++ b/src/Couchbase.Analytics/Internal/DI/CouchbaseServiceProvider.cs @@ -19,7 +19,6 @@ * ************************************************************/ #endregion -using System; using System.Collections.ObjectModel; namespace Couchbase.AnalyticsClient.Internal.DI; diff --git a/src/Couchbase.Analytics/Internal/DI/SingletonServiceFactory.cs b/src/Couchbase.Analytics/Internal/DI/SingletonServiceFactory.cs index 3727612..4f018f4 100644 --- a/src/Couchbase.Analytics/Internal/DI/SingletonServiceFactory.cs +++ b/src/Couchbase.Analytics/Internal/DI/SingletonServiceFactory.cs @@ -19,7 +19,6 @@ * ************************************************************/ #endregion -using System; using System.Diagnostics.CodeAnalysis; using Couchbase.AnalyticsClient.Utils; diff --git a/src/Couchbase.Analytics/Internal/HTTP/CouchbaseHttpClientFactory.cs b/src/Couchbase.Analytics/Internal/HTTP/CouchbaseHttpClientFactory.cs index 7b2fe34..b6f8814 100644 --- a/src/Couchbase.Analytics/Internal/HTTP/CouchbaseHttpClientFactory.cs +++ b/src/Couchbase.Analytics/Internal/HTTP/CouchbaseHttpClientFactory.cs @@ -31,7 +31,7 @@ namespace Couchbase.AnalyticsClient.Internal.HTTP; -internal class CouchbaseHttpClientFactory : ICouchbaseHttpClientFactory +internal partial class CouchbaseHttpClientFactory : ICouchbaseHttpClientFactory { /// /// Grace period before disposing a retired handler, allowing in-flight requests to complete. @@ -208,19 +208,22 @@ private void RecreateHandler(ICredential newCredential) _sharedHandler = CreateClientHandler(); _lastKnownCredential = newCredential; + LogHandlerRecreated(_logger, newCredential.GetType().Name, RetiredHandlerDisposeDelay.TotalSeconds); + // Schedule deferred disposal of the old handler. // In-flight requests may still reference it, so we wait before disposing. - _ = DisposeAfterDelayAsync(oldHandler); + _ = DisposeAfterDelayAsync(oldHandler, _logger); } } /// /// Disposes a retired handler after a grace period, allowing in-flight requests to drain. /// - private static async Task DisposeAfterDelayAsync(AuthenticationHandler handler) + private static async Task DisposeAfterDelayAsync(AuthenticationHandler handler, ILogger logger) { await Task.Delay(RetiredHandlerDisposeDelay).ConfigureAwait(false); handler.Dispose(); + LogOldHandlerDisposed(logger); } public void Dispose() @@ -229,4 +232,14 @@ public void Dispose() } public HttpCompletionOption DefaultCompletionOption { get; set; } = HttpCompletionOption.ResponseHeadersRead; + + #region Logging + + [LoggerMessage(1, LogLevel.Information, "HTTP handler rebuilt due to {CredentialType} credential change. Old handler scheduled for disposal in {DelaySeconds}s.")] + private static partial void LogHandlerRecreated(ILogger logger, string credentialType, double delaySeconds); + + [LoggerMessage(2, LogLevel.Information, "Retired HTTP handler successfully disposed after grace period.")] + private static partial void LogOldHandlerDisposed(ILogger logger); + + #endregion } diff --git a/src/Couchbase.Analytics/Internal/IAnalyticsService.cs b/src/Couchbase.Analytics/Internal/IAnalyticsService.cs index 6201737..a4eac50 100644 --- a/src/Couchbase.Analytics/Internal/IAnalyticsService.cs +++ b/src/Couchbase.Analytics/Internal/IAnalyticsService.cs @@ -22,7 +22,6 @@ using Couchbase.AnalyticsClient.Async; using Couchbase.AnalyticsClient.Options; using Couchbase.AnalyticsClient.Results; -using Couchbase.Core.Json; namespace Couchbase.AnalyticsClient.Internal; diff --git a/src/Couchbase.Analytics/Options/StartQueryOptions.cs b/src/Couchbase.Analytics/Options/StartQueryOptions.cs index 783fe99..271aa27 100644 --- a/src/Couchbase.Analytics/Options/StartQueryOptions.cs +++ b/src/Couchbase.Analytics/Options/StartQueryOptions.cs @@ -21,7 +21,6 @@ using System.Text.Json; using Couchbase.AnalyticsClient.Query; -using Couchbase.Core.Json; using Couchbase.Core.Utils; namespace Couchbase.AnalyticsClient.Options; diff --git a/src/Couchbase.Analytics/Query/AsyncQueryMetrics.cs b/src/Couchbase.Analytics/Query/AsyncQueryMetrics.cs new file mode 100644 index 0000000..f8ddf5c --- /dev/null +++ b/src/Couchbase.Analytics/Query/AsyncQueryMetrics.cs @@ -0,0 +1,26 @@ +#region License +/* ************************************************************ + * + * @author Couchbase + * @copyright 2025 Couchbase, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ************************************************************/ +#endregion + +namespace Couchbase.AnalyticsClient.Query; + +internal sealed class AsyncQueryMetrics : QueryMetricsBase +{ +} diff --git a/src/Couchbase.Analytics/Query/QueryMetrics.cs b/src/Couchbase.Analytics/Query/QueryMetrics.cs index ea19991..308f27e 100644 --- a/src/Couchbase.Analytics/Query/QueryMetrics.cs +++ b/src/Couchbase.Analytics/Query/QueryMetrics.cs @@ -20,27 +20,11 @@ #endregion using System.Text.Json.Serialization; -using Couchbase.Core.Json; namespace Couchbase.AnalyticsClient.Query; -public sealed class QueryMetrics +public sealed class QueryMetrics : QueryMetricsBase { - [JsonConverter(typeof(MillisecondsStringJsonConverter))] - [JsonPropertyName("elapsedTime")] - public TimeSpan? ElapsedTime { get; init; } - - [JsonConverter(typeof(MillisecondsStringJsonConverter))] - [JsonPropertyName("executionTime")] - public TimeSpan? ExecutionTime { get; init; } - - [JsonConverter(typeof(MillisecondsStringJsonConverter))] - [JsonPropertyName("compileTime")] - public TimeSpan? CompileTime { get; init; } - - [JsonConverter(typeof(MillisecondsStringJsonConverter))] - [JsonPropertyName("queueWaitTime")] - public TimeSpan? QueueWaitTime { get; init; } [JsonPropertyName("resultCount")] public int ResultCount { get; init; } diff --git a/src/Couchbase.Analytics/Query/QueryMetricsBase.cs b/src/Couchbase.Analytics/Query/QueryMetricsBase.cs new file mode 100644 index 0000000..9d6691f --- /dev/null +++ b/src/Couchbase.Analytics/Query/QueryMetricsBase.cs @@ -0,0 +1,44 @@ +#region License +/* ************************************************************ + * + * @author Couchbase + * @copyright 2025 Couchbase, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ************************************************************/ +#endregion + +using System.Text.Json.Serialization; +using Couchbase.Core.Json; + +namespace Couchbase.AnalyticsClient.Query; + +public abstract class QueryMetricsBase +{ + [JsonConverter(typeof(MillisecondsStringJsonConverter))] + [JsonPropertyName("elapsedTime")] + public TimeSpan? ElapsedTime { get; init; } + + [JsonConverter(typeof(MillisecondsStringJsonConverter))] + [JsonPropertyName("executionTime")] + public TimeSpan? ExecutionTime { get; init; } + + [JsonConverter(typeof(MillisecondsStringJsonConverter))] + [JsonPropertyName("compileTime")] + public TimeSpan? CompileTime { get; init; } + + [JsonConverter(typeof(MillisecondsStringJsonConverter))] + [JsonPropertyName("queueWaitTime")] + public TimeSpan? QueueWaitTime { get; init; } +} diff --git a/src/Couchbase.Analytics/Scope.cs b/src/Couchbase.Analytics/Scope.cs index edccdd7..f737887 100644 --- a/src/Couchbase.Analytics/Scope.cs +++ b/src/Couchbase.Analytics/Scope.cs @@ -19,10 +19,10 @@ * ************************************************************/ #endregion +using Couchbase.AnalyticsClient.Async; using Couchbase.AnalyticsClient.Options; using Couchbase.AnalyticsClient.Query; using Couchbase.AnalyticsClient.Results; -using Couchbase.AnalyticsClient.Async; namespace Couchbase.AnalyticsClient; diff --git a/tests/Couchbase.Analytics.FunctionalTests/AsyncAnalyticsTests.cs b/tests/Couchbase.Analytics.FunctionalTests/AsyncAnalyticsTests.cs index e4720ee..6c86ce0 100644 --- a/tests/Couchbase.Analytics.FunctionalTests/AsyncAnalyticsTests.cs +++ b/tests/Couchbase.Analytics.FunctionalTests/AsyncAnalyticsTests.cs @@ -1,8 +1,7 @@ -using System.Text.Json; +using Couchbase.AnalyticsClient.Async; using Couchbase.AnalyticsClient.Exceptions; using Couchbase.AnalyticsClient.FunctionalTests.Fixtures; using Couchbase.AnalyticsClient.Options; -using Couchbase.AnalyticsClient.Async; using Xunit; using Xunit.Abstractions; @@ -34,13 +33,13 @@ public async Task Test_AsyncAnalytics_EndToEnd_Cluster() Assert.NotNull(handle); Assert.NotNull(handle.Handle); Assert.NotNull(handle.RequestId); - + _outputHelper.WriteLine($"Handle: {handle.Handle}"); _outputHelper.WriteLine($"RequestId: {handle.RequestId}"); // 2. Poll for the result handle QueryResultHandle? resultHandle = null; - for (int i = 0; i < 20; i++) + for (var i = 0; i < 20; i++) { resultHandle = await handle.FetchResultHandleAsync(new FetchResultHandleOptions()); if (resultHandle != null) @@ -86,7 +85,7 @@ public async Task Test_AsyncAnalytics_Cancellation_Cluster() // It's possible the cancel takes a brief moment to process gracefully on the server. var ex = await Record.ExceptionAsync(async () => { - for (int i = 0; i < 20; i++) + for (var i = 0; i < 20; i++) { var resultHandle = await handle.FetchResultHandleAsync(new FetchResultHandleOptions()); if (resultHandle != null) @@ -101,7 +100,7 @@ public async Task Test_AsyncAnalytics_Cancellation_Cluster() // The query should have been killed, resulting in a QueryNotFoundException when it's purged, // or a cleanly mapped QueryException ("Job Killed") if the server responds gracefully before purging. Assert.NotNull(ex); - Assert.True(ex is QueryNotFoundException || ex is QueryException, + Assert.True(ex is QueryNotFoundException or QueryException, $"Expected QueryNotFoundException or QueryException upon cancellation, but received: {ex.GetType().FullName}"); } @@ -113,7 +112,7 @@ public async Task Test_AsyncAnalytics_DiscardResults_Cluster() // Poll for the result handle QueryResultHandle? resultHandle = null; - for (int i = 0; i < 20; i++) + for (var i = 0; i < 20; i++) { resultHandle = await handle.FetchResultHandleAsync(new FetchResultHandleOptions()); if (resultHandle != null) @@ -132,7 +131,7 @@ public async Task Test_AsyncAnalytics_DiscardResults_Cluster() await resultHandle!.DiscardResultsAsync(new DiscardResultsOptions()); // Attempting to fetch the results after discarding should throw QueryNotFoundException - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => { await resultHandle.FetchResultsAsync(new FetchResultsOptions()); }); diff --git a/tests/Couchbase.Analytics.FunctionalTests/CertificateAuthenticationTests.cs b/tests/Couchbase.Analytics.FunctionalTests/CertificateAuthenticationTests.cs index ec7ac9a..b895b87 100644 --- a/tests/Couchbase.Analytics.FunctionalTests/CertificateAuthenticationTests.cs +++ b/tests/Couchbase.Analytics.FunctionalTests/CertificateAuthenticationTests.cs @@ -1,4 +1,3 @@ -using Couchbase.AnalyticsClient.Exceptions; using Couchbase.AnalyticsClient.FunctionalTests.Fixtures; using Couchbase.AnalyticsClient.HTTP; using Couchbase.AnalyticsClient.Options; diff --git a/tests/Couchbase.Analytics.UnitTests/Async/QueryHandleTests.cs b/tests/Couchbase.Analytics.UnitTests/Async/QueryHandleTests.cs index 5475d66..4ab9547 100644 --- a/tests/Couchbase.Analytics.UnitTests/Async/QueryHandleTests.cs +++ b/tests/Couchbase.Analytics.UnitTests/Async/QueryHandleTests.cs @@ -1,7 +1,8 @@ +using System.Text.Json; using Couchbase.AnalyticsClient.Async; using Couchbase.AnalyticsClient.Internal; using Couchbase.AnalyticsClient.Options; -using Couchbase.AnalyticsClient.Results; +using Couchbase.AnalyticsClient.UnitTests.Helpers; using Moq; using Xunit; @@ -13,7 +14,7 @@ public class QueryHandleTests public void Constructor_InitializesProperties() { var serviceMock = new Mock(); - var handle = new QueryHandle("test-handle", "test-req", serviceMock.Object); + var handle = TestHandleFactory.CreateQueryHandle("test-handle", "test-req", "{}", serviceMock.Object); Assert.Equal("test-handle", handle.Handle); Assert.Equal("test-req", handle.RequestId); @@ -23,15 +24,15 @@ public void Constructor_InitializesProperties() public async Task FetchResultHandleAsync_DelegatesToService() { var serviceMock = new Mock(); - var handle = new QueryHandle("test-handle", "test-req", serviceMock.Object); - var expectedResult = new Mock("path", "req", serviceMock.Object).Object; + var handle = TestHandleFactory.CreateQueryHandle("test-handle", "test-req", "{}", serviceMock.Object); + var expectedResult = TestHandleFactory.CreateQueryResultHandle("path", "req", "{}", serviceMock.Object); serviceMock.Setup(x => x.FetchResultHandleAsync(handle, It.IsAny(), It.IsAny())) .ReturnsAsync(expectedResult); var result = await handle.FetchResultHandleAsync(new FetchResultHandleOptions()); Assert.Same(expectedResult, result); - + serviceMock.Verify(x => x.FetchResultHandleAsync(handle, It.IsAny(), default), Times.Once); } @@ -39,14 +40,14 @@ public async Task FetchResultHandleAsync_DelegatesToService() public async Task CancelAsync_DelegatesToService() { var serviceMock = new Mock(); - var handle = new QueryHandle("test-handle", "test-req", serviceMock.Object); + var handle = TestHandleFactory.CreateQueryHandle("test-handle", "test-req", "{}", serviceMock.Object); var options = new CancelOptions(); serviceMock.Setup(x => x.CancelQueryAsync("test-req", options, It.IsAny())) .Returns(Task.CompletedTask); await handle.CancelAsync(options); - + serviceMock.Verify(x => x.CancelQueryAsync("test-req", options, default), Times.Once); } @@ -55,17 +56,18 @@ public void Constructor_NullArguments_ThrowsArgumentNullException() { var serviceMock = new Mock(); - Assert.Throws(() => new QueryHandle(null!, "req", serviceMock.Object)); - Assert.Throws(() => new QueryHandle("handle", null!, serviceMock.Object)); - Assert.Throws(() => new QueryHandle("handle", "req", null!)); + Assert.Throws(() => TestHandleFactory.CreateQueryHandle(null!, "req", "{}", serviceMock.Object)); + Assert.Throws(() => TestHandleFactory.CreateQueryHandle("handle", null!, "{}", serviceMock.Object)); + Assert.Throws(() => new QueryHandle("handle", "req", default(JsonElement), serviceMock.Object)); + Assert.Throws(() => TestHandleFactory.CreateQueryHandle("handle", "req", "{}", null!)); } [Fact] public async Task FetchResultHandleAsync_FluentOptions_DelegatesProperly() { var serviceMock = new Mock(); - var handle = new QueryHandle("test-handle", "test-req", serviceMock.Object); - var expectedResult = new Mock("path", "req", serviceMock.Object).Object; + var handle = TestHandleFactory.CreateQueryHandle("test-handle", "test-req", "{}", serviceMock.Object); + var expectedResult = TestHandleFactory.CreateQueryResultHandle("path", "req", "{}", serviceMock.Object); serviceMock.Setup(x => x.FetchResultHandleAsync(handle, It.IsAny(), It.IsAny())) .ReturnsAsync(expectedResult); @@ -81,7 +83,7 @@ public async Task FetchResultHandleAsync_FluentOptions_DelegatesProperly() public async Task CancelAsync_FluentOptions_DelegatesProperly() { var serviceMock = new Mock(); - var handle = new QueryHandle("test-handle", "test-req", serviceMock.Object); + var handle = TestHandleFactory.CreateQueryHandle("test-handle", "test-req", "{}", serviceMock.Object); serviceMock.Setup(x => x.CancelQueryAsync("test-req", It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); diff --git a/tests/Couchbase.Analytics.UnitTests/Async/QueryResultHandleTests.cs b/tests/Couchbase.Analytics.UnitTests/Async/QueryResultHandleTests.cs index c913e8f..2f95a11 100644 --- a/tests/Couchbase.Analytics.UnitTests/Async/QueryResultHandleTests.cs +++ b/tests/Couchbase.Analytics.UnitTests/Async/QueryResultHandleTests.cs @@ -1,7 +1,9 @@ +using System.Text.Json; using Couchbase.AnalyticsClient.Async; using Couchbase.AnalyticsClient.Internal; using Couchbase.AnalyticsClient.Options; using Couchbase.AnalyticsClient.Results; +using Couchbase.AnalyticsClient.UnitTests.Helpers; using Moq; using Xunit; @@ -13,7 +15,7 @@ public class QueryResultHandleTests public void Constructor_InitializesProperties() { var serviceMock = new Mock(); - var handle = new QueryResultHandle("test-path", "test-req", serviceMock.Object); + var handle = TestHandleFactory.CreateQueryResultHandle("test-path", "test-req", "{}", serviceMock.Object); Assert.Equal("test-req", handle.RequestId); } @@ -22,7 +24,7 @@ public void Constructor_InitializesProperties() public async Task FetchResultsAsync_DelegatesToService() { var serviceMock = new Mock(); - var handle = new QueryResultHandle("test-path", "test-req", serviceMock.Object); + var handle = TestHandleFactory.CreateQueryResultHandle("test-path", "test-req", "{}", serviceMock.Object); var expectedResult = new Mock().Object; var options = new FetchResultsOptions(); @@ -31,7 +33,7 @@ public async Task FetchResultsAsync_DelegatesToService() var result = await handle.FetchResultsAsync(options); Assert.Same(expectedResult, result); - + serviceMock.Verify(x => x.FetchResultsAsync("test-req", "test-path", options, default), Times.Once); } @@ -39,14 +41,14 @@ public async Task FetchResultsAsync_DelegatesToService() public async Task DiscardResultsAsync_DelegatesToService() { var serviceMock = new Mock(); - var handle = new QueryResultHandle("test-path", "test-req", serviceMock.Object); + var handle = TestHandleFactory.CreateQueryResultHandle("test-path", "test-req", "{}", serviceMock.Object); var options = new DiscardResultsOptions(); serviceMock.Setup(x => x.DiscardResultsAsync("test-req", "test-path", options, It.IsAny())) .Returns(Task.CompletedTask); await handle.DiscardResultsAsync(options); - + serviceMock.Verify(x => x.DiscardResultsAsync("test-req", "test-path", options, default), Times.Once); } @@ -55,16 +57,17 @@ public void Constructor_NullArguments_ThrowsArgumentNullException() { var serviceMock = new Mock(); - Assert.Throws(() => new QueryResultHandle(null!, "req", serviceMock.Object)); - Assert.Throws(() => new QueryResultHandle("path", null!, serviceMock.Object)); - Assert.Throws(() => new QueryResultHandle("path", "req", null!)); + Assert.Throws(() => TestHandleFactory.CreateQueryResultHandle(null!, "req", "{}", serviceMock.Object)); + Assert.Throws(() => TestHandleFactory.CreateQueryResultHandle("path", null!, "{}", serviceMock.Object)); + Assert.Throws(() => new QueryResultHandle("path", "req", default(JsonElement), serviceMock.Object)); + Assert.Throws(() => TestHandleFactory.CreateQueryResultHandle("path", "req", "{}", null!)); } [Fact] public async Task FetchResultsAsync_FluentOptions_DelegatesProperly() { var serviceMock = new Mock(); - var handle = new QueryResultHandle("test-path", "test-req", serviceMock.Object); + var handle = TestHandleFactory.CreateQueryResultHandle("test-path", "test-req", "{}", serviceMock.Object); var expectedResult = new Mock().Object; serviceMock.Setup(x => x.FetchResultsAsync("test-req", "test-path", It.IsAny(), It.IsAny())) @@ -81,7 +84,7 @@ public async Task FetchResultsAsync_FluentOptions_DelegatesProperly() public async Task DiscardResultsAsync_FluentOptions_DelegatesProperly() { var serviceMock = new Mock(); - var handle = new QueryResultHandle("test-path", "test-req", serviceMock.Object); + var handle = TestHandleFactory.CreateQueryResultHandle("test-path", "test-req", "{}", serviceMock.Object); serviceMock.Setup(x => x.DiscardResultsAsync("test-req", "test-path", It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); diff --git a/tests/Couchbase.Analytics.UnitTests/ClusterDisposalTests.cs b/tests/Couchbase.Analytics.UnitTests/ClusterDisposalTests.cs index ce54bd5..fd90c11 100644 --- a/tests/Couchbase.Analytics.UnitTests/ClusterDisposalTests.cs +++ b/tests/Couchbase.Analytics.UnitTests/ClusterDisposalTests.cs @@ -1,4 +1,3 @@ -using System; using Couchbase.AnalyticsClient; using Couchbase.AnalyticsClient.HTTP; using Microsoft.Extensions.Logging; @@ -13,6 +12,7 @@ public class ClusterDisposalTests public void Cluster_Dispose_DisposesAllRegisteredSingletons() { var mockLoggerFactory = new Mock(); + mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); var cluster = Cluster.Create( "http://localhost:8095", diff --git a/tests/Couchbase.Analytics.UnitTests/Helpers/TestHandleFactory.cs b/tests/Couchbase.Analytics.UnitTests/Helpers/TestHandleFactory.cs new file mode 100644 index 0000000..8bc4288 --- /dev/null +++ b/tests/Couchbase.Analytics.UnitTests/Helpers/TestHandleFactory.cs @@ -0,0 +1,18 @@ +using System.Text.Json; +using Couchbase.AnalyticsClient.Async; +using Couchbase.AnalyticsClient.Internal; + +namespace Couchbase.AnalyticsClient.UnitTests.Helpers; + +/// +/// Convenience factory for creating handle objects from raw JSON strings in tests. +/// Keeps production constructors accepting only . +/// +internal static class TestHandleFactory +{ + public static QueryHandle CreateQueryHandle(string handle, string requestId, string responseJson, IAnalyticsService service) + => new(handle, requestId, JsonDocument.Parse(responseJson).RootElement, service); + + public static QueryResultHandle CreateQueryResultHandle(string handlePath, string requestId, string responseJson, IAnalyticsService service) + => new(handlePath, requestId, JsonDocument.Parse(responseJson).RootElement, service); +} diff --git a/tests/Couchbase.Analytics.UnitTests/Internal/AnalyticsServiceTests.cs b/tests/Couchbase.Analytics.UnitTests/Internal/AnalyticsServiceTests.cs index 82534ef..8038c11 100644 --- a/tests/Couchbase.Analytics.UnitTests/Internal/AnalyticsServiceTests.cs +++ b/tests/Couchbase.Analytics.UnitTests/Internal/AnalyticsServiceTests.cs @@ -1,13 +1,14 @@ using System.Net; using System.Text; +using Couchbase.AnalyticsClient.Async; +using Couchbase.AnalyticsClient.Exceptions; using Couchbase.AnalyticsClient.Internal; using Couchbase.AnalyticsClient.Internal.HTTP; using Couchbase.AnalyticsClient.Internal.Results; using Couchbase.AnalyticsClient.Logging; using Couchbase.AnalyticsClient.Options; +using Couchbase.AnalyticsClient.UnitTests.Helpers; using Couchbase.Core.Json; -using Couchbase.AnalyticsClient.Async; -using Couchbase.AnalyticsClient.Exceptions; using Microsoft.Extensions.Logging; using Moq; using Moq.Protected; @@ -181,7 +182,7 @@ public async Task FetchResultHandleAsync_When404_ThrowsQueryNotFoundException() _loggerMock.Object, new TypedRedactor(RedactionLevel.None)); - var handle = new QueryHandle("mock-handle", "mock-req", service); + var handle = TestHandleFactory.CreateQueryHandle("mock-handle", "mock-req", "{}", service); // Act & Assert await Assert.ThrowsAsync(() => diff --git a/tests/Couchbase.Analytics.UnitTests/Internal/DI/CouchbaseServiceProviderTests.cs b/tests/Couchbase.Analytics.UnitTests/Internal/DI/CouchbaseServiceProviderTests.cs index 1eec904..2819401 100644 --- a/tests/Couchbase.Analytics.UnitTests/Internal/DI/CouchbaseServiceProviderTests.cs +++ b/tests/Couchbase.Analytics.UnitTests/Internal/DI/CouchbaseServiceProviderTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Couchbase.AnalyticsClient.Internal.DI; using Moq; using Xunit; diff --git a/tests/Couchbase.Analytics.UnitTests/Internal/DI/SingletonGenericServiceFactoryTests.cs b/tests/Couchbase.Analytics.UnitTests/Internal/DI/SingletonGenericServiceFactoryTests.cs index 605350f..ec6b150 100644 --- a/tests/Couchbase.Analytics.UnitTests/Internal/DI/SingletonGenericServiceFactoryTests.cs +++ b/tests/Couchbase.Analytics.UnitTests/Internal/DI/SingletonGenericServiceFactoryTests.cs @@ -1,4 +1,3 @@ -using System; using Couchbase.AnalyticsClient.Internal.DI; using Moq; using Xunit; @@ -7,12 +6,12 @@ namespace Couchbase.Analytics.UnitTests.Internal.DI; public interface IFakeGenericService : IDisposable { } -public class FakeGenericService : IFakeGenericService +public class FakeGenericService : IFakeGenericService { public static Action? OnDispose { get; set; } - + public FakeGenericService() { } - + public void Dispose() { OnDispose?.Invoke(); @@ -26,27 +25,27 @@ public void SingletonGenericServiceFactory_Dispose_DisposesAllCachedPermutations { // Arrange var factory = new SingletonGenericServiceFactory(typeof(FakeGenericService<>)); - + var mockServiceProvider = new Mock(); factory.Initialize(mockServiceProvider.Object); - + // Populate the concurrent dictionary with different permutations of the generic factory.CreateService(typeof(IFakeGenericService)); factory.CreateService(typeof(IFakeGenericService)); factory.CreateService(typeof(IFakeGenericService)); - - int disposeCount = 0; + + var disposeCount = 0; FakeGenericService.OnDispose = () => disposeCount++; FakeGenericService.OnDispose = () => disposeCount++; FakeGenericService.OnDispose = () => disposeCount++; // Act factory.Dispose(); - + // Assert // The factory should naturally iterate and call Dispose exactly 3 times (once per instantiated T) Assert.Equal(3, disposeCount); - + // Cleanup statics FakeGenericService.OnDispose = null; FakeGenericService.OnDispose = null; diff --git a/tests/Couchbase.Analytics.UnitTests/Internal/DI/SingletonServiceFactoryTests.cs b/tests/Couchbase.Analytics.UnitTests/Internal/DI/SingletonServiceFactoryTests.cs index 91a35ce..8688a13 100644 --- a/tests/Couchbase.Analytics.UnitTests/Internal/DI/SingletonServiceFactoryTests.cs +++ b/tests/Couchbase.Analytics.UnitTests/Internal/DI/SingletonServiceFactoryTests.cs @@ -1,4 +1,3 @@ -using System; using Couchbase.AnalyticsClient.Internal.DI; using Moq; using Xunit; @@ -13,10 +12,10 @@ public void SingletonServiceFactory_Dispose_DisposesInnerSingleton() // Arrange var mockDisposable = new Mock(); var factory = new SingletonServiceFactory(mockDisposable.Object); - + // Act factory.Dispose(); - + // Assert mockDisposable.Verify(d => d.Dispose(), Times.Once); } diff --git a/tests/Couchbase.Analytics.UnitTests/Internal/DI/TransientServiceFactoryTests.cs b/tests/Couchbase.Analytics.UnitTests/Internal/DI/TransientServiceFactoryTests.cs index 6038e06..24c75a6 100644 --- a/tests/Couchbase.Analytics.UnitTests/Internal/DI/TransientServiceFactoryTests.cs +++ b/tests/Couchbase.Analytics.UnitTests/Internal/DI/TransientServiceFactoryTests.cs @@ -1,4 +1,3 @@ -using System; using Couchbase.AnalyticsClient.Internal.DI; using Moq; using Xunit; @@ -13,16 +12,16 @@ public void TransientServiceFactory_Dispose_DoesNotDisposeGeneratedInstances() // Arrange var mockDisposable = new Mock(); var factory = new TransientServiceFactory(_ => mockDisposable.Object); - + var mockServiceProvider = new Mock(); factory.Initialize(mockServiceProvider.Object); - + // Act // We simulate the lifetime: The user asks for a Transient object, then later the Cluster shuts down entirely. var instance = factory.CreateService(typeof(IDisposable)); - + factory.Dispose(); - + // Assert // We explicitly confirm that the Transient factory completely ignores the instances // it generates, thus avoiding long-term memory leaks in the Cluster's DI container! diff --git a/tests/Couchbase.Analytics.UnitTests/Internal/ExecuteQueryTests.cs b/tests/Couchbase.Analytics.UnitTests/Internal/ExecuteQueryTests.cs index 861002a..f0284d5 100644 --- a/tests/Couchbase.Analytics.UnitTests/Internal/ExecuteQueryTests.cs +++ b/tests/Couchbase.Analytics.UnitTests/Internal/ExecuteQueryTests.cs @@ -5,7 +5,7 @@ using Couchbase.AnalyticsClient.Options; using Couchbase.AnalyticsClient.Query; using Couchbase.AnalyticsClient.Results; -using Couchbase.Core.Json; +using Couchbase.AnalyticsClient.UnitTests.Helpers; using Xunit; using Xunit.Abstractions; @@ -58,7 +58,7 @@ public Task SendAsync(string statement, QueryOptions options, Canc public Task StartQueryAsync(string statement, StartQueryOptions options, CancellationToken cancellationToken = default) { LastStartOptions = options; - return Task.FromResult(new QueryHandle("handle", "reqId", this)); + return Task.FromResult(TestHandleFactory.CreateQueryHandle("handle", "reqId", "{}", this)); } public Task FetchResultHandleAsync(QueryHandle handle, FetchResultHandleOptions options, CancellationToken cancellationToken = default) diff --git a/tests/Couchbase.Analytics.UnitTests/Internal/HTTP/CouchbaseHttpClientFactoryTests.cs b/tests/Couchbase.Analytics.UnitTests/Internal/HTTP/CouchbaseHttpClientFactoryTests.cs index 1272a5c..717a4db 100644 --- a/tests/Couchbase.Analytics.UnitTests/Internal/HTTP/CouchbaseHttpClientFactoryTests.cs +++ b/tests/Couchbase.Analytics.UnitTests/Internal/HTTP/CouchbaseHttpClientFactoryTests.cs @@ -1,7 +1,3 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Couchbase.AnalyticsClient; using Couchbase.AnalyticsClient.HTTP; using Couchbase.AnalyticsClient.Internal.HTTP; using Couchbase.AnalyticsClient.Options; @@ -18,18 +14,18 @@ public async Task CouchbaseHttpClientFactory_Dispose_CascadesToUnderlyingHandler // Arrange var options = new ClusterOptions { ConnectionString = "http://localhost:8095" }; var factory = new CouchbaseHttpClientFactory( - () => new Credential("Administrator", "password"), - options, + () => new Credential("Administrator", "password"), + options, new NullLogger() ); - + // Create an active HTTP client wrapping the shared AuthenticationHandler -> SocketsHttpHandler var httpClient = factory.Create(); // Act // Executing Dispose must trigger teardowns down the chain. factory.Dispose(); - + // Assert // A perfectly disposed downstream handler natively refuses any new HttpClient execution // by immediately throwing an ObjectDisposedException preflight.