From 8cd0b341ddb518465f53ba893af9a0f79e0f08c8 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sun, 22 Sep 2019 15:09:56 -0300 Subject: [PATCH] Begin implementation of Enterprise ManagementConsole API, redux (#2010) * Initial implementation of ManagementConsole - maintenance mode * Add environment var support for management console password for integration tests * Add reactive client and unit tests * Update some xmlDoc * I think this is a better way to setup the underlying baseUri on IConneciton, to achieve managemet console access rather than requiring a specific GitHubClient that cant call normal API's Instead, the management client methods can check the base Url and if it contains /api/v3/ they can set their relative endpoint Uri to include a leading "/" which will cause the /api/v3/ to be removed. * Update EnterpriseClient.cs Fix xml comments * Update IEnterpriseClient.cs Fix xml comments * Still trying to get the xmDoc perfect, thanks app veyor :) * XmlDoc'ing my way to success * Add specific test attribute for management console tests * check chronic string empty/null * Use helper's password field in test * Tidy up maintenance mode tests by using a context/destructor to manage the initial/end state of maintenance mode * make internal and tidy up URL concatenation * move GHE endpoint fixup inside ApiUrls methods * Rework request object to be the correct structure so SimpleJsonSerializer can be used to serialize it. Remove MaintenanceDate class and just pass in the Date/string for when Still need to use UrlFormEncoding rather than json in the POST body though... * Create abstract base class for FormUrlEncoded parameters (similar to existing RequetParameters) and inherit from it in UpdateMaintenanceRequest * Fix maintenance context logic - destructor should always turn maintenance OFF regardless of initial requested state * Fix xml comment * Fix Xml comment * Those pesky xml comments! * Fine, I give up! * Fix string.Format * fix bad rebase * fix failing convention tests * restore missing whitespace * writing some docs * some edits * edit --- .../Enterprise/IObservableEnterpriseClient.cs | 18 ++- ...rvableEnterpriseManagementConsoleClient.cs | 33 ++++++ .../Enterprise/ObservableEnterpriseClient.cs | 19 +++- ...rvableEnterpriseManagementConsoleClient.cs | 52 +++++++++ .../EnterpriseManagementConsoleClientTests.cs | 88 ++++++++++++++ Octokit.Tests.Integration/EnterpriseHelper.cs | 20 +++- ...nterpriseManagementConsoleTestAttribute.cs | 38 +++++++ .../Helpers/GithubClientExtensions.cs | 5 + .../Helpers/MaintenanceModeContext.cs | 28 +++++ .../EnterpriseManagementConsoleClientTests.cs | 102 +++++++++++++++++ ...eEnterpriseManagementConsoleClientTests.cs | 92 +++++++++++++++ .../Clients/Enterprise/EnterpriseClient.cs | 19 +++- .../EnterpriseManagementConsoleClient.cs | 50 ++++++++ .../Clients/Enterprise/IEnterpriseClient.cs | 18 ++- .../IEnterpriseManagementConsoleClient.cs | 31 +++++ Octokit/Helpers/ApiUrls.cs | 12 ++ Octokit/Helpers/ReferenceExtensions.cs | 4 +- .../Enterprise/UpdateMaintenanceRequest.cs | 107 ++++++++++++++++++ .../Request/FormUrlEncodedParameters.cs | 71 ++++++++++++ .../Models/Response/Enterprise/AdminStats.cs | 2 +- .../Response/Enterprise/AdminStatsComments.cs | 2 +- .../Response/Enterprise/AdminStatsGists.cs | 2 +- .../Response/Enterprise/AdminStatsHooks.cs | 2 +- .../Response/Enterprise/AdminStatsIssues.cs | 2 +- .../Enterprise/AdminStatsMilestones.cs | 2 +- .../Response/Enterprise/AdminStatsOrgs.cs | 2 +- .../Response/Enterprise/AdminStatsPages.cs | 2 +- .../Response/Enterprise/AdminStatsPulls.cs | 2 +- .../Response/Enterprise/AdminStatsRepos.cs | 2 +- .../Response/Enterprise/AdminStatsUsers.cs | 2 +- .../Response/Enterprise/LdapSyncResponse.cs | 2 +- .../Models/Response/Enterprise/LicenseInfo.cs | 2 +- .../MaintenanceModeActiveProcesses.cs | 34 ++++++ .../Enterprise/MaintenanceModeResponse.cs | 62 ++++++++++ .../Enterprise/SearchIndexingResponse.cs | 2 +- docs/enterprise-administration.md | 73 ++++++++++++ script/configure-integration-tests.ps1 | 1 + 37 files changed, 965 insertions(+), 40 deletions(-) create mode 100644 Octokit.Reactive/Clients/Enterprise/IObservableEnterpriseManagementConsoleClient.cs create mode 100644 Octokit.Reactive/Clients/Enterprise/ObservableEnterpriseManagementConsoleClient.cs create mode 100644 Octokit.Tests.Integration/Clients/Enterprise/EnterpriseManagementConsoleClientTests.cs create mode 100644 Octokit.Tests.Integration/Helpers/GitHubEnterpriseManagementConsoleTestAttribute.cs create mode 100644 Octokit.Tests.Integration/Helpers/MaintenanceModeContext.cs create mode 100644 Octokit.Tests/Clients/Enterprise/EnterpriseManagementConsoleClientTests.cs create mode 100644 Octokit.Tests/Reactive/Enterprise/ObservableEnterpriseManagementConsoleClientTests.cs create mode 100644 Octokit/Clients/Enterprise/EnterpriseManagementConsoleClient.cs create mode 100644 Octokit/Clients/Enterprise/IEnterpriseManagementConsoleClient.cs create mode 100644 Octokit/Models/Request/Enterprise/UpdateMaintenanceRequest.cs create mode 100644 Octokit/Models/Request/FormUrlEncodedParameters.cs create mode 100644 Octokit/Models/Response/Enterprise/MaintenanceModeActiveProcesses.cs create mode 100644 Octokit/Models/Response/Enterprise/MaintenanceModeResponse.cs create mode 100644 docs/enterprise-administration.md diff --git a/Octokit.Reactive/Clients/Enterprise/IObservableEnterpriseClient.cs b/Octokit.Reactive/Clients/Enterprise/IObservableEnterpriseClient.cs index 27a7d1a2..f3aa7fdc 100644 --- a/Octokit.Reactive/Clients/Enterprise/IObservableEnterpriseClient.cs +++ b/Octokit.Reactive/Clients/Enterprise/IObservableEnterpriseClient.cs @@ -13,7 +13,7 @@ /// /// /// See the Enterprise Admin Stats API documentation for more information. - /// + /// IObservableEnterpriseAdminStatsClient AdminStats { get; } /// @@ -21,7 +21,7 @@ /// /// /// See the Enterprise LDAP API documentation for more information. - /// + /// IObservableEnterpriseLdapClient Ldap { get; } /// @@ -29,15 +29,23 @@ /// /// /// See the Enterprise License API documentation for more information. - /// + /// IObservableEnterpriseLicenseClient License { get; } + /// + /// A client for GitHub's Enterprise Management Console API + /// + /// + /// See the Enterprise Management Console API documentation for more information. + /// + IObservableEnterpriseManagementConsoleClient ManagementConsole { get; } + /// /// A client for GitHub's Enterprise Organization API /// /// /// See the Enterprise Organization API documentation for more information. - /// + /// IObservableEnterpriseOrganizationClient Organization { get; } /// @@ -45,7 +53,7 @@ /// /// /// See the Enterprise Search Indexing API documentation for more information. - /// + /// IObservableEnterpriseSearchIndexingClient SearchIndexing { get; } /// diff --git a/Octokit.Reactive/Clients/Enterprise/IObservableEnterpriseManagementConsoleClient.cs b/Octokit.Reactive/Clients/Enterprise/IObservableEnterpriseManagementConsoleClient.cs new file mode 100644 index 00000000..816f850e --- /dev/null +++ b/Octokit.Reactive/Clients/Enterprise/IObservableEnterpriseManagementConsoleClient.cs @@ -0,0 +1,33 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Octokit.Reactive +{ + /// + /// A client for GitHub's Enterprise Management Console API + /// + /// + /// See the Enterprise Management Console API documentation for more information. + /// + public interface IObservableEnterpriseManagementConsoleClient + { + /// + /// Gets GitHub Enterprise Maintenance Mode Status + /// + /// + /// https://developer.github.com/v3/enterprise/management_console/#check-maintenance-status + /// + /// The . + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + IObservable GetMaintenanceMode(string managementConsolePassword); + + /// + /// Sets GitHub Enterprise Maintenance Mode + /// + /// + /// https://developer.github.com/v3/enterprise/management_console/#check-maintenance-status + /// + /// The . + IObservable EditMaintenanceMode(UpdateMaintenanceRequest maintenance, string managementConsolePassword); + } +} diff --git a/Octokit.Reactive/Clients/Enterprise/ObservableEnterpriseClient.cs b/Octokit.Reactive/Clients/Enterprise/ObservableEnterpriseClient.cs index 067e81cf..e9d01ae2 100644 --- a/Octokit.Reactive/Clients/Enterprise/ObservableEnterpriseClient.cs +++ b/Octokit.Reactive/Clients/Enterprise/ObservableEnterpriseClient.cs @@ -15,6 +15,7 @@ AdminStats = new ObservableEnterpriseAdminStatsClient(client); Ldap = new ObservableEnterpriseLdapClient(client); License = new ObservableEnterpriseLicenseClient(client); + ManagementConsole = new ObservableEnterpriseManagementConsoleClient(client); Organization = new ObservableEnterpriseOrganizationClient(client); SearchIndexing = new ObservableEnterpriseSearchIndexingClient(client); PreReceiveEnvironment = new ObservableEnterprisePreReceiveEnvironmentsClient(client); @@ -25,7 +26,7 @@ /// /// /// See the Enterprise Admin Stats API documentation for more information. - /// + /// public IObservableEnterpriseAdminStatsClient AdminStats { get; private set; } /// @@ -33,7 +34,7 @@ /// /// /// See the Enterprise LDAP API documentation for more information. - /// + /// public IObservableEnterpriseLdapClient Ldap { get; private set; } /// @@ -41,15 +42,23 @@ /// /// /// See the Enterprise License API documentation for more information. - /// + /// public IObservableEnterpriseLicenseClient License { get; private set; } + /// + /// A client for GitHub's Enterprise Management Console API + /// + /// + /// See the Enterprise Management Console API documentation for more information. + /// + public IObservableEnterpriseManagementConsoleClient ManagementConsole { get; private set; } + /// /// A client for GitHub's Enterprise Organization API /// /// /// See the Enterprise Organization API documentation for more information. - /// + /// public IObservableEnterpriseOrganizationClient Organization { get; private set; } /// @@ -57,7 +66,7 @@ /// /// /// See the Enterprise Search Indexing API documentation for more information. - /// + /// public IObservableEnterpriseSearchIndexingClient SearchIndexing { get; private set; } /// diff --git a/Octokit.Reactive/Clients/Enterprise/ObservableEnterpriseManagementConsoleClient.cs b/Octokit.Reactive/Clients/Enterprise/ObservableEnterpriseManagementConsoleClient.cs new file mode 100644 index 00000000..b887da76 --- /dev/null +++ b/Octokit.Reactive/Clients/Enterprise/ObservableEnterpriseManagementConsoleClient.cs @@ -0,0 +1,52 @@ +using System; +using System.Reactive.Threading.Tasks; + +namespace Octokit.Reactive +{ + /// + /// A client for GitHub's Enterprise Management Console API + /// + /// + /// See the Enterprise Management Console API documentation for more information. + /// + public class ObservableEnterpriseManagementConsoleClient : IObservableEnterpriseManagementConsoleClient + { + readonly IEnterpriseManagementConsoleClient _client; + + public ObservableEnterpriseManagementConsoleClient(IGitHubClient client) + { + Ensure.ArgumentNotNull(client, "client"); + + _client = client.Enterprise.ManagementConsole; + } + + /// + /// Gets GitHub Enterprise Maintenance Mode Status + /// + /// + /// https://developer.github.com/v3/enterprise/management_console/#check-maintenance-status + /// + /// The . + public IObservable GetMaintenanceMode(string managementConsolePassword) + { + Ensure.ArgumentNotNullOrEmptyString(managementConsolePassword, "managementConsolePassword"); + + return _client.GetMaintenanceMode(managementConsolePassword).ToObservable(); + } + + /// + /// Sets GitHub Enterprise Maintenance Mode + /// + /// + /// https://developer.github.com/v3/enterprise/management_console/#check-maintenance-status + /// + /// The . + public IObservable EditMaintenanceMode(UpdateMaintenanceRequest maintenance, string managementConsolePassword) + { + Ensure.ArgumentNotNull(maintenance, "maintenance"); + Ensure.ArgumentNotNullOrEmptyString(managementConsolePassword, "managementConsolePassword"); + + return _client.EditMaintenanceMode(maintenance, managementConsolePassword).ToObservable(); + } + } +} diff --git a/Octokit.Tests.Integration/Clients/Enterprise/EnterpriseManagementConsoleClientTests.cs b/Octokit.Tests.Integration/Clients/Enterprise/EnterpriseManagementConsoleClientTests.cs new file mode 100644 index 00000000..ea62615b --- /dev/null +++ b/Octokit.Tests.Integration/Clients/Enterprise/EnterpriseManagementConsoleClientTests.cs @@ -0,0 +1,88 @@ +using System; +using System.Threading.Tasks; +using Octokit; +using Octokit.Tests.Integration; +using Octokit.Tests.Integration.Helpers; +using Xunit; + +public class EnterpriseManagementConsoleClientTests +{ + readonly IGitHubClient _github; + + public EnterpriseManagementConsoleClientTests() + { + _github = EnterpriseHelper.GetAuthenticatedClient(); + } + + [GitHubEnterpriseManagementConsoleTest] + public async Task CanGetMaintenanceMode() + { + var maintenance = await _github.Enterprise.ManagementConsole.GetMaintenanceMode(EnterpriseHelper.ManagementConsolePassword); + + Assert.NotNull(maintenance); + } + + [GitHubEnterpriseManagementConsoleTest] + public async Task CanSetMaintenanceModeOff() + { + using (_github.CreateMaintenanceModeContext(true)) + { + // Set maintenance mode OFF now + var maintenance = await + _github.Enterprise.ManagementConsole.EditMaintenanceMode( + new UpdateMaintenanceRequest(), + EnterpriseHelper.ManagementConsolePassword); + + Assert.Equal(maintenance.Status, MaintenanceModeStatus.Off); + } + } + + [GitHubEnterpriseManagementConsoleTest] + public async Task CanSetMaintenanceModeOnNow() + { + using (_github.CreateMaintenanceModeContext(false)) + { + // Set maintenance mode ON now + var maintenance = await + _github.Enterprise.ManagementConsole.EditMaintenanceMode( + new UpdateMaintenanceRequest( + new UpdateMaintenanceRequestDetails(true)), + EnterpriseHelper.ManagementConsolePassword); + + Assert.Equal(maintenance.Status, MaintenanceModeStatus.On); + } + } + + [GitHubEnterpriseManagementConsoleTest] + public async Task CanScheduleMaintenanceModeOnWithDateTime() + { + using (_github.CreateMaintenanceModeContext(false)) + { + // Schedule maintenance mode ON in 5 minutes + var scheduledTime = DateTimeOffset.Now.AddMinutes(5); + var maintenance = await + _github.Enterprise.ManagementConsole.EditMaintenanceMode( + new UpdateMaintenanceRequest( + new UpdateMaintenanceRequestDetails(true, scheduledTime)), + EnterpriseHelper.ManagementConsolePassword); + + Assert.Equal(maintenance.Status, MaintenanceModeStatus.Scheduled); + } + } + + [GitHubEnterpriseManagementConsoleTest] + public async Task CanScheduleMaintenanceModeOnWithPhrase() + { + using (_github.CreateMaintenanceModeContext(false)) + { + // Schedule maintenance mode ON with phrase + var maintenance = await + _github.Enterprise.ManagementConsole.EditMaintenanceMode( + new UpdateMaintenanceRequest( + new UpdateMaintenanceRequestDetails(true, "tomorrow at 5pm")), + EnterpriseHelper.ManagementConsolePassword); + + Assert.Equal(maintenance.Status, MaintenanceModeStatus.Scheduled); + } + } +} \ No newline at end of file diff --git a/Octokit.Tests.Integration/EnterpriseHelper.cs b/Octokit.Tests.Integration/EnterpriseHelper.cs index 6db94379..bd9f5cde 100644 --- a/Octokit.Tests.Integration/EnterpriseHelper.cs +++ b/Octokit.Tests.Integration/EnterpriseHelper.cs @@ -69,7 +69,7 @@ namespace Octokit.Tests.Integration static EnterpriseHelper() { // Force reading of environment variables. - // This wasn't happening if UserName/Organization were + // This wasn't happening if UserName/Organization were // retrieved before Credentials. Debug.WriteIf(Credentials == null, "No credentials specified."); } @@ -108,10 +108,9 @@ namespace Octokit.Tests.Integration get { return Environment.GetEnvironmentVariable("OCTOKIT_GHE_CLIENTSECRET"); } } - public static void DeleteUser(IConnection connection, User user) + public static string ManagementConsolePassword { - if (user != null) - DeleteUser(connection, user.Login); + get { return Environment.GetEnvironmentVariable("OCTOKIT_GHE_CONSOLEPASSWORD"); } } public static void DeleteUser(IConnection connection, string username) @@ -162,6 +161,19 @@ namespace Octokit.Tests.Integration } } + public static void SetMaintenanceMode(IConnection connection, bool enabled) + { + try + { + var client = new GitHubClient(connection); + client.Enterprise.ManagementConsole.EditMaintenanceMode( + new UpdateMaintenanceRequest(new UpdateMaintenanceRequestDetails(enabled)), + EnterpriseHelper.ManagementConsolePassword) + .Wait(TimeSpan.FromSeconds(15)); + } + catch { } + } + public static IGitHubClient GetAuthenticatedClient() { return new GitHubClient(new ProductHeaderValue("OctokitEnterpriseTests"), GitHubEnterpriseUrl) diff --git a/Octokit.Tests.Integration/Helpers/GitHubEnterpriseManagementConsoleTestAttribute.cs b/Octokit.Tests.Integration/Helpers/GitHubEnterpriseManagementConsoleTestAttribute.cs new file mode 100644 index 00000000..4b219b22 --- /dev/null +++ b/Octokit.Tests.Integration/Helpers/GitHubEnterpriseManagementConsoleTestAttribute.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Octokit.Tests.Integration +{ + public class GitHubEnterpriseManagementConsoleTestDiscoverer : IXunitTestCaseDiscoverer + { + readonly IMessageSink diagnosticMessageSink; + + public GitHubEnterpriseManagementConsoleTestDiscoverer(IMessageSink diagnosticMessageSink) + { + this.diagnosticMessageSink = diagnosticMessageSink; + } + + public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + { + if (Helper.Credentials == null) + return Enumerable.Empty(); + + if (!EnterpriseHelper.IsGitHubEnterpriseEnabled) + return Enumerable.Empty(); + + if (String.IsNullOrEmpty(EnterpriseHelper.ManagementConsolePassword)) + return Enumerable.Empty(); + + return new[] { new XunitTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod) }; + } + } + + [XunitTestCaseDiscoverer("Octokit.Tests.Integration.GitHubEnterpriseManagementConsoleTestDiscoverer", "Octokit.Tests.Integration")] + public class GitHubEnterpriseManagementConsoleTestAttribute : FactAttribute + { + } +} \ No newline at end of file diff --git a/Octokit.Tests.Integration/Helpers/GithubClientExtensions.cs b/Octokit.Tests.Integration/Helpers/GithubClientExtensions.cs index 1dbd2d68..329b6360 100644 --- a/Octokit.Tests.Integration/Helpers/GithubClientExtensions.cs +++ b/Octokit.Tests.Integration/Helpers/GithubClientExtensions.cs @@ -79,5 +79,10 @@ VO/+BCBsaoT4g1FFOmJhbBAD3G72yslBnUJmqKP/39pi return new GpgKeyContext(client.Connection, key); } + + internal static MaintenanceModeContext CreateMaintenanceModeContext(this IGitHubClient client, bool enabled) + { + return new MaintenanceModeContext(client.Connection, enabled); + } } } \ No newline at end of file diff --git a/Octokit.Tests.Integration/Helpers/MaintenanceModeContext.cs b/Octokit.Tests.Integration/Helpers/MaintenanceModeContext.cs new file mode 100644 index 00000000..ccfc80ea --- /dev/null +++ b/Octokit.Tests.Integration/Helpers/MaintenanceModeContext.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Octokit.Reactive; + +namespace Octokit.Tests.Integration.Helpers +{ + internal sealed class MaintenanceModeContext : IDisposable + { + internal MaintenanceModeContext(IConnection connection, bool enabled) + { + _connection = connection; + + // Ensure maintenance mode is in the desired initial state + EnterpriseHelper.SetMaintenanceMode(_connection, enabled); + } + + private IConnection _connection; + + public void Dispose() + { + // Ensure maintenance mode is OFF + EnterpriseHelper.SetMaintenanceMode(_connection, false); + } + } +} diff --git a/Octokit.Tests/Clients/Enterprise/EnterpriseManagementConsoleClientTests.cs b/Octokit.Tests/Clients/Enterprise/EnterpriseManagementConsoleClientTests.cs new file mode 100644 index 00000000..6ba091b8 --- /dev/null +++ b/Octokit.Tests/Clients/Enterprise/EnterpriseManagementConsoleClientTests.cs @@ -0,0 +1,102 @@ +using System; +using System.Threading.Tasks; +using NSubstitute; +using Xunit; + +namespace Octokit.Tests.Clients +{ + public class EnterpriseManagementConsoleClientTests + { + public class TheGetMaintenanceModeMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var connection = Substitute.For(); + var client = new EnterpriseManagementConsoleClient(connection); + + string expectedUri = "setup/api/maintenance?api_key=Password01"; + client.GetMaintenanceMode("Password01"); + + connection.Received().Get(Arg.Is(u => u.ToString() == expectedUri)); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var connection = Substitute.For(); + var client = new EnterpriseManagementConsoleClient(connection); + + await Assert.ThrowsAsync(() => client.GetMaintenanceMode(null)); + } + + [Fact] + public async Task EnsuresNonEmptyArguments() + { + var connection = Substitute.For(); + var client = new EnterpriseManagementConsoleClient(connection); + + await Assert.ThrowsAsync(() => client.GetMaintenanceMode("")); + } + } + + public class TheEditMaintenanceModeMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var connection = Substitute.For(); + var client = new EnterpriseManagementConsoleClient(connection); + + string expectedUri = "setup/api/maintenance?api_key=Password01"; + client.EditMaintenanceMode(new UpdateMaintenanceRequest(), "Password01"); + + connection.Received().Post( + Arg.Is(u => u.ToString() == expectedUri), + Arg.Any()); + } + + [Fact] + public void PassesRequestObject() + { + var connection = Substitute.For(); + var client = new EnterpriseManagementConsoleClient(connection); + + client.EditMaintenanceMode(new UpdateMaintenanceRequest(new UpdateMaintenanceRequestDetails(true)), "Password01"); + + connection.Received().Post( + Arg.Any(), + Arg.Is(a => + a == "maintenance={\"enabled\":true,\"when\":\"now\"}")); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var connection = Substitute.For(); + var client = new EnterpriseManagementConsoleClient(connection); + + await Assert.ThrowsAsync(() => client.EditMaintenanceMode(null, "Password01")); + await Assert.ThrowsAsync(() => client.EditMaintenanceMode(new UpdateMaintenanceRequest(), null)); + } + + [Fact] + public async Task EnsuresNonEmptyArguments() + { + var connection = Substitute.For(); + var client = new EnterpriseManagementConsoleClient(connection); + + await Assert.ThrowsAsync(() => client.EditMaintenanceMode(new UpdateMaintenanceRequest(), "")); + } + } + + public class TheCtor + { + [Fact] + public void EnsuresNonNullArguments() + { + Assert.Throws(() => new EnterpriseManagementConsoleClient(null)); + } + } + } +} diff --git a/Octokit.Tests/Reactive/Enterprise/ObservableEnterpriseManagementConsoleClientTests.cs b/Octokit.Tests/Reactive/Enterprise/ObservableEnterpriseManagementConsoleClientTests.cs new file mode 100644 index 00000000..c02bdeb0 --- /dev/null +++ b/Octokit.Tests/Reactive/Enterprise/ObservableEnterpriseManagementConsoleClientTests.cs @@ -0,0 +1,92 @@ +using System; +using NSubstitute; +using Octokit.Reactive; +using Xunit; + +namespace Octokit.Tests +{ + public class ObservableEnterpriseManagementConsoleClientTests + { + public class TheGetMaintenanceModeMethod + { + [Fact] + public void CallsIntoClient() + { + var github = Substitute.For(); + var client = new ObservableEnterpriseManagementConsoleClient(github); + + client.GetMaintenanceMode("Password01"); + + github.Enterprise.ManagementConsole.Received(1). + GetMaintenanceMode(Arg.Is("Password01")); + } + + [Fact] + public void EnsuresNonNullArguments() + { + var github = Substitute.For(); + var client = new ObservableEnterpriseManagementConsoleClient(github); + + Assert.Throws(() => client.GetMaintenanceMode(null)); + } + + [Fact] + public void EnsuresNonEmptyArguments() + { + var github = Substitute.For(); + var client = new ObservableEnterpriseManagementConsoleClient(github); + + Assert.Throws(() => client.GetMaintenanceMode("")); + } + } + + public class TheEditMaintenanceModeMethod + { + [Fact] + public void CallsIntoClient() + { + var github = Substitute.For(); + var client = new ObservableEnterpriseManagementConsoleClient(github); + + client.EditMaintenanceMode( + new UpdateMaintenanceRequest(new UpdateMaintenanceRequestDetails(true)), + "Password01"); + + github.Enterprise.ManagementConsole.Received(1). + EditMaintenanceMode( + Arg.Is(a => + a.Maintenance.Enabled == true && + a.Maintenance.When == "now"), + Arg.Is("Password01")); + } + + [Fact] + public void EnsuresNonNullArguments() + { + var github = Substitute.For(); + var client = new ObservableEnterpriseManagementConsoleClient(github); + + Assert.Throws(() => client.EditMaintenanceMode(null, "Password01")); + Assert.Throws(() => client.EditMaintenanceMode(new UpdateMaintenanceRequest(), null)); + } + + [Fact] + public void EnsuresNonEmptyArguments() + { + var github = Substitute.For(); + var client = new ObservableEnterpriseManagementConsoleClient(github); + + Assert.Throws(() => client.EditMaintenanceMode(new UpdateMaintenanceRequest(), "")); + } + } + + public class TheCtor + { + [Fact] + public void EnsuresNonNullArguments() + { + Assert.Throws(() => new ObservableEnterpriseManagementConsoleClient(null)); + } + } + } +} diff --git a/Octokit/Clients/Enterprise/EnterpriseClient.cs b/Octokit/Clients/Enterprise/EnterpriseClient.cs index d5b6d30b..d3d37fd4 100644 --- a/Octokit/Clients/Enterprise/EnterpriseClient.cs +++ b/Octokit/Clients/Enterprise/EnterpriseClient.cs @@ -17,6 +17,7 @@ AdminStats = new EnterpriseAdminStatsClient(apiConnection); Ldap = new EnterpriseLdapClient(apiConnection); License = new EnterpriseLicenseClient(apiConnection); + ManagementConsole = new EnterpriseManagementConsoleClient(apiConnection); Organization = new EnterpriseOrganizationClient(apiConnection); SearchIndexing = new EnterpriseSearchIndexingClient(apiConnection); PreReceiveEnvironment = new EnterprisePreReceiveEnvironmentsClient(apiConnection); @@ -27,7 +28,7 @@ /// /// /// See the Enterprise Admin Stats API documentation for more information. - /// + /// public IEnterpriseAdminStatsClient AdminStats { get; private set; } /// @@ -35,7 +36,7 @@ /// /// /// See the Enterprise LDAP API documentation for more information. - /// + /// public IEnterpriseLdapClient Ldap { get; private set; } /// @@ -43,15 +44,23 @@ /// /// /// See the Enterprise License API documentation for more information. - /// + /// public IEnterpriseLicenseClient License { get; private set; } + /// + /// A client for GitHub's Enterprise Management Console API + /// + /// + /// See the Enterprise Management Console API documentation for more information. + /// + public IEnterpriseManagementConsoleClient ManagementConsole { get; private set; } + /// /// A client for GitHub's Enterprise Organization API /// /// /// See the Enterprise Organization API documentation for more information. - /// + /// public IEnterpriseOrganizationClient Organization { get; private set; } /// @@ -59,7 +68,7 @@ /// /// /// See the Enterprise Search Indexing API documentation for more information. - /// + /// public IEnterpriseSearchIndexingClient SearchIndexing { get; private set; } /// diff --git a/Octokit/Clients/Enterprise/EnterpriseManagementConsoleClient.cs b/Octokit/Clients/Enterprise/EnterpriseManagementConsoleClient.cs new file mode 100644 index 00000000..7a3ada73 --- /dev/null +++ b/Octokit/Clients/Enterprise/EnterpriseManagementConsoleClient.cs @@ -0,0 +1,50 @@ +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// A client for GitHub's Enterprise Management Console API + /// + /// + /// See the Enterprise Management Console API documentation for more information. + /// + public class EnterpriseManagementConsoleClient : ApiClient, IEnterpriseManagementConsoleClient + { + public EnterpriseManagementConsoleClient(IApiConnection apiConnection) + : base(apiConnection) + { } + + /// + /// Gets GitHub Enterprise Maintenance Mode Status + /// + /// + /// https://developer.github.com/v3/enterprise/management_console/#check-maintenance-status + /// + /// The . + public Task GetMaintenanceMode(string managementConsolePassword) + { + Ensure.ArgumentNotNullOrEmptyString(managementConsolePassword, "managementConsolePassword"); + + var endpoint = ApiUrls.EnterpriseManagementConsoleMaintenance(managementConsolePassword, ApiConnection.Connection.BaseAddress); + + return ApiConnection.Get(endpoint); + } + + /// + /// Sets GitHub Enterprise Maintenance Mode + /// + /// + /// https://developer.github.com/v3/enterprise/management_console/#check-maintenance-status + /// + /// The . + public Task EditMaintenanceMode(UpdateMaintenanceRequest maintenance, string managementConsolePassword) + { + Ensure.ArgumentNotNull(maintenance, "maintenance"); + Ensure.ArgumentNotNullOrEmptyString(managementConsolePassword, "managementConsolePassword"); + + var endpoint = ApiUrls.EnterpriseManagementConsoleMaintenance(managementConsolePassword, ApiConnection.Connection.BaseAddress); + + return ApiConnection.Post(endpoint, maintenance.ToFormUrlEncodedParameterString()); + } + } +} diff --git a/Octokit/Clients/Enterprise/IEnterpriseClient.cs b/Octokit/Clients/Enterprise/IEnterpriseClient.cs index 8f3ace37..8a7841d5 100644 --- a/Octokit/Clients/Enterprise/IEnterpriseClient.cs +++ b/Octokit/Clients/Enterprise/IEnterpriseClient.cs @@ -13,7 +13,7 @@ /// /// /// See the Enterprise Admin Stats API documentation for more information. - /// + /// IEnterpriseAdminStatsClient AdminStats { get; } /// @@ -21,7 +21,7 @@ /// /// /// See the Enterprise LDAP API documentation for more information. - /// + /// IEnterpriseLdapClient Ldap { get; } /// @@ -29,15 +29,23 @@ /// /// /// See the Enterprise License API documentation for more information. - /// + /// IEnterpriseLicenseClient License { get; } + /// + /// A client for GitHub's Enterprise Management Console API + /// + /// + /// See the Enterprise Management Console API documentation for more information. + /// + IEnterpriseManagementConsoleClient ManagementConsole { get; } + /// /// A client for GitHub's Enterprise Organization API /// /// /// See the Enterprise Organization API documentation for more information. - /// + /// IEnterpriseOrganizationClient Organization { get; } /// @@ -45,7 +53,7 @@ /// /// /// See the Enterprise Search Indexing API documentation for more information. - /// + /// IEnterpriseSearchIndexingClient SearchIndexing { get; } /// diff --git a/Octokit/Clients/Enterprise/IEnterpriseManagementConsoleClient.cs b/Octokit/Clients/Enterprise/IEnterpriseManagementConsoleClient.cs new file mode 100644 index 00000000..00e46075 --- /dev/null +++ b/Octokit/Clients/Enterprise/IEnterpriseManagementConsoleClient.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// A client for GitHub's Enterprise Management Console API + /// + /// + /// See the Enterprise Management Console API documentation for more information. + /// + public interface IEnterpriseManagementConsoleClient + { + /// + /// Gets GitHub Enterprise Maintenance Mode Status + /// + /// + /// https://developer.github.com/v3/enterprise/management_console/#check-maintenance-status + /// + /// The . + Task GetMaintenanceMode(string managementConsolePassword); + + /// + /// Sets GitHub Enterprise Maintenance Mode + /// + /// + /// https://developer.github.com/v3/enterprise/management_console/#check-maintenance-status + /// + /// The . + Task EditMaintenanceMode(UpdateMaintenanceRequest maintenance, string managementConsolePassword); + } +} diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs index 1b77364a..07f47607 100644 --- a/Octokit/Helpers/ApiUrls.cs +++ b/Octokit/Helpers/ApiUrls.cs @@ -2548,6 +2548,18 @@ namespace Octokit return "orgs/{0}/migrations/{1}/repos/{2}/lock".FormatUri(org, id, repo); } + public static Uri EnterpriseManagementConsoleMaintenance(string managementConsolePassword, Uri baseAddress) + { + if (baseAddress != null + && baseAddress.ToString().EndsWith("/api/v3/", StringComparison.OrdinalIgnoreCase)) + { + // note: leading slash here means the /api/v3/ prefix inherited from baseAddress is ignored + return "/setup/api/maintenance?api_key={0}".FormatUri(managementConsolePassword); + } + + return "setup/api/maintenance?api_key={0}".FormatUri(managementConsolePassword); + } + public static Uri EnterpriseOrganization() { return "admin/organizations".FormatUri(); diff --git a/Octokit/Helpers/ReferenceExtensions.cs b/Octokit/Helpers/ReferenceExtensions.cs index fe3917b6..97c5ba82 100644 --- a/Octokit/Helpers/ReferenceExtensions.cs +++ b/Octokit/Helpers/ReferenceExtensions.cs @@ -26,7 +26,7 @@ namespace Octokit.Helpers if (branchName.StartsWith("refs/heads")) { - throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, "The specified branch name '{0}' appears to be a ref name and not a branch name because it starts with the string 'refs/heads'. Either specify just the branch name or use the Create method if you need to specify the full ref name", branchName), "branchName"); + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "The specified branch name '{0}' appears to be a ref name and not a branch name because it starts with the string 'refs/heads'. Either specify just the branch name or use the Create method if you need to specify the full ref name", branchName), "branchName"); } var newReference = new NewReference("refs/heads/" + branchName, baseReference.Object.Sha); @@ -48,7 +48,7 @@ namespace Octokit.Helpers if (branchName.StartsWith("refs/heads")) { - throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, "The specified branch name '{0}' appears to be a ref name and not a branch name because it starts with the string 'refs/heads'. Either specify just the branch name or use the Create method if you need to specify the full ref name", branchName), "branchName"); + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "The specified branch name '{0}' appears to be a ref name and not a branch name because it starts with the string 'refs/heads'. Either specify just the branch name or use the Create method if you need to specify the full ref name", branchName), "branchName"); } var baseBranch = await referencesClient.Get(owner, name, "heads/master").ConfigureAwait(false); diff --git a/Octokit/Models/Request/Enterprise/UpdateMaintenanceRequest.cs b/Octokit/Models/Request/Enterprise/UpdateMaintenanceRequest.cs new file mode 100644 index 00000000..45621388 --- /dev/null +++ b/Octokit/Models/Request/Enterprise/UpdateMaintenanceRequest.cs @@ -0,0 +1,107 @@ +using System; +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class UpdateMaintenanceRequest : FormUrlEncodedParameters + { + /// + /// Maintenance request with default details (results in Maintenance mode being turned off immediately) + /// + public UpdateMaintenanceRequest() + { + Maintenance = new UpdateMaintenanceRequestDetails(); + } + + /// + /// Maintenance request with specific details + /// + public UpdateMaintenanceRequest(UpdateMaintenanceRequestDetails maintenance) + { + Ensure.ArgumentNotNull(maintenance, "maintenance"); + + Maintenance = maintenance; + } + + /// + /// Details for the maintenance request + /// + public UpdateMaintenanceRequestDetails Maintenance { get; protected set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "Maintenance: {0}", this.Maintenance.DebuggerDisplay); + } + } + } + + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class UpdateMaintenanceRequestDetails + { + /// + /// Maintenance request details with default values (results in Maintenance mode being turned off immediately) + /// + public UpdateMaintenanceRequestDetails() + { } + + /// + /// Maintenance request details to enable/disable maintenance mode immediately + /// + /// true to enable, false to disable + public UpdateMaintenanceRequestDetails(bool enabled) + { + Enabled = enabled; + When = "now"; + } + + /// + /// Maintenance request details to enable/disable maintenance at a specified time (only applicable when enabling maintenance) + /// + /// true to enable, false to disable + /// A phrase specifying when maintenance mode will be enabled. Phrase uses humanised forms supported by the mojombo/chronic library used by the GitHub API + /// such as "this friday at 5pm" or "5 minutes before midday tomorrow" + /// If enabled is false, the when parameter is ignored and maintenance is turned off immediately + public UpdateMaintenanceRequestDetails(bool enabled, string when) + { + Ensure.ArgumentNotNullOrEmptyString(when, "when"); + + Enabled = enabled; + When = when; + } + + /// + /// Maintenance request details to enable/disable maintenance at a specified time (only applicable when enabling maintenance) + /// + /// true to enable, false to disable + /// A specifying when maintenance mode will be enabled. + /// If enabled is false, the when parameter is ignored and maintenance is turned off immediately + public UpdateMaintenanceRequestDetails(bool enabled, DateTimeOffset when) + { + Enabled = enabled; + When = when.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", + CultureInfo.InvariantCulture); + } + + /// + /// Whether maintenance mode is enabled or disabled + /// + public bool Enabled { get; protected set; } + + /// + /// When maintenance mode will take effect (only applicable when enabling maintenance) + /// + public string When { get; protected set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "Enabled: {0} When: {1}", this.Enabled, this.When); + } + } + } +} diff --git a/Octokit/Models/Request/FormUrlEncodedParameters.cs b/Octokit/Models/Request/FormUrlEncodedParameters.cs new file mode 100644 index 00000000..7b615d52 --- /dev/null +++ b/Octokit/Models/Request/FormUrlEncodedParameters.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Reflection; +using Octokit.Internal; + +namespace Octokit +{ + /// + /// Base class for classes which represent UrlFormEncoded parameters to certain API endpoints. + /// + public abstract class FormUrlEncodedParameters + { + /// + /// Converts the derived object into a UrlFormEncoded parameter string containing named parameters and their json serialized values + /// This format is required for particular API calls (eg the GitHub Enterprise Management Console API) that take a parameter formatted as json but not in the request body + /// + [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings")] + public string ToFormUrlEncodedParameterString() + { + var parameters = new List(); + foreach (var prop in GetPropertyParametersForType(this.GetType())) + { + parameters.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", prop.Key, prop.GetValue(this))); + } + + return string.Join("&", parameters); + } + + static List GetPropertyParametersForType(Type type) + { + return type.GetAllProperties() + .Where(p => p.Name != "DebuggerDisplay") + .Select(p => new JsonParameter(p)) + .ToList(); + } + + class JsonParameter + { + readonly PropertyInfo _property; + public JsonParameter(PropertyInfo property) + { + _property = property; + Key = GetParameterKeyFromProperty(property); + } + + public string Key { get; private set; } + + public string GetValue(object instance) + { + var value = _property.GetValue(instance, null); + return value != null ? new SimpleJsonSerializer().Serialize(value) : null; + } + + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", + Justification = "GitHub API depends on lower case strings")] + static string GetParameterKeyFromProperty(PropertyInfo property) + { + var attribute = property.GetCustomAttributes(typeof(ParameterAttribute), false) + .Cast() + .FirstOrDefault(attr => attr.Key != null); + + return attribute == null + ? property.Name.ToLowerInvariant() + : attribute.Key; + } + } + } +} diff --git a/Octokit/Models/Response/Enterprise/AdminStats.cs b/Octokit/Models/Response/Enterprise/AdminStats.cs index 67e3dc71..8affd73d 100644 --- a/Octokit/Models/Response/Enterprise/AdminStats.cs +++ b/Octokit/Models/Response/Enterprise/AdminStats.cs @@ -100,7 +100,7 @@ namespace Octokit Comments != null ? "Comments," : "" ).Trim(','); - return String.Format(CultureInfo.InvariantCulture, "Statistics: {0}", fieldsPresent); + return string.Format(CultureInfo.InvariantCulture, "Statistics: {0}", fieldsPresent); } } } diff --git a/Octokit/Models/Response/Enterprise/AdminStatsComments.cs b/Octokit/Models/Response/Enterprise/AdminStatsComments.cs index 2502b9a9..1b900f79 100644 --- a/Octokit/Models/Response/Enterprise/AdminStatsComments.cs +++ b/Octokit/Models/Response/Enterprise/AdminStatsComments.cs @@ -45,7 +45,7 @@ namespace Octokit { get { - return String.Format(CultureInfo.InvariantCulture, "TotalCommitComments: {0} TotalGistComments: {1} TotalIssueComments: {2} TotalPullRequestComments: {3}", TotalCommitComments, TotalGistComments, TotalIssueComments, TotalPullRequestComments); + return string.Format(CultureInfo.InvariantCulture, "TotalCommitComments: {0} TotalGistComments: {1} TotalIssueComments: {2} TotalPullRequestComments: {3}", TotalCommitComments, TotalGistComments, TotalIssueComments, TotalPullRequestComments); } } } diff --git a/Octokit/Models/Response/Enterprise/AdminStatsGists.cs b/Octokit/Models/Response/Enterprise/AdminStatsGists.cs index 2b6488c2..379b867f 100644 --- a/Octokit/Models/Response/Enterprise/AdminStatsGists.cs +++ b/Octokit/Models/Response/Enterprise/AdminStatsGists.cs @@ -38,7 +38,7 @@ namespace Octokit { get { - return String.Format(CultureInfo.InvariantCulture, "TotalGists: {0} PrivateGists: {1} PublicGists: {2}", TotalGists, PrivateGists, PublicGists); + return string.Format(CultureInfo.InvariantCulture, "TotalGists: {0} PrivateGists: {1} PublicGists: {2}", TotalGists, PrivateGists, PublicGists); } } } diff --git a/Octokit/Models/Response/Enterprise/AdminStatsHooks.cs b/Octokit/Models/Response/Enterprise/AdminStatsHooks.cs index b960d6d4..96fa12a9 100644 --- a/Octokit/Models/Response/Enterprise/AdminStatsHooks.cs +++ b/Octokit/Models/Response/Enterprise/AdminStatsHooks.cs @@ -38,7 +38,7 @@ namespace Octokit { get { - return String.Format(CultureInfo.InvariantCulture, "TotalHooks: {0} ActiveHooks: {1} InactiveHooks: {2}", TotalHooks, ActiveHooks, InactiveHooks); + return string.Format(CultureInfo.InvariantCulture, "TotalHooks: {0} ActiveHooks: {1} InactiveHooks: {2}", TotalHooks, ActiveHooks, InactiveHooks); } } } diff --git a/Octokit/Models/Response/Enterprise/AdminStatsIssues.cs b/Octokit/Models/Response/Enterprise/AdminStatsIssues.cs index 1b02ae75..b95e359a 100644 --- a/Octokit/Models/Response/Enterprise/AdminStatsIssues.cs +++ b/Octokit/Models/Response/Enterprise/AdminStatsIssues.cs @@ -38,7 +38,7 @@ namespace Octokit { get { - return String.Format(CultureInfo.InvariantCulture, "TotalIssues: {0} OpenIssues: {1} ClosedIssues: {2}", TotalIssues, OpenIssues, ClosedIssues); + return string.Format(CultureInfo.InvariantCulture, "TotalIssues: {0} OpenIssues: {1} ClosedIssues: {2}", TotalIssues, OpenIssues, ClosedIssues); } } } diff --git a/Octokit/Models/Response/Enterprise/AdminStatsMilestones.cs b/Octokit/Models/Response/Enterprise/AdminStatsMilestones.cs index 2fd1e8e8..4b5d58b0 100644 --- a/Octokit/Models/Response/Enterprise/AdminStatsMilestones.cs +++ b/Octokit/Models/Response/Enterprise/AdminStatsMilestones.cs @@ -38,7 +38,7 @@ namespace Octokit { get { - return String.Format(CultureInfo.InvariantCulture, "TotalMilestones: {0} OpenMilestones: {1} ClosedMilestones: {2}", TotalMilestones, OpenMilestones, ClosedMilestones); + return string.Format(CultureInfo.InvariantCulture, "TotalMilestones: {0} OpenMilestones: {1} ClosedMilestones: {2}", TotalMilestones, OpenMilestones, ClosedMilestones); } } } diff --git a/Octokit/Models/Response/Enterprise/AdminStatsOrgs.cs b/Octokit/Models/Response/Enterprise/AdminStatsOrgs.cs index 2149227a..e63d4005 100644 --- a/Octokit/Models/Response/Enterprise/AdminStatsOrgs.cs +++ b/Octokit/Models/Response/Enterprise/AdminStatsOrgs.cs @@ -45,7 +45,7 @@ namespace Octokit { get { - return String.Format(CultureInfo.InvariantCulture, "TotalOrgs: {0} DisabledOrgs: {1} TotalTeams: {2} TotalTeamMembers: {3}", TotalOrgs, DisabledOrgs, TotalTeams, TotalTeamMembers); + return string.Format(CultureInfo.InvariantCulture, "TotalOrgs: {0} DisabledOrgs: {1} TotalTeams: {2} TotalTeamMembers: {3}", TotalOrgs, DisabledOrgs, TotalTeams, TotalTeamMembers); } } } diff --git a/Octokit/Models/Response/Enterprise/AdminStatsPages.cs b/Octokit/Models/Response/Enterprise/AdminStatsPages.cs index 4f0965ec..9b6e2223 100644 --- a/Octokit/Models/Response/Enterprise/AdminStatsPages.cs +++ b/Octokit/Models/Response/Enterprise/AdminStatsPages.cs @@ -24,7 +24,7 @@ namespace Octokit { get { - return String.Format(CultureInfo.InvariantCulture, "TotalPages: {0}", TotalPages); + return string.Format(CultureInfo.InvariantCulture, "TotalPages: {0}", TotalPages); } } } diff --git a/Octokit/Models/Response/Enterprise/AdminStatsPulls.cs b/Octokit/Models/Response/Enterprise/AdminStatsPulls.cs index 44e6cf34..286470a3 100644 --- a/Octokit/Models/Response/Enterprise/AdminStatsPulls.cs +++ b/Octokit/Models/Response/Enterprise/AdminStatsPulls.cs @@ -48,7 +48,7 @@ namespace Octokit { get { - return String.Format(CultureInfo.InvariantCulture, "TotalPulls: {0} MergedPulls: {1} MergeablePulls: {2} UnmergeablePulls: {3}", TotalPulls, MergedPulls, MergeablePulls, UnmergeablePulls); + return string.Format(CultureInfo.InvariantCulture, "TotalPulls: {0} MergedPulls: {1} MergeablePulls: {2} UnmergeablePulls: {3}", TotalPulls, MergedPulls, MergeablePulls, UnmergeablePulls); } } } diff --git a/Octokit/Models/Response/Enterprise/AdminStatsRepos.cs b/Octokit/Models/Response/Enterprise/AdminStatsRepos.cs index 3895389b..23dcab03 100644 --- a/Octokit/Models/Response/Enterprise/AdminStatsRepos.cs +++ b/Octokit/Models/Response/Enterprise/AdminStatsRepos.cs @@ -59,7 +59,7 @@ namespace Octokit { get { - return String.Format(CultureInfo.InvariantCulture, "TotalRepos: {0} RootRepos: {1} ForkRepos: {2} OrgRepos: {3} TotalPushes: {4} TotalWikis: {5}", TotalRepos, RootRepos, ForkRepos, OrgRepos, TotalPushes, TotalWikis); + return string.Format(CultureInfo.InvariantCulture, "TotalRepos: {0} RootRepos: {1} ForkRepos: {2} OrgRepos: {3} TotalPushes: {4} TotalWikis: {5}", TotalRepos, RootRepos, ForkRepos, OrgRepos, TotalPushes, TotalWikis); } } } diff --git a/Octokit/Models/Response/Enterprise/AdminStatsUsers.cs b/Octokit/Models/Response/Enterprise/AdminStatsUsers.cs index 4dcc8242..34347f37 100644 --- a/Octokit/Models/Response/Enterprise/AdminStatsUsers.cs +++ b/Octokit/Models/Response/Enterprise/AdminStatsUsers.cs @@ -38,7 +38,7 @@ namespace Octokit { get { - return String.Format(CultureInfo.InvariantCulture, "TotalUsers: {0} AdminUsers: {1} SuspendedUsers: {2}", TotalUsers, AdminUsers, SuspendedUsers); + return string.Format(CultureInfo.InvariantCulture, "TotalUsers: {0} AdminUsers: {1} SuspendedUsers: {2}", TotalUsers, AdminUsers, SuspendedUsers); } } } diff --git a/Octokit/Models/Response/Enterprise/LdapSyncResponse.cs b/Octokit/Models/Response/Enterprise/LdapSyncResponse.cs index 3ee598a1..c3aabdda 100644 --- a/Octokit/Models/Response/Enterprise/LdapSyncResponse.cs +++ b/Octokit/Models/Response/Enterprise/LdapSyncResponse.cs @@ -24,7 +24,7 @@ namespace Octokit { get { - return String.Format(CultureInfo.InvariantCulture, "Status: {0}", Status); + return string.Format(CultureInfo.InvariantCulture, "Status: {0}", Status); } } } diff --git a/Octokit/Models/Response/Enterprise/LicenseInfo.cs b/Octokit/Models/Response/Enterprise/LicenseInfo.cs index 03bd174c..1805ffe4 100644 --- a/Octokit/Models/Response/Enterprise/LicenseInfo.cs +++ b/Octokit/Models/Response/Enterprise/LicenseInfo.cs @@ -59,7 +59,7 @@ namespace Octokit { get { - return String.Format(CultureInfo.InvariantCulture, "Seats: {0} SeatsUsed: {1} DaysUntilExpiration: {2}", Seats, SeatsUsed, DaysUntilExpiration); + return string.Format(CultureInfo.InvariantCulture, "Seats: {0} SeatsUsed: {1} DaysUntilExpiration: {2}", Seats, SeatsUsed, DaysUntilExpiration); } } } diff --git a/Octokit/Models/Response/Enterprise/MaintenanceModeActiveProcesses.cs b/Octokit/Models/Response/Enterprise/MaintenanceModeActiveProcesses.cs new file mode 100644 index 00000000..57b475c8 --- /dev/null +++ b/Octokit/Models/Response/Enterprise/MaintenanceModeActiveProcesses.cs @@ -0,0 +1,34 @@ +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class MaintenanceModeActiveProcesses + { + public MaintenanceModeActiveProcesses() { } + + public MaintenanceModeActiveProcesses(string name, int number) + { + Name = name; + Number = number; + } + + public string Name { get; protected set; } + + public int Number { get; protected set; } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0} ({1})", Name, Number); + } + + internal string DebuggerDisplay + { + get + { + return this.ToString(); + } + } + } +} diff --git a/Octokit/Models/Response/Enterprise/MaintenanceModeResponse.cs b/Octokit/Models/Response/Enterprise/MaintenanceModeResponse.cs new file mode 100644 index 00000000..127aa509 --- /dev/null +++ b/Octokit/Models/Response/Enterprise/MaintenanceModeResponse.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using Octokit.Internal; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class MaintenanceModeResponse + { + public MaintenanceModeResponse() { } + + public MaintenanceModeResponse(MaintenanceModeStatus status, string scheduledTime, IReadOnlyList activeProcesses) + { + Status = status; + ScheduledTime = scheduledTime; + ActiveProcesses = activeProcesses; + } + + public StringEnum Status + { + get; + private set; + } + + public string ScheduledTime + { + get; + private set; + } + + [Parameter(Key = "connection_services")] + public IReadOnlyList ActiveProcesses + { + get; + private set; + } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, + "Status: {0} ScheduledTime: {1} Connections: {2}", + Status, + ScheduledTime ?? "", + string.Join(", ", ActiveProcesses)); + } + } + } + + public enum MaintenanceModeStatus + { + [Parameter(Value = "off")] + Off, + [Parameter(Value = "on")] + On, + [Parameter(Value = "scheduled")] + Scheduled + } +} \ No newline at end of file diff --git a/Octokit/Models/Response/Enterprise/SearchIndexingResponse.cs b/Octokit/Models/Response/Enterprise/SearchIndexingResponse.cs index 6b5b194d..fbb46eec 100644 --- a/Octokit/Models/Response/Enterprise/SearchIndexingResponse.cs +++ b/Octokit/Models/Response/Enterprise/SearchIndexingResponse.cs @@ -25,7 +25,7 @@ namespace Octokit { get { - return String.Format(CultureInfo.InvariantCulture, "Message: {0}", string.Join("\r\n", Message)); + return string.Format(CultureInfo.InvariantCulture, "Message: {0}", string.Join("\r\n", Message)); } } } diff --git a/docs/enterprise-administration.md b/docs/enterprise-administration.md new file mode 100644 index 00000000..b4c99060 --- /dev/null +++ b/docs/enterprise-administration.md @@ -0,0 +1,73 @@ +# GitHub Enterprise Administration + +Octokit also ships with support for the administration APIs that GitHub +Enterprise includes to script tasks for administrators. + +```c# +var enterprise = new Uri("https://github.myenterprise.com/"); +var github = new GitHubClient("some app name", enterprise); +github.Credentials = new Credentials("some-token-here"); + +var stats = await github.Enterprise.AdminStats.GetStatisticsUsers(); +Console.WriteLine($"Found {stats.AdminUsers} admins, {stats.TotalUsers} total users and {stats.SuspendedUsers} suspended users"); +``` + +Some caveats on using these APIs that you should be aware of: + + - only administrators of the GitHub Enterprise instance are able to access + these APIs + - administrators creating OAuth tokens to use this endpoint must ensure the + `site_admin` scope is set + - the [Management Console API](https://developer.github.com/enterprise/2.18/v3/enterprise/management_console/) + also require providing the password created during setup of the GitHub + Enterprise installation to confirm the action + +You can read more about this support [on the GitHub website](https://developer.github.com/enterprise/2.18/v3/enterprise-admin/). + + +## Management console + +To view the maintenance mode status of a GitHub Enteprise installation: + +```C# +var maintenance = await github.Enterprise.ManagementConsole.GetMaintenanceMode("management-console-password"); +``` + +To put the GitHub Enterprise installation into maintenance mode immediately: + +```C# +var request = new UpdateMaintenanceRequest(new UpdateMaintenanceRequestDetails(true)); +var maintenance = await github.Enterprise.ManagementConsole.EditMaintenanceMode( + request, + "management-console-password"); +``` + +You can also provide a human-friendly phrase based on the rules in +[`mojombo/chronic`](https://github.com/mojombo/chronic): + +```C# +var request = new UpdateMaintenanceRequest(new UpdateMaintenanceRequestDetails(true, "tomorrow at 5pm")); +var maintenance = await github.Enterprise.ManagementConsole.EditMaintenanceMode( + request, + "management-console-password"); +``` + +To put the GitHub Enterprise installation into maintenance mode after a period +of time: + +```C# +var scheduledTime = DateTimeOffset.Now.AddMinutes(10); +var request = new UpdateMaintenanceRequest(new UpdateMaintenanceRequestDetails(true, scheduledTime)); +var maintenance = await github.Enterprise.ManagementConsole.EditMaintenanceMode( + request, + "management-console-password"); +``` + +To disable maintenance mode, simple pass in `false` or leave the `request` +constructor empty: + +```c# +var maintenance = await github.Enterprise.ManagementConsole.EditMaintenanceMode( + new UpdateMaintenanceRequest(), // off by default if nothing specified + "management-console-password"); +``` diff --git a/script/configure-integration-tests.ps1 b/script/configure-integration-tests.ps1 index e06dacb8..4a3d6cdd 100644 --- a/script/configure-integration-tests.ps1 +++ b/script/configure-integration-tests.ps1 @@ -109,6 +109,7 @@ if (AskYesNoQuestion "Do you wish to enable GitHub Enterprise (GHE) Integration { VerifyEnvironmentVariable "GitHub Enterprise account name" "OCTOKIT_GHE_USERNAME" VerifyEnvironmentVariable "GitHub Enterprise account password" "OCTOKIT_GHE_PASSWORD" $true + VerifyEnvironmentVariable "GitHub Enterprise Management Console password" "OCTOKIT_GHE_CONSOLEPASSWORD" $true VerifyEnvironmentVariable "GitHub Enterprise OAuth token" "OCTOKIT_GHE_OAUTHTOKEN" VerifyEnvironmentVariable "GitHub Enterprise organization name" "OCTOKIT_GHE_ORGANIZATION" $true