diff --git a/Octokit.Reactive/Clients/IObservableAuthorizationsClient.cs b/Octokit.Reactive/Clients/IObservableAuthorizationsClient.cs index 5109bc2c..a61641d1 100644 --- a/Octokit.Reactive/Clients/IObservableAuthorizationsClient.cs +++ b/Octokit.Reactive/Clients/IObservableAuthorizationsClient.cs @@ -31,6 +31,56 @@ namespace Octokit.Reactive Justification = "It's fiiiine. It's fine. Trust us.")] IObservable Get(int id); + /// + /// Creates a new authorization for the specified OAuth application if an authorization for that application + /// doesn’t already exist for the user; otherwise, it fails. + /// + /// + /// This method requires authentication. + /// See the API documentation for more information. + /// + /// Client ID of the OAuth application for the token + /// The client secret + /// Describes the new authorization to create + /// + /// Thrown when the current user does not have permission to make this request. + /// + /// + /// Thrown when the current account has two-factor authentication enabled and an authentication code is required. + /// + /// Thrown when a general API error occurs. + /// The created . + IObservable Create( + string clientId, + string clientSecret, + NewAuthorization newAuthorization); + + /// + /// Creates a new authorization for the specified OAuth application if an authorization for that application + /// doesn’t already exist for the user; otherwise, it fails. + /// + /// + /// This method requires authentication. + /// See the API documentation for more information. + /// + /// Client ID of the OAuth application for the token + /// The client secret + /// The two-factor authentication code in response to the current user's previous challenge + /// Describes the new authorization to create + /// + /// Thrown when the current user does not have permission to make this request. + /// + /// + /// Thrown when the current account has two-factor authentication enabled and an authentication code is required. + /// + /// Thrown when a general API error occurs. + /// The created . + IObservable Create( + string clientId, + string clientSecret, + NewAuthorization newAuthorization, + string twoFactorAuthenticationCode); + /// /// This method will create a new authorization for the specified OAuth application, only if an authorization /// for that application doesn’t already exist for the user. It returns the user’s token for the application @@ -133,10 +183,34 @@ namespace Octokit.Reactive IObservable Update(int id, AuthorizationUpdate authorizationUpdate); /// - /// Deletes an . + /// Deletes the specified . /// - /// The systemwide id of the authorization - /// + /// + /// This method requires authentication. + /// See the API + /// documentation for more details. + /// + /// The system-wide ID of the authorization to delete + /// + /// Thrown when the current user does not have permission to make the request. + /// + /// Thrown when a general API error occurs. IObservable Delete(int id); + + /// + /// Deletes the specified . + /// + /// + /// This method requires authentication. + /// See the API + /// documentation for more details. + /// + /// The system-wide ID of the authorization to delete + /// Two factor authorization code + /// + /// Thrown when the current user does not have permission to make the request. + /// + /// Thrown when a general API error occurs. + IObservable Delete(int id, string twoFactorAuthenticationCode); } } diff --git a/Octokit.Reactive/Clients/IObservableGistsClient.cs b/Octokit.Reactive/Clients/IObservableGistsClient.cs index 31ea9da5..dbb9b5a0 100644 --- a/Octokit.Reactive/Clients/IObservableGistsClient.cs +++ b/Octokit.Reactive/Clients/IObservableGistsClient.cs @@ -91,6 +91,24 @@ namespace Octokit.Reactive /// Only gists updated at or after this time are returned IObservable GetAllForUser(string user, DateTimeOffset since); + /// + /// List gist commits + /// + /// + /// http://developer.github.com/v3/gists/#list-gists-commits + /// + /// The id of the gist + IObservable GetAllCommits(string id); + + /// + /// List gist forks + /// + /// + /// http://developer.github.com/v3/gists/#list-gists-forks + /// + /// The id of the gist + IObservable GetAllForks(string id); + /// /// Creates a new gist /// diff --git a/Octokit.Reactive/Clients/IObservableRepositoryContentsClient.cs b/Octokit.Reactive/Clients/IObservableRepositoryContentsClient.cs index bfe4cb3e..4b56ed1b 100644 --- a/Octokit.Reactive/Clients/IObservableRepositoryContentsClient.cs +++ b/Octokit.Reactive/Clients/IObservableRepositoryContentsClient.cs @@ -24,6 +24,45 @@ namespace Octokit.Reactive /// IObservable GetReadmeHtml(string owner, string name); + /// + /// This method will return a 302 to a URL to download a tarball or zipball archive for a repository. + /// Please make sure your HTTP framework is configured to follow redirects or you will need to use the + /// Location header to make a second GET request. + /// Note: For private repositories, these links are temporary and expire quickly. + /// + /// https://developer.github.com/v3/repos/contents/#get-archive-link + /// The owner of the repository + /// The name of the repository + /// + IObservable GetArchiveLink(string owner, string name); + + /// + /// This method will return a 302 to a URL to download a tarball or zipball archive for a repository. + /// Please make sure your HTTP framework is configured to follow redirects or you will need to use the + /// Location header to make a second GET request. + /// Note: For private repositories, these links are temporary and expire quickly. + /// + /// https://developer.github.com/v3/repos/contents/#get-archive-link + /// The owner of the repository + /// The name of the repository + /// The format of the archive. Can be either tarball or zipball + /// + IObservable GetArchiveLink(string owner, string name, ArchiveFormat archiveFormat); + + /// + /// This method will return a 302 to a URL to download a tarball or zipball archive for a repository. + /// Please make sure your HTTP framework is configured to follow redirects or you will need to use the + /// Location header to make a second GET request. + /// Note: For private repositories, these links are temporary and expire quickly. + /// + /// https://developer.github.com/v3/repos/contents/#get-archive-link + /// The owner of the repository + /// The name of the repository + /// The format of the archive. Can be either tarball or zipball + /// A valid Git reference. + /// + IObservable GetArchiveLink(string owner, string name, ArchiveFormat archiveFormat, string reference); + /// /// Returns the contents of a file or directory in a repository. /// diff --git a/Octokit.Reactive/Clients/ObservableAuthorizationsClient.cs b/Octokit.Reactive/Clients/ObservableAuthorizationsClient.cs index 82453680..2a4d1961 100644 --- a/Octokit.Reactive/Clients/ObservableAuthorizationsClient.cs +++ b/Octokit.Reactive/Clients/ObservableAuthorizationsClient.cs @@ -45,6 +45,71 @@ namespace Octokit.Reactive return _client.Get(id).ToObservable(); } + /// + /// Creates a new authorization for the specified OAuth application if an authorization for that application + /// doesn’t already exist for the user; otherwise, it fails. + /// + /// + /// This method requires authentication. + /// See the API documentation for more information. + /// + /// Client ID of the OAuth application for the token + /// The client secret + /// Describes the new authorization to create + /// + /// Thrown when the current user does not have permission to make this request. + /// + /// + /// Thrown when the current account has two-factor authentication enabled and an authentication code is required. + /// + /// Thrown when a general API error occurs. + /// The created . + public IObservable Create( + string clientId, + string clientSecret, + NewAuthorization newAuthorization) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, "clientId"); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, "clientSecret"); + Ensure.ArgumentNotNull(newAuthorization, "authorization"); + + return _client.Create(clientId, clientSecret, newAuthorization).ToObservable(); + } + + /// + /// Creates a new authorization for the specified OAuth application if an authorization for that application + /// doesn’t already exist for the user; otherwise, it fails. + /// + /// + /// This method requires authentication. + /// See the API documentation for more information. + /// + /// Client ID of the OAuth application for the token + /// The client secret + /// The two-factor authentication code in response to the current user's previous challenge + /// Describes the new authorization to create + /// + /// Thrown when the current user does not have permission to make this request. + /// + /// + /// Thrown when the current account has two-factor authentication enabled and an authentication code is required. + /// + /// Thrown when a general API error occurs. + /// The created . + public IObservable Create( + string clientId, + string clientSecret, + NewAuthorization newAuthorization, + string twoFactorAuthenticationCode) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, "clientId"); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, "clientSecret"); + Ensure.ArgumentNotNull(newAuthorization, "authorization"); + Ensure.ArgumentNotNullOrEmptyString(twoFactorAuthenticationCode, "twoFactorAuthenticationCode"); + + return _client.Create(clientId, clientSecret, newAuthorization, twoFactorAuthenticationCode).ToObservable(); + } + /// /// This method will create a new authorization for the specified OAuth application, only if an authorization /// for that application doesn’t already exist for the user. It returns the user’s token for the application @@ -201,13 +266,40 @@ namespace Octokit.Reactive } /// - /// Deletes an . + /// Deletes the specified . /// - /// The systemwide id of the authorization - /// + /// + /// This method requires authentication. + /// See the API + /// documentation for more details. + /// + /// The system-wide ID of the authorization to delete + /// + /// Thrown when the current user does not have permission to make the request. + /// + /// Thrown when a general API error occurs. public IObservable Delete(int id) { return _client.Delete(id).ToObservable(); } + + /// + /// Deletes the specified . + /// + /// + /// This method requires authentication. + /// See the API + /// documentation for more details. + /// + /// The system-wide ID of the authorization to delete + /// Two factor authorization code + /// + /// Thrown when the current user does not have permission to make the request. + /// + /// Thrown when a general API error occurs. + public IObservable Delete(int id, string twoFactorAuthenticationCode) + { + return _client.Delete(id, twoFactorAuthenticationCode).ToObservable(); + } } } diff --git a/Octokit.Reactive/Clients/ObservableGistsClient.cs b/Octokit.Reactive/Clients/ObservableGistsClient.cs index 3525b870..f7539fa3 100644 --- a/Octokit.Reactive/Clients/ObservableGistsClient.cs +++ b/Octokit.Reactive/Clients/ObservableGistsClient.cs @@ -180,6 +180,34 @@ namespace Octokit.Reactive return _connection.GetAndFlattenAllPages(ApiUrls.UsersGists(user), request.ToParametersDictionary()); } + /// + /// List gist commits + /// + /// + /// http://developer.github.com/v3/gists/#list-gists-commits + /// + /// The id of the gist + public IObservable GetAllCommits(string id) + { + Ensure.ArgumentNotNullOrEmptyString(id, "id"); + + return _connection.GetAndFlattenAllPages(ApiUrls.GistCommits(id)); + } + + /// + /// List gist forks + /// + /// + /// http://developer.github.com/v3/gists/#list-gists-forks + /// + /// The id of the gist + public IObservable GetAllForks(string id) + { + Ensure.ArgumentNotNullOrEmptyString(id, "id"); + + return _connection.GetAndFlattenAllPages(ApiUrls.ForkGist(id)); + } + /// /// Edits a gist /// diff --git a/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs b/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs index ecc3c7b0..144f97ed 100644 --- a/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs +++ b/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs @@ -116,7 +116,9 @@ namespace Octokit.Reactive { Ensure.ArgumentNotNull(request, "request"); - return _connection.GetAndFlattenAllPages(ApiUrls.AllPublicRepositories(), request.ToParametersDictionary()); + var url = ApiUrls.AllPublicRepositories(request.Since); + + return _connection.GetAndFlattenAllPages(url); } /// diff --git a/Octokit.Reactive/Clients/ObservableRepositoryContentsClient.cs b/Octokit.Reactive/Clients/ObservableRepositoryContentsClient.cs index 0a45ca68..ca2889e4 100644 --- a/Octokit.Reactive/Clients/ObservableRepositoryContentsClient.cs +++ b/Octokit.Reactive/Clients/ObservableRepositoryContentsClient.cs @@ -49,6 +49,57 @@ namespace Octokit.Reactive } + /// + /// This method will return a 302 to a URL to download a tarball or zipball archive for a repository. + /// Please make sure your HTTP framework is configured to follow redirects or you will need to use the + /// Location header to make a second GET request. + /// Note: For private repositories, these links are temporary and expire quickly. + /// + /// https://developer.github.com/v3/repos/contents/#get-archive-link + /// The owner of the repository + /// The name of the repository + /// + public IObservable GetArchiveLink(string owner, string name) + { + return GetArchiveLink(owner, name, ArchiveFormat.Tarball, string.Empty); + } + + /// + /// This method will return a 302 to a URL to download a tarball or zipball archive for a repository. + /// Please make sure your HTTP framework is configured to follow redirects or you will need to use the + /// Location header to make a second GET request. + /// Note: For private repositories, these links are temporary and expire quickly. + /// + /// https://developer.github.com/v3/repos/contents/#get-archive-link + /// The owner of the repository + /// The name of the repository + /// The format of the archive. Can be either tarball or zipball + /// + public IObservable GetArchiveLink(string owner, string name, ArchiveFormat archiveFormat) + { + return GetArchiveLink(owner, name, archiveFormat, String.Empty); + } + + /// + /// This method will return a 302 to a URL to download a tarball or zipball archive for a repository. + /// Please make sure your HTTP framework is configured to follow redirects or you will need to use the + /// Location header to make a second GET request. + /// Note: For private repositories, these links are temporary and expire quickly. + /// + /// https://developer.github.com/v3/repos/contents/#get-archive-link + /// The owner of the repository + /// The name of the repository + /// The format of the archive. Can be either tarball or zipball + /// A valid Git reference. + /// + public IObservable GetArchiveLink(string owner, string name, ArchiveFormat archiveFormat, string reference) + { + Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + + return _client.Repository.Content.GetArchiveLink(owner, name, archiveFormat, reference).ToObservable(); + } + /// /// Returns the contents of a file or directory in a repository. /// diff --git a/Octokit.Reactive/Helpers/AuthorizationExtensions.cs b/Octokit.Reactive/Helpers/AuthorizationExtensions.cs index 36a110eb..536fcab5 100644 --- a/Octokit.Reactive/Helpers/AuthorizationExtensions.cs +++ b/Octokit.Reactive/Helpers/AuthorizationExtensions.cs @@ -33,8 +33,7 @@ namespace Octokit string clientId, string clientSecret, NewAuthorization newAuthorization, - Func> twoFactorChallengeHandler - ) + Func> twoFactorChallengeHandler) { Ensure.ArgumentNotNull(authorizationsClient, "authorizationsClient"); Ensure.ArgumentNotNullOrEmptyString(clientId, "clientId"); @@ -55,5 +54,147 @@ namespace Octokit newAuthorization, result.AuthenticationCode))); } + + /// + /// This method will create a new authorization for the specified OAuth application. If an authorization + /// for that application already exists for the user and fingerprint, it'll delete the existing one and + /// recreate it. + /// + /// + /// + /// This method is typically used to initiate an application authentication flow. + /// This method allows the caller to provide a callback which is used to retrieve the two-factor code from + /// the user. Typically the callback is used to show some user interface to the user. + /// + /// + /// See API documentation + /// for more details. + /// + /// + /// The this method extends + /// Client ID for the OAuth application that is requesting the token + /// The client secret + /// Defines the scopes and metadata for the token + /// Callback used to retrieve the two-factor authentication code + /// from the user + /// If true, instead of completing when the two factor code supplied + /// is invalid, we go through the whole cycle again and prompt the two factor dialog. + /// + public static IObservable CreateAndDeleteExistingApplicationAuthorization( + this IObservableAuthorizationsClient authorizationsClient, + string clientId, + string clientSecret, + NewAuthorization newAuthorization, + Func> twoFactorChallengeHandler, + bool retryInvalidTwoFactorCode) + { + return authorizationsClient.CreateAndDeleteExistingApplicationAuthorization( + clientId, + clientSecret, + newAuthorization, + twoFactorChallengeHandler, + null, + retryInvalidTwoFactorCode); + } + + public static IObservable CreateAndDeleteExistingApplicationAuthorization( + this IObservableAuthorizationsClient authorizationsClient, + string clientId, + string clientSecret, + NewAuthorization newAuthorization, + Func> twoFactorChallengeHandler, + string twoFactorAuthenticationCode, + bool retryInvalidTwoFactorCode) + { + Ensure.ArgumentNotNull(authorizationsClient, "authorizationsClient"); + Ensure.ArgumentNotNullOrEmptyString(clientId, "clientId"); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, "clientSecret"); + Ensure.ArgumentNotNull(newAuthorization, "newAuthorization"); + + // If retryInvalidTwoFactorCode is false, then we only show the TwoFactorDialog when we catch + // a TwoFactorRequiredException. If it's true, we show it for TwoFactorRequiredException and + // TwoFactorChallengeFailedException + Func> twoFactorHandler = ex => + retryInvalidTwoFactorCode || ex is TwoFactorRequiredException + ? twoFactorChallengeHandler(ex) + : Observable.Throw(ex); + + return authorizationsClient.CreateAuthorizationAndDeleteExisting( + clientId, + clientSecret, + newAuthorization, + twoFactorAuthenticationCode) + .Catch( + exception => twoFactorHandler(exception) + .SelectMany(result => + result.ResendCodeRequested + ? authorizationsClient.CreateAndDeleteExistingApplicationAuthorization( + clientId, + clientSecret, + newAuthorization, + twoFactorHandler, + null, // twoFactorAuthenticationCode + retryInvalidTwoFactorCode) + : authorizationsClient.CreateAndDeleteExistingApplicationAuthorization( + clientId, + clientSecret, + newAuthorization, + twoFactorHandler, + result.AuthenticationCode, + retryInvalidTwoFactorCode))); + } + + // If the Application Authorization already exists, the result might have an empty string as the token. This is + // because GitHub.com no longer stores the token, but stores a hashed version of it. It is assumed that clients + // will store the token locally. + // The only reason to be calling GetOrCreateApplicationAuthentication is pretty much the case when you are + // logging in and thus don't have the token already. So if the token returned is an empty string, we'll go + // ahead and delete it for you and then recreate it. + static IObservable CreateAuthorizationAndDeleteExisting( + this IObservableAuthorizationsClient authorizationsClient, + string clientId, + string clientSecret, + NewAuthorization newAuthorization, + string twoFactorAuthenticationCode = null) + { + return authorizationsClient.GetOrCreateAuthorizationUnified( + clientId, + clientSecret, + newAuthorization, + twoFactorAuthenticationCode) + .SelectMany(authorization => string.IsNullOrEmpty(authorization.Token) + ? authorizationsClient.Delete(authorization.Id, twoFactorAuthenticationCode) + .SelectMany(_ => + authorizationsClient.CreateNewAuthorization( + clientId, + clientSecret, + newAuthorization, + twoFactorAuthenticationCode)) + : Observable.Return(authorization)); + } + + static IObservable GetOrCreateAuthorizationUnified( + this IObservableAuthorizationsClient authorizationsClient, + string clientId, + string clientSecret, + NewAuthorization newAuthorization, + string twoFactorAuthenticationCode = null) + { + return string.IsNullOrEmpty(twoFactorAuthenticationCode) + ? authorizationsClient.GetOrCreateApplicationAuthentication(clientId, clientSecret, newAuthorization) + : authorizationsClient.GetOrCreateApplicationAuthentication(clientId, clientSecret, newAuthorization, twoFactorAuthenticationCode); + } + + static IObservable CreateNewAuthorization( + this IObservableAuthorizationsClient authorizationsClient, + string clientId, + string clientSecret, + NewAuthorization newAuthorization, + string twoFactorAuthenticationCode = null) + { + return string.IsNullOrEmpty(twoFactorAuthenticationCode) + ? authorizationsClient.Create(clientId, clientSecret, newAuthorization) + : authorizationsClient.Create(clientId, clientSecret, newAuthorization, twoFactorAuthenticationCode); + } } } diff --git a/Octokit.Tests.Integration/Clients/AuthorizationClientTests.cs b/Octokit.Tests.Integration/Clients/AuthorizationClientTests.cs index 5872af17..710a4c9a 100644 --- a/Octokit.Tests.Integration/Clients/AuthorizationClientTests.cs +++ b/Octokit.Tests.Integration/Clients/AuthorizationClientTests.cs @@ -22,10 +22,9 @@ namespace Octokit.Tests.Integration.Clients Helper.ClientSecret, newAuthorization); - Assert.NotNull(created); Assert.False(String.IsNullOrWhiteSpace(created.Token)); - Assert.True(String.IsNullOrWhiteSpace(created.TokenLastEight)); - Assert.True(String.IsNullOrWhiteSpace(created.HashedToken)); + Assert.False(String.IsNullOrWhiteSpace(created.TokenLastEight)); + Assert.False(String.IsNullOrWhiteSpace(created.HashedToken)); // we can then query it through the regular API var get = await client.Authorization.Get(created.Id); @@ -42,13 +41,11 @@ namespace Octokit.Tests.Integration.Clients Assert.Equal(created.Id, getExisting.Id); - // NOTE: the old API will continue to return the full - // token if no Fingerprint is included - Assert.False(String.IsNullOrWhiteSpace(getExisting.Token)); - - // NOTE: and these new values are not included - Assert.True(String.IsNullOrWhiteSpace(getExisting.TokenLastEight)); - Assert.True(String.IsNullOrWhiteSpace(getExisting.HashedToken)); + // the token is no longer returned for subsequent calls + Assert.True(String.IsNullOrWhiteSpace(getExisting.Token)); + // however the hashed and last 8 characters are available + Assert.False(String.IsNullOrWhiteSpace(getExisting.TokenLastEight)); + Assert.False(String.IsNullOrWhiteSpace(getExisting.HashedToken)); await client.Authorization.Delete(created.Id); } diff --git a/Octokit.Tests.Integration/Clients/EventsClientTests.cs b/Octokit.Tests.Integration/Clients/EventsClientTests.cs index 666d4104..2802bbff 100644 --- a/Octokit.Tests.Integration/Clients/EventsClientTests.cs +++ b/Octokit.Tests.Integration/Clients/EventsClientTests.cs @@ -34,7 +34,7 @@ namespace Octokit.Tests.Integration.Clients Assert.True(_events.All(e => e.Payload != null)); } - [IntegrationTest] + [IntegrationTest(Skip = "no longer able to access this event")] public void IssueCommentPayloadEventDeserializesCorrectly() { var commentEvent = _events.FirstOrDefault(e => e.Id == "2628548686"); @@ -49,7 +49,7 @@ namespace Octokit.Tests.Integration.Clients Assert.Equal(742, commentPayload.Issue.Number); } - [IntegrationTest] + [IntegrationTest(Skip = "no longer able to access this event")] public void PushEventPayloadDeserializesCorrectly() { var pushEvent = _events.FirstOrDefault(e => e.Id == "2628858765"); @@ -65,7 +65,7 @@ namespace Octokit.Tests.Integration.Clients Assert.Equal(1, pushPayload.Size); } - [IntegrationTest] + [IntegrationTest(Skip = "no longer able to access this event")] public void PREventPayloadDeserializesCorrectly() { var prEvent = _events.FirstOrDefault(e => e.Id == "2628718313"); @@ -79,7 +79,7 @@ namespace Octokit.Tests.Integration.Clients Assert.Equal(743, prPayload.PullRequest.Number); } - [IntegrationTest] + [IntegrationTest(Skip = "no longer able to access this event")] public void PRReviewCommentEventPayloadDeserializesCorrectly() { var prrcEvent = _events.First(e => e.Id == "2623246246"); diff --git a/Octokit.Tests.Integration/Clients/GistsClientTests.cs b/Octokit.Tests.Integration/Clients/GistsClientTests.cs index 96ba37b3..5b3c0ed0 100644 --- a/Octokit.Tests.Integration/Clients/GistsClientTests.cs +++ b/Octokit.Tests.Integration/Clients/GistsClientTests.cs @@ -117,4 +117,16 @@ public class GistsClientTests await _fixture.Delete(createdGist.Id); } + + [IntegrationTest] + public async Task CanGetGistChildren() + { + // Test History/Commits + var commits = await _fixture.GetAllCommits(testGistId); + Assert.NotEmpty(commits); + + // Test Forks + var forks = await _fixture.GetAllForks(testGistId); + Assert.NotEmpty(forks); + } } diff --git a/Octokit.Tests.Integration/Clients/RepositoriesClientTests.cs b/Octokit.Tests.Integration/Clients/RepositoriesClientTests.cs index 6c40f537..6f3b5344 100644 --- a/Octokit.Tests.Integration/Clients/RepositoriesClientTests.cs +++ b/Octokit.Tests.Integration/Clients/RepositoriesClientTests.cs @@ -553,7 +553,7 @@ public class RepositoriesClientTests Assert.True(repositories.Count > 80); } - [IntegrationTest] + [IntegrationTest(Skip = "Takes too long to run.")] public async Task ReturnsAllPublicRepositoriesSinceLastSeen() { var github = Helper.GetAuthenticatedClient(); diff --git a/Octokit.Tests.Integration/Clients/RepositoryContentsClientTests.cs b/Octokit.Tests.Integration/Clients/RepositoryContentsClientTests.cs index 05bd0607..707c4c47 100644 --- a/Octokit.Tests.Integration/Clients/RepositoryContentsClientTests.cs +++ b/Octokit.Tests.Integration/Clients/RepositoryContentsClientTests.cs @@ -12,10 +12,7 @@ namespace Octokit.Tests.Integration.Clients [IntegrationTest] public async Task ReturnsReadmeForSeeGit() { - var github = new GitHubClient(new ProductHeaderValue("OctokitTests")) - { - Credentials = Helper.Credentials - }; + var github = Helper.GetAuthenticatedClient(); var readme = await github.Repository.Content.GetReadme("octokit", "octokit.net"); Assert.Equal("README.md", readme.Name); @@ -28,10 +25,7 @@ namespace Octokit.Tests.Integration.Clients [IntegrationTest] public async Task ReturnsReadmeHtmlForSeeGit() { - var github = new GitHubClient(new ProductHeaderValue("OctokitTests")) - { - Credentials = Helper.Credentials - }; + var github = Helper.GetAuthenticatedClient(); var readmeHtml = await github.Repository.Content.GetReadmeHtml("octokit", "octokit.net"); Assert.True(readmeHtml.StartsWith("
( Arg.Is(u => u.ToString() == "authorizations"), - null, - Arg.Any()); + null); } } @@ -53,8 +52,7 @@ namespace Octokit.Tests.Clients client.Received().Get( Arg.Is(u => u.ToString() == "authorizations/1"), - null, - Arg.Any()); + null); } } @@ -238,9 +236,7 @@ namespace Octokit.Tests.Clients authEndpoint.GetOrCreateApplicationAuthentication("clientId", "secret", data); client.Received().Put(Arg.Is(u => u.ToString() == "authorizations/clients/clientId/ha-ha-fingerprint"), - Args.Object, - Args.String, - Args.String); // NOTE: preview API + Args.Object); } } @@ -256,8 +252,7 @@ namespace Octokit.Tests.Clients client.Received().Get( Arg.Is(u => u.ToString() == "applications/clientId/tokens/accessToken"), - null, - Arg.Any()); + null); } [Fact] diff --git a/Octokit.Tests/Clients/GistsClientTests.cs b/Octokit.Tests/Clients/GistsClientTests.cs index 7690fd92..9bb21e52 100644 --- a/Octokit.Tests/Clients/GistsClientTests.cs +++ b/Octokit.Tests/Clients/GistsClientTests.cs @@ -122,6 +122,44 @@ public class GistsClientTests } } + public class TheGetChildrenMethods + { + [Fact] + public async Task EnsureNonNullArguments() + { + var connection = Substitute.For(); + var client = new GistsClient(connection); + + await Assert.ThrowsAsync(() => client.GetAllCommits(null)); + await Assert.ThrowsAsync(() => client.GetAllCommits("")); + + await Assert.ThrowsAsync(() => client.GetAllForks(null)); + await Assert.ThrowsAsync(() => client.GetAllForks("")); + } + + [Fact] + public void RequestsCorrectGetCommitsUrl() + { + var connection = Substitute.For(); + var client = new GistsClient(connection); + + client.GetAllCommits("9257657"); + + connection.Received().GetAll(Arg.Is(u => u.ToString() == "gists/9257657/commits")); + } + + [Fact] + public void RequestsCorrectGetForksUrl() + { + var connection = Substitute.For(); + var client = new GistsClient(connection); + + client.GetAllForks("9257657"); + + connection.Received().GetAll(Arg.Is(u => u.ToString() == "gists/9257657/forks")); + } + } + public class TheCreateMethod { [Fact] diff --git a/Octokit.Tests/Clients/RepositoriesClientTests.cs b/Octokit.Tests/Clients/RepositoriesClientTests.cs index ed73c0f7..1576fb18 100644 --- a/Octokit.Tests/Clients/RepositoriesClientTests.cs +++ b/Octokit.Tests/Clients/RepositoriesClientTests.cs @@ -285,8 +285,7 @@ namespace Octokit.Tests.Clients client.GetAllPublic(new PublicRepositoryRequest(364)); connection.Received() - .GetAll(Arg.Is(u => u.ToString() == "/repositories"), - Arg.Any>()); + .GetAll(Arg.Is(u => u.ToString() == "/repositories?since=364")); } [Fact] @@ -298,9 +297,7 @@ namespace Octokit.Tests.Clients client.GetAllPublic(new PublicRepositoryRequest(364)); connection.Received() - .GetAll(Arg.Is(u => u.ToString() == "/repositories"), - Arg.Is>(d => d.Count == 1 - && d["since"] == "364")); + .GetAll(Arg.Is(u => u.ToString() == "/repositories?since=364")); } } diff --git a/Octokit.Tests/Clients/RepositoryContentsClientTests.cs b/Octokit.Tests/Clients/RepositoryContentsClientTests.cs index 48df1bee..6626f7ef 100644 --- a/Octokit.Tests/Clients/RepositoryContentsClientTests.cs +++ b/Octokit.Tests/Clients/RepositoryContentsClientTests.cs @@ -2,6 +2,7 @@ using System.Text; using System.Threading.Tasks; using NSubstitute; +using Octokit.Tests.Helpers; using Xunit; namespace Octokit.Tests.Clients @@ -52,6 +53,60 @@ namespace Octokit.Tests.Clients connection.Received().GetHtml(Arg.Is(u => u.ToString() == "repos/fake/repo/readme"), null); Assert.Equal("README", readme); } - } + } + + public class TheGetArchiveLinkMethod + { + [Fact] + public async Task ReturnsArchiveLinkWithDefaults() + { + var connection = Substitute.For(); + connection.GetRedirect(Args.Uri).Returns(Task.FromResult("https://codeload.github.com/fake/repo/legacy.tar.gz/master")); + var contentsClient = new RepositoryContentsClient(connection); + + var archiveLink = await contentsClient.GetArchiveLink("fake", "repo"); + + connection.Received().GetRedirect(Arg.Is(u => u.ToString() == "repos/fake/repo/tarball/")); + Assert.Equal("https://codeload.github.com/fake/repo/legacy.tar.gz/master", archiveLink); + } + + [Fact] + public async Task ReturnsArchiveLinkAsZipball() + { + var connection = Substitute.For(); + connection.GetRedirect(Args.Uri).Returns(Task.FromResult("https://codeload.github.com/fake/repo/legacy.tar.gz/master")); + var contentsClient = new RepositoryContentsClient(connection); + + var archiveLink = await contentsClient.GetArchiveLink("fake", "repo", ArchiveFormat.Zipball); + + connection.Received().GetRedirect(Arg.Is(u => u.ToString() == "repos/fake/repo/zipball/")); + Assert.Equal("https://codeload.github.com/fake/repo/legacy.tar.gz/master", archiveLink); + } + + [Fact] + public async Task ReturnsArchiveLinkWithSpecifiedValues() + { + var connection = Substitute.For(); + connection.GetRedirect(Args.Uri).Returns(Task.FromResult("https://codeload.github.com/fake/repo/legazy.zip/release")); + var contentsClient = new RepositoryContentsClient(connection); + + var archiveLink = await contentsClient.GetArchiveLink("fake", "repo", ArchiveFormat.Zipball, "release"); + + connection.Received().GetRedirect(Arg.Is(u => u.ToString() == "repos/fake/repo/zipball/release")); + Assert.Equal("https://codeload.github.com/fake/repo/legazy.zip/release", archiveLink); + } + + [Fact] + public async Task EnsuresArgumentsNotNull() + { + var connection = Substitute.For(); + var contentsClient = new RepositoryContentsClient(connection); + + AssertEx.Throws(async () => await contentsClient.GetArchiveLink(null, "name")); + AssertEx.Throws(async () => await contentsClient.GetArchiveLink("owner", null)); + AssertEx.Throws(async () => await contentsClient.GetArchiveLink("", "name")); + AssertEx.Throws(async () => await contentsClient.GetArchiveLink("owner", "")); + } + } } } \ No newline at end of file diff --git a/Octokit.Tests/Http/HttpClientAdapterTests.cs b/Octokit.Tests/Http/HttpClientAdapterTests.cs index e1c76c48..029b7052 100644 --- a/Octokit.Tests/Http/HttpClientAdapterTests.cs +++ b/Octokit.Tests/Http/HttpClientAdapterTests.cs @@ -16,11 +16,15 @@ namespace Octokit.Tests.Http { public class TheBuildRequestMessageMethod { + readonly Uri _endpoint = new Uri("/ha-ha-business", UriKind.Relative); + [Fact] public void AddsHeadersToRequestMessage() { var request = new Request { + BaseAddress = GitHubClient.GitHubApiUrl, + Endpoint = _endpoint, Method = HttpMethod.Post, Headers = { @@ -47,6 +51,8 @@ namespace Octokit.Tests.Http { var request = new Request { + BaseAddress = GitHubClient.GitHubApiUrl, + Endpoint = _endpoint, Method = HttpMethod.Post, Body = "{}", ContentType = "text/plain" @@ -64,6 +70,8 @@ namespace Octokit.Tests.Http { var request = new Request { + BaseAddress = GitHubClient.GitHubApiUrl, + Endpoint = _endpoint, Method = HttpMethod.Post, Body = new MemoryStream(), ContentType = "text/plain" @@ -82,6 +90,8 @@ namespace Octokit.Tests.Http { var request = new Request { + BaseAddress = GitHubClient.GitHubApiUrl, + Endpoint = _endpoint, Method = HttpMethod.Post, Body = new FormUrlEncodedContent(new Dictionary {{"foo", "bar"}}) }; diff --git a/Octokit.Tests/OctoKit.Tests-NetCore45.csproj b/Octokit.Tests/OctoKit.Tests-NetCore45.csproj index de424f8f..868b8eeb 100644 --- a/Octokit.Tests/OctoKit.Tests-NetCore45.csproj +++ b/Octokit.Tests/OctoKit.Tests-NetCore45.csproj @@ -75,6 +75,7 @@ + @@ -149,6 +150,7 @@ + diff --git a/Octokit.Tests/Octokit.Tests-Portable.csproj b/Octokit.Tests/Octokit.Tests-Portable.csproj index 77072b10..04e328c8 100644 --- a/Octokit.Tests/Octokit.Tests-Portable.csproj +++ b/Octokit.Tests/Octokit.Tests-Portable.csproj @@ -75,6 +75,7 @@ + @@ -147,6 +148,7 @@ + diff --git a/Octokit.Tests/Octokit.Tests.csproj b/Octokit.Tests/Octokit.Tests.csproj index bab5ee9c..e1cbb6f4 100644 --- a/Octokit.Tests/Octokit.Tests.csproj +++ b/Octokit.Tests/Octokit.Tests.csproj @@ -181,6 +181,7 @@ + diff --git a/Octokit.Tests/Reactive/ObservableGistsTests.cs b/Octokit.Tests/Reactive/ObservableGistsTests.cs new file mode 100644 index 00000000..3057f021 --- /dev/null +++ b/Octokit.Tests/Reactive/ObservableGistsTests.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Threading.Tasks; +using NSubstitute; +using Octokit; +using Octokit.Internal; +using Octokit.Reactive; +using Octokit.Reactive.Internal; +using Octokit.Tests.Helpers; +using Xunit; +using Xunit.Extensions; + +namespace Octokit.Tests.Reactive +{ + public class ObservableGistsTests + { + public class TheGetChildrenMethods + { + [Fact] + public async Task EnsureNonNullArguments() + { + var client = new ObservableGistsClient(Substitute.For()); + + await AssertEx.Throws(async () => await client.GetAllCommits(null)); + await AssertEx.Throws(async () => await client.GetAllCommits("")); + + await AssertEx.Throws(async () => await client.GetAllForks(null)); + await AssertEx.Throws(async () => await client.GetAllForks("")); + } + + [Fact] + public void RequestsCorrectGetCommitsUrl() + { + var github = Substitute.For(); + var client = new ObservableGistsClient(github); + var expected = new Uri("gists/9257657/commits", UriKind.Relative); + + client.GetAllCommits("9257657"); + + github.Connection.Received(1).Get>(expected, Arg.Any>(), null); + } + + [Fact] + public void RequestsCorrectGetForksUrl() + { + var github = Substitute.For(); + var client = new ObservableGistsClient(github); + var expected = new Uri("gists/9257657/forks", UriKind.Relative); + + client.GetAllForks("9257657"); + + github.Connection.Received(1).Get>(expected, Arg.Any>(), null); + } + } + } +} diff --git a/Octokit.Tests/Reactive/ObservableRepositoriesClientTests.cs b/Octokit.Tests/Reactive/ObservableRepositoriesClientTests.cs index d1668b14..f0ea85b1 100644 --- a/Octokit.Tests/Reactive/ObservableRepositoriesClientTests.cs +++ b/Octokit.Tests/Reactive/ObservableRepositoriesClientTests.cs @@ -163,7 +163,7 @@ namespace Octokit.Tests.Reactive [Fact] public async Task ReturnsEveryPageOfRepositories() { - var firstPageUrl = new Uri("/repositories", UriKind.Relative); + var firstPageUrl = new Uri("/repositories?since=364", UriKind.Relative); var secondPageUrl = new Uri("https://example.com/page/2"); var firstPageLinks = new Dictionary { { "next", secondPageUrl } }; IApiResponse> firstPageResponse = new ApiResponse>( @@ -195,9 +195,7 @@ namespace Octokit.Tests.Reactive }); var gitHubClient = Substitute.For(); - gitHubClient.Connection.Get>(firstPageUrl, - Arg.Is>(d => d.Count == 1 - && d["since"] == "364"), null) + gitHubClient.Connection.Get>(firstPageUrl, null, null) .Returns(Task.FromResult(firstPageResponse)); gitHubClient.Connection.Get>(secondPageUrl, null, null) .Returns(Task.FromResult(secondPageResponse)); @@ -209,9 +207,7 @@ namespace Octokit.Tests.Reactive var results = await repositoriesClient.GetAllPublic(new PublicRepositoryRequest(364)).ToArray(); Assert.Equal(7, results.Length); - gitHubClient.Connection.Received(1).Get>(firstPageUrl, - Arg.Is>(d=>d.Count == 1 - && d["since"] == "364"), null); + gitHubClient.Connection.Received(1).Get>(firstPageUrl, null, null); gitHubClient.Connection.Received(1).Get>(secondPageUrl, null, null); gitHubClient.Connection.Received(1).Get>(thirdPageUrl, null, null); } diff --git a/Octokit/Clients/AuthorizationsClient.cs b/Octokit/Clients/AuthorizationsClient.cs index 029cc313..716cfad1 100644 --- a/Octokit/Clients/AuthorizationsClient.cs +++ b/Octokit/Clients/AuthorizationsClient.cs @@ -14,8 +14,6 @@ namespace Octokit /// public class AuthorizationsClient : ApiClient, IAuthorizationsClient { - const string previewAcceptsHeader = "application/vnd.github.mirage-preview+json"; - /// /// Initializes a new GitHub OAuth API client. /// @@ -38,7 +36,7 @@ namespace Octokit /// A list of s. public Task> GetAll() { - return ApiConnection.GetAll(ApiUrls.Authorizations(), null, previewAcceptsHeader); + return ApiConnection.GetAll(ApiUrls.Authorizations(), null); } /// @@ -56,9 +54,97 @@ namespace Octokit /// The specified . public Task Get(int id) { - return ApiConnection.Get(ApiUrls.Authorizations(id), null, previewAcceptsHeader); + return ApiConnection.Get(ApiUrls.Authorizations(id), null); } + /// + /// Creates a new authorization for the specified OAuth application if an authorization for that application + /// doesn’t already exist for the user; otherwise, it fails. + /// + /// + /// This method requires authentication. + /// See the API documentation for more information. + /// + /// Client ID of the OAuth application for the token + /// The client secret + /// Describes the new authorization to create + /// + /// Thrown when the current user does not have permission to make this request. + /// + /// + /// Thrown when the current account has two-factor authentication enabled and an authentication code is required. + /// + /// Thrown when a general API error occurs. + /// The created . + public Task Create( + string clientId, + string clientSecret, + NewAuthorization newAuthorization) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, "clientId"); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, "clientSecret"); + Ensure.ArgumentNotNull(newAuthorization, "authorization"); + + var requestData = new + { + client_id = clientId, + client_secret = clientSecret, + scopes = newAuthorization.Scopes, + note = newAuthorization.Note, + note_url = newAuthorization.NoteUrl, + fingerprint = newAuthorization.Fingerprint + }; + + var endpoint = ApiUrls.Authorizations(); + + return ApiConnection.Post(endpoint, requestData); + } + + /// + /// Creates a new authorization for the specified OAuth application if an authorization for that application + /// doesn’t already exist for the user; otherwise, it fails. + /// + /// + /// This method requires authentication. + /// See the API documentation for more information. + /// + /// Client ID of the OAuth application for the token + /// The client secret + /// The two-factor authentication code in response to the current user's previous challenge + /// Describes the new authorization to create + /// + /// Thrown when the current user does not have permission to make this request. + /// + /// + /// Thrown when the current account has two-factor authentication enabled and an authentication code is required. + /// + /// Thrown when a general API error occurs. + /// The created . + public Task Create( + string clientId, + string clientSecret, + NewAuthorization newAuthorization, + string twoFactorAuthenticationCode) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, "clientId"); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, "clientSecret"); + Ensure.ArgumentNotNull(newAuthorization, "authorization"); + Ensure.ArgumentNotNullOrEmptyString(twoFactorAuthenticationCode, "twoFactorAuthenticationCode"); + + var requestData = new + { + client_id = clientId, + client_secret = clientSecret, + scopes = newAuthorization.Scopes, + note = newAuthorization.Note, + note_url = newAuthorization.NoteUrl, + fingerprint = newAuthorization.Fingerprint + }; + + var endpoint = ApiUrls.Authorizations(); + return ApiConnection.Post(endpoint, requestData, null, null, twoFactorAuthenticationCode); + } + /// /// Creates a new authorization for the specified OAuth application if an authorization for that application doesn’t already /// exist for the user; otherwise, returns the user’s existing authorization for that application. @@ -95,18 +181,11 @@ namespace Octokit note_url = newAuthorization.NoteUrl }; - if (String.IsNullOrWhiteSpace(newAuthorization.Fingerprint)) - { - // use classic API - var endpoint = ApiUrls.AuthorizationsForClient(clientId); - return ApiConnection.Put(endpoint, requestData); - } - else - { - // use new API - var endpoint = ApiUrls.AuthorizationsForClient(clientId, newAuthorization.Fingerprint); - return ApiConnection.Put(endpoint, requestData, null, previewAcceptsHeader); - } + var endpoint = string.IsNullOrWhiteSpace(newAuthorization.Fingerprint) + ? ApiUrls.AuthorizationsForClient(clientId) + : ApiUrls.AuthorizationsForClient(clientId, newAuthorization.Fingerprint); + + return ApiConnection.Put(endpoint, requestData); } /// @@ -150,29 +229,18 @@ namespace Octokit try { - if (String.IsNullOrWhiteSpace(newAuthorization.Fingerprint)) - { - // use classic API - var endpoint = ApiUrls.AuthorizationsForClient(clientId); - return await ApiConnection.Put( - endpoint, - requestData, - twoFactorAuthenticationCode); - } - else - { - // use new API - var endpoint = ApiUrls.AuthorizationsForClient(clientId, newAuthorization.Fingerprint); - return await ApiConnection.Put( - endpoint, - requestData, - twoFactorAuthenticationCode, - previewAcceptsHeader); - } + var endpoint = string.IsNullOrWhiteSpace(newAuthorization.Fingerprint) + ? ApiUrls.AuthorizationsForClient(clientId) + : ApiUrls.AuthorizationsForClient(clientId, newAuthorization.Fingerprint); + + return await ApiConnection.Put( + endpoint, + requestData, + twoFactorAuthenticationCode); } catch (AuthorizationException e) { - throw new TwoFactorChallengeFailedException(e); + throw new TwoFactorChallengeFailedException(twoFactorAuthenticationCode, e); } } @@ -194,8 +262,7 @@ namespace Octokit var endpoint = ApiUrls.ApplicationAuthorization(clientId, accessToken); return await ApiConnection.Get( endpoint, - null, - previewAcceptsHeader); + null); } /// @@ -274,19 +341,9 @@ namespace Octokit { Ensure.ArgumentNotNull(authorizationUpdate, "authorizationUpdate"); - if (String.IsNullOrWhiteSpace(authorizationUpdate.Fingerprint)) - { - return ApiConnection.Patch( - ApiUrls.Authorizations(id), - authorizationUpdate); - } - else - { - return ApiConnection.Patch( - ApiUrls.Authorizations(id), - authorizationUpdate, - previewAcceptsHeader); - } + return ApiConnection.Patch( + ApiUrls.Authorizations(id), + authorizationUpdate); } /// @@ -307,5 +364,25 @@ namespace Octokit { return ApiConnection.Delete(ApiUrls.Authorizations(id)); } + + /// + /// Deletes the specified . + /// + /// + /// This method requires authentication. + /// See the API + /// documentation for more details. + /// + /// The system-wide ID of the authorization to delete + /// Two factor authorization code + /// + /// Thrown when the current user does not have permission to make the request. + /// + /// Thrown when a general API error occurs. + /// A for the request's execution. + public Task Delete(int id, string twoFactorAuthenticationCode) + { + return ApiConnection.Delete(ApiUrls.Authorizations(id), twoFactorAuthenticationCode); + } } } diff --git a/Octokit/Clients/GistsClient.cs b/Octokit/Clients/GistsClient.cs index 9847690c..96664c66 100644 --- a/Octokit/Clients/GistsClient.cs +++ b/Octokit/Clients/GistsClient.cs @@ -196,6 +196,34 @@ namespace Octokit return ApiConnection.GetAll(ApiUrls.UsersGists(user), request.ToParametersDictionary()); } + /// + /// List gist commits + /// + /// + /// http://developer.github.com/v3/gists/#list-gists-commits + /// + /// The id of the gist + public Task> GetAllCommits(string id) + { + Ensure.ArgumentNotNullOrEmptyString(id, "id"); + + return ApiConnection.GetAll(ApiUrls.GistCommits(id)); + } + + /// + /// List gist forks + /// + /// + /// http://developer.github.com/v3/gists/#list-gists-forks + /// + /// The id of the gist + public Task> GetAllForks(string id) + { + Ensure.ArgumentNotNullOrEmptyString(id, "id"); + + return ApiConnection.GetAll(ApiUrls.ForkGist(id)); + } + /// /// Edits a gist /// diff --git a/Octokit/Clients/IAuthorizationsClient.cs b/Octokit/Clients/IAuthorizationsClient.cs index 583bcd89..8fb80bae 100644 --- a/Octokit/Clients/IAuthorizationsClient.cs +++ b/Octokit/Clients/IAuthorizationsClient.cs @@ -49,6 +49,56 @@ namespace Octokit Justification = "It's fiiiine. It's fine. Trust us.")] Task Get(int id); + /// + /// Creates a new authorization for the specified OAuth application if an authorization for that application + /// doesn’t already exist for the user; otherwise, it fails. + /// + /// + /// This method requires authentication. + /// See the API documentation for more information. + /// + /// Client ID of the OAuth application for the token + /// The client secret + /// Describes the new authorization to create + /// + /// Thrown when the current user does not have permission to make this request. + /// + /// + /// Thrown when the current account has two-factor authentication enabled and an authentication code is required. + /// + /// Thrown when a general API error occurs. + /// The created . + Task Create( + string clientId, + string clientSecret, + NewAuthorization newAuthorization); + + /// + /// Creates a new authorization for the specified OAuth application if an authorization for that application + /// doesn’t already exist for the user; otherwise, it fails. + /// + /// + /// This method requires authentication. + /// See the API documentation for more information. + /// + /// Client ID of the OAuth application for the token + /// The client secret + /// The two-factor authentication code in response to the current user's previous challenge + /// Describes the new authorization to create + /// + /// Thrown when the current user does not have permission to make this request. + /// + /// + /// Thrown when the current account has two-factor authentication enabled and an authentication code is required. + /// + /// Thrown when a general API error occurs. + /// The created . + Task Create( + string clientId, + string clientSecret, + NewAuthorization newAuthorization, + string twoFactorAuthenticationCode); + /// /// Creates a new authorization for the specified OAuth application if an authorization for that application doesn’t already /// exist for the user; otherwise, returns the user’s existing authorization for that application. @@ -178,5 +228,22 @@ namespace Octokit /// Thrown when a general API error occurs. /// A for the request's execution. Task Delete(int id); + + /// + /// Deletes the specified . + /// + /// + /// This method requires authentication. + /// See the API + /// documentation for more details. + /// + /// The system-wide ID of the authorization to delete + /// Two factor authorization code + /// + /// Thrown when the current user does not have permission to make the request. + /// + /// Thrown when a general API error occurs. + /// A for the request's execution. + Task Delete(int id, string twoFactorAuthenticationCode); } } diff --git a/Octokit/Clients/IGistsClient.cs b/Octokit/Clients/IGistsClient.cs index c344c576..5ac92298 100644 --- a/Octokit/Clients/IGistsClient.cs +++ b/Octokit/Clients/IGistsClient.cs @@ -98,6 +98,24 @@ namespace Octokit /// Only gists updated at or after this time are returned Task> GetAllForUser(string user, DateTimeOffset since); + /// + /// List gist commits + /// + /// + /// http://developer.github.com/v3/gists/#list-gists-commits + /// + /// The id of the gist + Task> GetAllCommits(string id); + + /// + /// List gist forks + /// + /// + /// http://developer.github.com/v3/gists/#list-gists-forks + /// + /// The id of the gist + Task> GetAllForks(string id); + /// /// Creates a new gist /// diff --git a/Octokit/Clients/IRepositoryContentsClient.cs b/Octokit/Clients/IRepositoryContentsClient.cs index 84b7db96..45a217b0 100644 --- a/Octokit/Clients/IRepositoryContentsClient.cs +++ b/Octokit/Clients/IRepositoryContentsClient.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Octokit.Internal; namespace Octokit { @@ -46,6 +47,45 @@ namespace Octokit /// Task GetReadmeHtml(string owner, string name); + /// + /// This method will return a 302 to a URL to download a tarball or zipball archive for a repository. + /// Please make sure your HTTP framework is configured to follow redirects or you will need to use the + /// Location header to make a second GET request. + /// Note: For private repositories, these links are temporary and expire quickly. + /// + /// https://developer.github.com/v3/repos/contents/#get-archive-link + /// The owner of the repository + /// The name of the repository + /// + Task GetArchiveLink(string owner, string name); + + /// + /// This method will return a 302 to a URL to download a tarball or zipball archive for a repository. + /// Please make sure your HTTP framework is configured to follow redirects or you will need to use the + /// Location header to make a second GET request. + /// Note: For private repositories, these links are temporary and expire quickly. + /// + /// https://developer.github.com/v3/repos/contents/#get-archive-link + /// The owner of the repository + /// The name of the repository + /// The format of the archive. Can be either tarball or zipball + /// + Task GetArchiveLink(string owner, string name, ArchiveFormat archiveFormat); + + /// + /// This method will return a 302 to a URL to download a tarball or zipball archive for a repository. + /// Please make sure your HTTP framework is configured to follow redirects or you will need to use the + /// Location header to make a second GET request. + /// Note: For private repositories, these links are temporary and expire quickly. + /// + /// https://developer.github.com/v3/repos/contents/#get-archive-link + /// The owner of the repository + /// The name of the repository + /// The format of the archive. Can be either tarball or zipball + /// A valid Git reference. + /// + Task GetArchiveLink(string owner, string name, ArchiveFormat archiveFormat, string reference); + /// /// Creates a commit that creates a new file in a repository. /// @@ -75,4 +115,14 @@ namespace Octokit /// Information about the file to delete Task DeleteFile(string owner, string name, string path, DeleteFileRequest request); } + + public enum ArchiveFormat + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Tarball")] + [Parameter(Value = "tarball")] + Tarball, + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Zipball")] + [Parameter(Value = "zipball")] + Zipball + } } \ No newline at end of file diff --git a/Octokit/Clients/RepositoriesClient.cs b/Octokit/Clients/RepositoriesClient.cs index e992e57b..089da63d 100644 --- a/Octokit/Clients/RepositoriesClient.cs +++ b/Octokit/Clients/RepositoriesClient.cs @@ -205,7 +205,9 @@ namespace Octokit { Ensure.ArgumentNotNull(request, "request"); - return ApiConnection.GetAll(ApiUrls.AllPublicRepositories(), request.ToParametersDictionary()); + var url = ApiUrls.AllPublicRepositories(request.Since); + + return ApiConnection.GetAll(url); } /// diff --git a/Octokit/Clients/RepositoryContentsClient.cs b/Octokit/Clients/RepositoryContentsClient.cs index 5cf1caae..7b867f01 100644 --- a/Octokit/Clients/RepositoryContentsClient.cs +++ b/Octokit/Clients/RepositoryContentsClient.cs @@ -74,6 +74,57 @@ namespace Octokit return ApiConnection.GetHtml(ApiUrls.RepositoryReadme(owner, name), null); } + /// + /// This method will return a 302 to a URL to download a tarball or zipball archive for a repository. + /// Please make sure your HTTP framework is configured to follow redirects or you will need to use the + /// Location header to make a second GET request. + /// Note: For private repositories, these links are temporary and expire quickly. + /// + /// https://developer.github.com/v3/repos/contents/#get-archive-link + /// The owner of the repository + /// The name of the repository + /// + public Task GetArchiveLink(string owner, string name) + { + return GetArchiveLink(owner, name, ArchiveFormat.Tarball, string.Empty); + } + + /// + /// This method will return a 302 to a URL to download a tarball or zipball archive for a repository. + /// Please make sure your HTTP framework is configured to follow redirects or you will need to use the + /// Location header to make a second GET request. + /// Note: For private repositories, these links are temporary and expire quickly. + /// + /// https://developer.github.com/v3/repos/contents/#get-archive-link + /// The owner of the repository + /// The name of the repository + /// The format of the archive. Can be either tarball or zipball + /// + public Task GetArchiveLink(string owner, string name, ArchiveFormat archiveFormat) + { + return GetArchiveLink(owner, name, archiveFormat, string.Empty); + } + + /// + /// This method will return a 302 to a URL to download a tarball or zipball archive for a repository. + /// Please make sure your HTTP framework is configured to follow redirects or you will need to use the + /// Location header to make a second GET request. + /// Note: For private repositories, these links are temporary and expire quickly. + /// + /// https://developer.github.com/v3/repos/contents/#get-archive-link + /// The owner of the repository + /// The name of the repository + /// The format of the archive. Can be either tarball or zipball + /// A valid Git reference. + /// + public Task GetArchiveLink(string owner, string name, ArchiveFormat archiveFormat, string reference) + { + Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + + return ApiConnection.GetRedirect(ApiUrls.RepositoryArchiveLink(owner, name, archiveFormat, reference)); + } + /// /// Creates a commit that creates a new file in a repository. /// diff --git a/Octokit/Exceptions/ApiException.cs b/Octokit/Exceptions/ApiException.cs index 9e16c540..fa30f9f2 100644 --- a/Octokit/Exceptions/ApiException.cs +++ b/Octokit/Exceptions/ApiException.cs @@ -67,6 +67,7 @@ namespace Octokit StatusCode = response.StatusCode; ApiError = GetApiErrorFromExceptionMessage(response); + HttpResponse = response; } /// @@ -97,6 +98,8 @@ namespace Octokit StatusCode = statusCode; } + public IResponse HttpResponse { get; private set; } + public override string Message { get { return ApiErrorMessageSafe ?? "An error occurred with this API request"; } diff --git a/Octokit/Exceptions/TwoFactorAuthorizationException.cs b/Octokit/Exceptions/TwoFactorAuthorizationException.cs new file mode 100644 index 00000000..c8f16cb2 --- /dev/null +++ b/Octokit/Exceptions/TwoFactorAuthorizationException.cs @@ -0,0 +1,115 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Runtime.Serialization; + +namespace Octokit +{ +#if !NETFX_CORE + /// + /// Represents a failed 2FA challenge from the API + /// + [Serializable] +#endif + [SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors", + Justification = "These exceptions are specific to the GitHub API and not general purpose exceptions")] + public abstract class TwoFactorAuthorizationException : AuthorizationException + { + /// + /// Constructs an instance of TwoFactorRequiredException. + /// + /// Expected 2FA response type + /// The inner exception + protected TwoFactorAuthorizationException(TwoFactorType twoFactorType, Exception innerException) + : base(HttpStatusCode.Unauthorized, innerException) + { + TwoFactorType = twoFactorType; + } + + /// + /// Constructs an instance of TwoFactorRequiredException. + /// + /// The HTTP payload from the server + /// Expected 2FA response type + protected TwoFactorAuthorizationException(IResponse response, TwoFactorType twoFactorType) + : base(response) + { + Debug.Assert(response != null && response.StatusCode == HttpStatusCode.Unauthorized, + "TwoFactorRequiredException status code should be 401"); + + TwoFactorType = twoFactorType; + } + + /// + /// Constructs an instance of TwoFactorRequiredException. + /// + /// The HTTP payload from the server + /// Expected 2FA response type + /// The inner exception + protected TwoFactorAuthorizationException(IResponse response, TwoFactorType twoFactorType, Exception innerException) + : base(response, innerException) + { + Debug.Assert(response != null && response.StatusCode == HttpStatusCode.Unauthorized, + "TwoFactorRequiredException status code should be 401"); + + TwoFactorType = twoFactorType; + } + + /// + /// Expected 2FA response type + /// + public TwoFactorType TwoFactorType { get; private set; } + +#if !NETFX_CORE + /// + /// Constructs an instance of TwoFactorRequiredException. + /// + /// + /// The that holds the + /// serialized object data about the exception being thrown. + /// + /// + /// The that contains + /// contextual information about the source or destination. + /// + protected TwoFactorAuthorizationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + if (info == null) return; + TwoFactorType = (TwoFactorType) (info.GetInt32("TwoFactorType")); + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue("TwoFactorType", TwoFactorType); + } +#endif + + } + + /// + /// Methods for receiving 2FA authentication codes + /// + public enum TwoFactorType + { + /// + /// No method configured + /// + None, + /// + /// Unknown method + /// + Unknown, + /// + /// Receive via SMS + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sms")] + Sms, + /// + /// Receive via application + /// + AuthenticatorApp + } +} diff --git a/Octokit/Exceptions/TwoFactorChallengeFailedException.cs b/Octokit/Exceptions/TwoFactorChallengeFailedException.cs index 7a93bbae..d501fe18 100644 --- a/Octokit/Exceptions/TwoFactorChallengeFailedException.cs +++ b/Octokit/Exceptions/TwoFactorChallengeFailedException.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Net; using System.Runtime.Serialization; namespace Octokit @@ -13,23 +12,26 @@ namespace Octokit #endif [SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors", Justification = "These exceptions are specific to the GitHub API and not general purpose exceptions")] - public class TwoFactorChallengeFailedException : AuthorizationException + public class TwoFactorChallengeFailedException : TwoFactorAuthorizationException { /// /// Constructs an instance of TwoFactorChallengeFailedException /// - public TwoFactorChallengeFailedException() : - base(HttpStatusCode.Unauthorized, null) + public TwoFactorChallengeFailedException() : base(TwoFactorType.None, null) { } /// /// Constructs an instance of TwoFactorChallengeFailedException /// + /// The authorization code that was incorrect /// The inner exception - public TwoFactorChallengeFailedException(Exception innerException) - : base(HttpStatusCode.Unauthorized, innerException) + public TwoFactorChallengeFailedException( + string authorizationCode, + ApiException innerException) + : base(ParseTwoFactorType(innerException), innerException) { + AuthorizationCode = authorizationCode; } public override string Message @@ -37,9 +39,16 @@ namespace Octokit get { return "The two-factor authentication code supplied is not correct"; } } + public string AuthorizationCode { get; private set; } + + static TwoFactorType ParseTwoFactorType(ApiException exception) + { + return exception == null ? TwoFactorType.None : Connection.ParseTwoFactorType(exception.HttpResponse); + } + #if !NETFX_CORE /// - /// Constructs an instance of TwoFactorChallengeFailedException + /// Constructs an instance of TwoFactorChallengeFailedException. /// /// /// The that holds the @@ -52,6 +61,14 @@ namespace Octokit protected TwoFactorChallengeFailedException(SerializationInfo info, StreamingContext context) : base(info, context) { + if (info == null) return; + AuthorizationCode = info.GetString("AuthorizationCode"); + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue("AuthorizationCode", AuthorizationCode); } #endif } diff --git a/Octokit/Exceptions/TwoFactorRequiredException.cs b/Octokit/Exceptions/TwoFactorRequiredException.cs index ad7a2ee3..f2361c24 100644 --- a/Octokit/Exceptions/TwoFactorRequiredException.cs +++ b/Octokit/Exceptions/TwoFactorRequiredException.cs @@ -14,7 +14,7 @@ namespace Octokit #endif [SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors", Justification = "These exceptions are specific to the GitHub API and not general purpose exceptions")] - public class TwoFactorRequiredException : AuthorizationException + public class TwoFactorRequiredException : TwoFactorAuthorizationException { /// /// Constructs an instance of TwoFactorRequiredException. @@ -27,10 +27,8 @@ namespace Octokit /// Constructs an instance of TwoFactorRequiredException. /// /// Expected 2FA response type - public TwoFactorRequiredException(TwoFactorType twoFactorType) - : base(HttpStatusCode.Unauthorized, null) + public TwoFactorRequiredException(TwoFactorType twoFactorType) : base(twoFactorType, null) { - TwoFactorType = twoFactorType; } /// @@ -38,12 +36,11 @@ namespace Octokit /// /// The HTTP payload from the server /// Expected 2FA response type - public TwoFactorRequiredException(IResponse response, TwoFactorType twoFactorType) : base(response) + public TwoFactorRequiredException(IResponse response, TwoFactorType twoFactorType) + : base(response, twoFactorType) { Debug.Assert(response != null && response.StatusCode == HttpStatusCode.Unauthorized, "TwoFactorRequiredException status code should be 401"); - - TwoFactorType = twoFactorType; } public override string Message @@ -66,44 +63,7 @@ namespace Octokit protected TwoFactorRequiredException(SerializationInfo info, StreamingContext context) : base(info, context) { - if (info == null) return; - TwoFactorType = (TwoFactorType) (info.GetInt32("TwoFactorType")); - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - info.AddValue("TwoFactorType", TwoFactorType); } #endif - - /// - /// Expected 2FA response type - /// - public TwoFactorType TwoFactorType { get; private set; } - } - - /// - /// Methods for receiving 2FA authentication codes - /// - public enum TwoFactorType - { - /// - /// No method configured - /// - None, - /// - /// Unknown method - /// - Unknown, - /// - /// Receive via SMS - /// - [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sms")] - Sms, - /// - /// Receive via application - /// - AuthenticatorApp } } diff --git a/Octokit/GitHubClient.cs b/Octokit/GitHubClient.cs index a1e5e67f..d88a6257 100644 --- a/Octokit/GitHubClient.cs +++ b/Octokit/GitHubClient.cs @@ -23,7 +23,7 @@ namespace Octokit /// the user agent for analytics purposes. /// public GitHubClient(ProductHeaderValue productInformation) - : this(new Connection(productInformation)) + : this(new Connection(productInformation, GitHubApiUrl)) { } diff --git a/Octokit/Helpers/ApiExtensions.cs b/Octokit/Helpers/ApiExtensions.cs index bf164a6b..032c7e3c 100644 --- a/Octokit/Helpers/ApiExtensions.cs +++ b/Octokit/Helpers/ApiExtensions.cs @@ -90,6 +90,14 @@ namespace Octokit return connection.Get(uri, null, null); } + public static Task> GetRedirect(this IConnection connection, Uri uri) + { + Ensure.ArgumentNotNull(connection, "connection"); + Ensure.ArgumentNotNull(uri, "uri"); + + return connection.Get(uri, null, null, false); + } + /// /// Gets the API resource at the specified URI. /// diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs index 8b9ae86e..c569d5aa 100644 --- a/Octokit/Helpers/ApiUrls.cs +++ b/Octokit/Helpers/ApiUrls.cs @@ -19,15 +19,23 @@ namespace Octokit static readonly Uri _oauthAuthorize = new Uri("login/oauth/authorize", UriKind.Relative); static readonly Uri _oauthAccesToken = new Uri("login/oauth/access_token", UriKind.Relative); + /// + /// Returns the that returns all public repositories in + /// response to a GET request. + /// + public static Uri AllPublicRepositories() + { + return "/repositories".FormatUri(); + } /// /// Returns the that returns all public repositories in /// response to a GET request. /// - /// - public static Uri AllPublicRepositories() + /// The integer ID of the last Repository that you’ve seen. + public static Uri AllPublicRepositories(long since) { - return "/repositories".FormatUri(); + return "/repositories?since={0}".FormatUri(since); } /// @@ -773,7 +781,7 @@ namespace Octokit } /// - /// Returns the for the forks of a given gist. + /// Returns the for the forks for the specified gist. /// /// The id of the gist public static Uri ForkGist(string id) @@ -824,6 +832,15 @@ namespace Octokit return "gists/{0}/comments".FormatUri(gistId); } + /// + /// Returns the for the commits for the specified gist. + /// + /// The id of the gist + public static Uri GistCommits(string id) + { + return "gists/{0}/commits".FormatUri(id); + } + /// /// Returns the that returns the specified pull request. /// @@ -1508,5 +1525,10 @@ namespace Octokit { return "repos/{0}/{1}/contents/{2}".FormatUri(owner, name, path); } + + public static Uri RepositoryArchiveLink(string owner, string name, ArchiveFormat archiveFormat, string reference) + { + return "repos/{0}/{1}/{2}/{3}".FormatUri(owner, name, archiveFormat.ToParameter(), reference); + } } } diff --git a/Octokit/Http/ApiConnection.cs b/Octokit/Http/ApiConnection.cs index 066ecf56..9b1656f5 100644 --- a/Octokit/Http/ApiConnection.cs +++ b/Octokit/Http/ApiConnection.cs @@ -199,6 +199,33 @@ namespace Octokit return response.Body; } + /// + /// Creates a new API resource in the list at the specified URI. + /// + /// The API resource's type. + /// URI of the API resource to get + /// Object that describes the new API resource; this will be serialized and used as the request's body + /// Accept header to use for the API request + /// Content type of the API request + /// Two Factor Authentication Code + /// The created API resource. + /// Thrown when an API error occurs. + public async Task Post(Uri uri, object data, string accepts, string contentType, string twoFactorAuthenticationCode) + { + Ensure.ArgumentNotNull(uri, "uri"); + Ensure.ArgumentNotNull(data, "data"); + Ensure.ArgumentNotNull(twoFactorAuthenticationCode, "twoFactorAuthenticationCode"); + + var response = await Connection.Post( + uri, + data, + accepts, + contentType, + twoFactorAuthenticationCode).ConfigureAwait(false); + return response.Body; + } + + public async Task Post(Uri uri, object data, string accepts, string contentType, TimeSpan timeout) { Ensure.ArgumentNotNull(uri, "uri"); @@ -345,6 +372,19 @@ namespace Octokit return Connection.Delete(uri); } + /// + /// Deletes the API object at the specified URI. + /// + /// URI of the API resource to delete + /// Two Factor Code + /// A for the request's execution. + public Task Delete(Uri uri, string twoFactorAuthenticationCode) + { + Ensure.ArgumentNotNull(uri, "uri"); + + return Connection.Delete(uri, twoFactorAuthenticationCode); + } + /// /// Deletes the API object at the specified URI. /// @@ -359,6 +399,28 @@ namespace Octokit return Connection.Delete(uri, data); } + /// + /// Executes a GET to the API object at the specified URI. This operation is appropriate for + /// API calls which wants to return the redirect URL. + /// It expects the API to respond with a 302 Found. + /// + /// URI of the API resource to get + /// The URL returned by the API in the Location header + /// Thrown when an API error occurs, or the API does not respond with a 302 Found + public async Task GetRedirect(Uri uri) + { + Ensure.ArgumentNotNull(uri, "uri"); + var response = await Connection.GetRedirect(uri); + + if (response.HttpResponse.StatusCode == HttpStatusCode.Redirect) + { + return response.HttpResponse.Headers["Location"]; + } + + throw new ApiException("Redirect Operation expect status code of Redirect.", + response.HttpResponse.StatusCode); + } + /// /// Executes a GET to the API object at the specified URI. This operation is appropriate for /// API calls which queue long running calculations. diff --git a/Octokit/Http/Connection.cs b/Octokit/Http/Connection.cs index 8debc2e2..182ac9b5 100644 --- a/Octokit/Http/Connection.cs +++ b/Octokit/Http/Connection.cs @@ -142,6 +142,24 @@ namespace Octokit return SendData(uri.ApplyParameters(parameters), HttpMethod.Get, null, accepts, null, CancellationToken.None); } + /// + /// Performs an asynchronous HTTP GET request. + /// Attempts to map the response to an object of type + /// + /// The type to map the response to + /// URI endpoint to send request to + /// Querystring parameters for the request + /// Specifies accepted response media types. + /// To follow redirect links automatically or not + /// representing the received HTTP response + + public Task> Get(Uri uri, IDictionary parameters, string accepts, bool allowAutoRedirect) + { + Ensure.ArgumentNotNull(uri, "uri"); + + return SendData(uri.ApplyParameters(parameters), HttpMethod.Get, null, accepts, null, CancellationToken.None, allowAutoRedirect: allowAutoRedirect); + } + public Task> Get(Uri uri, IDictionary parameters, string accepts, CancellationToken cancellationToken) { Ensure.ArgumentNotNull(uri, "uri"); @@ -205,6 +223,27 @@ namespace Octokit return SendData(uri, HttpMethod.Post, body, accepts, contentType, CancellationToken.None); } + /// + /// Performs an asynchronous HTTP POST request. + /// Attempts to map the response body to an object of type + /// + /// The type to map the response to + /// URI endpoint to send request to + /// The object to serialize as the body of the request + /// Specifies accepted response media types. + /// Specifies the media type of the request body + /// Two Factor Authentication Code + /// representing the received HTTP response + public Task> Post(Uri uri, object body, string accepts, string contentType, string twoFactorAuthenticationCode) + { + Ensure.ArgumentNotNull(uri, "uri"); + Ensure.ArgumentNotNull(body, "body"); + Ensure.ArgumentNotNullOrEmptyString(twoFactorAuthenticationCode, "twoFactorAuthenticationCode"); + + return SendData(uri, HttpMethod.Post, body, accepts, contentType, CancellationToken.None, twoFactorAuthenticationCode); + + } + public Task> Post(Uri uri, object body, string accepts, string contentType, TimeSpan timeout) { Ensure.ArgumentNotNull(uri, "uri"); @@ -281,7 +320,8 @@ namespace Octokit string contentType, CancellationToken cancellationToken, string twoFactorAuthenticationCode = null, - Uri baseAddress = null) + Uri baseAddress = null, + bool allowAutoRedirect = true) { Ensure.ArgumentNotNull(uri, "uri"); @@ -290,6 +330,7 @@ namespace Octokit Method = method, BaseAddress = baseAddress ?? BaseAddress, Endpoint = uri, + AllowAutoRedirect = allowAutoRedirect, }; return SendDataInternal(body, accepts, contentType, cancellationToken, twoFactorAuthenticationCode, request); @@ -374,6 +415,27 @@ namespace Octokit return response.HttpResponse.StatusCode; } + /// + /// Performs an asynchronous HTTP DELETE request that expects an empty response. + /// + /// URI endpoint to send request to + /// Two Factor Code + /// The returned + public async Task Delete(Uri uri, string twoFactorAuthenticationCode) + { + Ensure.ArgumentNotNull(uri, "uri"); + + var response = await SendData( + uri, + HttpMethod.Delete, + null, + null, + null, + CancellationToken.None, + twoFactorAuthenticationCode); + return response.HttpResponse.StatusCode; + } + /// /// Performs an asynchronous HTTP DELETE request that expects an empty response. /// @@ -502,9 +564,9 @@ namespace Octokit : new ForbiddenException(response); } - static TwoFactorType ParseTwoFactorType(IResponse restResponse) + internal static TwoFactorType ParseTwoFactorType(IResponse restResponse) { - if (restResponse.Headers == null || !restResponse.Headers.Any()) return TwoFactorType.None; + if (restResponse == null || restResponse.Headers == null || !restResponse.Headers.Any()) return TwoFactorType.None; var otpHeader = restResponse.Headers.FirstOrDefault(header => header.Key.Equals("X-GitHub-OTP", StringComparison.OrdinalIgnoreCase)); if (String.IsNullOrEmpty(otpHeader.Value)) return TwoFactorType.None; diff --git a/Octokit/Http/HttpClientAdapter.cs b/Octokit/Http/HttpClientAdapter.cs index ec7ffd4b..0e9888fc 100644 --- a/Octokit/Http/HttpClientAdapter.cs +++ b/Octokit/Http/HttpClientAdapter.cs @@ -50,15 +50,21 @@ namespace Octokit.Internal httpOptions.Proxy = _webProxy; } - var http = new HttpClient(httpOptions) + var http = new HttpClient(httpOptions); + var cancellationTokenForRequest = cancellationToken; + + if (request.Timeout != TimeSpan.Zero) { - BaseAddress = request.BaseAddress, - Timeout = request.Timeout - }; + var timeoutCancellation = new CancellationTokenSource(request.Timeout); + var unifiedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellation.Token); + + cancellationTokenForRequest = unifiedCancellationToken.Token; + } + using (var requestMessage = BuildRequestMessage(request)) { // Make the request - var responseMessage = await http.SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken) + var responseMessage = await http.SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationTokenForRequest) .ConfigureAwait(false); return await BuildResponse(responseMessage).ConfigureAwait(false); } @@ -76,14 +82,14 @@ namespace Octokit.Internal { contentType = GetContentMediaType(responseMessage.Content); - // We added support for downloading images. Let's constrain this appropriately. - if (contentType == null || !contentType.StartsWith("image/")) + // We added support for downloading images and zip-files. Let's constrain this appropriately. + if (contentType != null && (contentType.StartsWith("image/") || contentType.Equals("application/zip", StringComparison.OrdinalIgnoreCase))) { - responseBody = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); + responseBody = await responseMessage.Content.ReadAsByteArrayAsync().ConfigureAwait(false); } else { - responseBody = await responseMessage.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + responseBody = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); } } } @@ -101,7 +107,8 @@ namespace Octokit.Internal HttpRequestMessage requestMessage = null; try { - requestMessage = new HttpRequestMessage(request.Method, request.Endpoint); + var fullUri = new Uri(request.BaseAddress, request.Endpoint); + requestMessage = new HttpRequestMessage(request.Method, fullUri); foreach (var header in request.Headers) { requestMessage.Headers.Add(header.Key, header.Value); diff --git a/Octokit/Http/IApiConnection.cs b/Octokit/Http/IApiConnection.cs index 28b5b73a..8a7c36bd 100644 --- a/Octokit/Http/IApiConnection.cs +++ b/Octokit/Http/IApiConnection.cs @@ -123,6 +123,19 @@ namespace Octokit /// Thrown when an API error occurs. Task Post(Uri uri, object data, string accepts, string contentType); + /// + /// Creates a new API resource in the list at the specified URI. + /// + /// The API resource's type. + /// URI of the API resource to get + /// Object that describes the new API resource; this will be serialized and used as the request's body + /// Accept header to use for the API request + /// Content type of the API request + /// Two Factor Authentication Code + /// The created API resource. + /// Thrown when an API error occurs. + Task Post(Uri uri, object data, string accepts, string contentType, string twoFactorAuthenticationCode); + /// /// Creates a new API resource in the list at the specified URI. /// @@ -212,6 +225,14 @@ namespace Octokit /// A for the request's execution. Task Delete(Uri uri); + /// + /// Deletes the API object at the specified URI. + /// + /// URI of the API resource to delete + /// Two Factor Code + /// A for the request's execution. + Task Delete(Uri uri, string twoFactorAuthenticationCode); + /// /// Deletes the API object at the specified URI. /// @@ -220,6 +241,16 @@ namespace Octokit /// A for the request's execution. Task Delete(Uri uri, object data); + /// + /// Executes a GET to the API object at the specified URI. This operation is appropriate for + /// API calls which wants to return the redirect URL. + /// It expects the API to respond with a 302 Found. + /// + /// URI of the API resource to get + /// The URL returned by the API in the Location header + /// Thrown when an API error occurs, or the API does not respond with a 302 Found + Task GetRedirect(Uri uri); + /// /// Executes a GET to the API object at the specified URI. This operation is appropriate for /// API calls which queue long running calculations. diff --git a/Octokit/Http/IConnection.cs b/Octokit/Http/IConnection.cs index 7b5318b9..00b9c3be 100644 --- a/Octokit/Http/IConnection.cs +++ b/Octokit/Http/IConnection.cs @@ -32,6 +32,19 @@ namespace Octokit [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Get")] Task> Get(Uri uri, IDictionary parameters, string accepts); + /// + /// Performs an asynchronous HTTP GET request. + /// Attempts to map the response to an object of type + /// + /// The type to map the response to + /// URI endpoint to send request to + /// Querystring parameters for the request + /// Specifies accepted response media types. + /// To follow redirect links automatically or not + /// representing the received HTTP response + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Get")] + Task> Get(Uri uri, IDictionary parameters, string accepts, bool allowAutoRedirect); + /// /// Performs an asynchronous HTTP GET request. /// Attempts to map the response to an object of type @@ -92,6 +105,19 @@ namespace Octokit /// representing the received HTTP response Task> Post(Uri uri, object body, string accepts, string contentType); + /// + /// Performs an asynchronous HTTP POST request. + /// Attempts to map the response body to an object of type + /// + /// The type to map the response to + /// URI endpoint to send request to + /// The object to serialize as the body of the request + /// Specifies accepted response media types. + /// Specifies the media type of the request body + /// Two Factor Authentication Code + /// representing the received HTTP response + Task> Post(Uri uri, object body, string accepts, string contentType, string twoFactorAuthenticationCode); + /// /// Performs an asynchronous HTTP POST request. /// Attempts to map the response body to an object of type @@ -171,6 +197,14 @@ namespace Octokit /// The returned Task Delete(Uri uri); + /// + /// Performs an asynchronous HTTP DELETE request that expects an empty response. + /// + /// URI endpoint to send request to + /// Two Factor Code + /// The returned + Task Delete(Uri uri, string twoFactorAuthenticationCode); + /// /// Performs an asynchronous HTTP DELETE request that expects an empty response. /// diff --git a/Octokit/Models/Response/CommitComment.cs b/Octokit/Models/Response/CommitComment.cs index 9685cc0e..f2db8e87 100644 --- a/Octokit/Models/Response/CommitComment.cs +++ b/Octokit/Models/Response/CommitComment.cs @@ -56,7 +56,7 @@ namespace Octokit /// /// Line index in the diff that was commented on. /// - public int Position { get; protected set; } + public int? Position { get; protected set; } /// /// The line number in the file that was commented on. diff --git a/Octokit/Models/Response/GistChangeStatus.cs b/Octokit/Models/Response/GistChangeStatus.cs index 7cf1f371..6b5ecce0 100644 --- a/Octokit/Models/Response/GistChangeStatus.cs +++ b/Octokit/Models/Response/GistChangeStatus.cs @@ -5,7 +5,7 @@ using System.Globalization; namespace Octokit { /// - /// User by to indicate the level of change. + /// Used by to indicate the level of change. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public class GistChangeStatus diff --git a/Octokit/Models/Response/PullRequestFile.cs b/Octokit/Models/Response/PullRequestFile.cs index 703a0477..efa97899 100644 --- a/Octokit/Models/Response/PullRequestFile.cs +++ b/Octokit/Models/Response/PullRequestFile.cs @@ -14,7 +14,7 @@ namespace Octokit { public PullRequestFile() { } - public PullRequestFile(string sha, string fileName, string status, int additions, int deletions, int changes, Uri blobUri, Uri rawUri, Uri contentsUri, string patch) + public PullRequestFile(string sha, string fileName, string status, int additions, int deletions, int changes, Uri blobUrl, Uri rawUrl, Uri contentsUrl, string patch) { Sha = sha; FileName = fileName; @@ -22,9 +22,9 @@ namespace Octokit Additions = additions; Deletions = deletions; Changes = changes; - BlobUri = blobUri; - RawUri = rawUri; - ContentsUri = contentsUri; + BlobUrl = blobUrl; + RawUrl = rawUrl; + ContentsUrl = contentsUrl; Patch = patch; } @@ -35,9 +35,9 @@ namespace Octokit public int Additions { get; protected set; } public int Deletions { get; protected set; } public int Changes { get; protected set; } - public Uri BlobUri { get; protected set; } - public Uri RawUri { get; protected set; } - public Uri ContentsUri { get; protected set; } + public Uri BlobUrl { get; protected set; } + public Uri RawUrl { get; protected set; } + public Uri ContentsUrl { get; protected set; } public string Patch { get; protected set; } internal string DebuggerDisplay diff --git a/Octokit/Octokit-Mono.csproj b/Octokit/Octokit-Mono.csproj index ee427fb7..4d049e80 100644 --- a/Octokit/Octokit-Mono.csproj +++ b/Octokit/Octokit-Mono.csproj @@ -392,6 +392,7 @@ + \ No newline at end of file diff --git a/Octokit/Octokit-MonoAndroid.csproj b/Octokit/Octokit-MonoAndroid.csproj index 7416f37b..4a1ca1f5 100644 --- a/Octokit/Octokit-MonoAndroid.csproj +++ b/Octokit/Octokit-MonoAndroid.csproj @@ -407,6 +407,7 @@ + diff --git a/Octokit/Octokit-Monotouch.csproj b/Octokit/Octokit-Monotouch.csproj index 32d17ab3..d306f674 100644 --- a/Octokit/Octokit-Monotouch.csproj +++ b/Octokit/Octokit-Monotouch.csproj @@ -400,6 +400,7 @@ + diff --git a/Octokit/Octokit-Portable.csproj b/Octokit/Octokit-Portable.csproj index 61733266..30be17bc 100644 --- a/Octokit/Octokit-Portable.csproj +++ b/Octokit/Octokit-Portable.csproj @@ -390,6 +390,7 @@ + diff --git a/Octokit/Octokit-netcore45.csproj b/Octokit/Octokit-netcore45.csproj index 9eec1cfc..38e80b1a 100644 --- a/Octokit/Octokit-netcore45.csproj +++ b/Octokit/Octokit-netcore45.csproj @@ -394,6 +394,7 @@ + diff --git a/Octokit/Octokit.csproj b/Octokit/Octokit.csproj index e1e5042b..a3cac0ad 100644 --- a/Octokit/Octokit.csproj +++ b/Octokit/Octokit.csproj @@ -73,6 +73,7 @@ + diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 9090c8da..9718262f 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,3 +1,21 @@ +### New in 0.11.0 (released 2015/05/10) +* New: Added overload to `IRepositoryClient.GetAllPublic` specifying a `since` parameter - #774 via @alfhenrik +* New: Added `IGistsClient.GetAllCommits` and `IGistsClient.GetAllForks` implementations - #542 via @haagenson, #794 via @shiftkey +* New: Added `IRepositoryContentsClient.GetArchiveLink` for getting archived code - #765 via @alfhenrik +* Fixed: `PullRequestFile` properties were not serialized correctly - #789 via @thedillonb +* Fixed: Allow to download zip-attachments - #792 via @csware + +### New in 0.10.0 (released 2015/04/22) +* Fixed: renamed methods to follow `GetAll` convention - #771 via @alfhenrik +* Fixed: helper functions and cleanup to make using Authorization API easier to consume - #786 via @haacked + +**Breaking Changes:** + - As part of #771 there were many method which were returning collections + but the method name made it unclear. You might think that it wasn't much, but + you'd be wrong. So if you have a method that no longer compile, + it is likely that you need to set the prefix to `GetAll` to re-disocver that API. + - `CommitComment.Position` is now a nullable `int` to prevent serialization issues. + ### New in 0.9.0 (released 2015/04/04) * New: added `PullRequest.Files` APIs - #752 via @alfhenrik * Fixed: `PullRequestRequest` now supports `SortDirection` and `SortProperty` - #752 via @alfhenrik diff --git a/SolutionInfo.cs b/SolutionInfo.cs index 1798e252..b53121bd 100644 --- a/SolutionInfo.cs +++ b/SolutionInfo.cs @@ -3,11 +3,11 @@ using System.Reflection; using System.Runtime.InteropServices; [assembly: AssemblyProductAttribute("Octokit")] -[assembly: AssemblyVersionAttribute("0.9.0")] -[assembly: AssemblyFileVersionAttribute("0.9.0")] +[assembly: AssemblyVersionAttribute("0.11.0")] +[assembly: AssemblyFileVersionAttribute("0.11.0")] [assembly: ComVisibleAttribute(false)] namespace System { internal static class AssemblyVersionInformation { - internal const string Version = "0.9.0"; + internal const string Version = "0.11.0"; } }