diff --git a/.gitignore b/.gitignore index 4184f93872..db81db57cf 100644 --- a/.gitignore +++ b/.gitignore @@ -105,4 +105,7 @@ tools/* coverage-results/* # Rider -**/.idea/* \ No newline at end of file +**/.idea/* + +# macOS +.DS_Store \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 0c1ea17bae..77504b0729 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,5 +11,11 @@ "files.insertFinalNewline": true, "editor.detectIndentation": false, "editor.tabSize": 2, - "editor.insertSpaces": true + "editor.insertSpaces": true, + "[csharp]": { + "editor.tabSize": 4 + }, + "explorer.fileNesting.patterns": { + "*.cs": "I${capture}.cs", + }, } diff --git a/Octokit.AsyncPaginationExtension/Extensions.cs b/Octokit.AsyncPaginationExtension/Extensions.cs index 3a4a541e6e..9c8a798099 100644 --- a/Octokit.AsyncPaginationExtension/Extensions.cs +++ b/Octokit.AsyncPaginationExtension/Extensions.cs @@ -235,14 +235,6 @@ public static IPaginatedList GetAllAsync(this IRepoCollaboratorsCl public static IPaginatedList GetAllAsync(this IRepoCollaboratorsClient t, long repositoryId, RepositoryCollaboratorListRequest request, int pageSize = DEFAULT_PAGE_SIZE) => pageSize > 0 ? new PaginatedList(options => t.GetAll(repositoryId, request, options), pageSize) : throw new ArgumentOutOfRangeException(nameof(pageSize), pageSize, "The page size must be positive."); - /// - public static IPaginatedList ListAllRunnerGroupOrganizationsForEnterpriseAsync(this IActionsSelfHostedRunnerGroupsClient t, string enterprise, long runnerGroupId, int pageSize = DEFAULT_PAGE_SIZE) - => pageSize > 0 ? new PaginatedList(options => t.ListAllRunnerGroupOrganizationsForEnterprise(enterprise, runnerGroupId, options), pageSize) : throw new ArgumentOutOfRangeException(nameof(pageSize), pageSize, "The page size must be positive."); - - /// - public static IPaginatedList ListAllRunnerGroupRepositoriesForOrganizationAsync(this IActionsSelfHostedRunnerGroupsClient t, string org, long runnerGroupId, int pageSize = DEFAULT_PAGE_SIZE) - => pageSize > 0 ? new PaginatedList(options => t.ListAllRunnerGroupRepositoriesForOrganization(org, runnerGroupId, options), pageSize) : throw new ArgumentOutOfRangeException(nameof(pageSize), pageSize, "The page size must be positive."); - /// public static IPaginatedList GetAllAsync(this IProjectColumnsClient t, int projectId, int pageSize = DEFAULT_PAGE_SIZE) => pageSize > 0 ? new PaginatedList(options => t.GetAll(projectId, options), pageSize) : throw new ArgumentOutOfRangeException(nameof(pageSize), pageSize, "The page size must be positive."); diff --git a/Octokit.Reactive/Clients/IObservableActionsSelfHostedRunnerGroupsClient.cs b/Octokit.Reactive/Clients/IObservableActionsSelfHostedRunnerGroupsClient.cs index 4e6aaa2987..04c6e90d9b 100644 --- a/Octokit.Reactive/Clients/IObservableActionsSelfHostedRunnerGroupsClient.cs +++ b/Octokit.Reactive/Clients/IObservableActionsSelfHostedRunnerGroupsClient.cs @@ -117,7 +117,7 @@ public interface IObservableActionsSelfHostedRunnerGroupsClient /// /// The enterprise name /// The runner group id - IObservable ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId); + IObservable ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId); /// /// List organization access to a self-hosted runner group in an enterprise @@ -128,7 +128,7 @@ public interface IObservableActionsSelfHostedRunnerGroupsClient /// The enterprise name /// The runner group id /// Options for changing the API response - IObservable ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId, ApiOptions options); + IObservable ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId, ApiOptions options); /// /// List repository access to a self-hosted runner group in an organization @@ -138,7 +138,7 @@ public interface IObservableActionsSelfHostedRunnerGroupsClient /// /// The organization name /// The runner group id - IObservable ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId); + IObservable ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId); /// /// List repository access to a self-hosted runner group in an organization @@ -149,7 +149,7 @@ public interface IObservableActionsSelfHostedRunnerGroupsClient /// The organization name /// The runner group id /// Options for changing the API response - IObservable ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId, ApiOptions options); + IObservable ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId, ApiOptions options); } } diff --git a/Octokit.Reactive/Clients/IObservableMetaClient.cs b/Octokit.Reactive/Clients/IObservableMetaClient.cs index 97d63d19e3..3555670358 100644 --- a/Octokit.Reactive/Clients/IObservableMetaClient.cs +++ b/Octokit.Reactive/Clients/IObservableMetaClient.cs @@ -10,6 +10,11 @@ namespace Octokit.Reactive /// public interface IObservableMetaClient { + /// + /// Returns a client to get public keys for validating request signatures. + /// + IObservablePublicKeysClient PublicKeys { get; } + /// /// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation. /// diff --git a/Octokit.Reactive/Clients/IObservablePublicKeysClient.cs b/Octokit.Reactive/Clients/IObservablePublicKeysClient.cs new file mode 100644 index 0000000000..02aae89a0a --- /dev/null +++ b/Octokit.Reactive/Clients/IObservablePublicKeysClient.cs @@ -0,0 +1,20 @@ +using System; + +namespace Octokit.Reactive +{ + /// + /// A client for GitHub's meta public keys API. + /// + /// + /// See the Secret scanning documentation for more details. + /// + public interface IObservablePublicKeysClient + { + /// + /// Retrieves public keys for validating request signatures. + /// + /// Thrown when a general API error occurs. + /// An containing public keys for validating request signatures. + IObservable Get(PublicKeyType keysType); + } +} diff --git a/Octokit.Reactive/Clients/ObservableActionsSelfHostedRunnerGroupsClient.cs b/Octokit.Reactive/Clients/ObservableActionsSelfHostedRunnerGroupsClient.cs index 7f7c0ec5a5..bb4369125e 100644 --- a/Octokit.Reactive/Clients/ObservableActionsSelfHostedRunnerGroupsClient.cs +++ b/Octokit.Reactive/Clients/ObservableActionsSelfHostedRunnerGroupsClient.cs @@ -175,7 +175,7 @@ public IObservable ListAllRunnersForOrganizationRunnerGroup(stri /// /// The enterprise name /// The runner group ID - public IObservable ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId) + public IObservable ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId) { return ListAllRunnerGroupOrganizationsForEnterprise(enterprise, runnerGroupId, ApiOptions.None); } @@ -189,12 +189,12 @@ public IObservable ListAllRunnerGroupOrganizationsForEnterprise(st /// The enterprise name /// The runner group ID /// Options for changing the API response - public IObservable ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId, ApiOptions options) + public IObservable ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId, ApiOptions options) { Ensure.ArgumentNotNullOrEmptyString(enterprise, nameof(enterprise)); Ensure.ArgumentNotNull(options, nameof(options)); - return _connection.GetAndFlattenAllPages(ApiUrls.ActionsListEnterpriseRunnerGroupOrganizations(enterprise, runnerGroupId), options); + return _client.ListAllRunnerGroupOrganizationsForEnterprise(enterprise, runnerGroupId, options).ToObservable(); } /// @@ -205,7 +205,7 @@ public IObservable ListAllRunnerGroupOrganizationsForEnterprise(st /// /// The organization name /// The runner group ID - public IObservable ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId) + public IObservable ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId) { return ListAllRunnerGroupRepositoriesForOrganization(org, runnerGroupId, ApiOptions.None); } @@ -219,12 +219,12 @@ public IObservable ListAllRunnerGroupRepositoriesForOrganization(str /// The organization name /// The runner group ID /// Options for changing the API response - public IObservable ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId, ApiOptions options) + public IObservable ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId, ApiOptions options) { Ensure.ArgumentNotNullOrEmptyString(org, nameof(org)); Ensure.ArgumentNotNull(options, nameof(options)); - return _connection.GetAndFlattenAllPages(ApiUrls.ActionsListOrganizationRunnerGroupRepositories(org, runnerGroupId), options); + return _client.ListAllRunnerGroupRepositoriesForOrganization(org, runnerGroupId, options).ToObservable(); } } diff --git a/Octokit.Reactive/Clients/ObservableMetaClient.cs b/Octokit.Reactive/Clients/ObservableMetaClient.cs index 3df88872d7..d8283ed623 100644 --- a/Octokit.Reactive/Clients/ObservableMetaClient.cs +++ b/Octokit.Reactive/Clients/ObservableMetaClient.cs @@ -18,9 +18,16 @@ public ObservableMetaClient(IGitHubClient client) { Ensure.ArgumentNotNull(client, nameof(client)); + PublicKeys = new ObservablePublicKeysClient(client); + _client = client.Meta; } + /// + /// Returns a client to manage get public keys for validating request signatures. + /// + public IObservablePublicKeysClient PublicKeys { get; private set; } + /// /// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation. /// diff --git a/Octokit.Reactive/Clients/ObservablePublicKeysClient.cs b/Octokit.Reactive/Clients/ObservablePublicKeysClient.cs new file mode 100644 index 0000000000..5339645561 --- /dev/null +++ b/Octokit.Reactive/Clients/ObservablePublicKeysClient.cs @@ -0,0 +1,34 @@ +using System; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; + +namespace Octokit.Reactive +{ + /// + /// A client for GitHub's public keys API. + /// + /// + /// See the Secret scanning documentation for more details. + /// + public class ObservablePublicKeysClient : IObservablePublicKeysClient + { + private readonly IPublicKeysClient _client; + + public ObservablePublicKeysClient(IGitHubClient client) + { + Ensure.ArgumentNotNull(client, nameof(client)); + + _client = client.Meta.PublicKeys; + } + + /// + /// Retrieves public keys for validating request signatures. + /// + /// Thrown when a general API error occurs. + /// An containing public keys for validating request signatures. + public IObservable Get(PublicKeyType keysType) + { + return _client.Get(keysType).ToObservable(); + } + } +} diff --git a/Octokit.Tests.Conventions/Octokit.Tests.Conventions.csproj b/Octokit.Tests.Conventions/Octokit.Tests.Conventions.csproj index ec7ad3d4f3..94cbc9dc43 100644 --- a/Octokit.Tests.Conventions/Octokit.Tests.Conventions.csproj +++ b/Octokit.Tests.Conventions/Octokit.Tests.Conventions.csproj @@ -33,8 +33,8 @@ - - + + diff --git a/Octokit.Tests.Integration/Clients/PublicKeysClientTest.cs b/Octokit.Tests.Integration/Clients/PublicKeysClientTest.cs new file mode 100644 index 0000000000..a6b2effb1f --- /dev/null +++ b/Octokit.Tests.Integration/Clients/PublicKeysClientTest.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using Xunit; + +namespace Octokit.Tests.Integration.Clients +{ + public class PublicKeysClientTests + { + public class TheGetMethod + { + [IntegrationTest] + public async Task CanRetrievePublicKeys() + { + var github = Helper.GetAnonymousClient(); + + var result = await github.Meta.PublicKeys.Get(PublicKeyType.SecretScanning); + + Assert.NotNull(result); + Assert.Equal(2, result.PublicKeys.Count); + + Assert.NotNull(result.PublicKeys[0].KeyIdentifier); + Assert.NotNull(result.PublicKeys[0].Key); + + Assert.NotNull(result.PublicKeys[1].KeyIdentifier); + Assert.NotNull(result.PublicKeys[1].Key); + } + } + } +} diff --git a/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj b/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj index 58e3b13418..64664094ab 100644 --- a/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj +++ b/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj @@ -34,9 +34,9 @@ - + - + diff --git a/Octokit.Tests/Clients/ActionsSelfHostedRunnerGroupsClientTests.cs b/Octokit.Tests/Clients/ActionsSelfHostedRunnerGroupsClientTests.cs index a1886ad8d2..f9af6067ff 100644 --- a/Octokit.Tests/Clients/ActionsSelfHostedRunnerGroupsClientTests.cs +++ b/Octokit.Tests/Clients/ActionsSelfHostedRunnerGroupsClientTests.cs @@ -217,7 +217,7 @@ public async Task RequestsCorrectUrl() await client.ListAllRunnerGroupOrganizationsForEnterprise("fake", 1); - connection.Received().GetAll( + connection.Received().GetAll( Arg.Is(u => u.ToString() == "enterprises/fake/actions/runner-groups/1/organizations"), Args.ApiOptions); } @@ -248,7 +248,7 @@ public async Task RequestsCorrectUrl() await client.ListAllRunnerGroupRepositoriesForOrganization("fake", 1, ApiOptions.None); - connection.Received().GetAll( + connection.Received().GetAll( Arg.Is(u => u.ToString() == "orgs/fake/actions/runner-groups/1/repositories"), Args.ApiOptions); } diff --git a/Octokit.Tests/Clients/PublicKeysClientTests.cs b/Octokit.Tests/Clients/PublicKeysClientTests.cs new file mode 100644 index 0000000000..ee416a6604 --- /dev/null +++ b/Octokit.Tests/Clients/PublicKeysClientTests.cs @@ -0,0 +1,90 @@ +using System; +using System.Threading.Tasks; +using NSubstitute; +using Xunit; + +namespace Octokit.Tests.Clients +{ + public class PublicKeysClientTests + { + public class TheCtor + { + [Fact] + public void EnsuresNonNullArguments() + { + Assert.Throws(() => new PublicKeysClient(null)); + } + } + + public class TheGetMethod + { + [Fact] + public async Task RequestsTheCorrectUrl() + { + var connection = Substitute.For(); + var client = new PublicKeysClient(connection); + + await client.Get(PublicKeyType.CopilotApi); + + connection.Received() + .Get(Arg.Is(u => u.ToString() == "meta/public_keys/copilot_api")); + } + + [Fact] + public async Task RequestsCopilotApiPublicKeysEndpoint() + { + var publicKeys = new MetaPublicKeys(publicKeys: new[] { + new MetaPublicKey("4fe6b016179b74078ade7581abf4e84fb398c6fae4fb973972235b84fcd70ca3", "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELPuPiLVQbHY/clvpNnY+0BzYIXgo\nS0+XhEkTWUZEEznIVpS3rQseDTG6//gEWr4j9fY35+dGOxwOx3Z9mK3i7w==\n-----END PUBLIC KEY-----\n", true), + new MetaPublicKey("df3454252d91570ae1bc597182d1183c7a8d42ff0ae96e0f2be4ba278d776546", "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEl5xbyr5bmETCJzqAvDnYl1ZKJrkf\n89Nyq5j06TTKrnHXXDw4FYNY1uF2S/w6EOaxbf9BxOidCLvjJ8ZgKzNpww==\n-----END PUBLIC KEY-----\n", false) + }); + + var apiConnection = Substitute.For(); + apiConnection.Get(Arg.Is(u => u.ToString() == "meta/public_keys/copilot_api")).Returns(Task.FromResult(publicKeys)); + + var client = new PublicKeysClient(apiConnection); + + var result = await client.Get(PublicKeyType.CopilotApi); + + Assert.Equal(2, result.PublicKeys.Count); + Assert.Equal("4fe6b016179b74078ade7581abf4e84fb398c6fae4fb973972235b84fcd70ca3", result.PublicKeys[0].KeyIdentifier); + Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELPuPiLVQbHY/clvpNnY+0BzYIXgo\nS0+XhEkTWUZEEznIVpS3rQseDTG6//gEWr4j9fY35+dGOxwOx3Z9mK3i7w==\n-----END PUBLIC KEY-----\n", result.PublicKeys[0].Key); + Assert.True(result.PublicKeys[0].IsCurrent); + + Assert.Equal("df3454252d91570ae1bc597182d1183c7a8d42ff0ae96e0f2be4ba278d776546", result.PublicKeys[1].KeyIdentifier); + Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEl5xbyr5bmETCJzqAvDnYl1ZKJrkf\n89Nyq5j06TTKrnHXXDw4FYNY1uF2S/w6EOaxbf9BxOidCLvjJ8ZgKzNpww==\n-----END PUBLIC KEY-----\n", result.PublicKeys[1].Key); + Assert.False(result.PublicKeys[1].IsCurrent); + + apiConnection.Received() + .Get(Arg.Is(u => u.ToString() == "meta/public_keys/copilot_api")); + } + + [Fact] + public async Task RequestSecretScanningPublicKeysEndpoint() + { + var publicKeys = new MetaPublicKeys(publicKeys: new[] { + new MetaPublicKey("90a421169f0a406205f1563a953312f0be898d3c7b6c06b681aa86a874555f4a", "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9MJJHnMfn2+H4xL4YaPDA4RpJqUq\nkCmRCBnYERxZanmcpzQSXs1X/AljlKkbJ8qpVIW4clayyef9gWhFbNHWAA==\n-----END PUBLIC KEY-----\n", false), + new MetaPublicKey("bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c", "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYAGMWO8XgCamYKMJS6jc/qgvSlAd\nAjPuDPRcXU22YxgBrz+zoN19MzuRyW87qEt9/AmtoNP5GrobzUvQSyJFVw==\n-----END PUBLIC KEY-----\n", true) + }); + + var apiConnection = Substitute.For(); + apiConnection.Get(Arg.Is(u => u.ToString() == "meta/public_keys/secret_scanning")).Returns(Task.FromResult(publicKeys)); + + var client = new PublicKeysClient(apiConnection); + + var result = await client.Get(PublicKeyType.SecretScanning); + + Assert.Equal(2, result.PublicKeys.Count); + Assert.Equal("90a421169f0a406205f1563a953312f0be898d3c7b6c06b681aa86a874555f4a", result.PublicKeys[0].KeyIdentifier); + Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9MJJHnMfn2+H4xL4YaPDA4RpJqUq\nkCmRCBnYERxZanmcpzQSXs1X/AljlKkbJ8qpVIW4clayyef9gWhFbNHWAA==\n-----END PUBLIC KEY-----\n", result.PublicKeys[0].Key); + Assert.False(result.PublicKeys[0].IsCurrent); + + Assert.Equal("bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c", result.PublicKeys[1].KeyIdentifier); + Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYAGMWO8XgCamYKMJS6jc/qgvSlAd\nAjPuDPRcXU22YxgBrz+zoN19MzuRyW87qEt9/AmtoNP5GrobzUvQSyJFVw==\n-----END PUBLIC KEY-----\n", result.PublicKeys[1].Key); + Assert.True(result.PublicKeys[1].IsCurrent); + + apiConnection.Received() + .Get(Arg.Is(u => u.ToString() == "meta/public_keys/secret_scanning")); + } + } + } +} diff --git a/Octokit.Tests/Models/MetaPublicKeysTests.cs b/Octokit.Tests/Models/MetaPublicKeysTests.cs new file mode 100644 index 0000000000..9e5f64ea89 --- /dev/null +++ b/Octokit.Tests/Models/MetaPublicKeysTests.cs @@ -0,0 +1,44 @@ +using Octokit.Internal; +using Xunit; + +namespace Octokit.Tests.Models +{ + public class MetaPublicKeysTests + { + [Fact] + public void CanBeDeserialized() + { + const string json = @"{ + ""public_keys"": [ + { + ""key_identifier"": ""90a421169f0a406205f1563a953312f0be898d3c7b6c06b681aa86a874555f4a"", + ""key"": ""-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9MJJHnMfn2+H4xL4YaPDA4RpJqUq\nkCmRCBnYERxZanmcpzQSXs1X/AljlKkbJ8qpVIW4clayyef9gWhFbNHWAA==\n-----END PUBLIC KEY-----\n"", + ""is_current"": false + }, + { + ""key_identifier"": ""bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c"", + ""key"": ""-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYAGMWO8XgCamYKMJS6jc/qgvSlAd\nAjPuDPRcXU22YxgBrz+zoN19MzuRyW87qEt9/AmtoNP5GrobzUvQSyJFVw==\n-----END PUBLIC KEY-----\n"", + ""is_current"": true + } + ] +} +"; + var serializer = new SimpleJsonSerializer(); + + var keys = serializer.Deserialize(json); + + Assert.NotNull(keys); + Assert.Equal(2, keys.PublicKeys.Count); + + var key1 = keys.PublicKeys[0]; + Assert.Equal("90a421169f0a406205f1563a953312f0be898d3c7b6c06b681aa86a874555f4a", key1.KeyIdentifier); + Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9MJJHnMfn2+H4xL4YaPDA4RpJqUq\nkCmRCBnYERxZanmcpzQSXs1X/AljlKkbJ8qpVIW4clayyef9gWhFbNHWAA==\n-----END PUBLIC KEY-----\n", key1.Key); + Assert.False(key1.IsCurrent); + + var key2 = keys.PublicKeys[1]; + Assert.Equal("bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c", key2.KeyIdentifier); + Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYAGMWO8XgCamYKMJS6jc/qgvSlAd\nAjPuDPRcXU22YxgBrz+zoN19MzuRyW87qEt9/AmtoNP5GrobzUvQSyJFVw==\n-----END PUBLIC KEY-----\n", key2.Key); + Assert.True(key2.IsCurrent); + } + } +} diff --git a/Octokit.Tests/Octokit.Tests.csproj b/Octokit.Tests/Octokit.Tests.csproj index ae01e13da4..90d44fcbc7 100644 --- a/Octokit.Tests/Octokit.Tests.csproj +++ b/Octokit.Tests/Octokit.Tests.csproj @@ -29,13 +29,13 @@ - + - + - + diff --git a/Octokit.Tests/Reactive/ObservablePublicKeysClientTests.cs b/Octokit.Tests/Reactive/ObservablePublicKeysClientTests.cs new file mode 100644 index 0000000000..14939de515 --- /dev/null +++ b/Octokit.Tests/Reactive/ObservablePublicKeysClientTests.cs @@ -0,0 +1,33 @@ +using System; +using NSubstitute; +using Octokit.Reactive; +using Xunit; + +namespace Octokit.Tests.Reactive +{ + public class ObservablePublicKeysClientTests + { + public class TheGetMethod + { + [Fact] + public void CallsIntoClient() + { + var gitHubClient = Substitute.For(); + var client = new ObservablePublicKeysClient(gitHubClient); + + client.Get(PublicKeyType.SecretScanning); + + gitHubClient.Meta.PublicKeys.Received(1).Get(PublicKeyType.SecretScanning); + } + } + + public class TheCtor + { + [Fact] + public void EnsuresNonNullArguments() + { + Assert.Throws(() => new ObservablePublicKeysClient((IGitHubClient)null)); + } + } + } +} diff --git a/Octokit/Clients/ActionsSelfHostedRunnerGroupsClient.cs b/Octokit/Clients/ActionsSelfHostedRunnerGroupsClient.cs index 5520af6e06..a5bf26fecb 100644 --- a/Octokit/Clients/ActionsSelfHostedRunnerGroupsClient.cs +++ b/Octokit/Clients/ActionsSelfHostedRunnerGroupsClient.cs @@ -203,7 +203,7 @@ public async Task ListAllRunnersForOrganizationRunnerGroup(strin /// The enterprise name /// The runner group id [ManualRoute("GET", "/enterprises/{enterprise}/actions/runner-groups/{runner_group_id}/organizations")] - public Task> ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId) + public Task ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId) { return ListAllRunnerGroupOrganizationsForEnterprise(enterprise, runnerGroupId, ApiOptions.None); } @@ -218,11 +218,16 @@ public Task> ListAllRunnerGroupOrganizationsForEnter /// The runner group id /// Options for changing the API response [ManualRoute("GET", "/enterprises/{enterprise}/actions/runner-groups/{runner_group_id}/organizations")] - public Task> ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId, ApiOptions options) + public async Task ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId, ApiOptions options) { Ensure.ArgumentNotNullOrEmptyString(enterprise, nameof(enterprise)); - return ApiConnection.GetAll(ApiUrls.ActionsListEnterpriseRunnerGroupOrganizations(enterprise, runnerGroupId), options); + var results = await ApiConnection.GetAll(ApiUrls.ActionsListEnterpriseRunnerGroupOrganizations(enterprise, runnerGroupId), options).ConfigureAwait(false); + + return new OrganizationsResponse( + results.Count > 0 ? results.Max(x => x.TotalCount) : 0, + results.SelectMany(x => x.Organizations).ToList() + ); } /// @@ -234,7 +239,7 @@ public Task> ListAllRunnerGroupOrganizationsForEnter /// The organization name /// The runner group id [ManualRoute("GET", "/orgs/{org}/actions/runner-groups/{runner_group_id}/repositories")] - public Task> ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId) + public Task ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId) { return ListAllRunnerGroupRepositoriesForOrganization(org, runnerGroupId, ApiOptions.None); } @@ -249,11 +254,16 @@ public Task> ListAllRunnerGroupRepositoriesForOrganiza /// The runner group id /// Options for changing the API response [ManualRoute("GET", "/orgs/{org}/actions/runner-groups/{runner_group_id}/repositories")] - public Task> ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId, ApiOptions options) + public async Task ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId, ApiOptions options) { Ensure.ArgumentNotNullOrEmptyString(org, nameof(org)); - return ApiConnection.GetAll(ApiUrls.ActionsListOrganizationRunnerGroupRepositories(org, runnerGroupId), options); + var results = await ApiConnection.GetAll(ApiUrls.ActionsListOrganizationRunnerGroupRepositories(org, runnerGroupId), options).ConfigureAwait(false); + + return new RepositoriesResponse( + results.Count > 0 ? results.Max(x => x.TotalCount) : 0, + results.SelectMany(x => x.Repositories).ToList() + ); } } } diff --git a/Octokit/Clients/IActionsSelfHostedRunnerGroupsClient.cs b/Octokit/Clients/IActionsSelfHostedRunnerGroupsClient.cs index 6e419407b3..5007467795 100644 --- a/Octokit/Clients/IActionsSelfHostedRunnerGroupsClient.cs +++ b/Octokit/Clients/IActionsSelfHostedRunnerGroupsClient.cs @@ -119,7 +119,7 @@ public interface IActionsSelfHostedRunnerGroupsClient /// /// The enterprise name /// The runner group id - Task> ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId); + Task ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId); /// /// List organization access to a self-hosted runner group in an enterprise @@ -130,7 +130,7 @@ public interface IActionsSelfHostedRunnerGroupsClient /// The enterprise name /// The runner group id /// Options for changing the API response - Task> ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId, ApiOptions options); + Task ListAllRunnerGroupOrganizationsForEnterprise(string enterprise, long runnerGroupId, ApiOptions options); /// /// List repository access to a self-hosted runner group in an organization @@ -140,7 +140,7 @@ public interface IActionsSelfHostedRunnerGroupsClient /// /// The organization name /// The runner group id - Task> ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId); + Task ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId); /// /// List repository access to a self-hosted runner group in an organization @@ -151,6 +151,6 @@ public interface IActionsSelfHostedRunnerGroupsClient /// The organization name /// The runner group id /// Options for changing the API response - Task> ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId, ApiOptions options); + Task ListAllRunnerGroupRepositoriesForOrganization(string org, long runnerGroupId, ApiOptions options); } } diff --git a/Octokit/Clients/IMetaClient.cs b/Octokit/Clients/IMetaClient.cs index f4c21714d0..9c7900608f 100644 --- a/Octokit/Clients/IMetaClient.cs +++ b/Octokit/Clients/IMetaClient.cs @@ -10,6 +10,11 @@ namespace Octokit /// public interface IMetaClient { + /// + /// Returns a client to get public keys for validating request signatures. + /// + IPublicKeysClient PublicKeys { get; } + /// /// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation. /// diff --git a/Octokit/Clients/IPublicKeysClient.cs b/Octokit/Clients/IPublicKeysClient.cs new file mode 100644 index 0000000000..eae756c8d5 --- /dev/null +++ b/Octokit/Clients/IPublicKeysClient.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// A client for GitHub's meta public keys API. + /// + /// + /// See the Secret scanning documentation for more details. + /// + public interface IPublicKeysClient + { + /// + /// Retrieves public keys for validating request signatures. + /// + /// Thrown when a general API error occurs. + /// An containing public keys for validating request signatures. + Task Get(PublicKeyType keysType); + } +} diff --git a/Octokit/Clients/MetaClient.cs b/Octokit/Clients/MetaClient.cs index c683a34bba..52a61f14a6 100644 --- a/Octokit/Clients/MetaClient.cs +++ b/Octokit/Clients/MetaClient.cs @@ -18,8 +18,14 @@ public class MetaClient : ApiClient, IMetaClient public MetaClient(IApiConnection apiConnection) : base(apiConnection) { + PublicKeys = new PublicKeysClient(apiConnection); } + /// + /// Returns a client to manage get public keys for validating request signatures. + /// + public IPublicKeysClient PublicKeys { get; private set; } + /// /// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation. /// diff --git a/Octokit/Clients/PublicKeysClient.cs b/Octokit/Clients/PublicKeysClient.cs new file mode 100644 index 0000000000..cc34141f5d --- /dev/null +++ b/Octokit/Clients/PublicKeysClient.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// A client for GitHub's public keys API. + /// + /// + /// See the Secret scanning documentation for more details. + /// + public class PublicKeysClient : ApiClient, IPublicKeysClient + { + /// + /// Initializes a new GitHub Meta Public Keys API client. + /// + /// An API connection. + public PublicKeysClient(IApiConnection apiConnection) + : base(apiConnection) + { + } + + /// + /// Retrieves public keys for validating request signatures. + /// + /// Thrown when a general API error occurs. + /// An containing public keys for validating request signatures. + [ManualRoute("GET", "/meta/public_keys/{keysType}")] + public Task Get(PublicKeyType keysType) + { + return ApiConnection.Get(ApiUrls.PublicKeys(keysType)); + } + } +} diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs index 21bec374b6..2305be0c30 100644 --- a/Octokit/Helpers/ApiUrls.cs +++ b/Octokit/Helpers/ApiUrls.cs @@ -4833,6 +4833,16 @@ public static Uri Meta() return "meta".FormatUri(); } + /// + /// Returns the that returns meta in + /// response to a GET request. + /// + /// The to meta. + public static Uri PublicKeys(PublicKeyType keysType) + { + return "meta/public_keys/{0}".FormatUri(keysType.ToParameter()); + } + /// /// Returns the that returns all organization credentials in /// response to a GET request. diff --git a/Octokit/Models/Common/PublicKeyType.cs b/Octokit/Models/Common/PublicKeyType.cs new file mode 100644 index 0000000000..3a8c09a64f --- /dev/null +++ b/Octokit/Models/Common/PublicKeyType.cs @@ -0,0 +1,19 @@ +using Octokit.Internal; + +namespace Octokit +{ + public enum PublicKeyType + { + /// + /// Copilot API public keys for validating request signatures + /// + [Parameter(Value = "copilot_api")] + CopilotApi, + + /// + /// Secret scanning public keys for validating request signatures + /// + [Parameter(Value = "secret_scanning")] + SecretScanning + } +} diff --git a/Octokit/Models/Response/MetaPublicKey.cs b/Octokit/Models/Response/MetaPublicKey.cs new file mode 100644 index 0000000000..05b6a86c96 --- /dev/null +++ b/Octokit/Models/Response/MetaPublicKey.cs @@ -0,0 +1,29 @@ +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class MetaPublicKey + { + public MetaPublicKey() { } + + public MetaPublicKey(string keyIdentifier, string key, bool isCurrent) + { + KeyIdentifier = keyIdentifier; + Key = key; + IsCurrent = isCurrent; + } + + public string KeyIdentifier { get; protected set; } + + public string Key { get; protected set; } + + public bool IsCurrent { get; protected set; } + + internal string DebuggerDisplay + { + get { return string.Format(CultureInfo.InvariantCulture, "KeyIdentifier: {0} IsCurrent: {1}", KeyIdentifier, IsCurrent); } + } + } +} diff --git a/Octokit/Models/Response/MetaPublicKeys.cs b/Octokit/Models/Response/MetaPublicKeys.cs new file mode 100644 index 0000000000..7927ab7a6e --- /dev/null +++ b/Octokit/Models/Response/MetaPublicKeys.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class MetaPublicKeys + { + public MetaPublicKeys() { } + + public MetaPublicKeys(IReadOnlyList publicKeys) + { + PublicKeys = publicKeys; + } + + public IReadOnlyList PublicKeys { get; protected set; } + + internal string DebuggerDisplay + { + get { return string.Format(CultureInfo.InvariantCulture, "PublicKeys: {0}", PublicKeys.Count); } + } + } +} diff --git a/Octokit/Models/Response/OrganizationsResponse.cs b/Octokit/Models/Response/OrganizationsResponse.cs new file mode 100644 index 0000000000..3f4bbbf19b --- /dev/null +++ b/Octokit/Models/Response/OrganizationsResponse.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class OrganizationsResponse + { + public OrganizationsResponse() + { + } + + public OrganizationsResponse(int totalCount, IReadOnlyList organizations) + { + TotalCount = totalCount; + Organizations = organizations; + } + + /// + /// The total number of organizations + /// + public int TotalCount { get; private set; } + + /// + /// The retrieved organizations + /// + public IReadOnlyList Organizations { get; private set; } + + internal string DebuggerDisplay => string.Format(CultureInfo.CurrentCulture, "TotalCount: {0}, Organizations: {1}", TotalCount, Organizations.Count); + } +} diff --git a/Octokit/Models/Response/RepositoriesResponse.cs b/Octokit/Models/Response/RepositoriesResponse.cs index cf17e710b2..69ef9e3b68 100644 --- a/Octokit/Models/Response/RepositoriesResponse.cs +++ b/Octokit/Models/Response/RepositoriesResponse.cs @@ -18,12 +18,12 @@ public RepositoriesResponse(int totalCount, IReadOnlyList repositori } /// - /// The total number of check suites that match the request filter + /// The total number of repositories /// public int TotalCount { get; private set; } /// - /// The retrieved check suites + /// The retrieved repositories /// public IReadOnlyList Repositories { get; private set; } diff --git a/build/Build.csproj b/build/Build.csproj index a719b91b10..ac858f4788 100644 --- a/build/Build.csproj +++ b/build/Build.csproj @@ -9,7 +9,7 @@ - +