From a42dafb3ab831e1f3ae54b592d609d978669d3d5 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 11 Aug 2024 17:01:05 -0400 Subject: [PATCH] Add support for GitHub App installation authentication to `IGitHubClientFactory` --- .../Remote/GitHubRemoteDeploymentManager.cs | 12 +- .../Repository/GitHubRemoteFeatures.cs | 4 +- .../Controllers/AdministrationController.cs | 2 +- .../Core/ServerUpdater.cs | 2 +- .../Security/OAuth/GitHubOAuthValidator.cs | 4 +- .../Utils/GitHub/GitHubClientFactory.cs | 131 ++++++++++++++++-- .../Utils/GitHub/GitHubServiceFactory.cs | 13 +- .../Utils/GitHub/IGitHubClientFactory.cs | 20 ++- .../Utils/GitHub/IGitHubServiceFactory.cs | 15 +- .../Utils/GitHub/TestGitHubClientFactory.cs | 19 +-- .../Utils/GitHub/TestGitHubServiceFactory.cs | 16 ++- .../Live/DummyGitHubServiceFactory.cs | 8 +- .../Live/TestingGitHubService.cs | 2 +- 13 files changed, 190 insertions(+), 58 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs index 4e37e7d008..9a04c0ac94 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs @@ -79,13 +79,13 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote IGitHubService gitHubService; if (instanceAuthenticated) { - authenticatedGitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken!); + authenticatedGitHubService = await gitHubServiceFactory.CreateService(repositorySettings.AccessToken!, cancellationToken); gitHubService = authenticatedGitHubService; } else { authenticatedGitHubService = null; - gitHubService = gitHubServiceFactory.CreateService(); + gitHubService = await gitHubServiceFactory.CreateService(cancellationToken); } var repoOwner = remoteInformation.RemoteRepositoryOwner!; @@ -175,8 +175,8 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote } var gitHubService = repositorySettings.AccessToken != null - ? gitHubServiceFactory.CreateService(repositorySettings.AccessToken) - : gitHubServiceFactory.CreateService(); + ? await gitHubServiceFactory.CreateService(repositorySettings.AccessToken, cancellationToken) + : await gitHubServiceFactory.CreateService(cancellationToken); var tasks = revisionInformation .ActiveTestMerges @@ -255,7 +255,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote int testMergeNumber, CancellationToken cancellationToken) { - var gitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken!); + var gitHubService = await gitHubServiceFactory.CreateService(repositorySettings.AccessToken!, cancellationToken); try { @@ -343,7 +343,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote return; } - var gitHubService = gitHubServiceFactory.CreateService(gitHubAccessToken); + var gitHubService = await gitHubServiceFactory.CreateService(gitHubAccessToken, cancellationToken); try { diff --git a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs index 6109f490bb..69289bb5d4 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs @@ -49,8 +49,8 @@ namespace Tgstation.Server.Host.Components.Repository CancellationToken cancellationToken) { var gitHubService = repositorySettings.AccessToken != null - ? gitHubServiceFactory.CreateService(repositorySettings.AccessToken) - : gitHubServiceFactory.CreateService(); + ? await gitHubServiceFactory.CreateService(repositorySettings.AccessToken, cancellationToken) + : await gitHubServiceFactory.CreateService(cancellationToken); PullRequest? pr = null; ApiException? exception = null; diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index d4c1335df7..c41113069d 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -165,7 +165,7 @@ namespace Tgstation.Server.Host.Controllers Uri? repoUrl = null; try { - var gitHubService = gitHubServiceFactory.CreateService(); + var gitHubService = await gitHubServiceFactory.CreateService(cancellationToken); var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken); var releases = await gitHubService.GetTgsReleases(cancellationToken); diff --git a/src/Tgstation.Server.Host/Core/ServerUpdater.cs b/src/Tgstation.Server.Host/Core/ServerUpdater.cs index 8c9d968168..1170537a12 100644 --- a/src/Tgstation.Server.Host/Core/ServerUpdater.cs +++ b/src/Tgstation.Server.Host/Core/ServerUpdater.cs @@ -288,7 +288,7 @@ namespace Tgstation.Server.Host.Core { logger.LogDebug("Looking for GitHub releases version {version}...", newVersion); - var gitHubService = gitHubServiceFactory.CreateService(); + var gitHubService = await gitHubServiceFactory.CreateService(cancellationToken); var releases = await gitHubService.GetTgsReleases(cancellationToken); foreach (var kvp in releases) { diff --git a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs index 3f3180f9da..d535d97884 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs @@ -60,12 +60,12 @@ namespace Tgstation.Server.Host.Security.OAuth { logger.LogTrace("Validating response code..."); - var gitHubService = gitHubServiceFactory.CreateService(); + var gitHubService = await gitHubServiceFactory.CreateService(cancellationToken); var token = await gitHubService.CreateOAuthAccessToken(oAuthConfiguration, code, cancellationToken); if (token == null) return null; - var authenticatedClient = gitHubServiceFactory.CreateService(token); + var authenticatedClient = await gitHubServiceFactory.CreateService(token, cancellationToken); logger.LogTrace("Getting user details..."); var userId = await authenticatedClient.GetCurrentUserId(cancellationToken); diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs index e479388b79..4cf31a5d28 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs @@ -1,9 +1,16 @@ using System; using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + using Octokit; using Tgstation.Server.Host.Configuration; @@ -12,7 +19,7 @@ using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Utils.GitHub { /// - sealed class GitHubClientFactory : IGitHubClientFactory + sealed class GitHubClientFactory : IGitHubClientFactory, IDisposable { /// /// Limit to the amount of days a can live in the . @@ -45,6 +52,11 @@ namespace Tgstation.Server.Host.Utils.GitHub /// readonly Dictionary clientCache; + /// + /// The used to guard access to . + /// + readonly SemaphoreSlim clientCacheSemaphore; + /// /// Initializes a new instance of the class. /// @@ -61,50 +73,139 @@ namespace Tgstation.Server.Host.Utils.GitHub generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); clientCache = new Dictionary(); + clientCacheSemaphore = new SemaphoreSlim(1, 1); } /// - public IGitHubClient CreateClient() => GetOrCreateClient(generalConfiguration.GitHubAccessToken); + public void Dispose() => clientCacheSemaphore.Dispose(); /// - public IGitHubClient CreateClient(string accessToken) - => GetOrCreateClient( - accessToken ?? throw new ArgumentNullException(nameof(accessToken))); + public async ValueTask CreateClient(CancellationToken cancellationToken) + => (await GetOrCreateClient( + generalConfiguration.GitHubAccessToken, + null, + cancellationToken))!; + + /// + public async ValueTask CreateClient(string accessToken, CancellationToken cancellationToken) + => (await GetOrCreateClient( + accessToken ?? throw new ArgumentNullException(nameof(accessToken)), + null, + cancellationToken))!; + + /// + public ValueTask CreateInstallationClient(string serializedPem, long repositoryId, CancellationToken cancellationToken) + => GetOrCreateClient(serializedPem, repositoryId, cancellationToken); /// - /// Retrieve a from the or add a new one based on a given . + /// Retrieve a from the or add a new one based on a given . /// - /// Optional access token to use as credentials. - /// The for the given . - GitHubClient GetOrCreateClient(string? accessToken) + /// Optional access token to use as credentials or GitHub App private key. If using a private key, must be set. + /// Setting this specifies is a private key and a GitHub App installation authenticated client will be returned. + /// The for the operation. + /// A resulting in the for the given or if authentication failed. +#pragma warning disable CA1506 // TODO: Decomplexify + async ValueTask GetOrCreateClient(string? accessTokenOrSerializedPem, long? installationRepositoryId, CancellationToken cancellationToken) +#pragma warning restore CA1506 { GitHubClient client; bool cacheHit; DateTimeOffset? lastUsed; - lock (clientCache) + using (await SemaphoreSlimContext.Lock(clientCacheSemaphore, cancellationToken)) { string cacheKey; - if (String.IsNullOrWhiteSpace(accessToken)) + if (String.IsNullOrWhiteSpace(accessTokenOrSerializedPem)) { - accessToken = null; + accessTokenOrSerializedPem = null; cacheKey = DefaultCacheKey; } else - cacheKey = accessToken; + cacheKey = accessTokenOrSerializedPem; cacheHit = clientCache.TryGetValue(cacheKey, out var tuple); var now = DateTimeOffset.UtcNow; if (!cacheHit) { + logger.LogTrace("Creating new GitHubClient..."); var product = assemblyInformationProvider.ProductInfoHeaderValue.Product!; client = new GitHubClient( new ProductHeaderValue( product.Name, product.Version)); - if (accessToken != null) - client.Credentials = new Credentials(accessToken); + if (accessTokenOrSerializedPem != null) + { + if (installationRepositoryId.HasValue) + { + logger.LogTrace("Performing GitHub App authentication for installation on repository {installationRepositoryId}", installationRepositoryId.Value); + var splits = accessTokenOrSerializedPem.Split(':'); + if (splits.Length != 2) + { + logger.LogError("Failed to parse serialized Client ID & PEM! Expected 2 chunks, got {chunkCount}", splits.Length); + return null; + } + + byte[] pemBytes; + try + { + pemBytes = Convert.FromBase64String(splits[1]); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to parse supposed base64 PEM!"); + return null; + } + + var pem = Encoding.UTF8.GetString(pemBytes); + + using var rsa = RSA.Create(); + rsa.ImportFromPem(pem); + + var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256); + var jwtSecurityTokenHandler = new JwtSecurityTokenHandler { SetDefaultTimesOnTokenCreation = false }; + + var nowDateTime = DateTime.UtcNow; + + var jwt = jwtSecurityTokenHandler.CreateToken(new SecurityTokenDescriptor + { + Issuer = splits[0], + Expires = nowDateTime.AddMinutes(10), + IssuedAt = nowDateTime, + SigningCredentials = signingCredentials, + }); + + var jwtStr = jwtSecurityTokenHandler.WriteToken(jwt); + + client.Credentials = new Credentials(jwtStr, AuthenticationType.Bearer); + + Installation installation; + try + { + installation = await client.GitHubApps.GetRepositoryInstallationForCurrent(installationRepositoryId.Value); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to perform app authentication!"); + return null; + } + + cancellationToken.ThrowIfCancellationRequested(); + try + { + var installToken = await client.GitHubApps.CreateInstallationToken(installation.Id); + + client.Credentials = new Credentials(installToken.Token); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to perform installation authentication!"); + return null; + } + } + else + client.Credentials = new Credentials(accessTokenOrSerializedPem); + } clientCache.Add(cacheKey, (Client: client, LastUsed: now)); lastUsed = null; diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs index efa08d13bb..a38b84b271 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -44,13 +46,16 @@ namespace Tgstation.Server.Host.Utils.GitHub } /// - public IGitHubService CreateService() => CreateServiceImpl(gitHubClientFactory.CreateClient()); + public async ValueTask CreateService(CancellationToken cancellationToken) + => CreateServiceImpl( + await gitHubClientFactory.CreateClient(cancellationToken)); /// - public IAuthenticatedGitHubService CreateService(string accessToken) + public async ValueTask CreateService(string accessToken, CancellationToken cancellationToken) => CreateServiceImpl( - gitHubClientFactory.CreateClient( - accessToken ?? throw new ArgumentNullException(nameof(accessToken)))); + await gitHubClientFactory.CreateClient( + accessToken ?? throw new ArgumentNullException(nameof(accessToken)), + cancellationToken)); /// /// Create a . diff --git a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubClientFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubClientFactory.cs index ec95fd16ab..1808b4ca3c 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubClientFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubClientFactory.cs @@ -1,4 +1,7 @@ -using Octokit; +using System.Threading; +using System.Threading.Tasks; + +using Octokit; namespace Tgstation.Server.Host.Utils.GitHub { @@ -10,14 +13,25 @@ namespace Tgstation.Server.Host.Utils.GitHub /// /// Create a client. Low rate limit unless the server's GitHubAccessToken is set to bypass it. /// + /// The for the operation. /// A new . - IGitHubClient CreateClient(); + ValueTask CreateClient(CancellationToken cancellationToken); /// /// Create a client with authentication using a personal access token. /// /// The GitHub personal access token. + /// The for the operation. /// A new . - IGitHubClient CreateClient(string accessToken); + ValueTask CreateClient(string accessToken, CancellationToken cancellationToken); + + /// + /// Creates a GitHub App client for an installation. + /// + /// The private key . + /// The GitHub repository ID. + /// The for the operation. + /// A resulting in a new for the given or if authentication failed. + ValueTask CreateInstallationClient(string pem, long repositoryId, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubServiceFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubServiceFactory.cs index f4e7be70f5..adb0ec88eb 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubServiceFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubServiceFactory.cs @@ -1,4 +1,7 @@ -namespace Tgstation.Server.Host.Utils.GitHub +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Utils.GitHub { /// /// Factory for s. @@ -8,14 +11,16 @@ /// /// Create a . /// - /// A new . - public IGitHubService CreateService(); + /// The for the operation. + /// A resulting in a new . + public ValueTask CreateService(CancellationToken cancellationToken); /// /// Create an . /// /// The access token to use for communication with GitHub. - /// A new . - public IAuthenticatedGitHubService CreateService(string accessToken); + /// The for the operation. + /// A resulting in a new . + public ValueTask CreateService(string accessToken, CancellationToken cancellationToken); } } diff --git a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs index 6f4fac0ae2..33058be75f 100644 --- a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs @@ -1,5 +1,6 @@ using System; using System.Net.Http.Headers; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -57,14 +58,14 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests mockOptions.SetupGet(x => x.Value).Returns(gc); var factory = new GitHubClientFactory(mockApp.Object, loggerFactory.CreateLogger(), mockOptions.Object); - var client = factory.CreateClient(); + var client = await factory.CreateClient(CancellationToken.None); Assert.IsNotNull(client); var credentials = await client.Connection.CredentialStore.GetCredentials(); Assert.AreEqual(AuthenticationType.Anonymous, credentials.AuthenticationType); gc.GitHubAccessToken = "asdfasdfasdfasdfasdfasdf"; - client = factory.CreateClient(); + client = await factory.CreateClient(CancellationToken.None); Assert.IsNotNull(client); credentials = await client.Connection.CredentialStore.GetCredentials(); @@ -83,9 +84,9 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration()); var factory = new GitHubClientFactory(mockApp.Object, loggerFactory.CreateLogger(), mockOptions.Object); - Assert.ThrowsException(() => factory.CreateClient(null)); + await Assert.ThrowsExceptionAsync(() => factory.CreateClient(null, CancellationToken.None).AsTask()); - var client = factory.CreateClient("asdf"); + var client = await factory.CreateClient("asdf", CancellationToken.None); Assert.IsNotNull(client); var credentials = await client.Connection.CredentialStore.GetCredentials(); @@ -96,7 +97,7 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests } [TestMethod] - public void TestClientCaching() + public async Task TestClientCaching() { var mockApp = new Mock(); mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable(); @@ -105,10 +106,10 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration()); var factory = new GitHubClientFactory(mockApp.Object, loggerFactory.CreateLogger(), mockOptions.Object); - var client1 = factory.CreateClient(); - var client2 = factory.CreateClient("asdf"); - var client3 = factory.CreateClient(); - var client4 = factory.CreateClient("asdf"); + var client1 = await factory.CreateClient(CancellationToken.None); + var client2 = await factory.CreateClient("asdf", CancellationToken.None); + var client3 = await factory.CreateClient(CancellationToken.None); + var client4 = await factory.CreateClient("asdf", CancellationToken.None); Assert.ReferenceEquals(client1, client3); Assert.ReferenceEquals(client2, client4); } diff --git a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs index daa9afdd4c..24212eaa93 100644 --- a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -28,27 +30,29 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests } [TestMethod] - public void TestCreateService() + public async Task TestCreateService() { var mockFactory = new Mock(); - mockFactory.Setup(x => x.CreateClient()).Returns(Mock.Of()).Verifiable(); +#pragma warning disable CA2012 // Use ValueTasks correctly + mockFactory.Setup(x => x.CreateClient(It.IsAny())).Returns(ValueTask.FromResult(Mock.Of())).Verifiable(); var mockToken = "asdf"; - mockFactory.Setup(x => x.CreateClient(mockToken)).Returns(Mock.Of()).Verifiable(); + mockFactory.Setup(x => x.CreateClient(mockToken, It.IsAny())).Returns(ValueTask.FromResult(Mock.Of())).Verifiable(); +#pragma warning restore CA2012 // Use ValueTasks correctly var mockOptions = new Mock>(); mockOptions.SetupGet(x => x.Value).Returns(new UpdatesConfiguration()); var factory = new GitHubServiceFactory(mockFactory.Object, Mock.Of(), mockOptions.Object); - Assert.ThrowsException(() => factory.CreateService(null)); + await Assert.ThrowsExceptionAsync(() => factory.CreateService(null, CancellationToken.None).AsTask()); Assert.AreEqual(0, mockFactory.Invocations.Count); - var result1 = factory.CreateService(); + var result1 = await factory.CreateService(CancellationToken.None); Assert.IsNotNull(result1); - var result2 = factory.CreateService(mockToken); + var result2 = factory.CreateService(mockToken, CancellationToken.None); Assert.IsNotNull(result2); mockFactory.VerifyAll(); diff --git a/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs b/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs index 2f65757782..d05faed9a6 100644 --- a/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs +++ b/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -18,13 +20,13 @@ namespace Tgstation.Server.Tests.Live this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public IGitHubService CreateService() => CreateDummyService(); + public ValueTask CreateService(CancellationToken cancellationToken) => ValueTask.FromResult(CreateDummyService()); - public IAuthenticatedGitHubService CreateService(string accessToken) + public ValueTask CreateService(string accessToken, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(accessToken); - return CreateDummyService(); + return ValueTask.FromResult(CreateDummyService()); } TestingGitHubService CreateDummyService() => new TestingGitHubService(cryptographySuite, logger); diff --git a/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs b/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs index d104dbd6d3..585dd56999 100644 --- a/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs +++ b/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs @@ -39,7 +39,7 @@ namespace Tgstation.Server.Tests.Live }); var gitHubClientFactory = new GitHubClientFactory(new AssemblyInformationProvider(), Mock.Of>(), mockOptions.Object); - RealClient = gitHubClientFactory.CreateClient(); + RealClient = gitHubClientFactory.CreateClient(CancellationToken.None).GetAwaiter().GetResult(); } public static async Task InitializeAndInject(CancellationToken cancellationToken)