Refactor MaybeThrowGraphqlException to throw AggregateException with all GraphQL errors (#169)
* first pass * Updated tests * Removed path from message
This commit is contained in:
@@ -5,15 +5,15 @@ namespace Speckle.Sdk.Dependencies;
|
||||
|
||||
public static class GraphQLRetry
|
||||
{
|
||||
public static async Task<T> ExecuteAsync<T, TException>(
|
||||
public static async Task<T> ExecuteAsync<T, TInnerException>(
|
||||
Func<Task<T>> func,
|
||||
Action<Exception, TimeSpan>? onRetry = null
|
||||
)
|
||||
where TException : Exception
|
||||
where TInnerException : Exception
|
||||
{
|
||||
var delay = Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromSeconds(1), 5);
|
||||
var graphqlRetry = Policy
|
||||
.Handle<TException>()
|
||||
.HandleInner<TInnerException>()
|
||||
.WaitAndRetryAsync(
|
||||
delay,
|
||||
(ex, timeout, _) =>
|
||||
|
||||
@@ -1,53 +1,17 @@
|
||||
using GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL;
|
||||
|
||||
namespace Speckle.Sdk.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for GraphQL API exceptions
|
||||
/// The base class for all GraphQL errors (these are errors in the graphql response)
|
||||
/// Some specific codes are maped to subtypes <see cref="GraphQLErrorHandler"/>
|
||||
/// <seealso cref="SpeckleGraphQLForbiddenException"/>
|
||||
/// <seealso cref="SpeckleGraphQLInternalErrorException"/>
|
||||
/// <seealso cref="SpeckleGraphQLBadInputException"/>
|
||||
/// <seealso cref="SpeckleGraphQLInvalidQueryException"/>
|
||||
/// </summary>
|
||||
public class SpeckleGraphQLException<T> : SpeckleGraphQLException
|
||||
{
|
||||
public new GraphQLResponse<T>? Response => (GraphQLResponse<T>?)base.Response;
|
||||
|
||||
public SpeckleGraphQLException(
|
||||
string message,
|
||||
GraphQLRequest request,
|
||||
GraphQLResponse<T>? response,
|
||||
Exception? innerException = null
|
||||
)
|
||||
: base(message, request, response, innerException) { }
|
||||
|
||||
public SpeckleGraphQLException() { }
|
||||
|
||||
public SpeckleGraphQLException(string? message)
|
||||
: base(message) { }
|
||||
|
||||
public SpeckleGraphQLException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
public class SpeckleGraphQLException : SpeckleException
|
||||
{
|
||||
private readonly GraphQLRequest _request;
|
||||
public IGraphQLResponse? Response { get; }
|
||||
|
||||
public IEnumerable<string> ErrorMessages =>
|
||||
Response?.Errors != null ? Response.Errors.Select(e => e.Message) : Enumerable.Empty<string>();
|
||||
|
||||
public IDictionary<string, object>? Extensions => Response?.Extensions;
|
||||
|
||||
public SpeckleGraphQLException(
|
||||
string? message,
|
||||
GraphQLRequest request,
|
||||
IGraphQLResponse? response,
|
||||
Exception? innerException = null
|
||||
)
|
||||
: base(message, innerException)
|
||||
{
|
||||
_request = request;
|
||||
Response = response;
|
||||
}
|
||||
|
||||
public SpeckleGraphQLException() { }
|
||||
|
||||
public SpeckleGraphQLException(string? message)
|
||||
@@ -58,19 +22,12 @@ public class SpeckleGraphQLException : SpeckleException
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a "FORBIDDEN" on "UNAUTHORIZED" GraphQL error as an exception.
|
||||
/// Represents a "FORBIDDEN" or "UNAUTHORIZED" GraphQL error as an exception.
|
||||
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#unauthenticated
|
||||
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#forbidden
|
||||
/// </summary>
|
||||
public class SpeckleGraphQLForbiddenException : SpeckleGraphQLException
|
||||
public sealed class SpeckleGraphQLForbiddenException : SpeckleGraphQLException
|
||||
{
|
||||
public SpeckleGraphQLForbiddenException(
|
||||
GraphQLRequest request,
|
||||
IGraphQLResponse response,
|
||||
Exception? innerException = null
|
||||
)
|
||||
: base("Your request was forbidden", request, response, innerException) { }
|
||||
|
||||
public SpeckleGraphQLForbiddenException() { }
|
||||
|
||||
public SpeckleGraphQLForbiddenException(string? message)
|
||||
@@ -80,15 +37,12 @@ public class SpeckleGraphQLForbiddenException : SpeckleGraphQLException
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
public class SpeckleGraphQLInternalErrorException : SpeckleGraphQLException
|
||||
/// <summary>
|
||||
/// Represents a "INTERNAL_SERVER_ERROR" GraphQL error as an exception.
|
||||
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors#internal_server_error
|
||||
/// </summary>
|
||||
public sealed class SpeckleGraphQLInternalErrorException : SpeckleGraphQLException
|
||||
{
|
||||
public SpeckleGraphQLInternalErrorException(
|
||||
GraphQLRequest request,
|
||||
IGraphQLResponse response,
|
||||
Exception? innerException = null
|
||||
)
|
||||
: base("Your request failed on the server side", request, response, innerException) { }
|
||||
|
||||
public SpeckleGraphQLInternalErrorException() { }
|
||||
|
||||
public SpeckleGraphQLInternalErrorException(string? message)
|
||||
@@ -98,15 +52,11 @@ public class SpeckleGraphQLInternalErrorException : SpeckleGraphQLException
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
public class SpeckleGraphQLStreamNotFoundException : SpeckleGraphQLException
|
||||
/// <summary>
|
||||
/// Represents the custom "STREAM_NOT_FOUND" GraphQL error as an exception.
|
||||
/// </summary>
|
||||
public sealed class SpeckleGraphQLStreamNotFoundException : SpeckleGraphQLException
|
||||
{
|
||||
public SpeckleGraphQLStreamNotFoundException(
|
||||
GraphQLRequest request,
|
||||
IGraphQLResponse response,
|
||||
Exception? innerException = null
|
||||
)
|
||||
: base("Stream not found", request, response, innerException) { }
|
||||
|
||||
public SpeckleGraphQLStreamNotFoundException() { }
|
||||
|
||||
public SpeckleGraphQLStreamNotFoundException(string? message)
|
||||
@@ -115,3 +65,34 @@ public class SpeckleGraphQLStreamNotFoundException : SpeckleGraphQLException
|
||||
public SpeckleGraphQLStreamNotFoundException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a "BAD_USER_INPUT" GraphQL error as an exception.
|
||||
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors#bad_user_input
|
||||
/// </summary>
|
||||
public sealed class SpeckleGraphQLBadInputException : SpeckleGraphQLException
|
||||
{
|
||||
public SpeckleGraphQLBadInputException() { }
|
||||
|
||||
public SpeckleGraphQLBadInputException(string? message)
|
||||
: base(message) { }
|
||||
|
||||
public SpeckleGraphQLBadInputException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a "GRAPHQL_PARSE_FAILED" or "GRAPHQL_VALIDATION_FAILED" GraphQL error as an exception.
|
||||
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors#graphql_parse_failed
|
||||
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors#graphql_validation_failed
|
||||
/// </summary>
|
||||
public sealed class SpeckleGraphQLInvalidQueryException : SpeckleGraphQLException
|
||||
{
|
||||
public SpeckleGraphQLInvalidQueryException() { }
|
||||
|
||||
public SpeckleGraphQLInvalidQueryException(string? message)
|
||||
: base(message) { }
|
||||
|
||||
public SpeckleGraphQLInvalidQueryException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ public sealed class Client : ISpeckleGraphQLClient, IDisposable
|
||||
GraphQLResponse<T> result = await GQLClient
|
||||
.SendMutationAsync<T>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
MaybeThrowFromGraphQLErrors(request, result);
|
||||
result.EnsureGraphQLSuccess();
|
||||
return result.Data;
|
||||
})
|
||||
.ConfigureAwait(false);
|
||||
@@ -132,129 +132,48 @@ public sealed class Client : ISpeckleGraphQLClient, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
internal void MaybeThrowFromGraphQLErrors<T>(GraphQLRequest request, GraphQLResponse<T> response)
|
||||
{
|
||||
// The errors reflect the Apollo server v2 API, which is deprecated. It is bound to change,
|
||||
// once we migrate to a newer version.
|
||||
var errors = response.Errors;
|
||||
if (errors != null && errors.Length != 0)
|
||||
{
|
||||
if (
|
||||
errors.Any(e =>
|
||||
e.Extensions != null
|
||||
&& (
|
||||
e.Extensions.Contains(new KeyValuePair<string, object>("code", "FORBIDDEN"))
|
||||
|| e.Extensions.Contains(new KeyValuePair<string, object>("code", "UNAUTHENTICATED"))
|
||||
)
|
||||
)
|
||||
)
|
||||
{
|
||||
throw new SpeckleGraphQLForbiddenException(request, response);
|
||||
}
|
||||
|
||||
if (
|
||||
errors.Any(e =>
|
||||
e.Extensions != null && e.Extensions.Contains(new KeyValuePair<string, object>("code", "STREAM_NOT_FOUND"))
|
||||
)
|
||||
)
|
||||
{
|
||||
throw new SpeckleGraphQLStreamNotFoundException(request, response);
|
||||
}
|
||||
|
||||
if (
|
||||
errors.Any(e =>
|
||||
e.Extensions != null
|
||||
&& e.Extensions.Contains(new KeyValuePair<string, object>("code", "INTERNAL_SERVER_ERROR"))
|
||||
)
|
||||
)
|
||||
{
|
||||
throw new SpeckleGraphQLInternalErrorException(request, response);
|
||||
}
|
||||
|
||||
throw new SpeckleGraphQLException<T>("Request failed with errors", request, response);
|
||||
}
|
||||
}
|
||||
|
||||
IDisposable ISpeckleGraphQLClient.SubscribeTo<T>(GraphQLRequest request, Action<object, T> callback) =>
|
||||
SubscribeTo(request, callback);
|
||||
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/>
|
||||
private IDisposable SubscribeTo<T>(GraphQLRequest request, Action<object, T> callback)
|
||||
{
|
||||
//using (LogContext.Push(CreateEnrichers<T>(request)))
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var res = GQLClient.CreateSubscriptionStream<T>(request);
|
||||
return res.Subscribe(
|
||||
response =>
|
||||
var res = GQLClient.CreateSubscriptionStream<T>(request);
|
||||
return res.Subscribe(
|
||||
response =>
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
MaybeThrowFromGraphQLErrors(request, response);
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
if (response.Data != null)
|
||||
{
|
||||
callback(this, response.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Serilog.Log.ForContext("graphqlResponse", response)
|
||||
_logger.LogError(
|
||||
"Cannot execute graphql callback for {resultType}, the response has no data.",
|
||||
typeof(T).Name
|
||||
);
|
||||
}
|
||||
}
|
||||
// we catch forbidden to rethrow, making sure its not logged.
|
||||
catch (SpeckleGraphQLForbiddenException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
// anything else related to graphql gets logged
|
||||
catch (SpeckleGraphQLException<T> gqlException)
|
||||
{
|
||||
/* Speckle.Sdk.Logging..ForContext("graphqlResponse", gqlException.Response)
|
||||
.ForContext("graphqlExtensions", gqlException.Extensions)
|
||||
.ForContext("graphqlErrorMessages", gqlException.ErrorMessages.ToList())*/
|
||||
_logger.LogWarning(
|
||||
gqlException,
|
||||
"Execution of the graphql request to get {resultType} failed with {graphqlExceptionType} {exceptionMessage}.",
|
||||
typeof(T).Name,
|
||||
gqlException.GetType().Name,
|
||||
gqlException.Message
|
||||
);
|
||||
throw;
|
||||
}
|
||||
// we're not handling the bare Exception type here,
|
||||
// since we have a response object on the callback, we know the Exceptions
|
||||
// can only be thrown from the MaybeThrowFromGraphQLErrors which wraps
|
||||
// every exception into SpeckleGraphQLException
|
||||
},
|
||||
ex =>
|
||||
{
|
||||
// we're logging this as an error for now, to keep track of failures
|
||||
// so far we've swallowed these errors
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Subscription for {resultType} terminated unexpectedly with {exceptionMessage}",
|
||||
typeof(T).Name,
|
||||
ex.Message
|
||||
);
|
||||
// we could be throwing like this:
|
||||
// throw ex;
|
||||
callback(this, response.Data);
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
throw new SpeckleGraphQLException<T>(
|
||||
"The graphql request failed without a graphql response",
|
||||
request,
|
||||
null,
|
||||
ex
|
||||
);
|
||||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Subscription for {type} got a response with errors", typeof(T).Name);
|
||||
throw;
|
||||
}
|
||||
},
|
||||
ex =>
|
||||
{
|
||||
// we're logging this as an error for now, to keep track of failures
|
||||
// so far we've swallowed these errors
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Subscription for {resultType} terminated unexpectedly with {exceptionMessage}",
|
||||
typeof(T).Name,
|
||||
ex.Message
|
||||
);
|
||||
// we could be throwing like this:
|
||||
// throw ex;
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal() && ex is not ObjectDisposedException)
|
||||
{
|
||||
throw new SpeckleGraphQLException($"Subscription for {typeof(T)} failed to start", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Diagnostics.Contracts;
|
||||
using GraphQL;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL;
|
||||
|
||||
internal static class GraphQLErrorHandler
|
||||
{
|
||||
/// <exception cref="AggregateException"><inheritdoc cref="EnsureGraphQLSuccess(IReadOnlyCollection{GraphQLError}?)"/></exception>
|
||||
public static void EnsureGraphQLSuccess(this IGraphQLResponse response) => EnsureGraphQLSuccess(response.Errors);
|
||||
|
||||
/// <exception cref="AggregateException">Containing a <see cref="SpeckleGraphQLException"/> (or subclass of) for each graphql Error</exception>
|
||||
public static void EnsureGraphQLSuccess(IReadOnlyCollection<GraphQLError>? errors)
|
||||
{
|
||||
// The errors reflect the Apollo server v2 API, which is deprecated. It is bound to change,
|
||||
// once we migrate to a newer version.
|
||||
if (errors == null || errors.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<SpeckleGraphQLException> exceptions = new(errors.Count);
|
||||
foreach (var error in errors)
|
||||
{
|
||||
object? code = null;
|
||||
_ = error.Extensions?.TryGetValue("code", out code);
|
||||
|
||||
var message = FormatErrorMessage(error, code);
|
||||
var ex = code switch
|
||||
{
|
||||
"GRAPHQL_PARSE_FAILED" or "GRAPHQL_VALIDATION_FAILED" => new SpeckleGraphQLInvalidQueryException(message),
|
||||
"FORBIDDEN" or "UNAUTHENTICATED" => new SpeckleGraphQLForbiddenException(message),
|
||||
"STREAM_NOT_FOUND" => new SpeckleGraphQLStreamNotFoundException(message),
|
||||
"BAD_USER_INPUT" => new SpeckleGraphQLBadInputException(message),
|
||||
"INTERNAL_SERVER_ERROR" => new SpeckleGraphQLInternalErrorException(message),
|
||||
_ => new SpeckleGraphQLException(message),
|
||||
};
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
|
||||
throw new AggregateException("Request failed with GraphQL errors, see inner exceptions", exceptions);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
private static string FormatErrorMessage(GraphQLError error, object? code)
|
||||
{
|
||||
code ??= "ERROR";
|
||||
return $"{code}: {error.Message}";
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using GraphQL;
|
||||
using GraphQL.Client.Http;
|
||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
using Speckle.Sdk.Common;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL;
|
||||
|
||||
@@ -10,46 +9,51 @@ public static class GraphQLHttpClientExtensions
|
||||
/// <summary>
|
||||
/// Gets the version of the current server. Useful for guarding against unsupported api calls on newer or older servers.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">[Optional] defaults to an empty cancellation token</param>
|
||||
/// <returns><see cref="Version"/> object excluding any strings (eg "2.7.2-alpha.6995" becomes "2.7.2.6995")</returns>
|
||||
/// <exception cref="SpeckleGraphQLException{ServerInfoResponse}"></exception>
|
||||
/// <remarks>
|
||||
/// Expects the response to either be<br/>
|
||||
/// - 1. The literal string <c>dev</c>, which will return <c>999.999.999</c><br/>
|
||||
/// - 2. A 3 numeral semver (anything after the first <c>-</c> character will be ignored)<br/>
|
||||
/// </remarks>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>A 3 numeral <see cref="Version"/> object (e.g. <c>2.21.3.alpha123</c> becomes <c>2.21.3</c>)</returns>
|
||||
/// <exception cref="AggregateException"><inheritdoc cref="GraphQLErrorHandler.EnsureGraphQLSuccess(IGraphQLResponse)"/></exception>
|
||||
/// <exception cref="FormatException">Server responded with a server version, but it was not in an expected format</exception>
|
||||
public static async Task<System.Version> GetServerVersion(
|
||||
this GraphQLHttpClient client,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var request = new GraphQLRequest
|
||||
{
|
||||
Query =
|
||||
@"query Server {
|
||||
serverInfo {
|
||||
version
|
||||
}
|
||||
}",
|
||||
};
|
||||
//lang=graphql
|
||||
const string QUERY = """
|
||||
query Server {
|
||||
data:serverInfo {
|
||||
data:version
|
||||
}
|
||||
}
|
||||
""";
|
||||
var request = new GraphQLRequest { Query = QUERY };
|
||||
|
||||
var response = await client.SendQueryAsync<ServerInfoResponse>(request, cancellationToken).ConfigureAwait(false);
|
||||
var response = await client
|
||||
.SendQueryAsync<RequiredResponse<RequiredResponse<string>>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.Errors != null)
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
string versionString = response.Data.data.data;
|
||||
if (versionString == "dev")
|
||||
{
|
||||
throw new SpeckleGraphQLException<ServerInfoResponse>(
|
||||
$"Query {nameof(GetServerVersion)} failed",
|
||||
request,
|
||||
response
|
||||
);
|
||||
return new Version(999, 999, 999);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(response.Data.serverInfo.version))
|
||||
{
|
||||
throw new SpeckleGraphQLException<ServerInfoResponse>(
|
||||
$"Query {nameof(GetServerVersion)} did not provide a valid server version",
|
||||
request,
|
||||
response
|
||||
);
|
||||
}
|
||||
string? semverString = versionString.Split('-').First();
|
||||
|
||||
return response.Data.serverInfo.version == "dev"
|
||||
? new System.Version(999, 999, 999)
|
||||
: new System.Version(response.Data.serverInfo.version.NotNull().Split('-').First());
|
||||
if (Version.TryParse(semverString!, out Version? semver))
|
||||
{
|
||||
return semver;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FormatException($"Server responded with an invalid semver string \"{semverString}\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
using GraphQL;
|
||||
using GraphQL.Client.Http;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL;
|
||||
|
||||
internal interface ISpeckleGraphQLClient
|
||||
{
|
||||
/// <exception cref="SpeckleGraphQLForbiddenException">"FORBIDDEN" on "UNAUTHORIZED" response from server</exception>
|
||||
/// <exception cref="SpeckleGraphQLException">All other request errors</exception>
|
||||
/// <exception cref="AggregateException">Request failed on the GraphQL layer, each GraphQL error will be a <see cref="SpeckleGraphQLException"/> (or subclass of) as an inner exception</exception>
|
||||
/// <exception cref="GraphQLHttpRequestException">Request failed on the HTTP layer (non-successful response code)</exception>
|
||||
/// <exception cref="HttpRequestException">The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout</exception>
|
||||
/// <exception cref="OperationCanceledException">The <paramref name="cancellationToken"/> requested a cancel</exception>
|
||||
/// <exception cref="ObjectDisposedException">This <see cref="Client"/> already been disposed</exception>
|
||||
/// <exception cref="JsonSerializationException">The response failed to deserialize, probably because the server version is incompatible with this version of the SDK, or there is a mistake in a query (queried for a property that isn't in the C# model, or a required property was null)</exception>
|
||||
/// <exception cref="JsonException">The response failed to deserialize, probably because the server version is incompatible with this version of the SDK, or there is a mistake in a query (queried for a property that isn't in the C# model, or a required property was null)</exception>
|
||||
internal Task<T> ExecuteGraphQLRequest<T>(GraphQLRequest request, CancellationToken cancellationToken);
|
||||
|
||||
/// <exception cref="SpeckleGraphQLForbiddenException">"FORBIDDEN" on "UNAUTHORIZED" response from server</exception>
|
||||
/// <exception cref="SpeckleGraphQLException">All other request errors</exception>
|
||||
/// <exception cref="AggregateException">Containing a <see cref="SpeckleGraphQLException"/> (or subclass of) for each graphql Error</exception>
|
||||
/// <exception cref="ObjectDisposedException">This <see cref="Client"/> already been disposed</exception>
|
||||
internal IDisposable SubscribeTo<T>(GraphQLRequest request, Action<object, T> callback);
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@ public sealed class ActiveUserResource
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <exception cref="SpeckleException">The ActiveUser could not be found (e.g. the client is not authenticated)</exception>
|
||||
public async Task<List<PendingStreamCollaborator>> GetProjectInvites(CancellationToken cancellationToken = default)
|
||||
{
|
||||
//language=graphql
|
||||
@@ -192,7 +193,7 @@ public sealed class ActiveUserResource
|
||||
|
||||
if (response.data is null)
|
||||
{
|
||||
throw new SpeckleGraphQLException("GraphQL response indicated that the ActiveUser could not be found");
|
||||
throw new SpeckleException("GraphQL response indicated that the ActiveUser could not be found");
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: https://app.speckle.systems/graphql
|
||||
documents: '**/*.graphql'
|
||||
@@ -5,6 +5,7 @@ namespace Speckle.Sdk.Common;
|
||||
|
||||
public static class NotNullExtensions
|
||||
{
|
||||
/// <exception cref="ArgumentNullException">Thrown when the awaited <paramref name="task"/> returns <see langword="null"/></exception>
|
||||
public static async ValueTask<T> NotNull<T>(
|
||||
this ValueTask<T?> task,
|
||||
[CallerArgumentExpression(nameof(task))] string? message = null
|
||||
@@ -19,6 +20,7 @@ public static class NotNullExtensions
|
||||
return x;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="NotNull{T}(System.Threading.Tasks.ValueTask{T?},string?)"/>
|
||||
public static async ValueTask<T> NotNull<T>(
|
||||
this ValueTask<T?> task,
|
||||
[CallerArgumentExpression(nameof(task))] string? message = null
|
||||
@@ -33,6 +35,7 @@ public static class NotNullExtensions
|
||||
return x.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="NotNull{T}(System.Threading.Tasks.ValueTask{T?},string?)"/>
|
||||
public static async Task<T> NotNull<T>(
|
||||
this Task<T?> task,
|
||||
[CallerArgumentExpression(nameof(task))] string? message = null
|
||||
@@ -47,6 +50,7 @@ public static class NotNullExtensions
|
||||
return x;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="NotNull{T}(System.Threading.Tasks.ValueTask{T?},string?)"/>
|
||||
public static async Task<T> NotNull<T>(
|
||||
this Task<T?> task,
|
||||
[CallerArgumentExpression(nameof(task))] string? message = null
|
||||
@@ -61,6 +65,7 @@ public static class NotNullExtensions
|
||||
return x.Value;
|
||||
}
|
||||
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="obj"/> is <see langword="null"/></exception>
|
||||
public static T NotNull<T>([NotNull] this T? obj, [CallerArgumentExpression(nameof(obj))] string? paramName = null)
|
||||
where T : class
|
||||
{
|
||||
@@ -71,6 +76,7 @@ public static class NotNullExtensions
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="NotNull{T}(T?,string?)"/>
|
||||
public static T NotNull<T>([NotNull] this T? obj, [CallerArgumentExpression(nameof(obj))] string? paramName = null)
|
||||
where T : struct
|
||||
{
|
||||
|
||||
@@ -9,7 +9,6 @@ using GraphQL.Client.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
@@ -38,8 +37,10 @@ public class AccountManager(ISpeckleApplication application, ILogger<AccountMana
|
||||
/// <summary>
|
||||
/// Gets the basic information about a server.
|
||||
/// </summary>
|
||||
/// <param name="server">Server URL</param>
|
||||
/// <param name="server">Server Information</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="GraphQLHttpRequestException">Request failed on the HTTP layer (received a non-successful response code)</exception>
|
||||
/// <exception cref="AggregateException"><inheritdoc cref="GraphQLErrorHandler.EnsureGraphQLSuccess(IGraphQLResponse)"/></exception>
|
||||
public async Task<ServerInfo> GetServerInfo(Uri server, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var httpClient = speckleHttp.CreateHttpClient();
|
||||
@@ -78,14 +79,7 @@ public class AccountManager(ISpeckleApplication application, ILogger<AccountMana
|
||||
|
||||
var response = await gqlClient.SendQueryAsync<ServerInfoResponse>(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.Errors is not null)
|
||||
{
|
||||
throw new SpeckleGraphQLException<ServerInfoResponse>(
|
||||
$"GraphQL request {nameof(GetServerInfo)} failed",
|
||||
request,
|
||||
response
|
||||
);
|
||||
}
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
ServerInfo serverInfo = response.Data.serverInfo;
|
||||
serverInfo.url = server.ToString().TrimEnd('/');
|
||||
@@ -100,6 +94,8 @@ public class AccountManager(ISpeckleApplication application, ILogger<AccountMana
|
||||
/// <param name="token"></param>
|
||||
/// <param name="server">Server URL</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="GraphQLHttpRequestException">Request failed on the HTTP layer (received a non-successful response code)</exception>
|
||||
/// <exception cref="AggregateException"><inheritdoc cref="GraphQLErrorHandler.EnsureGraphQLSuccess(IGraphQLResponse)"/></exception>
|
||||
public async Task<UserInfo> GetUserInfo(string token, Uri server, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var httpClient = speckleHttp.CreateHttpClient(authorizationToken: token);
|
||||
@@ -126,10 +122,7 @@ public class AccountManager(ISpeckleApplication application, ILogger<AccountMana
|
||||
.SendQueryAsync<RequiredResponse<UserInfo>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.Errors != null)
|
||||
{
|
||||
throw new SpeckleGraphQLException($"GraphQL request {nameof(GetUserInfo)} failed", request, response);
|
||||
}
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
return response.Data.data;
|
||||
}
|
||||
@@ -146,59 +139,45 @@ public class AccountManager(ISpeckleApplication application, ILogger<AccountMana
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
try
|
||||
using var httpClient = speckleHttp.CreateHttpClient(authorizationToken: token);
|
||||
|
||||
using var client = new GraphQLHttpClient(
|
||||
new GraphQLHttpClientOptions { EndPoint = new Uri(server, "/graphql") },
|
||||
new NewtonsoftJsonSerializer(),
|
||||
httpClient
|
||||
);
|
||||
|
||||
System.Version version = await client.GetServerVersion(ct).ConfigureAwait(false);
|
||||
|
||||
// serverMigration property was added in 2.18.5, so only query for it
|
||||
// if the server has been updated past that version
|
||||
System.Version serverMigrationVersion = new(2, 18, 5);
|
||||
|
||||
string queryString;
|
||||
if (version >= serverMigrationVersion)
|
||||
{
|
||||
using var httpClient = speckleHttp.CreateHttpClient(authorizationToken: token);
|
||||
|
||||
using var client = new GraphQLHttpClient(
|
||||
new GraphQLHttpClientOptions { EndPoint = new Uri(server, "/graphql") },
|
||||
new NewtonsoftJsonSerializer(),
|
||||
httpClient
|
||||
);
|
||||
|
||||
System.Version version = await client.GetServerVersion(ct).ConfigureAwait(false);
|
||||
|
||||
// serverMigration property was added in 2.18.5, so only query for it
|
||||
// if the server has been updated past that version
|
||||
System.Version serverMigrationVersion = new(2, 18, 5);
|
||||
|
||||
string queryString;
|
||||
if (version >= serverMigrationVersion)
|
||||
{
|
||||
//language=graphql
|
||||
queryString =
|
||||
"query { activeUser { id name email company avatar streams { totalCount } commits { totalCount } } serverInfo { name company adminContact description version migration { movedFrom movedTo } } }";
|
||||
}
|
||||
else
|
||||
{
|
||||
//language=graphql
|
||||
queryString =
|
||||
"query { activeUser { id name email company avatar streams { totalCount } commits { totalCount } } serverInfo { name company adminContact description version } }";
|
||||
}
|
||||
|
||||
var request = new GraphQLRequest { Query = queryString };
|
||||
|
||||
var response = await client.SendQueryAsync<ActiveUserServerInfoResponse>(request, ct).ConfigureAwait(false);
|
||||
|
||||
if (response.Errors != null)
|
||||
{
|
||||
throw new SpeckleGraphQLException<ActiveUserServerInfoResponse>(
|
||||
$"Query {nameof(GetUserServerInfo)} failed",
|
||||
request,
|
||||
response
|
||||
);
|
||||
}
|
||||
|
||||
ServerInfo serverInfo = response.Data.serverInfo;
|
||||
serverInfo.url = server.ToString().TrimEnd('/');
|
||||
serverInfo.frontend2 = await IsFrontend2Server(server).ConfigureAwait(false);
|
||||
|
||||
return response.Data;
|
||||
//language=graphql
|
||||
queryString =
|
||||
"query { activeUser { id name email company avatar streams { totalCount } commits { totalCount } } serverInfo { name company adminContact description version migration { movedFrom movedTo } } }";
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
else
|
||||
{
|
||||
throw new SpeckleException($"Failed to get user + server info from {server}", ex);
|
||||
//language=graphql
|
||||
queryString =
|
||||
"query { activeUser { id name email company avatar streams { totalCount } commits { totalCount } } serverInfo { name company adminContact description version } }";
|
||||
}
|
||||
|
||||
var request = new GraphQLRequest { Query = queryString };
|
||||
|
||||
var response = await client.SendQueryAsync<ActiveUserServerInfoResponse>(request, ct).ConfigureAwait(false);
|
||||
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
ServerInfo serverInfo = response.Data.serverInfo;
|
||||
serverInfo.url = server.ToString().TrimEnd('/');
|
||||
serverInfo.frontend2 = await IsFrontend2Server(server).ConfigureAwait(false);
|
||||
|
||||
return response.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
using GraphQL;
|
||||
using GraphQL.Client.Http;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Integration.Api.GraphQL;
|
||||
|
||||
[TestOf(typeof(Client))]
|
||||
public class GraphQLClientExceptionHandling
|
||||
{
|
||||
private Client _sut;
|
||||
|
||||
[SetUp]
|
||||
public async Task Setup()
|
||||
{
|
||||
_sut = await Fixtures.SeedUserWithClient();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description($"Attempts to execute a query on a non-existent server, expect a {nameof(GraphQLHttpRequestException)}")]
|
||||
public void TestHttpLayer()
|
||||
{
|
||||
_sut.GQLClient.Options.EndPoint = new Uri("http://127.0.0.1:1234"); //There is no server on this port...
|
||||
|
||||
Assert.ThrowsAsync<HttpRequestException>(async () => await _sut.ActiveUser.Get().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description(
|
||||
$"Attempts to execute a admin only command from a regular user, expect an inner {nameof(SpeckleGraphQLForbiddenException)}"
|
||||
)]
|
||||
public void TestGraphQLLayer_Forbidden()
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query Query {
|
||||
admin {
|
||||
userList {
|
||||
items {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
GraphQLRequest request = new(query: QUERY);
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(
|
||||
async () => await _sut.ExecuteGraphQLRequest<dynamic>(request).ConfigureAwait(false)
|
||||
);
|
||||
Assert.That(ex?.InnerExceptions, Has.Exactly(1).TypeOf<SpeckleGraphQLForbiddenException>());
|
||||
}
|
||||
|
||||
[Test, Description($"Attempts to execute a bad query, expect an inner {nameof(SpeckleGraphQLInvalidQueryException)}")]
|
||||
public void TestGraphQLLayer_BadQuery()
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query User {
|
||||
data:NonExistentQuery {
|
||||
id
|
||||
}
|
||||
}
|
||||
""";
|
||||
GraphQLRequest request = new(query: QUERY);
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(
|
||||
async () => await _sut.ExecuteGraphQLRequest<dynamic>(request).ConfigureAwait(false)
|
||||
);
|
||||
|
||||
Assert.That(ex?.InnerExceptions, Has.Exactly(1).TypeOf<SpeckleGraphQLInvalidQueryException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description(
|
||||
$"Attempts to execute a query with an invalid input, expect an inner {nameof(SpeckleGraphQLBadInputException)}"
|
||||
)]
|
||||
public void TestGraphQLLayer_BadInput()
|
||||
{
|
||||
ProjectUpdateRoleInput input = new(null!, null!, null);
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(
|
||||
async () => await _sut.Project.UpdateRole(input).ConfigureAwait(false)
|
||||
);
|
||||
|
||||
Assert.That(ex?.InnerExceptions, Has.Exactly(2).TypeOf<SpeckleGraphQLBadInputException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCancel()
|
||||
{
|
||||
using CancellationTokenSource cts = new();
|
||||
cts.Cancel();
|
||||
|
||||
var ex = Assert.CatchAsync<OperationCanceledException>(
|
||||
async () => await _sut.ActiveUser.Get(cts.Token).ConfigureAwait(false)
|
||||
);
|
||||
|
||||
Assert.That(ex?.CancellationToken, Is.EqualTo(cts.Token));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisposal()
|
||||
{
|
||||
_sut.Dispose();
|
||||
|
||||
Assert.Throws<ObjectDisposedException>(() => _ = _sut.Subscription.CreateUserProjectsUpdatedSubscription());
|
||||
}
|
||||
|
||||
[
|
||||
Test,
|
||||
Description($"Attempts to execute a query with a mismatched type, expect an {nameof(JsonSerializationException)}")
|
||||
]
|
||||
public void TestDeserialization()
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query User {
|
||||
data:activeUser {
|
||||
id
|
||||
}
|
||||
}
|
||||
""";
|
||||
GraphQLRequest request = new(query: QUERY);
|
||||
Assert.CatchAsync<JsonException>(async () => await _sut.ExecuteGraphQLRequest<int>(request).ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
+18
-11
@@ -22,33 +22,36 @@ public class ModelResourceExceptionalTests
|
||||
_model = await _testUser.Model.Create(new("Test Model", "", _project.id));
|
||||
}
|
||||
|
||||
[TestCase(null)]
|
||||
[TestCase("")]
|
||||
[TestCase(" ")]
|
||||
public void ModelCreate_Throws_InvalidInput(string name)
|
||||
{
|
||||
CreateModelInput input = new(name, null, _project.id);
|
||||
Assert.CatchAsync<SpeckleGraphQLException>(async () => await Sut.Create(input));
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(async () => await Sut.Create(input));
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ModelGet_Throws_NoAuth()
|
||||
{
|
||||
Assert.CatchAsync<SpeckleGraphQLException>(async () => await Fixtures.Unauthed.Model.Get(_model.id, _project.id));
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(
|
||||
async () => await Fixtures.Unauthed.Model.Get(_model.id, _project.id)
|
||||
);
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLForbiddenException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ModelGet_Throws_NonExistentModel()
|
||||
{
|
||||
Assert.CatchAsync<SpeckleGraphQLException>(async () => await Sut.Get("non existent model", _project.id));
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(async () => await Sut.Get("non existent model", _project.id));
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ModelGet_Throws_NonExistentProject()
|
||||
{
|
||||
Assert.ThrowsAsync<SpeckleGraphQLStreamNotFoundException>(
|
||||
async () => await Sut.Get(_model.id, "non existent project")
|
||||
);
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(async () => await Sut.Get(_model.id, "non existent project"));
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLStreamNotFoundException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -56,7 +59,8 @@ public class ModelResourceExceptionalTests
|
||||
{
|
||||
UpdateModelInput input = new("non-existent model", "MY new name", "MY new desc", _project.id);
|
||||
|
||||
Assert.CatchAsync<SpeckleGraphQLException>(async () => await Sut.Update(input));
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(async () => await Sut.Update(input));
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -64,7 +68,8 @@ public class ModelResourceExceptionalTests
|
||||
{
|
||||
UpdateModelInput input = new(_model.id, "MY new name", "MY new desc", "non-existent project");
|
||||
|
||||
Assert.ThrowsAsync<SpeckleGraphQLForbiddenException>(async () => await Sut.Update(input));
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(async () => await Sut.Update(input));
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLForbiddenException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -72,7 +77,8 @@ public class ModelResourceExceptionalTests
|
||||
{
|
||||
UpdateModelInput input = new(_model.id, "MY new name", "MY new desc", _project.id);
|
||||
|
||||
Assert.CatchAsync<SpeckleGraphQLException>(async () => await Fixtures.Unauthed.Model.Update(input));
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(async () => await Fixtures.Unauthed.Model.Update(input));
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLForbiddenException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -82,6 +88,7 @@ public class ModelResourceExceptionalTests
|
||||
DeleteModelInput input = new(toDelete.id, _project.id);
|
||||
await Sut.Delete(input);
|
||||
|
||||
Assert.CatchAsync<SpeckleGraphQLException>(async () => await Sut.Delete(input));
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(async () => await Sut.Delete(input));
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLException>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,10 @@ public class ModelResourceTests
|
||||
|
||||
await Sut.Delete(input);
|
||||
|
||||
Assert.CatchAsync<SpeckleGraphQLException>(async () => await Sut.Get(_model.id, _project.id));
|
||||
Assert.CatchAsync<SpeckleGraphQLException>(async () => await Sut.Delete(input));
|
||||
var getEx = Assert.CatchAsync<AggregateException>(async () => await Sut.Get(_model.id, _project.id));
|
||||
Assert.That(getEx?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLException>());
|
||||
|
||||
var delEx = Assert.CatchAsync<AggregateException>(async () => await Sut.Delete(input));
|
||||
Assert.That(delEx?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLException>());
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -23,10 +23,11 @@ public class ProjectInviteResourceExceptionalTests
|
||||
[TestCase(null, "something", "something", null)]
|
||||
public void ProjectInviteCreate_InvalidInput(string email, string role, string serverRole, string userId)
|
||||
{
|
||||
Assert.CatchAsync<SpeckleGraphQLException>(async () =>
|
||||
var ex = Assert.CatchAsync<AggregateException>(async () =>
|
||||
{
|
||||
var input = new ProjectInviteCreateInput(email, role, serverRole, userId);
|
||||
await Sut.Create(_project.id, input);
|
||||
});
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLException>());
|
||||
}
|
||||
}
|
||||
|
||||
+18
-9
@@ -38,7 +38,8 @@ public class ProjectResourceExceptionalTests
|
||||
ProjectCreateInput input =
|
||||
new("The best project", "The best description for the best project", ProjectVisibility.Private);
|
||||
|
||||
Assert.ThrowsAsync<SpeckleGraphQLForbiddenException>(async () => await _unauthedUser.Project.Create(input));
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(async () => _ = await _unauthedUser.Project.Create(input));
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLForbiddenException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -48,29 +49,33 @@ public class ProjectResourceExceptionalTests
|
||||
|
||||
Project privateStream = await Sut.Create(input);
|
||||
|
||||
Assert.ThrowsAsync<SpeckleGraphQLForbiddenException>(async () => await _unauthedUser.Project.Get(privateStream.id));
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(async () => _ = await _unauthedUser.Project.Get(privateStream.id));
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLForbiddenException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ProjectGet_NonExistentProject()
|
||||
{
|
||||
Assert.ThrowsAsync<SpeckleGraphQLStreamNotFoundException>(async () => await Sut.Get("NonExistentProject"));
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(async () => await Sut.Get("NonExistentProject"));
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLStreamNotFoundException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ProjectUpdate_NonExistentProject()
|
||||
{
|
||||
Assert.ThrowsAsync<SpeckleGraphQLForbiddenException>(
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(
|
||||
async () => _ = await Sut.Update(new("NonExistentProject", "My new name"))
|
||||
);
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLForbiddenException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ProjectUpdate_NoAuth()
|
||||
{
|
||||
Assert.ThrowsAsync<SpeckleGraphQLForbiddenException>(
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(
|
||||
async () => _ = await _unauthedUser.Project.Update(new(_testProject.id, "My new name"))
|
||||
);
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLForbiddenException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -82,7 +87,8 @@ public class ProjectResourceExceptionalTests
|
||||
{
|
||||
ProjectUpdateRoleInput input = new(_secondUser.Account.id, "NonExistentProject", newRole);
|
||||
|
||||
Assert.ThrowsAsync<SpeckleGraphQLForbiddenException>(async () => await Sut.UpdateRole(input));
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.UpdateRole(input));
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLForbiddenException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -93,7 +99,9 @@ public class ProjectResourceExceptionalTests
|
||||
public void ProjectUpdateRole_NonAuth(string newRole)
|
||||
{
|
||||
ProjectUpdateRoleInput input = new(_secondUser.Account.id, "NonExistentProject", newRole);
|
||||
Assert.ThrowsAsync<SpeckleGraphQLForbiddenException>(async () => await _unauthedUser.Project.UpdateRole(input));
|
||||
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(async () => _ = await _unauthedUser.Project.UpdateRole(input));
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLForbiddenException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -101,12 +109,13 @@ public class ProjectResourceExceptionalTests
|
||||
{
|
||||
await Sut.Delete(_testProject.id);
|
||||
|
||||
Assert.ThrowsAsync<SpeckleGraphQLStreamNotFoundException>(async () => _ = await Sut.Get(_testProject.id)); //TODO: Exception types
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.Get(_testProject.id));
|
||||
Assert.That(ex?.InnerExceptions, Has.One.Items.And.All.TypeOf<SpeckleGraphQLStreamNotFoundException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ProjectInvites_NoAuth()
|
||||
{
|
||||
Assert.ThrowsAsync<SpeckleGraphQLException>(async () => await Fixtures.Unauthed.ActiveUser.ProjectInvites());
|
||||
Assert.ThrowsAsync<SpeckleException>(async () => await Fixtures.Unauthed.ActiveUser.ProjectInvites());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ public class ProjectResourceTests
|
||||
Project toDelete = await Sut.Create(new("Delete me", null, null));
|
||||
await Sut.Delete(toDelete.id);
|
||||
|
||||
Assert.ThrowsAsync<SpeckleGraphQLStreamNotFoundException>(async () => _ = await Sut.Get(toDelete.id));
|
||||
var getEx = Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.Get(toDelete.id));
|
||||
Assert.That(getEx?.InnerExceptions, Has.Exactly(1).TypeOf<SpeckleGraphQLStreamNotFoundException>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,9 @@ public class VersionResourceTests
|
||||
|
||||
await Sut.Delete(input);
|
||||
|
||||
Assert.CatchAsync<SpeckleGraphQLException>(async () => await Sut.Get(_version.id, _project.id));
|
||||
Assert.CatchAsync<SpeckleGraphQLException>(async () => await Sut.Delete(input));
|
||||
var getEx = Assert.ThrowsAsync<AggregateException>(async () => await Sut.Get(_version.id, _project.id));
|
||||
Assert.That(getEx?.InnerExceptions, Has.Exactly(1).TypeOf<SpeckleGraphQLException>());
|
||||
var delEx = Assert.ThrowsAsync<AggregateException>(async () => await Sut.Delete(input));
|
||||
Assert.That(delEx?.InnerExceptions, Has.Exactly(1).TypeOf<SpeckleGraphQLException>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
using GraphQL;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Credentials;
|
||||
using Speckle.Sdk.Host;
|
||||
using Speckle.Sdk.Models;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Integration;
|
||||
|
||||
public class GraphQLClientTests : IDisposable
|
||||
{
|
||||
private Account _account;
|
||||
private Client _client;
|
||||
private IOperations _operations;
|
||||
|
||||
[SetUp]
|
||||
public async Task Setup()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(DataChunk).Assembly);
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
_account = await Fixtures.SeedUser();
|
||||
_client = serviceProvider.GetRequiredService<IClientFactory>().Create(_account);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ThrowsForbiddenException()
|
||||
{
|
||||
Assert.ThrowsAsync<SpeckleGraphQLForbiddenException>(
|
||||
async () =>
|
||||
await _client.ExecuteGraphQLRequest<Dictionary<string, object>>(
|
||||
new GraphQLRequest
|
||||
{
|
||||
Query =
|
||||
@"query {
|
||||
adminStreams{
|
||||
totalCount
|
||||
}
|
||||
}",
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Cancellation()
|
||||
{
|
||||
using CancellationTokenSource tokenSource = new();
|
||||
tokenSource.Cancel();
|
||||
Assert.CatchAsync<OperationCanceledException>(
|
||||
async () =>
|
||||
await _client.ExecuteGraphQLRequest<Dictionary<string, object>>(
|
||||
new GraphQLRequest
|
||||
{
|
||||
Query =
|
||||
@"query {
|
||||
adminStreams{
|
||||
totalCount
|
||||
}
|
||||
}",
|
||||
},
|
||||
tokenSource.Token
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_client?.Dispose();
|
||||
}
|
||||
}
|
||||
+1
-36
@@ -3,10 +3,8 @@ using GraphQL;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NUnit.Framework;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Credentials;
|
||||
using Speckle.Sdk.Host;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Api;
|
||||
|
||||
@@ -35,39 +33,6 @@ public sealed class GraphQLClientTests : IDisposable
|
||||
_client?.Dispose();
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> ErrorCases()
|
||||
{
|
||||
yield return new TestCaseData(typeof(SpeckleGraphQLForbiddenException), new Map { { "code", "FORBIDDEN" } });
|
||||
yield return new TestCaseData(typeof(SpeckleGraphQLForbiddenException), new Map { { "code", "UNAUTHENTICATED" } });
|
||||
yield return new TestCaseData(
|
||||
typeof(SpeckleGraphQLInternalErrorException),
|
||||
new Map { { "code", "INTERNAL_SERVER_ERROR" } }
|
||||
);
|
||||
yield return new TestCaseData(typeof(SpeckleGraphQLException<FakeGqlResponseModel>), new Map { { "foo", "bar" } });
|
||||
}
|
||||
|
||||
[Test, TestCaseSource(nameof(ErrorCases))]
|
||||
public void TestExceptionThrowingFromGraphQLErrors(Type exType, Map extensions)
|
||||
{
|
||||
Assert.Throws(
|
||||
exType,
|
||||
() =>
|
||||
_client.MaybeThrowFromGraphQLErrors(
|
||||
new GraphQLRequest(),
|
||||
new GraphQLResponse<FakeGqlResponseModel>
|
||||
{
|
||||
Errors = new GraphQLError[] { new() { Extensions = extensions } },
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMaybeThrowsDoesntThrowForNoErrors()
|
||||
{
|
||||
Assert.DoesNotThrow(() => _client.MaybeThrowFromGraphQLErrors(new GraphQLRequest(), new GraphQLResponse<string>()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExecuteWithResiliencePoliciesDoesntRetryTaskCancellation()
|
||||
{
|
||||
@@ -110,7 +75,7 @@ public sealed class GraphQLClientTests : IDisposable
|
||||
counter++;
|
||||
if (counter < maxRetryCount)
|
||||
{
|
||||
throw new SpeckleGraphQLInternalErrorException(new GraphQLRequest(), new GraphQLResponse<string>());
|
||||
throw new SpeckleGraphQLInternalErrorException();
|
||||
}
|
||||
|
||||
return Task.FromResult(expectedResult);
|
||||
@@ -0,0 +1,55 @@
|
||||
using GraphQL;
|
||||
using NUnit.Framework;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Api;
|
||||
|
||||
public class GraphQLErrorHandlerTests
|
||||
{
|
||||
private static IEnumerable<TestCaseData> ErrorCases()
|
||||
{
|
||||
yield return new TestCaseData(typeof(SpeckleGraphQLForbiddenException), new Map { { "code", "FORBIDDEN" } });
|
||||
yield return new TestCaseData(typeof(SpeckleGraphQLForbiddenException), new Map { { "code", "UNAUTHENTICATED" } });
|
||||
yield return new TestCaseData(
|
||||
typeof(SpeckleGraphQLInternalErrorException),
|
||||
new Map { { "code", "INTERNAL_SERVER_ERROR" } }
|
||||
);
|
||||
yield return new TestCaseData(
|
||||
typeof(SpeckleGraphQLStreamNotFoundException),
|
||||
new Map { { "code", "STREAM_NOT_FOUND" } }
|
||||
);
|
||||
yield return new TestCaseData(typeof(SpeckleGraphQLBadInputException), new Map { { "code", "BAD_USER_INPUT" } });
|
||||
yield return new TestCaseData(
|
||||
typeof(SpeckleGraphQLInvalidQueryException),
|
||||
new Map { { "code", "GRAPHQL_PARSE_FAILED" } }
|
||||
);
|
||||
yield return new TestCaseData(
|
||||
typeof(SpeckleGraphQLInvalidQueryException),
|
||||
new Map { { "code", "GRAPHQL_VALIDATION_FAILED" } }
|
||||
);
|
||||
yield return new TestCaseData(typeof(SpeckleGraphQLException), new Map { { "foo", "bar" } });
|
||||
yield return new TestCaseData(typeof(SpeckleGraphQLException), new Map { { "code", "CUSTOM_THING" } });
|
||||
}
|
||||
|
||||
[Test, TestCaseSource(nameof(ErrorCases))]
|
||||
public void TestExceptionThrowingFromGraphQLErrors(Type exType, Map extensions)
|
||||
{
|
||||
var ex = Assert.Throws<AggregateException>(
|
||||
() =>
|
||||
GraphQLErrorHandler.EnsureGraphQLSuccess(
|
||||
new GraphQLResponse<GraphQLClientTests.FakeGqlResponseModel>
|
||||
{
|
||||
Errors = new GraphQLError[] { new() { Extensions = extensions } },
|
||||
}
|
||||
)
|
||||
);
|
||||
Assert.That(ex?.InnerExceptions, Has.Exactly(1).TypeOf(exType));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMaybeThrowsDoesntThrowForNoErrors()
|
||||
{
|
||||
Assert.DoesNotThrow(() => GraphQLErrorHandler.EnsureGraphQLSuccess(new GraphQLResponse<string>()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user