diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index b8e50b458c..63ef457aee 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -19,7 +19,6 @@ using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; -using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.System; @@ -40,9 +39,9 @@ namespace Tgstation.Server.Host.Controllers const string OctokitException = "Bad GitHub API response, check configuration!"; /// - /// The for the . + /// The for the . /// - readonly IGitHubClientFactory gitHubClientFactory; + readonly IGitHubService gitHubService; /// /// The for the . @@ -74,11 +73,6 @@ namespace Tgstation.Server.Host.Controllers /// readonly IFileTransferTicketProvider fileTransferService; - /// - /// The for the . - /// - readonly UpdatesConfiguration updatesConfiguration; - /// /// The for the . /// @@ -89,7 +83,7 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the . /// The for the . - /// The value of . + /// The value of . /// The value of . /// The value of . /// The value of . @@ -97,12 +91,11 @@ namespace Tgstation.Server.Host.Controllers /// The value of . /// The value of . /// The for the . - /// The containing value of . /// The containing value of . public AdministrationController( IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, - IGitHubClientFactory gitHubClientFactory, + IGitHubService gitHubService, IServerControl serverControl, IServerUpdateInitiator serverUpdateInitiator, IAssemblyInformationProvider assemblyInformationProvider, @@ -110,7 +103,6 @@ namespace Tgstation.Server.Host.Controllers IPlatformIdentifier platformIdentifier, IFileTransferTicketProvider fileTransferService, ILogger logger, - IOptions updatesConfigurationOptions, IOptions fileLoggingConfigurationOptions) : base( databaseContext, @@ -118,14 +110,13 @@ namespace Tgstation.Server.Host.Controllers logger, true) { - this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.gitHubService = gitHubService ?? throw new ArgumentNullException(nameof(gitHubService)); this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); this.serverUpdateInitiator = serverUpdateInitiator ?? throw new ArgumentNullException(nameof(serverUpdateInitiator)); this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); - updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions)); fileLoggingConfiguration = fileLoggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions)); } @@ -150,26 +141,19 @@ namespace Tgstation.Server.Host.Controllers Uri repoUrl = null; try { - var gitHubClient = gitHubClientFactory.CreateClient(); - var repositoryTask = gitHubClient - .Repository - .Get(updatesConfiguration.GitHubRepositoryId) - .WithToken(cancellationToken); - var releases = (await gitHubClient - .Repository - .Release - .GetAll(updatesConfiguration.GitHubRepositoryId) - .WithToken(cancellationToken)) - .Where(x => x.TagName.StartsWith( - updatesConfiguration.GitTagPrefix, - StringComparison.InvariantCulture)); + var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken); + var releases = await gitHubService.GetTgsReleases(cancellationToken); - foreach (var release in releases) - if (Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version) - && version.Major > 3 // Forward/backward compatible but not before TGS4 + foreach (var kvp in releases) + { + var version = kvp.Key; + var release = kvp.Value; + if (version.Major > 3 // Forward/backward compatible but not before TGS4 && (greatestVersion == null || version > greatestVersion)) greatestVersion = version; - repoUrl = new Uri((await repositoryTask).HtmlUrl); + } + + repoUrl = await repositoryUrlTask; } catch (NotFoundException e) { diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 552ab73290..2bdac39c75 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -370,7 +370,10 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(x => x.GetRequiredService().CreateService()); // configure root services services.AddSingleton(); diff --git a/src/Tgstation.Server.Host/Utils/GitHubClientFactory.cs b/src/Tgstation.Server.Host/Utils/GitHubClientFactory.cs index 5bdb7091e1..da74748e6f 100644 --- a/src/Tgstation.Server.Host/Utils/GitHubClientFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHubClientFactory.cs @@ -67,7 +67,9 @@ namespace Tgstation.Server.Host.Utils public IGitHubClient CreateClient() => GetOrCreateClient(generalConfiguration.GitHubAccessToken); /// - public IGitHubClient CreateClient(string accessToken) => GetOrCreateClient(accessToken ?? throw new ArgumentNullException(nameof(accessToken))); + public IGitHubClient CreateClient(string accessToken) + => GetOrCreateClient( + accessToken ?? throw new ArgumentNullException(nameof(accessToken))); /// /// Retrieve a from the or add a new one based on a given . diff --git a/src/Tgstation.Server.Host/Utils/GitHubService.cs b/src/Tgstation.Server.Host/Utils/GitHubService.cs new file mode 100644 index 0000000000..5d1d6ef5d4 --- /dev/null +++ b/src/Tgstation.Server.Host/Utils/GitHubService.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Octokit; + +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Extensions; + +namespace Tgstation.Server.Host.Utils +{ + /// + sealed class GitHubService : IGitHubService + { + /// + /// The for the . + /// + readonly IGitHubClient gitHubClient; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// The for the . + /// + readonly UpdatesConfiguration updatesConfiguration; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + public GitHubService(IGitHubClient gitHubClient, ILogger logger, UpdatesConfiguration updatesConfiguration) + { + this.gitHubClient = gitHubClient ?? throw new ArgumentNullException(nameof(gitHubClient)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.updatesConfiguration = updatesConfiguration ?? throw new ArgumentNullException(nameof(updatesConfiguration)); + } + + /// + public async Task> GetTgsReleases(CancellationToken cancellationToken) + { + logger.LogTrace("GetTgsReleases"); + var allReleases = await gitHubClient + .Repository + .Release + .GetAll(updatesConfiguration.GitHubRepositoryId) + .WithToken(cancellationToken); + + logger.LogTrace("{totalReleases} total releases", allReleases.Count); + var releases = allReleases + .Select(release => + { + if (!release.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture) + || !Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version)) + return null; + + return Tuple.Create(version, release); + }) + .Where(tuple => tuple != null) + .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2); + + logger.LogTrace("{parsedReleases} parsed releases", releases.Count); + return releases; + } + + /// + public async Task GetUpdatesRepositoryUrl(CancellationToken cancellationToken) + { + logger.LogTrace("GetUpdatesRepositoryUrl"); + var repository = await gitHubClient + .Repository + .Get(updatesConfiguration.GitHubRepositoryId) + .WithToken(cancellationToken); + + var repoUrl = new Uri(repository.HtmlUrl); + logger.LogTrace("Maps to {repostioryUrl}", repoUrl); + + return repoUrl; + } + } +} diff --git a/src/Tgstation.Server.Host/Utils/GitHubServiceFactory.cs b/src/Tgstation.Server.Host/Utils/GitHubServiceFactory.cs new file mode 100644 index 0000000000..85332165c8 --- /dev/null +++ b/src/Tgstation.Server.Host/Utils/GitHubServiceFactory.cs @@ -0,0 +1,66 @@ +using System; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Octokit; + +using Tgstation.Server.Host.Configuration; + +namespace Tgstation.Server.Host.Utils +{ + /// + sealed class GitHubServiceFactory : IGitHubServiceFactory + { + /// + /// The for the . + /// + readonly IGitHubClientFactory gitHubClientFactory; + + /// + /// The for the . + /// + readonly ILoggerFactory loggerFactory; + + /// + /// The for the . + /// + readonly UpdatesConfiguration updatesConfiguration; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The containing value of . + public GitHubServiceFactory( + IGitHubClientFactory gitHubClientFactory, + ILoggerFactory loggerFactory, + IOptions updatesConfigurationOptions) + { + this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions)); + } + + /// + public IGitHubService CreateService() => CreateServiceImpl(gitHubClientFactory.CreateClient()); + + /// + public IGitHubService CreateService(string accessToken) + => CreateServiceImpl( + gitHubClientFactory.CreateClient( + accessToken ?? throw new ArgumentNullException(nameof(accessToken)))); + + /// + /// Create a . + /// + /// The for the . + /// A new . + GitHubService CreateServiceImpl(IGitHubClient gitHubClient) + => new ( + gitHubClient, + loggerFactory.CreateLogger(), + updatesConfiguration); + } +} diff --git a/src/Tgstation.Server.Host/Utils/IGitHubService.cs b/src/Tgstation.Server.Host/Utils/IGitHubService.cs new file mode 100644 index 0000000000..2a6da1aef4 --- /dev/null +++ b/src/Tgstation.Server.Host/Utils/IGitHubService.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using Octokit; + +namespace Tgstation.Server.Host.Utils +{ + /// + /// Service for interacting with GitHub. + /// + public interface IGitHubService + { + /// + /// Gets the of the repository designated as the updates repository. + /// + /// The for the operation. + /// A resulting in the of the designated updates repository. + Task GetUpdatesRepositoryUrl(CancellationToken cancellationToken); + + /// + /// Get all valid TGS s from the configured update source. + /// + /// The for the operation. + /// A resulting in a of TGS s keyed by their . + Task> GetTgsReleases(CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Utils/IGitHubServiceFactory.cs b/src/Tgstation.Server.Host/Utils/IGitHubServiceFactory.cs new file mode 100644 index 0000000000..c6997f9d5f --- /dev/null +++ b/src/Tgstation.Server.Host/Utils/IGitHubServiceFactory.cs @@ -0,0 +1,21 @@ +namespace Tgstation.Server.Host.Utils +{ + /// + /// Factory for s. + /// + public interface IGitHubServiceFactory + { + /// + /// Create a . + /// + /// A new . + public IGitHubService CreateService(); + + /// + /// Create a . + /// + /// The access token to use for communication with GitHub. + /// A new . + public IGitHubService CreateService(string accessToken); + } +} diff --git a/tests/Tgstation.Server.Host.Tests/Utils/TestGitHubServiceFactory.cs b/tests/Tgstation.Server.Host.Tests/Utils/TestGitHubServiceFactory.cs new file mode 100644 index 0000000000..6287e3b271 --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/Utils/TestGitHubServiceFactory.cs @@ -0,0 +1,57 @@ +using System; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Moq; + +using Octokit; + +using Tgstation.Server.Host.Configuration; + +namespace Tgstation.Server.Host.Utils.Tests +{ + [TestClass] + public sealed class TestGitHubServiceFactory + { + [TestMethod] + public void TestConstructor() + { + Assert.ThrowsException(() => new GitHubServiceFactory(null, null, null)); + Assert.ThrowsException(() => new GitHubServiceFactory(Mock.Of(), null, null)); + Assert.ThrowsException(() => new GitHubServiceFactory(Mock.Of(), Mock.Of(), null)); + var mockOptions = new Mock>(); + mockOptions.SetupGet(x => x.Value).Returns(new UpdatesConfiguration()); + + _ = new GitHubServiceFactory(Mock.Of(), Mock.Of(), mockOptions.Object); + } + + [TestMethod] + public void TestCreateService() + { + var mockFactory = new Mock(); + + mockFactory.Setup(x => x.CreateClient()).Returns(Mock.Of()).Verifiable(); + + var mockToken = "asdf"; + mockFactory.Setup(x => x.CreateClient(mockToken)).Returns(Mock.Of()).Verifiable(); + + 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)); + Assert.AreEqual(0, mockFactory.Invocations.Count); + + var result1 = factory.CreateService(); + Assert.IsNotNull(result1); + + var result2 = factory.CreateService(mockToken); + Assert.IsNotNull(result2); + + mockFactory.VerifyAll(); + } + } +}