diff --git a/src/Tgstation.Server.Client.GraphQL/AuthenticatedGraphQLServerClient.cs b/src/Tgstation.Server.Client.GraphQL/AuthenticatedGraphQLServerClient.cs
new file mode 100644
index 0000000000..8babf9765e
--- /dev/null
+++ b/src/Tgstation.Server.Client.GraphQL/AuthenticatedGraphQLServerClient.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+
+using Microsoft.Extensions.Logging;
+
+using StrawberryShake;
+
+using Tgstation.Server.Common.Extensions;
+
+namespace Tgstation.Server.Client.GraphQL
+{
+ ///
+ sealed class AuthenticatedGraphQLServerClient : GraphQLServerClient, IAuthenticatedGraphQLServerClient
+ {
+ ///
+ public ITransferClient TransferClient => restClient!.Transfer;
+
+ ///
+ /// A that takes a bearer token as input and outputs a that uses it.
+ ///
+ readonly Func? getRestClientForToken;
+
+ ///
+ /// The current .
+ ///
+ IRestServerClient? restClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The to use.
+ /// The to use.
+ /// The to use.
+ /// The value of .
+ public AuthenticatedGraphQLServerClient(
+ IGraphQLClient graphQLClient,
+ IAsyncDisposable serviceProvider,
+ ILogger logger,
+ IRestServerClient restClient)
+ : base(graphQLClient, serviceProvider, logger)
+ {
+ this.restClient = restClient ?? throw new ArgumentNullException(nameof(restClient));
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The to use.
+ /// The to use.
+ /// The to use.
+ /// The to call to set the async local for requests.
+ /// The basic to use for reauthentication.
+ /// The containing the initial JWT to use.
+ /// The value of .
+ public AuthenticatedGraphQLServerClient(
+ IGraphQLClient graphQLClient,
+ IAsyncDisposable serviceProvider,
+ ILogger logger,
+ Action setAuthenticationHeader,
+ AuthenticationHeaderValue? basicCredentialsHeader,
+ IOperationResult loginResult,
+ Func getRestClientForToken)
+ : base(
+ graphQLClient,
+ serviceProvider,
+ logger,
+ setAuthenticationHeader,
+ basicCredentialsHeader,
+ loginResult)
+ {
+ this.getRestClientForToken = getRestClientForToken ?? throw new ArgumentNullException(nameof(getRestClientForToken));
+ restClient = getRestClientForToken(loginResult.Data!.Login.Bearer!.EncodedToken);
+ }
+
+ ///
+ public sealed override ValueTask DisposeAsync()
+#pragma warning disable CA2012 // Use ValueTasks correctly
+ => ValueTaskExtensions.WhenAll(
+ base.DisposeAsync(),
+ restClient!.DisposeAsync());
+#pragma warning restore CA2012 // Use ValueTasks correctly
+
+ ///
+ protected sealed override async ValueTask CreateUpdatedAuthenticationHeader(string bearer)
+ {
+ var baseTask = base.CreateUpdatedAuthenticationHeader(bearer);
+ if (restClient != null)
+ await restClient.DisposeAsync().ConfigureAwait(false);
+
+ if (getRestClientForToken != null)
+ restClient = getRestClientForToken(bearer);
+
+ return await baseTask.ConfigureAwait(false);
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Client.GraphQL/AuthenticationException.cs b/src/Tgstation.Server.Client.GraphQL/AuthenticationException.cs
new file mode 100644
index 0000000000..d02134f6e3
--- /dev/null
+++ b/src/Tgstation.Server.Client.GraphQL/AuthenticationException.cs
@@ -0,0 +1,51 @@
+using System;
+
+namespace Tgstation.Server.Client.GraphQL
+{
+ ///
+ /// thrown when automatic authentication fails.
+ ///
+ public sealed class AuthenticationException : Exception
+ {
+ ///
+ /// The .
+ ///
+ public ILogin_Login_Errors_ErrorMessageError? ErrorMessage { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of .
+ public AuthenticationException(ILogin_Login_Errors_ErrorMessageError errorMessage)
+ : base(errorMessage?.Message)
+ {
+ ErrorMessage = errorMessage ?? throw new ArgumentNullException(nameof(errorMessage));
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AuthenticationException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ public AuthenticationException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ /// The .
+ public AuthenticationException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Client.GraphQL/AuthorizationMessageHandler.cs b/src/Tgstation.Server.Client.GraphQL/AuthorizationMessageHandler.cs
new file mode 100644
index 0000000000..949ac80f92
--- /dev/null
+++ b/src/Tgstation.Server.Client.GraphQL/AuthorizationMessageHandler.cs
@@ -0,0 +1,42 @@
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Tgstation.Server.Client.GraphQL
+{
+ ///
+ /// that applies the .
+ ///
+ sealed class AuthorizationMessageHandler : DelegatingHandler
+ {
+ ///
+ /// The to be applied.
+ ///
+ public static AsyncLocal Header { get; } = new AsyncLocal();
+
+ ///
+ /// override for .
+ ///
+ readonly AuthenticationHeaderValue? headerOverride;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of .
+ public AuthorizationMessageHandler(AuthenticationHeaderValue? headerOverride)
+ {
+ this.headerOverride = headerOverride;
+ }
+
+ ///
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ var currentAuthHeader = headerOverride ?? Header.Value;
+ if (currentAuthHeader != null)
+ request.Headers.Authorization = currentAuthHeader;
+
+ return base.SendAsync(request, cancellationToken);
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/OAuthInformation.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/OAuthInformation.graphql
new file mode 100644
index 0000000000..e32a409b60
--- /dev/null
+++ b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/OAuthInformation.graphql
@@ -0,0 +1,18 @@
+query OAuthInformation {
+ swarm {
+ currentNode {
+ gateway {
+ information {
+ oAuthProviderInfos {
+ key
+ value {
+ clientId
+ redirectUri
+ serverUrl
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ServerVersion.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ServerVersion.graphql
new file mode 100644
index 0000000000..45dbf4ebbc
--- /dev/null
+++ b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ServerVersion.graphql
@@ -0,0 +1,11 @@
+query ServerVersion {
+ swarm {
+ currentNode {
+ gateway {
+ information {
+ version
+ }
+ }
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Client.GraphQL/GraphQLServerClient.cs b/src/Tgstation.Server.Client.GraphQL/GraphQLServerClient.cs
index b359139b69..60004ce1fd 100644
--- a/src/Tgstation.Server.Client.GraphQL/GraphQLServerClient.cs
+++ b/src/Tgstation.Server.Client.GraphQL/GraphQLServerClient.cs
@@ -1,11 +1,35 @@
using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Net.Http.Headers;
+using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+using StrawberryShake;
+
+using Tgstation.Server.Api;
+
namespace Tgstation.Server.Client.GraphQL
{
///
class GraphQLServerClient : IGraphQLServerClient
{
+ ///
+ /// If the was initially authenticated.
+ ///
+ [MemberNotNullWhen(true, nameof(setAuthenticationHeader))]
+ [MemberNotNullWhen(true, nameof(bearerCredentialsTask))]
+ bool Authenticated => basicCredentialsHeader != null;
+
+ ///
+ /// If the supports reauthentication.
+ ///
+ [MemberNotNullWhen(true, nameof(bearerCredentialsHeaderTaskLock))]
+ [MemberNotNullWhen(true, nameof(basicCredentialsHeader))]
+ bool CanReauthenticate => basicCredentialsHeader != null;
+
///
/// The for the .
///
@@ -16,27 +40,232 @@ namespace Tgstation.Server.Client.GraphQL
///
readonly IAsyncDisposable serviceProvider;
+ ///
+ /// The for the .
+ ///
+ readonly ILogger logger;
+
+ ///
+ /// The which sets the for HTTP request in the current async context.
+ ///
+ readonly Action? setAuthenticationHeader;
+
+ ///
+ /// The containing the authenticated user's password credentials.
+ ///
+ readonly AuthenticationHeaderValue? basicCredentialsHeader;
+
+ ///
+ /// used to synchronize access to .
+ ///
+ readonly object? bearerCredentialsHeaderTaskLock;
+
+ ///
+ /// A resulting in a containing the current for the and the it expires.
+ ///
+ Task<(AuthenticationHeaderValue Header, DateTime Exp)?>? bearerCredentialsTask;
+
+ ///
+ /// Throws an for a login error that previously occured outside of the current call context.
+ ///
+ /// Always thrown.
+ [DoesNotReturn]
+ static void ThrowOtherCallerFailedAuthException()
+ => throw new AuthenticationException("Another caller failed to authenticate!");
+
+ ///
+ /// Checks if a given errored out with authentication errors.
+ ///
+ /// The .
+ /// if errored due to authentication issues, otherwise.
+ static bool IsAuthenticationError(IOperationResult operationResult)
+ => operationResult.Data == null
+ && operationResult.Errors.Any(
+ error => error.Extensions?.TryGetValue(
+ "code",
+ out object? codeExtension) == true
+ && codeExtension is string codeExtensionString
+ && codeExtensionString == "AUTH_NOT_AUTHENTICATED");
+
///
/// Initializes a new instance of the class.
///
/// The value of .
/// The value of .
+ /// The value of .
public GraphQLServerClient(
IGraphQLClient graphQLClient,
- IAsyncDisposable serviceProvider)
+ IAsyncDisposable serviceProvider,
+ ILogger logger)
{
this.graphQLClient = graphQLClient ?? throw new ArgumentNullException(nameof(graphQLClient));
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of .
+ /// The value of .
+ /// The value of .
+ /// The value of .
+ /// The value of .
+ /// The containing the initial JWT to use.
+ protected GraphQLServerClient(
+ IGraphQLClient graphQLClient,
+ IAsyncDisposable serviceProvider,
+ ILogger logger,
+ Action setAuthenticationHeader,
+ AuthenticationHeaderValue? basicCredentialsHeader,
+ IOperationResult loginResult)
+ : this(graphQLClient, serviceProvider, logger)
+ {
+ this.setAuthenticationHeader = setAuthenticationHeader ?? throw new ArgumentNullException(nameof(setAuthenticationHeader));
+ ArgumentNullException.ThrowIfNull(loginResult);
+ this.basicCredentialsHeader = basicCredentialsHeader;
+
+ var task = CreateCredentialsTuple(loginResult);
+ if (!task.IsCompleted)
+ throw new InvalidOperationException($"Expected {nameof(CreateCredentialsTuple)} to not await in constructor!");
+
+ bearerCredentialsTask = Task.FromResult<(AuthenticationHeaderValue Header, DateTime Exp)?>(task.Result);
+
+ if (Authenticated)
+ bearerCredentialsHeaderTaskLock = new object();
}
///
- public ValueTask DisposeAsync() => serviceProvider.DisposeAsync();
+ public virtual ValueTask DisposeAsync() => serviceProvider.DisposeAsync();
///
- public virtual ValueTask RunQuery(Func queryExector)
+ public ValueTask> RunOperationAsync(Func>> queryExector, CancellationToken cancellationToken)
+ where TResultData : class
{
ArgumentNullException.ThrowIfNull(queryExector);
- return queryExector(graphQLClient);
+ return WrapAuthentication(queryExector, cancellationToken);
+ }
+
+ ///
+ public ValueTask> RunOperation(Func>> queryExector, CancellationToken cancellationToken)
+ where TResultData : class
+ {
+ ArgumentNullException.ThrowIfNull(queryExector);
+ return WrapAuthentication(async localClient => await queryExector(localClient), cancellationToken);
+ }
+
+ ///
+ /// Create a from a given token.
+ ///
+ /// The .
+ /// A new .
+ protected virtual ValueTask CreateUpdatedAuthenticationHeader(string bearer)
+ => ValueTask.FromResult(
+ new AuthenticationHeaderValue(
+ ApiHeaders.BearerAuthenticationScheme,
+ bearer));
+
+ ///
+ /// Executes a given , potentially accounting for authentication issues.
+ ///
+ /// The of the 's .
+ /// A which executes a single query on a given and returns a resulting in the .
+ /// The for the operation.
+ /// A resulting in the .
+ async ValueTask> WrapAuthentication(Func>> operationExecutor, CancellationToken cancellationToken)
+ where TResultData : class
+ {
+ if (!Authenticated)
+ return await operationExecutor(graphQLClient).ConfigureAwait(false);
+
+ var tuple = await bearerCredentialsTask.ConfigureAwait(false);
+ if (!tuple.HasValue)
+ ThrowOtherCallerFailedAuthException();
+
+ async ValueTask Reauthenticate(AuthenticationHeaderValue currentToken, CancellationToken cancellationToken)
+ {
+ if (!CanReauthenticate)
+ throw new AuthenticationException("Authentication expired or invalid and cannot re-authenticate.");
+
+ TaskCompletionSource<(AuthenticationHeaderValue Header, DateTime Exp)?>? tcs = null;
+ do
+ {
+ var bearerCredentialsTaskLocal = bearerCredentialsTask;
+ if (!bearerCredentialsTaskLocal!.IsCompleted)
+ {
+ var currentTuple = await bearerCredentialsTaskLocal.ConfigureAwait(false);
+ if (!currentTuple.HasValue)
+ ThrowOtherCallerFailedAuthException();
+
+ return currentTuple.Value.Header;
+ }
+
+ lock (bearerCredentialsHeaderTaskLock!)
+ {
+ if (bearerCredentialsTask == bearerCredentialsTaskLocal)
+ {
+ var result = bearerCredentialsTaskLocal.Result;
+ if (result?.Header != currentToken)
+ {
+ if (!result.HasValue)
+ ThrowOtherCallerFailedAuthException();
+
+ return result.Value.Header;
+ }
+
+ tcs = new TaskCompletionSource<(AuthenticationHeaderValue, DateTime)?>();
+ bearerCredentialsTask = tcs.Task;
+ }
+ }
+ }
+ while (tcs == null);
+
+ setAuthenticationHeader!(basicCredentialsHeader!);
+ var loginResult = await graphQLClient.Login.ExecuteAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ var tuple = await CreateCredentialsTuple(loginResult).ConfigureAwait(false);
+ tcs.SetResult(tuple);
+ return tuple.Header;
+ }
+ catch (AuthenticationException)
+ {
+ tcs.SetResult(null);
+ throw;
+ }
+ }
+
+ var (currentAuthHeader, expires) = tuple.Value;
+ if (expires <= DateTimeOffset.UtcNow)
+ currentAuthHeader = await Reauthenticate(currentAuthHeader, cancellationToken).ConfigureAwait(false);
+
+ setAuthenticationHeader(currentAuthHeader);
+
+ var operationResult = await operationExecutor(graphQLClient);
+
+ if (IsAuthenticationError(operationResult))
+ {
+ currentAuthHeader = await Reauthenticate(currentAuthHeader, cancellationToken).ConfigureAwait(false);
+ setAuthenticationHeader(currentAuthHeader);
+ return await operationExecutor(graphQLClient);
+ }
+
+ return operationResult;
+ }
+
+ ///
+ /// Attempt to create the for .
+ ///
+ /// The to process.
+ /// A resulting in a new credentials .
+ /// Thrown if the errored.
+ async ValueTask<(AuthenticationHeaderValue Header, DateTime Exp)> CreateCredentialsTuple(IOperationResult loginResult)
+ {
+ var bearer = loginResult.EnsureSuccess(logger);
+
+ var header = await CreateUpdatedAuthenticationHeader(bearer.EncodedToken);
+
+ return (Header: header, Exp: bearer.ValidTo);
}
}
}
diff --git a/src/Tgstation.Server.Client.GraphQL/GraphQLServerClientFactory.cs b/src/Tgstation.Server.Client.GraphQL/GraphQLServerClientFactory.cs
index bbcd7f2f6e..50b602685d 100644
--- a/src/Tgstation.Server.Client.GraphQL/GraphQLServerClientFactory.cs
+++ b/src/Tgstation.Server.Client.GraphQL/GraphQLServerClientFactory.cs
@@ -1,11 +1,18 @@
using System;
+using System.Net.Http.Headers;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+using StrawberryShake;
using Tgstation.Server.Api;
+using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Client.GraphQL.Serializers;
+using Tgstation.Server.Common.Extensions;
namespace Tgstation.Server.Client.GraphQL
{
@@ -17,6 +24,45 @@ namespace Tgstation.Server.Client.GraphQL
///
readonly IRestServerClientFactory restClientFactory;
+ ///
+ /// Sets up a for providing the .
+ ///
+ /// The of the target tgstation-server.
+ /// If the should be configured.
+ /// The override for the .
+ /// The , if any.
+ /// A new .
+ static ServiceProvider SetupServiceProvider(Uri host, bool addAuthorizationHandler, AuthenticationHeaderValue? headerOverride = null, OAuthProvider? oAuthProvider = null)
+ {
+ var serviceCollection = new ServiceCollection();
+
+ var clientBuilder = serviceCollection
+ .AddGraphQLClient();
+ var graphQLEndpoint = new Uri(host, Routes.GraphQL);
+
+ clientBuilder.ConfigureHttpClient(
+ client =>
+ {
+ client.BaseAddress = graphQLEndpoint;
+ client.DefaultRequestHeaders.Add(ApiHeaders.ApiVersionHeader, $"Tgstation.Server.Api/{ApiHeaders.Version.Semver()}");
+ if (oAuthProvider.HasValue)
+ {
+ client.DefaultRequestHeaders.Add(ApiHeaders.OAuthProviderHeader, oAuthProvider.ToString());
+ }
+ },
+ clientBuilder =>
+ {
+ if (addAuthorizationHandler)
+ clientBuilder.AddHttpMessageHandler(() => new AuthorizationMessageHandler(headerOverride));
+ });
+
+ serviceCollection.AddSerializer();
+ serviceCollection.AddSerializer();
+ serviceCollection.AddSerializer();
+
+ return serviceCollection.BuildServiceProvider();
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -29,39 +75,138 @@ namespace Tgstation.Server.Client.GraphQL
///
public ValueTask CreateFromLogin(Uri host, string username, string password, bool attemptLoginRefresh = true, CancellationToken cancellationToken = default)
{
- throw new NotImplementedException();
+ var basicCredentials = new AuthenticationHeaderValue(
+ ApiHeaders.BasicAuthenticationScheme,
+ Convert.ToBase64String(
+ Encoding.UTF8.GetBytes($"{username}:{password}")));
+
+ return CreateWithAuthCall(
+ host,
+ basicCredentials,
+ null,
+ attemptLoginRefresh,
+ cancellationToken);
}
///
public ValueTask CreateFromOAuth(Uri host, string oAuthCode, OAuthProvider oAuthProvider, CancellationToken cancellationToken = default)
{
- throw new NotImplementedException();
+ var oAuthCredentials = new AuthenticationHeaderValue(
+ ApiHeaders.OAuthAuthenticationScheme,
+ oAuthCode);
+
+ return CreateWithAuthCall(
+ host,
+ oAuthCredentials,
+ oAuthProvider,
+ false,
+ cancellationToken);
}
///
public IAuthenticatedGraphQLServerClient CreateFromToken(Uri host, string token)
{
- throw new NotImplementedException();
+ var authenticationHeader = new AuthenticationHeaderValue(
+ ApiHeaders.BearerAuthenticationScheme,
+ token);
+
+ var serviceProvider = SetupServiceProvider(
+ host,
+ true,
+ authenticationHeader);
+
+ return new AuthenticatedGraphQLServerClient(
+ serviceProvider.GetRequiredService(),
+ serviceProvider,
+ serviceProvider.GetRequiredService>(),
+ CreateAuthenticatedTransferClient(host, token));
}
///
public IGraphQLServerClient CreateUnauthenticated(Uri host)
{
- var serviceCollection = new ServiceCollection();
-
- var clientBuilder = serviceCollection
- .AddGraphQLClient();
- var graphQLEndpoint = new Uri(host, Routes.GraphQL);
- clientBuilder.ConfigureHttpClient(client => client.BaseAddress = graphQLEndpoint);
-
- serviceCollection.AddSerializer();
- serviceCollection.AddSerializer();
-
- var serviceProvider = serviceCollection.BuildServiceProvider();
+ var serviceProvider = SetupServiceProvider(host, false);
return new GraphQLServerClient(
serviceProvider.GetRequiredService(),
- serviceProvider);
+ serviceProvider,
+ serviceProvider.GetRequiredService>());
+ }
+
+ ///
+ /// Create an from a remote login call.
+ ///
+ /// The URL to access TGS.
+ /// The initial to use to login.
+ /// The , if any.
+ /// If the client should attempt to renew its sessions with the .
+ /// Optional for the operation.
+ /// A resulting in a new .
+ /// Thrown when authentication fails.
+ async ValueTask CreateWithAuthCall(
+ Uri host,
+ AuthenticationHeaderValue initialCredentials,
+ OAuthProvider? oAuthProvider,
+ bool attemptLoginRefresh,
+ CancellationToken cancellationToken)
+ {
+ var serviceProvider = SetupServiceProvider(
+ host,
+ true,
+ oAuthProvider: oAuthProvider);
+ try
+ {
+ var client = serviceProvider.GetRequiredService();
+
+ IOperationResult result;
+
+ var previousAuthHeader = AuthorizationMessageHandler.Header.Value;
+ AuthorizationMessageHandler.Header.Value = initialCredentials;
+ try
+ {
+ result = await client.Login.ExecuteAsync(cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ AuthorizationMessageHandler.Header.Value = previousAuthHeader;
+ }
+
+ var serverClient = new AuthenticatedGraphQLServerClient(
+ client,
+ serviceProvider,
+ serviceProvider.GetRequiredService>(),
+ newHeader => AuthorizationMessageHandler.Header.Value = newHeader,
+ attemptLoginRefresh ? initialCredentials : null,
+ result,
+ bearer => CreateAuthenticatedTransferClient(host, bearer));
+
+ await Task.Yield();
+
+ return serverClient;
+ }
+ catch
+ {
+ await serviceProvider.DisposeAsync().ConfigureAwait(false);
+ throw;
+ }
+ }
+
+ ///
+ /// Create a for a given and token.
+ ///
+ /// The URL to access TGS.
+ /// The bearer token to access the API with.
+ /// A new .
+ IRestServerClient CreateAuthenticatedTransferClient(Uri host, string bearer)
+ {
+ var restClient = restClientFactory.CreateFromToken(
+ host,
+ new TokenResponse
+ {
+ Bearer = bearer,
+ });
+
+ return restClient;
}
}
}
diff --git a/src/Tgstation.Server.Client.GraphQL/IGraphQLServerClient.cs b/src/Tgstation.Server.Client.GraphQL/IGraphQLServerClient.cs
index 8c6d2ff2fe..c20a606fa0 100644
--- a/src/Tgstation.Server.Client.GraphQL/IGraphQLServerClient.cs
+++ b/src/Tgstation.Server.Client.GraphQL/IGraphQLServerClient.cs
@@ -1,6 +1,9 @@
using System;
+using System.Threading;
using System.Threading.Tasks;
+using StrawberryShake;
+
namespace Tgstation.Server.Client.GraphQL
{
///
@@ -9,10 +12,25 @@ namespace Tgstation.Server.Client.GraphQL
public interface IGraphQLServerClient : IAsyncDisposable
{
///
- /// Runs a given . It may be invoked multiple times depending on the behavior of the .
+ /// Runs a given . It may be invoked multiple times depending on the behavior of the if reauthentication is required.
///
- /// A which executes a single query on a given and returns a representing the running operation.
- /// A representing the running operation.
- ValueTask RunQuery(Func queryExector);
+ /// The of the 's .
+ /// A which executes a single query on a given and returns a resulting in the .
+ /// The for the operation.
+ /// A resulting in the .
+ /// Thrown when automatic reauthentication fails.
+ ValueTask> RunOperationAsync(Func>> operationExecutor, CancellationToken cancellationToken)
+ where TResultData : class;
+
+ ///
+ /// Runs a given . It may be invoked multiple times depending on the behavior of the if reauthentication is required.
+ ///
+ /// The of the 's .
+ /// A which executes a single query on a given and returns a resulting in the .
+ /// The for the operation.
+ /// A resulting in the .
+ /// Thrown when automatic reauthentication fails.
+ ValueTask> RunOperation(Func>> operationExecutor, CancellationToken cancellationToken)
+ where TResultData : class;
}
}
diff --git a/src/Tgstation.Server.Client.GraphQL/IGraphQLServerClientFactory.cs b/src/Tgstation.Server.Client.GraphQL/IGraphQLServerClientFactory.cs
index 06faa66adb..5ace7c2f8a 100644
--- a/src/Tgstation.Server.Client.GraphQL/IGraphQLServerClientFactory.cs
+++ b/src/Tgstation.Server.Client.GraphQL/IGraphQLServerClientFactory.cs
@@ -2,8 +2,6 @@
using System.Threading;
using System.Threading.Tasks;
-using Tgstation.Server.Api.Models.Response;
-
namespace Tgstation.Server.Client.GraphQL
{
///
@@ -24,9 +22,10 @@ namespace Tgstation.Server.Client.GraphQL
/// The URL to access TGS.
/// The username to for the .
/// The password for the .
- /// Attempt to refresh the received when it expires or becomes invalid. and will be stored in memory if this is .
+ /// Attempt to refresh the received bearer token when it expires or becomes invalid. and will be stored in memory if this is .
/// Optional for the operation.
/// A resulting in a new .
+ /// Thrown when authentication fails.
ValueTask CreateFromLogin(
Uri host,
string username,
@@ -42,6 +41,7 @@ namespace Tgstation.Server.Client.GraphQL
/// The .
/// Optional for the operation.
/// A resulting in a new .
+ /// Thrown when authentication fails.
ValueTask CreateFromOAuth(
Uri host,
string oAuthCode,
@@ -52,7 +52,7 @@ namespace Tgstation.Server.Client.GraphQL
/// Create a .
///
/// The URL to access TGS.
- /// The to access the API with.
+ /// The bearer token to access the API with.
/// A new .
IAuthenticatedGraphQLServerClient CreateFromToken(
Uri host,
diff --git a/src/Tgstation.Server.Client.GraphQL/LoginResultExtensions.cs b/src/Tgstation.Server.Client.GraphQL/LoginResultExtensions.cs
new file mode 100644
index 0000000000..5ec5360815
--- /dev/null
+++ b/src/Tgstation.Server.Client.GraphQL/LoginResultExtensions.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Linq;
+
+using Microsoft.Extensions.Logging;
+using Microsoft.IdentityModel.JsonWebTokens;
+using StrawberryShake;
+
+namespace Tgstation.Server.Client.GraphQL
+{
+ ///
+ /// Extensions for .
+ ///
+ static class LoginResultExtensions
+ {
+ ///
+ /// Check a given for errors.
+ ///
+ /// The containing the .
+ /// The to write to.
+ /// The from the successful .
+ /// Thrown when the is errored.
+ public static JsonWebToken EnsureSuccess(this IOperationResult loginResult, ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(loginResult);
+
+ try
+ {
+ loginResult.EnsureNoErrors();
+ }
+ catch (GraphQLClientException ex)
+ {
+ throw new AuthenticationException("Login attempt errored at the GraphQL level!", ex);
+ }
+
+ var data = loginResult.Data!.Login;
+ var errors = data.Errors;
+ if (errors != null)
+ {
+ foreach (var error in errors)
+ {
+ if (error is ILogin_Login_Errors_ErrorMessageError errorMessageError)
+ logger.LogError(
+ "Authentication error ({code}): {message}{additionalData}",
+ errorMessageError.ErrorCode?.ToString() ?? "No Code",
+ errorMessageError.Message,
+ errorMessageError.AdditionalData != null
+ ? $"{Environment.NewLine}{errorMessageError.AdditionalData}"
+ : String.Empty);
+ else
+ logger.LogError(
+ "Unknown authentication error: {error}",
+ error);
+ }
+ }
+
+ var bearer = data.Bearer;
+ if (bearer == null)
+ {
+ if (errors != null)
+ {
+ var errorMessage = errors.OfType().FirstOrDefault();
+ if (errorMessage != null)
+ throw new AuthenticationException(errorMessage);
+
+ throw new AuthenticationException($"Null bearer field and {errors.Count} non-ErrorMessage errors:{(errors.Count > 0 ? $"{Environment.NewLine}\t- {String.Join($"{Environment.NewLine}\t- ", errors)}" : String.Empty)}");
+ }
+
+ throw new AuthenticationException($"Null bearer and error fields!");
+ }
+
+ return bearer;
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Client.GraphQL/Serializers/JwtSerializer.cs b/src/Tgstation.Server.Client.GraphQL/Serializers/JwtSerializer.cs
new file mode 100644
index 0000000000..41a0a960a2
--- /dev/null
+++ b/src/Tgstation.Server.Client.GraphQL/Serializers/JwtSerializer.cs
@@ -0,0 +1,35 @@
+using System;
+
+using Microsoft.IdentityModel.JsonWebTokens;
+
+using StrawberryShake.Serialization;
+
+#pragma warning disable CA1812 // not detecting usage via annotation in schema.extensions.graphql
+
+namespace Tgstation.Server.Client.GraphQL.Serializers
+{
+ ///
+ /// for s.
+ ///
+ sealed class JwtSerializer : ScalarSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public JwtSerializer()
+ : base("Jwt")
+ {
+ }
+
+ ///
+ public override JsonWebToken Parse(string serializedValue)
+ => new(serializedValue ?? throw new ArgumentNullException(nameof(serializedValue)));
+
+ ///
+ protected override string Format(JsonWebToken runtimeValue)
+ {
+ ArgumentNullException.ThrowIfNull(runtimeValue);
+ return runtimeValue.EncodedToken;
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Client.GraphQL/Serializers/SemverSerializer.cs b/src/Tgstation.Server.Client.GraphQL/Serializers/SemverSerializer.cs
index cd7c6f378e..35fea8f509 100644
--- a/src/Tgstation.Server.Client.GraphQL/Serializers/SemverSerializer.cs
+++ b/src/Tgstation.Server.Client.GraphQL/Serializers/SemverSerializer.cs
@@ -4,12 +4,12 @@ using StrawberryShake.Serialization;
using Tgstation.Server.Common.Extensions;
-#pragma warning disable CA1812 // not detecting service provider usage
+#pragma warning disable CA1812 // not detecting usage via annotation in schema.extensions.graphql
namespace Tgstation.Server.Client.GraphQL.Serializers
{
///
- /// for s.
+ /// for s.
///
sealed class SemverSerializer : ScalarSerializer
{
diff --git a/src/Tgstation.Server.Client.GraphQL/Serializers/UnsignedIntSerializer.cs b/src/Tgstation.Server.Client.GraphQL/Serializers/UnsignedIntSerializer.cs
index 8656438b11..fdc610d07c 100644
--- a/src/Tgstation.Server.Client.GraphQL/Serializers/UnsignedIntSerializer.cs
+++ b/src/Tgstation.Server.Client.GraphQL/Serializers/UnsignedIntSerializer.cs
@@ -2,7 +2,7 @@
using StrawberryShake.Serialization;
-#pragma warning disable CA1812 // not detecting service provider usage
+#pragma warning disable CA1812 // not detecting usage via annotation in schema.extensions.graphql
namespace Tgstation.Server.Client.GraphQL.Serializers
{
diff --git a/src/Tgstation.Server.Client.GraphQL/Tgstation.Server.Client.GraphQL.csproj b/src/Tgstation.Server.Client.GraphQL/Tgstation.Server.Client.GraphQL.csproj
index 3299c2b4d9..4c1e5d1557 100644
--- a/src/Tgstation.Server.Client.GraphQL/Tgstation.Server.Client.GraphQL.csproj
+++ b/src/Tgstation.Server.Client.GraphQL/Tgstation.Server.Client.GraphQL.csproj
@@ -4,10 +4,12 @@
$(TgsFrameworkVersion)
$(TgsApiVersion)
+ enable
+
diff --git a/src/Tgstation.Server.Client.GraphQL/schema.extensions.graphql b/src/Tgstation.Server.Client.GraphQL/schema.extensions.graphql
index 023788fd33..8991e1fa70 100644
--- a/src/Tgstation.Server.Client.GraphQL/schema.extensions.graphql
+++ b/src/Tgstation.Server.Client.GraphQL/schema.extensions.graphql
@@ -15,3 +15,4 @@ extend schema @key(fields: "id")
extend scalar UnsignedInt @serializationType(name: "global::System.UInt32") @runtimeType(name: "global::System.UInt32")
extend scalar Semver @serializationType(name: "global::System.String") @runtimeType(name: "global::System.Version")
+extend scalar Jwt @serializationType(name: "global::System.String") @runtimeType(name: "global::Microsoft.IdentityModel.JsonWebTokens.JsonWebToken")
diff --git a/src/Tgstation.Server.Host/Authority/UserAuthority.cs b/src/Tgstation.Server.Host/Authority/UserAuthority.cs
index 40b48be96d..ed7dba0997 100644
--- a/src/Tgstation.Server.Host/Authority/UserAuthority.cs
+++ b/src/Tgstation.Server.Host/Authority/UserAuthority.cs
@@ -273,7 +273,11 @@ namespace Tgstation.Server.Host.Authority
}
///
+#pragma warning disable CA1502
+#pragma warning disable CA1506 // TODO: Decomplexify
public async ValueTask> Update(UserUpdateRequest model, CancellationToken cancellationToken)
+#pragma warning restore CA1502
+#pragma warning restore CA1506
{
ArgumentNullException.ThrowIfNull(model);
diff --git a/src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/LoginPayload.cs b/src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/LoginPayload.cs
index 5a055b6fcb..8d5cd5e683 100644
--- a/src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/LoginPayload.cs
+++ b/src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/LoginPayload.cs
@@ -1,5 +1,6 @@
using HotChocolate;
using Tgstation.Server.Api.Models.Response;
+using Tgstation.Server.Host.GraphQL.Types.Scalars;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.GraphQL.Mutations.Payloads
@@ -12,6 +13,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations.Payloads
///
/// The JSON Web Token (JWT) to use as a Bearer token for accessing the server. Contains an expiry time.
///
+ [GraphQLType]
public required string Bearer { get; init; }
///
diff --git a/src/Tgstation.Server.Host/GraphQL/Types/Scalars/JwtType.cs b/src/Tgstation.Server.Host/GraphQL/Types/Scalars/JwtType.cs
new file mode 100644
index 0000000000..47f10ccde3
--- /dev/null
+++ b/src/Tgstation.Server.Host/GraphQL/Types/Scalars/JwtType.cs
@@ -0,0 +1,38 @@
+using System;
+
+using HotChocolate.Language;
+using HotChocolate.Types;
+
+namespace Tgstation.Server.Host.GraphQL.Types.Scalars
+{
+ ///
+ /// A for encoded JSON Web Tokens.
+ ///
+ public sealed class JwtType : ScalarType
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public JwtType()
+ : base("Jwt")
+ {
+ Description = "Represents an encoded JSON Web Token";
+ SpecifiedBy = new Uri("https://datatracker.ietf.org/doc/html/rfc7519");
+ }
+
+ ///
+ public override IValueNode ParseResult(object? resultValue)
+ => ParseValue(resultValue);
+
+ ///
+ protected override string ParseLiteral(StringValueNode valueSyntax)
+ {
+ ArgumentNullException.ThrowIfNull(valueSyntax);
+ return valueSyntax.Value;
+ }
+
+ ///
+ protected override StringValueNode ParseValue(string runtimeValue)
+ => new(runtimeValue);
+ }
+}
diff --git a/src/Tgstation.Server.Host/GraphQL/Types/Scalars/SemverType.cs b/src/Tgstation.Server.Host/GraphQL/Types/Scalars/SemverType.cs
index bd07aad71f..dce31b4751 100644
--- a/src/Tgstation.Server.Host/GraphQL/Types/Scalars/SemverType.cs
+++ b/src/Tgstation.Server.Host/GraphQL/Types/Scalars/SemverType.cs
@@ -62,7 +62,7 @@ namespace Tgstation.Server.Host.GraphQL.Types.Scalars
///
protected override StringValueNode ParseValue(Version runtimeValue)
- => new StringValueNode(runtimeValue.Semver().ToString());
+ => new(runtimeValue.Semver().ToString());
///
protected override bool IsInstanceOfType(StringValueNode valueSyntax)
diff --git a/tests/Tgstation.Server.Tests/Live/MultiServerClient.cs b/tests/Tgstation.Server.Tests/Live/MultiServerClient.cs
index 801f4451bd..3fd2b4b8f6 100644
--- a/tests/Tgstation.Server.Tests/Live/MultiServerClient.cs
+++ b/tests/Tgstation.Server.Tests/Live/MultiServerClient.cs
@@ -1,48 +1,57 @@
using System;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using StrawberryShake;
+
using Tgstation.Server.Client;
using Tgstation.Server.Client.GraphQL;
+using Tgstation.Server.Common.Extensions;
namespace Tgstation.Server.Tests.Live
{
- sealed class MultiServerClient
+ sealed class MultiServerClient : IAsyncDisposable
{
- readonly IRestServerClient restServerClient;
- readonly IGraphQLServerClient graphQLServerClient;
-
- readonly bool useGraphQL;
+ public IRestServerClient RestClient { get; }
+ public IGraphQLServerClient GraphQLClient { get; }
public MultiServerClient(IRestServerClient restServerClient, IGraphQLServerClient graphQLServerClient)
{
- this.restServerClient = restServerClient ?? throw new ArgumentNullException(nameof(restServerClient));
- this.graphQLServerClient = graphQLServerClient ?? throw new ArgumentNullException(nameof(graphQLServerClient));
- this.useGraphQL = Boolean.TryParse(Environment.GetEnvironmentVariable("TGS_TEST_GRAPHQL"), out var result) && result;
+ RestClient = restServerClient ?? throw new ArgumentNullException(nameof(restServerClient));
+ GraphQLClient = graphQLServerClient ?? throw new ArgumentNullException(nameof(graphQLServerClient));
}
+ public static bool UseGraphQL => Boolean.TryParse(Environment.GetEnvironmentVariable("TGS_TEST_GRAPHQL"), out var result) && result;
+
+ public ValueTask DisposeAsync()
+ => ValueTaskExtensions.WhenAll(
+ RestClient.DisposeAsync(),
+ GraphQLClient.DisposeAsync());
+
public ValueTask Execute(
Func restAction,
- Func graphQLAction)
+ Func graphQLAction)
{
- if (useGraphQL)
- return graphQLServerClient.RunQuery(graphQLAction);
+ if (UseGraphQL)
+ return graphQLAction(GraphQLClient);
- return restAction(restServerClient);
+ return restAction(RestClient);
}
public async ValueTask ExecuteReadOnlyConfirmEquivalence(
Func> restAction,
- Func> graphQLAction,
- Func comparison)
+ Func>> graphQLAction,
+ Func comparison,
+ CancellationToken cancellationToken)
+ where TGraphQLResult : class
{
- var restTask = restAction(this.restServerClient);
- TGraphQLResult graphQLResult = default;
- await this.graphQLServerClient.RunQuery(async gqlClient => graphQLResult = await graphQLAction(gqlClient));
+ var restTask = restAction(RestClient);
+ var graphQLResult = await GraphQLClient.RunOperation(graphQLAction, cancellationToken);
var restResult = await restTask;
- Assert.IsTrue(comparison(restResult, graphQLResult), "REST/GraphQL results differ!");
+ Assert.IsTrue(comparison(restResult, graphQLResult.Data), "REST/GraphQL results differ!");
}
}
}
diff --git a/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs b/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs
index bac0433d4f..240a89c245 100644
--- a/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs
+++ b/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs
@@ -463,11 +463,7 @@ namespace Tgstation.Server.Tests.Live
static async Task TestGraphQLLogin(IRestServerClientFactory clientFactory, IRestServerClient restClient, CancellationToken cancellationToken)
{
await using var gqlClient = new GraphQLServerClientFactory(clientFactory).CreateUnauthenticated(restClient.Url);
- IOperationResult result = null;
- await gqlClient.RunQuery(async client =>
- {
- result = await client.Login.ExecuteAsync(cancellationToken);
- });
+ var result = await gqlClient.RunOperation(client => client.Login.ExecuteAsync(cancellationToken), cancellationToken);
Assert.IsNotNull(result.Data);
Assert.IsNull(result.Data.Login.Bearer);
diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs
index 8576950da5..e47fd8c822 100644
--- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs
+++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@@ -27,6 +27,8 @@ using Newtonsoft.Json;
using Npgsql;
+using StrawberryShake;
+
using Tgstation.Server.Api;
using Tgstation.Server.Api.Extensions;
using Tgstation.Server.Api.Models;
@@ -73,7 +75,14 @@ namespace Tgstation.Server.Tests.Live
_ = mainDMPort.Value;
}
- readonly RestServerClientFactory clientFactory = new (new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString()));
+ readonly RestServerClientFactory restClientFactory;
+ readonly GraphQLServerClientFactory graphQLClientFactory;
+
+ public TestLiveServer()
+ {
+ restClientFactory = new(new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString()));
+ graphQLClientFactory = new GraphQLServerClientFactory(restClientFactory);
+ }
public static List GetEngineServerProcessesOnPort(EngineType engineType, ushort? port)
{
@@ -295,7 +304,7 @@ namespace Tgstation.Server.Tests.Live
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version);
- request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.OAuthAuthenticationScheme, adminClient.Token.Bearer);
+ request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.OAuthAuthenticationScheme, adminClient.RestClient.Token.Bearer);
request.Headers.Add(ApiHeaders.OAuthProviderHeader, Api.Models.OAuthProvider.GitHub.ToString());
using var response = await httpClient.SendAsync(request, cancellationToken);
Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
@@ -306,7 +315,7 @@ namespace Tgstation.Server.Tests.Live
//attempt to update to stable
var responseModel = await TestWithoutAndWithPermission(
- () => adminClient.Administration.Update(
+ () => adminClient.RestClient.Administration.Update(
new ServerUpdateRequest
{
NewVersion = TestUpdateVersion,
@@ -314,7 +323,7 @@ namespace Tgstation.Server.Tests.Live
},
null,
cancellationToken),
- adminClient,
+ adminClient.RestClient,
AdministrationRights.ChangeVersion);
Assert.IsNotNull(responseModel);
@@ -323,7 +332,7 @@ namespace Tgstation.Server.Tests.Live
try
{
- var serverInfoTask = adminClient.ServerInformation(cancellationToken).AsTask();
+ var serverInfoTask = adminClient.RestClient.ServerInformation(cancellationToken).AsTask();
var completedTask = await Task.WhenAny(serverTask, serverInfoTask);
if (completedTask == serverInfoTask)
{
@@ -376,7 +385,7 @@ namespace Tgstation.Server.Tests.Live
var downloadStream = await download.GetResult(cancellationToken);
var responseModel = await TestWithoutAndWithPermission(
- () => adminClient.Administration.Update(
+ () => adminClient.RestClient.Administration.Update(
new ServerUpdateRequest
{
NewVersion = TestUpdateVersion,
@@ -384,7 +393,7 @@ namespace Tgstation.Server.Tests.Live
},
downloadStream,
cancellationToken),
- adminClient,
+ adminClient.RestClient,
AdministrationRights.UploadVersion);
Assert.IsNotNull(responseModel);
@@ -424,7 +433,7 @@ namespace Tgstation.Server.Tests.Live
var testUpdateVersion = new Version(5, 11, 20);
await using var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken);
await ApiAssert.ThrowsException(
- () => adminClient.Administration.Update(
+ () => adminClient.RestClient.Administration.Update(
new ServerUpdateRequest
{
NewVersion = testUpdateVersion
@@ -474,7 +483,7 @@ namespace Tgstation.Server.Tests.Live
{
await using var controllerClient = await CreateAdminClient(controller.ApiUrl, cancellationToken);
- var controllerInfo = await controllerClient.ServerInformation(cancellationToken);
+ var controllerInfo = await controllerClient.RestClient.ServerInformation(cancellationToken);
static void CheckInfo(ServerInformationResponse serverInformation)
{
@@ -489,7 +498,7 @@ namespace Tgstation.Server.Tests.Live
CheckInfo(controllerInfo);
// test update
- var responseModel = await controllerClient.Administration.Update(
+ var responseModel = await controllerClient.RestClient.Administration.Update(
new ServerUpdateRequest
{
NewVersion = TestUpdateVersion
@@ -577,7 +586,7 @@ namespace Tgstation.Server.Tests.Live
await using var node1Client = await CreateAdminClient(node1.ApiUrl, cancellationToken);
await using var node2Client = await CreateAdminClient(node2.ApiUrl, cancellationToken);
- var controllerInfo = await controllerClient.ServerInformation(cancellationToken);
+ var controllerInfo = await controllerClient.RestClient.ServerInformation(cancellationToken);
async Task WaitForSwarmServerUpdate()
{
@@ -585,7 +594,7 @@ namespace Tgstation.Server.Tests.Live
do
{
await Task.Delay(TimeSpan.FromSeconds(10));
- serverInformation = await node1Client.ServerInformation(cancellationToken);
+ serverInformation = await node1Client.RestClient.ServerInformation(cancellationToken);
}
while (serverInformation.SwarmServers.Count == 1);
}
@@ -618,13 +627,13 @@ namespace Tgstation.Server.Tests.Live
WaitForSwarmServerUpdate(),
Task.Delay(TimeSpan.FromMinutes(4), cancellationToken));
- var node2Info = await node2Client.ServerInformation(cancellationToken);
- var node1Info = await node1Client.ServerInformation(cancellationToken);
+ var node2Info = await node2Client.RestClient.ServerInformation(cancellationToken);
+ var node1Info = await node1Client.RestClient.ServerInformation(cancellationToken);
CheckInfo(node1Info);
CheckInfo(node2Info);
// check user info is shared
- var newUser = await node2Client.Users.Create(new UserCreateRequest
+ var newUser = await node2Client.RestClient.Users.Create(new UserCreateRequest
{
Name = "asdf",
Password = "asdfasdfasdfasdf",
@@ -635,20 +644,20 @@ namespace Tgstation.Server.Tests.Live
}
}, cancellationToken);
- var node1User = await node1Client.Users.GetId(newUser, cancellationToken);
+ var node1User = await node1Client.RestClient.Users.GetId(newUser, cancellationToken);
Assert.AreEqual(newUser.Name, node1User.Name);
Assert.AreEqual(newUser.Enabled, node1User.Enabled);
- await using var controllerUserClient = await clientFactory.CreateFromLogin(
+ await using var controllerUserClient = await restClientFactory.CreateFromLogin(
controllerAddress,
newUser.Name,
"asdfasdfasdfasdf");
- await using var node1TokenCopiedClient = clientFactory.CreateFromToken(node1.RootUrl, controllerUserClient.Token);
+ await using var node1TokenCopiedClient = restClientFactory.CreateFromToken(node1.RootUrl, controllerUserClient.Token);
await node1TokenCopiedClient.Administration.Read(false, cancellationToken);
// check instance info is not shared
- var controllerInstance = await controllerClient.Instances.CreateOrAttach(
+ var controllerInstance = await controllerClient.RestClient.Instances.CreateOrAttach(
new InstanceCreateRequest
{
Name = "ControllerInstance",
@@ -656,27 +665,27 @@ namespace Tgstation.Server.Tests.Live
},
cancellationToken);
- var node2Instance = await node2Client.Instances.CreateOrAttach(
+ var node2Instance = await node2Client.RestClient.Instances.CreateOrAttach(
new InstanceCreateRequest
{
Name = "Node2Instance",
Path = Path.Combine(node2.Directory, "Node2Instance")
},
cancellationToken);
- var node2InstanceList = await node2Client.Instances.List(null, cancellationToken);
+ var node2InstanceList = await node2Client.RestClient.Instances.List(null, cancellationToken);
Assert.AreEqual(1, node2InstanceList.Count);
Assert.AreEqual(node2Instance.Id, node2InstanceList[0].Id);
- Assert.IsNotNull(await node2Client.Instances.GetId(node2Instance, cancellationToken));
- var controllerInstanceList = await controllerClient.Instances.List(null, cancellationToken);
+ Assert.IsNotNull(await node2Client.RestClient.Instances.GetId(node2Instance, cancellationToken));
+ var controllerInstanceList = await controllerClient.RestClient.Instances.List(null, cancellationToken);
Assert.AreEqual(1, controllerInstanceList.Count);
Assert.AreEqual(controllerInstance.Id, controllerInstanceList[0].Id);
- Assert.IsNotNull(await controllerClient.Instances.GetId(controllerInstance, cancellationToken));
+ Assert.IsNotNull(await controllerClient.RestClient.Instances.GetId(controllerInstance, cancellationToken));
- await ApiAssert.ThrowsException(() => controllerClient.Instances.GetId(node2Instance, cancellationToken), Api.Models.ErrorCode.ResourceNotPresent);
- await ApiAssert.ThrowsException(() => node1Client.Instances.GetId(controllerInstance, cancellationToken), Api.Models.ErrorCode.ResourceNotPresent);
+ await ApiAssert.ThrowsException(() => controllerClient.RestClient.Instances.GetId(node2Instance, cancellationToken), Api.Models.ErrorCode.ResourceNotPresent);
+ await ApiAssert.ThrowsException(() => node1Client.RestClient.Instances.GetId(controllerInstance, cancellationToken), Api.Models.ErrorCode.ResourceNotPresent);
// test update
- await node1Client.Administration.Update(
+ await node1Client.RestClient.Administration.Update(
new ServerUpdateRequest
{
NewVersion = TestUpdateVersion
@@ -717,7 +726,7 @@ namespace Tgstation.Server.Tests.Live
await using var controllerClient2 = await CreateAdminClient(controller.ApiUrl, cancellationToken);
await using var node1Client2 = await CreateAdminClient(node1.ApiUrl, cancellationToken);
- await ApiAssert.ThrowsException(() => controllerClient2.Administration.Update(
+ await ApiAssert.ThrowsException(() => controllerClient2.RestClient.Administration.Update(
new ServerUpdateRequest
{
NewVersion = TestUpdateVersion
@@ -738,7 +747,7 @@ namespace Tgstation.Server.Tests.Live
do
{
await Task.Delay(TimeSpan.FromSeconds(10));
- serverInformation = await node2Client2.ServerInformation(cancellationToken);
+ serverInformation = await node2Client2.RestClient.ServerInformation(cancellationToken);
}
while (serverInformation.SwarmServers.Count == 1);
}
@@ -747,8 +756,8 @@ namespace Tgstation.Server.Tests.Live
WaitForSwarmServerUpdate2(),
Task.Delay(TimeSpan.FromMinutes(4), cancellationToken));
- var node2Info2 = await node2Client2.ServerInformation(cancellationToken);
- var node1Info2 = await node1Client2.ServerInformation(cancellationToken);
+ var node2Info2 = await node2Client2.RestClient.ServerInformation(cancellationToken);
+ var node1Info2 = await node1Client2.RestClient.ServerInformation(cancellationToken);
CheckInfo(node1Info2);
CheckInfo(node2Info2);
@@ -765,7 +774,7 @@ namespace Tgstation.Server.Tests.Live
gitHubToken);
var downloadStream = await download.GetResult(cancellationToken);
- var responseModel = await controllerClient2.Administration.Update(
+ var responseModel = await controllerClient2.RestClient.Administration.Update(
new ServerUpdateRequest
{
NewVersion = TestUpdateVersion,
@@ -848,7 +857,7 @@ namespace Tgstation.Server.Tests.Live
await using var node1Client = await CreateAdminClient(node1.ApiUrl, cancellationToken);
await using var node2Client = await CreateAdminClient(node2.ApiUrl, cancellationToken);
- var controllerInfo = await controllerClient.ServerInformation(cancellationToken);
+ var controllerInfo = await controllerClient.RestClient.ServerInformation(cancellationToken);
async Task WaitForSwarmServerUpdate(IRestServerClient client, int currentServerCount)
{
@@ -889,11 +898,11 @@ namespace Tgstation.Server.Tests.Live
// wait a few minutes for the updated server list to dispatch
await Task.WhenAny(
- WaitForSwarmServerUpdate(node1Client, 1),
+ WaitForSwarmServerUpdate(node1Client.RestClient, 1),
Task.Delay(TimeSpan.FromMinutes(4), cancellationToken));
- var node2Info = await node2Client.ServerInformation(cancellationToken);
- var node1Info = await node1Client.ServerInformation(cancellationToken);
+ var node2Info = await node2Client.RestClient.ServerInformation(cancellationToken);
+ var node1Info = await node1Client.RestClient.ServerInformation(cancellationToken);
CheckInfo(node1Info);
CheckInfo(node2Info);
@@ -905,21 +914,21 @@ namespace Tgstation.Server.Tests.Live
Assert.IsTrue(node1Task.IsCompleted);
// it should unregister
- controllerInfo = await controllerClient.ServerInformation(cancellationToken);
+ controllerInfo = await controllerClient.RestClient.ServerInformation(cancellationToken);
Assert.AreEqual(2, controllerInfo.SwarmServers.Count);
Assert.IsFalse(controllerInfo.SwarmServers.Any(x => x.Identifier == "node1"));
// wait a few minutes for the updated server list to dispatch
await Task.WhenAny(
- WaitForSwarmServerUpdate(node2Client, 3),
+ WaitForSwarmServerUpdate(node2Client.RestClient, 3),
Task.Delay(TimeSpan.FromMinutes(4), cancellationToken));
- node2Info = await node2Client.ServerInformation(cancellationToken);
+ node2Info = await node2Client.RestClient.ServerInformation(cancellationToken);
Assert.AreEqual(2, node2Info.SwarmServers.Count);
Assert.IsFalse(node2Info.SwarmServers.Any(x => x.Identifier == "node1"));
// restart the controller
- await controllerClient.Administration.Restart(cancellationToken);
+ await controllerClient.RestClient.Administration.Restart(cancellationToken);
await Task.WhenAny(
controllerTask,
Task.Delay(TimeSpan.FromMinutes(1), cancellationToken));
@@ -930,10 +939,10 @@ namespace Tgstation.Server.Tests.Live
// node 2 should reconnect once it's health check triggers
await Task.WhenAny(
- WaitForSwarmServerUpdate(controllerClient2, 1),
+ WaitForSwarmServerUpdate(controllerClient2.RestClient, 1),
Task.Delay(TimeSpan.FromMinutes(5), cancellationToken));
- controllerInfo = await controllerClient2.ServerInformation(cancellationToken);
+ controllerInfo = await controllerClient2.RestClient.ServerInformation(cancellationToken);
Assert.AreEqual(2, controllerInfo.SwarmServers.Count);
Assert.IsNotNull(controllerInfo.SwarmServers.SingleOrDefault(x => x.Identifier == "node2"));
@@ -941,20 +950,20 @@ namespace Tgstation.Server.Tests.Live
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
// restart node2
- await node2Client.Administration.Restart(cancellationToken);
+ await node2Client.RestClient.Administration.Restart(cancellationToken);
await Task.WhenAny(
node2Task,
Task.Delay(TimeSpan.FromMinutes(1)));
Assert.IsTrue(node1Task.IsCompleted);
// should have unregistered
- controllerInfo = await controllerClient2.ServerInformation(cancellationToken);
+ controllerInfo = await controllerClient2.RestClient.ServerInformation(cancellationToken);
Assert.AreEqual(1, controllerInfo.SwarmServers.Count);
Assert.IsNull(controllerInfo.SwarmServers.SingleOrDefault(x => x.Identifier == "node2"));
// update should fail
await ApiAssert.ThrowsException(
- () => controllerClient2.Administration.Update(new ServerUpdateRequest
+ () => controllerClient2.RestClient.Administration.Update(new ServerUpdateRequest
{
NewVersion = TestUpdateVersion
},
@@ -967,10 +976,10 @@ namespace Tgstation.Server.Tests.Live
// should re-register
await Task.WhenAny(
- WaitForSwarmServerUpdate(node2Client2, 1),
+ WaitForSwarmServerUpdate(node2Client2.RestClient, 1),
Task.Delay(TimeSpan.FromMinutes(4), cancellationToken));
- node2Info = await node2Client2.ServerInformation(cancellationToken);
+ node2Info = await node2Client2.RestClient.ServerInformation(cancellationToken);
Assert.AreEqual(2, node2Info.SwarmServers.Count);
Assert.IsNotNull(node2Info.SwarmServers.SingleOrDefault(x => x.Identifier == "controller"));
}
@@ -1021,10 +1030,10 @@ namespace Tgstation.Server.Tests.Live
try
{
await using var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken);
-
- var instanceManagerTest = new InstanceManagerTest(adminClient, server.Directory);
+ var restAdminClient = adminClient.RestClient;
+ var instanceManagerTest = new InstanceManagerTest(restAdminClient, server.Directory);
var instance = await instanceManagerTest.CreateTestInstance("TgTestInstance", cancellationToken);
- var instanceClient = adminClient.Instances.CreateClient(instance);
+ var instanceClient = restAdminClient.Instances.CreateClient(instance);
var ddUpdateTask = instanceClient.DreamDaemon.Update(new DreamDaemonRequest
@@ -1327,34 +1336,48 @@ namespace Tgstation.Server.Tests.Live
var serverTask = server.Run(cancellationToken).AsTask();
var fileDownloader = ((Host.Server)server.RealServer).Host.Services.GetRequiredService();
+ var graphQLClientFactory = new GraphQLServerClientFactory(restClientFactory);
try
{
Api.Models.Instance instance;
long initialStaged, initialActive, initialSessionId;
- await using var firstAdminClient = await CreateAdminClient(server.ApiUrl, cancellationToken);
- await using (var tokenOnlyClient = clientFactory.CreateFromToken(server.RootUrl, firstAdminClient.Token))
+ await using var firstAdminMultiClient = await CreateAdminClient(server.ApiUrl, cancellationToken);
+
+ var firstAdminRestClient = firstAdminMultiClient.RestClient;
+ await using (var tokenOnlyRestClient = restClientFactory.CreateFromToken(server.RootUrl, firstAdminRestClient.Token))
{
// regression test for password change issue
- var currentUser = await tokenOnlyClient.Users.Read(cancellationToken);
- var updatedUser = await tokenOnlyClient.Users.Update(new UserUpdateRequest
+ var currentUser = await tokenOnlyRestClient.Users.Read(cancellationToken);
+ var updatedUser = await tokenOnlyRestClient.Users.Update(new UserUpdateRequest
{
Id = currentUser.Id,
Password = DefaultCredentials.DefaultAdminUserPassword,
}, cancellationToken);
- await ApiAssert.ThrowsException(() => tokenOnlyClient.Users.Read(cancellationToken), null);
+ await ApiAssert.ThrowsException(() => tokenOnlyRestClient.Users.Read(cancellationToken), null);
+ }
+
+ await using (var tokenOnlyGraphQLClient = graphQLClientFactory.CreateFromToken(server.RootUrl, firstAdminRestClient.Token.Bearer))
+ {
+ // just testing auth works the same here
+ var result = await tokenOnlyGraphQLClient.RunOperation(client => client.ServerVersion.ExecuteAsync(cancellationToken), cancellationToken);
+ Assert.IsTrue(result.IsSuccessResult());
}
// basic graphql test, to be used everywhere eventually
- await using (var graphQLClient = new GraphQLServerClientFactory(clientFactory).CreateUnauthenticated(server.RootUrl))
+ await using (var unauthenticatedGraphQLClient = graphQLClientFactory.CreateUnauthenticated(server.RootUrl))
{
- // test getting server info
- var multiClient = new MultiServerClient(firstAdminClient, graphQLClient);
+ // check auth works as expected
+ var result = await unauthenticatedGraphQLClient.RunOperation(client => client.ServerVersion.ExecuteAsync(cancellationToken), cancellationToken);
+ Assert.IsTrue(result.IsErrorResult());
- await multiClient.ExecuteReadOnlyConfirmEquivalence(
+ // test getting server info
+ var unAuthedMultiClient = new MultiServerClient(firstAdminRestClient, unauthenticatedGraphQLClient);
+
+ await unAuthedMultiClient.ExecuteReadOnlyConfirmEquivalence(
restClient => restClient.ServerInformation(cancellationToken),
- async gqlClient => (await gqlClient.UnauthenticatedServerInformation.ExecuteAsync(cancellationToken)).Data,
+ gqlClient => gqlClient.UnauthenticatedServerInformation.ExecuteAsync(cancellationToken),
(restServerInfo, gqlServerInfo) => restServerInfo.ApiVersion.Major == gqlServerInfo.Swarm.CurrentNode.Gateway.Information.MajorApiVersion
&& (restServerInfo.OAuthProviderInfos == gqlServerInfo.Swarm.CurrentNode.Gateway.Information.OAuthProviderInfos
|| restServerInfo.OAuthProviderInfos.All(kvp =>
@@ -1364,7 +1387,8 @@ namespace Tgstation.Server.Tests.Live
&& info.Value.ServerUrl == kvp.Value.ServerUrl
&& info.Value.ClientId == kvp.Value.ClientId
&& info.Value.RedirectUri == kvp.Value.RedirectUri;
- })));
+ })),
+ cancellationToken);
}
async ValueTask CreateUserWithNoInstancePerms()
@@ -1380,13 +1404,13 @@ namespace Tgstation.Server.Tests.Live
}
};
- var user = await firstAdminClient.Users.Create(createRequest, cancellationToken);
+ var user = await firstAdminRestClient.Users.Create(createRequest, cancellationToken);
Assert.IsTrue(user.Enabled);
- return await clientFactory.CreateFromLogin(server.RootUrl, createRequest.Name, createRequest.Password, cancellationToken: cancellationToken);
+ return await restClientFactory.CreateFromLogin(server.RootUrl, createRequest.Name, createRequest.Password, cancellationToken: cancellationToken);
}
- var jobsHubTest = new JobsHubTests(firstAdminClient, await CreateUserWithNoInstancePerms());
+ var jobsHubTest = new JobsHubTests(firstAdminRestClient, await CreateUserWithNoInstancePerms());
Task jobsHubTestTask;
{
if (server.DumpOpenApiSpecpath)
@@ -1422,11 +1446,11 @@ namespace Tgstation.Server.Tests.Live
if (!openDreamOnly)
{
jobsHubTestTask = FailFast(await jobsHubTest.Run(cancellationToken)); // returns Task
- var rootTest = FailFast(RawRequestTests.Run(clientFactory, firstAdminClient, cancellationToken));
- var adminTest = FailFast(new AdministrationTest(firstAdminClient.Administration).Run(cancellationToken));
- var usersTest = FailFast(new UsersTest(firstAdminClient).Run(cancellationToken));
+ var rootTest = FailFast(RawRequestTests.Run(restClientFactory, firstAdminRestClient, cancellationToken));
+ var adminTest = FailFast(new AdministrationTest(firstAdminRestClient.Administration).Run(cancellationToken));
+ var usersTest = FailFast(new UsersTest(firstAdminRestClient).Run(cancellationToken));
- var instanceManagerTest = new InstanceManagerTest(firstAdminClient, server.Directory);
+ var instanceManagerTest = new InstanceManagerTest(firstAdminRestClient, server.Directory);
var compatInstanceTask = instanceManagerTest.CreateTestInstance("CompatTestsInstance", cancellationToken);
var odInstanceTask = instanceManagerTest.CreateTestInstance("OdTestsInstance", cancellationToken);
var byondApiCompatInstanceTask = instanceManagerTest.CreateTestInstance("BCAPITestsInstance", cancellationToken);
@@ -1436,7 +1460,7 @@ namespace Tgstation.Server.Tests.Live
var byondApiCompatInstance = await byondApiCompatInstanceTask;
var instancesTest = FailFast(instanceManagerTest.RunPreTest(cancellationToken));
Assert.IsTrue(Directory.Exists(instance.Path));
- instanceClient = firstAdminClient.Instances.CreateClient(instance);
+ instanceClient = firstAdminRestClient.Instances.CreateClient(instance);
Assert.IsTrue(Directory.Exists(instanceClient.Metadata.Path));
nonInstanceTests = Task.WhenAll(instancesTest, adminTest, rootTest, usersTest);
@@ -1447,13 +1471,13 @@ namespace Tgstation.Server.Tests.Live
nonInstanceTests = Task.CompletedTask;
jobsHubTestTask = null;
instance = null;
- var instanceManagerTest = new InstanceManagerTest(firstAdminClient, server.Directory);
+ var instanceManagerTest = new InstanceManagerTest(firstAdminRestClient, server.Directory);
var odInstanceTask = instanceManagerTest.CreateTestInstance("OdTestsInstance", cancellationToken);
odInstance = await odInstanceTask;
}
var instanceTest = new InstanceTest(
- firstAdminClient.Instances,
+ firstAdminRestClient.Instances,
fileDownloader,
GetInstanceManager(),
(ushort)server.ApiUrl.Port);
@@ -1482,7 +1506,7 @@ namespace Tgstation.Server.Tests.Live
.RunCompatTests(
await edgeODVersionTask,
server.OpenDreamUrl,
- firstAdminClient.Instances.CreateClient(odInstance),
+ firstAdminRestClient.Instances.CreateClient(odInstance),
odDMPort.Value,
odDDPort.Value,
server.HighPriorityDreamDaemon,
@@ -1509,7 +1533,7 @@ namespace Tgstation.Server.Tests.Live
: new Version(512, 1451) // http://www.byond.com/forum/?forum=5&command=search&scope=local&text=resolved%3a512.1451
},
server.OpenDreamUrl,
- firstAdminClient.Instances.CreateClient(compatInstance),
+ firstAdminRestClient.Instances.CreateClient(compatInstance),
compatDMPort.Value,
compatDDPort.Value,
server.HighPriorityDreamDaemon,
@@ -1550,7 +1574,7 @@ namespace Tgstation.Server.Tests.Live
initialSessionId = dd.SessionId.Value;
jobsHubTest.ExpectShutdown();
- await firstAdminClient.Administration.Restart(cancellationToken);
+ await firstAdminRestClient.Administration.Restart(cancellationToken);
}
await Task.WhenAny(serverTask, Task.Delay(TimeSpan.FromMinutes(1), cancellationToken));
@@ -1599,8 +1623,10 @@ namespace Tgstation.Server.Tests.Live
// chat bot start and DD reattach test
serverTask = server.Run(cancellationToken).AsTask();
- await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken))
+ await using (var multiClient = await CreateAdminClient(server.ApiUrl, cancellationToken))
{
+ var adminClient = multiClient.RestClient;
+
await jobsHubTest.WaitForReconnect(cancellationToken);
var instanceClient = adminClient.Instances.CreateClient(instance);
@@ -1704,8 +1730,9 @@ namespace Tgstation.Server.Tests.Live
var edgeVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, fileDownloader, cancellationToken);
await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken))
{
+ var restAdminClient = adminClient.RestClient;
await jobsHubTest.WaitForReconnect(cancellationToken);
- var instanceClient = adminClient.Instances.CreateClient(instance);
+ var instanceClient = restAdminClient.Instances.CreateClient(instance);
await WaitForInitialJobs(instanceClient);
var dd = await instanceClient.DreamDaemon.Read(cancellationToken);
@@ -1737,7 +1764,7 @@ namespace Tgstation.Server.Tests.Live
expectedStaged = compileJob.Id.Value;
jobsHubTest.ExpectShutdown();
- await adminClient.Administration.Restart(cancellationToken);
+ await restAdminClient.Administration.Restart(cancellationToken);
}
await Task.WhenAny(serverTask, Task.Delay(TimeSpan.FromMinutes(1), cancellationToken));
@@ -1747,8 +1774,9 @@ namespace Tgstation.Server.Tests.Live
serverTask = server.Run(cancellationToken).AsTask();
await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken))
{
+ var restAdminClient = adminClient.RestClient;
await jobsHubTest.WaitForReconnect(cancellationToken);
- var instanceClient = adminClient.Instances.CreateClient(instance);
+ var instanceClient = restAdminClient.Instances.CreateClient(instance);
await WaitForInitialJobs(instanceClient);
var currentDD = await instanceClient.DreamDaemon.Read(cancellationToken);
@@ -1763,7 +1791,7 @@ namespace Tgstation.Server.Tests.Live
await using var repoTestObj = new RepositoryTest(instanceClient.Repository, instanceClient.Jobs);
var repoTest = repoTestObj.RunPostTest(cancellationToken);
- await using var chatTestObj = new ChatTest(instanceClient.ChatBots, adminClient.Instances, instanceClient.Jobs, instance);
+ await using var chatTestObj = new ChatTest(instanceClient.ChatBots, restAdminClient.Instances, instanceClient.Jobs, instance);
await chatTestObj.RunPostTest(cancellationToken);
await repoTest;
@@ -1772,7 +1800,7 @@ namespace Tgstation.Server.Tests.Live
jobsHubTest.CompleteNow();
await jobsHubTestTask;
- await new InstanceManagerTest(adminClient, server.Directory).RunPostTest(instance, cancellationToken);
+ await new InstanceManagerTest(restAdminClient, server.Directory).RunPostTest(instance, cancellationToken);
}
}
catch (ApiException ex)
@@ -1807,20 +1835,64 @@ namespace Tgstation.Server.Tests.Live
await serverTask;
}
- async Task CreateAdminClient(Uri url, CancellationToken cancellationToken)
+ async ValueTask CreateAdminClient(Uri url, CancellationToken cancellationToken)
{
url = new Uri(url.ToString().Replace(Routes.ApiRoot, String.Empty));
var giveUpAt = DateTimeOffset.UtcNow.AddMinutes(2);
for (var I = 1; ; ++I)
{
+ ValueTask restClientTask;
+ ValueTask graphQLClientTask;
try
{
- System.Console.WriteLine($"TEST: CreateAdminClient attempt {I}...");
- return await clientFactory.CreateFromLogin(
+ Console.WriteLine($"TEST: CreateAdminClient attempt {I}...");
+
+ restClientTask = restClientFactory.CreateFromLogin(
url,
DefaultCredentials.AdminUserName,
DefaultCredentials.DefaultAdminUserPassword,
cancellationToken: cancellationToken);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ graphQLClientTask = graphQLClientFactory.CreateFromLogin(
+ url,
+ DefaultCredentials.AdminUserName,
+ DefaultCredentials.DefaultAdminUserPassword,
+ cancellationToken: cts.Token);
+
+ IRestServerClient restClient;
+ try
+ {
+ restClient = await restClientTask;
+ }
+ catch (Exception restException) when (restException is not HttpRequestException && restException is not ServiceUnavailableException)
+ {
+ cts.Cancel();
+ try
+ {
+ await (await graphQLClientTask).DisposeAsync();
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (Exception graphQLException)
+ {
+ throw new AggregateException(restException, graphQLException);
+ }
+
+ throw;
+ }
+
+ try
+ {
+ return new MultiServerClient(
+ restClient,
+ await graphQLClientTask);
+ }
+ catch
+ {
+ await restClient.DisposeAsync();
+ throw;
+ }
}
catch (HttpRequestException)
{