diff --git a/Octokit.Reactive/Clients/IObservableFollowersClient.cs b/Octokit.Reactive/Clients/IObservableFollowersClient.cs new file mode 100644 index 0000000000..f959b833bd --- /dev/null +++ b/Octokit.Reactive/Clients/IObservableFollowersClient.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reactive; +using System.Text; +using System.Threading.Tasks; + +namespace Octokit.Reactive +{ + public interface IObservableFollowersClient + { + /// + /// List the authenticated user’s followers + /// + /// + /// See the API documentation for more information. + /// + /// A of s that follow the authenticated user. + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + IObservable GetAllForCurrent(); + + /// + /// List a user’s followers + /// + /// The login name for the user + /// + /// See the API documentation for more information. + /// + /// A of s that follow the passed user. + IObservable GetAll(string login); + + /// + /// List who the authenticated user is following + /// + /// + /// See the API documentation for more information. + /// + /// A of s that the authenticated user follows. + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + IObservable GetFollowingForCurrent(); + + /// + /// List who a user is following + /// + /// The login name of the user + /// + /// See the API documentation for more information. + /// + /// A of s that the passed user follows. + IObservable GetFollowing(string login); + + /// + /// Check if the authenticated user follows another user + /// + /// The login name of the other user + /// + /// See the API documentation for more information. + /// + /// A bool representing the success of the operation. + IObservable IsFollowingForCurrent(string following); + + /// + /// Check if one user follows another user + /// + /// The login name of the user + /// The login name of the other user + /// + /// See the API documentation for more information. + /// + /// A bool representing the success of the operation. + IObservable IsFollowing(string login, string following); + + /// + /// Follow a user + /// + /// The login name of the user to follow + /// + /// See the API documentation for more information. + /// + /// A bool representing the success of the operation. + IObservable Follow(string login); + + /// + /// Unfollow a user + /// + /// The login name of the user to unfollow + /// + /// See the API documentation for more information. + /// + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Unfollow", + Justification = "Unfollow is consistent with the GitHub website")] + IObservable Unfollow(string login); + } +} diff --git a/Octokit.Reactive/Clients/ObservableFollowersClient.cs b/Octokit.Reactive/Clients/ObservableFollowersClient.cs new file mode 100644 index 0000000000..88e3159caf --- /dev/null +++ b/Octokit.Reactive/Clients/ObservableFollowersClient.cs @@ -0,0 +1,141 @@ +using System; +using System.Reactive; +using System.Reactive.Threading.Tasks; +using Octokit.Reactive.Internal; + +namespace Octokit.Reactive +{ + public class ObservableFollowersClient : IObservableFollowersClient + { + readonly IFollowersClient _client; + readonly IConnection _connection; + + /// + /// Initializes a new User Followers API client. + /// + /// An used to make the requests + public ObservableFollowersClient(IGitHubClient client) + { + Ensure.ArgumentNotNull(client, "client"); + + _client = client.User.Followers; + _connection = client.Connection; + } + + /// + /// List the authenticated user’s followers + /// + /// + /// See the API documentation for more information. + /// + /// A of s that follow the authenticated user. + public IObservable GetAllForCurrent() + { + return _connection.GetAndFlattenAllPages(ApiUrls.Followers()); + } + + /// + /// List a user’s followers + /// + /// The login name for the user + /// + /// See the API documentation for more information. + /// + /// A of s that follow the passed user. + public IObservable GetAll(string login) + { + Ensure.ArgumentNotNullOrEmptyString(login, "login"); + + return _connection.GetAndFlattenAllPages(ApiUrls.Followers(login)); + } + + /// + /// List who the authenticated user is following + /// + /// + /// See the API documentation for more information. + /// + /// A of s that the authenticated user follows. + public IObservable GetFollowingForCurrent() + { + return _connection.GetAndFlattenAllPages(ApiUrls.Following()); + } + + /// + /// List who a user is following + /// + /// The login name of the user + /// + /// See the API documentation for more information. + /// + /// A of s that the passed user follows. + public IObservable GetFollowing(string login) + { + Ensure.ArgumentNotNullOrEmptyString(login, "login"); + + return _connection.GetAndFlattenAllPages(ApiUrls.Following(login)); + } + + /// + /// Check if the authenticated user follows another user + /// + /// The login name of the other user + /// + /// See the API documentation for more information. + /// + /// A bool representing the success of the operation. + public IObservable IsFollowingForCurrent(string following) + { + Ensure.ArgumentNotNullOrEmptyString(following, "following"); + + return _client.IsFollowingForCurrent(following).ToObservable(); + } + + /// + /// Check if one user follows another user + /// + /// The login name of the user + /// The login name of the other user + /// + /// See the API documentation for more information. + /// + /// A bool representing the success of the operation. + public IObservable IsFollowing(string login, string following) + { + Ensure.ArgumentNotNullOrEmptyString(login, "login"); + Ensure.ArgumentNotNullOrEmptyString(following, "following"); + + return _client.IsFollowing(login, following).ToObservable(); + } + + /// + /// Follow a user + /// + /// The login name of the user to follow + /// + /// See the API documentation for more information. + /// + /// A bool representing the success of the operation. + public IObservable Follow(string login) + { + Ensure.ArgumentNotNullOrEmptyString(login, "login"); + + return _client.Follow(login).ToObservable(); + } + + /// + /// Unfollow a user + /// + /// The login name of the user to unfollow + /// + /// See the API documentation for more information. + /// + /// + public IObservable Unfollow(string login) + { + Ensure.ArgumentNotNullOrEmptyString(login, "login"); + + return _client.Unfollow(login).ToObservable(); + } + } +} diff --git a/Octokit.Reactive/Octokit.Reactive-Mono.csproj b/Octokit.Reactive/Octokit.Reactive-Mono.csproj index e2db17ad7f..2f21b5e66a 100644 --- a/Octokit.Reactive/Octokit.Reactive-Mono.csproj +++ b/Octokit.Reactive/Octokit.Reactive-Mono.csproj @@ -120,6 +120,8 @@ + + diff --git a/Octokit.Reactive/Octokit.Reactive-MonoAndroid.csproj b/Octokit.Reactive/Octokit.Reactive-MonoAndroid.csproj index d3ffec270d..cd6ab187ea 100644 --- a/Octokit.Reactive/Octokit.Reactive-MonoAndroid.csproj +++ b/Octokit.Reactive/Octokit.Reactive-MonoAndroid.csproj @@ -129,6 +129,8 @@ + + diff --git a/Octokit.Reactive/Octokit.Reactive-Monotouch.csproj b/Octokit.Reactive/Octokit.Reactive-Monotouch.csproj index c593c7dd2c..0f441e351f 100644 --- a/Octokit.Reactive/Octokit.Reactive-Monotouch.csproj +++ b/Octokit.Reactive/Octokit.Reactive-Monotouch.csproj @@ -124,6 +124,8 @@ + + diff --git a/Octokit.Reactive/Octokit.Reactive.csproj b/Octokit.Reactive/Octokit.Reactive.csproj index 75e3a0ef7b..6c5c7e71fc 100644 --- a/Octokit.Reactive/Octokit.Reactive.csproj +++ b/Octokit.Reactive/Octokit.Reactive.csproj @@ -74,6 +74,7 @@ Properties\SolutionInfo.cs + @@ -120,6 +121,7 @@ + diff --git a/Octokit.Tests.Integration/Clients/FollowersClientTests.cs b/Octokit.Tests.Integration/Clients/FollowersClientTests.cs new file mode 100644 index 0000000000..4d7f1e0f5b --- /dev/null +++ b/Octokit.Tests.Integration/Clients/FollowersClientTests.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Octokit; +using Octokit.Tests.Integration; +using Xunit; + +public class FollowersClientTests : IDisposable +{ + readonly GitHubClient _github; + readonly User _currentUser; + + public FollowersClientTests() + { + _github = new GitHubClient(new ProductHeaderValue("OctokitTests")) + { + Credentials = Helper.Credentials + }; + _currentUser = _github.User.Current().Result; + } + + [IntegrationTest] + public async Task ReturnsUsersTheCurrentUserIsFollowing() + { + await _github.User.Followers.Follow("alfhenrik"); + + var following = await _github.User.Followers.GetFollowingForCurrent(); + + Assert.NotNull(following); + Assert.True(following.Any(f => f.Login == "alfhenrik")); + } + + [IntegrationTest] + public async Task ReturnsUsersTheUserIsFollowing() + { + var following = await _github.User.Followers.GetFollowing("alfhenrik"); + + Assert.NotNull(following); + Assert.NotEmpty(following); + } + + [IntegrationTest] + public async Task ReturnsUsersFollowingTheUser() + { + await _github.User.Followers.Follow("alfhenrik"); + + var followers = await _github.User.Followers.GetAll("alfhenrik"); + + Assert.NotEmpty(followers); + Assert.True(followers.Any(f => f.Login == _currentUser.Login)); + } + + [IntegrationTest] + public async Task ChecksIfIsFollowingUserWhenFollowingUser() + { + await _github.User.Followers.Follow("alfhenrik"); + + var isFollowing = await _github.User.Followers.IsFollowingForCurrent("alfhenrik"); + + Assert.True(isFollowing); + } + + [IntegrationTest] + public async Task ChecksIfIsFollowingUserWhenNotFollowingUser() + { + var isFollowing = await _github.User.Followers.IsFollowingForCurrent("alfhenrik"); + + Assert.False(isFollowing); + } + + [IntegrationTest] + public async Task FollowUserNotBeingFollowedByTheUser() + { + var result = await _github.User.Followers.Follow("alfhenrik"); + var following = await _github.User.Followers.GetFollowingForCurrent(); + + Assert.True(result); + Assert.NotEmpty(following); + Assert.True(following.Any(f => f.Login == "alfhenrik")); + } + + [IntegrationTest] + public async Task UnfollowUserBeingFollowedByTheUser() + { + await _github.User.Followers.Follow("alfhenrik"); + var followers = await _github.User.Followers.GetAll("alfhenrik"); + Assert.True(followers.Any(f => f.Login == _currentUser.Login)); + + await _github.User.Followers.Unfollow("alfhenrik"); + followers = await _github.User.Followers.GetAll("alfhenrik"); + Assert.False(followers.Any(f => f.Login == _currentUser.Login)); + } + + [IntegrationTest] + public async Task UnfollowUserNotBeingFollowedTheUser() + { + var followers = await _github.User.Followers.GetAll("alfhenrik"); + Assert.False(followers.Any(f => f.Login == _currentUser.Login)); + + await _github.User.Followers.Unfollow("alfhenrik"); + } + + public void Dispose() + { + _github.User.Followers.Unfollow("alfhenrik"); + } +} diff --git a/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj b/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj index cf1aa4aad6..9587edf948 100644 --- a/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj +++ b/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj @@ -69,6 +69,7 @@ + diff --git a/Octokit.Tests/Clients/FollowersClientTests.cs b/Octokit.Tests/Clients/FollowersClientTests.cs new file mode 100644 index 0000000000..9c24b83951 --- /dev/null +++ b/Octokit.Tests/Clients/FollowersClientTests.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using NSubstitute; +using Octokit.Internal; +using Octokit.Tests; +using Octokit.Tests.Helpers; +using Xunit; +using Xunit.Extensions; + +namespace Octokit.Tests.Clients +{ + /// + /// Client tests mostly just need to make sure they call the IApiConnection with the correct + /// relative Uri. No need to fake up the response. All *those* tests are in ApiConnectionTests.cs. + /// + public class FollowersClientTests + { + public class TheConstructor + { + [Fact] + public void EnsuresNonNullArguments() + { + Assert.Throws(() => new FollowersClient(null)); + } + } + + public class TheGetAllForCurrentMethod + { + [Fact] + public void RequestsTheCorrectUrl() + { + var connection = Substitute.For(); + var client = new FollowersClient(connection); + + client.GetAllForCurrent(); + + connection.Received().GetAll( + Arg.Is(u => u.ToString() == "user/followers")); + } + } + + public class TheGetAllMethod + { + [Fact] + public void RequestsTheCorrectUrl() + { + var connection = Substitute.For(); + var client = new FollowersClient(connection); + + client.GetAll("alfhenrik"); + + connection.Received().GetAll( + Arg.Is(u => u.ToString() == "users/alfhenrik/followers")); + } + + [Fact] + public void EnsureNonNullArguments() + { + var connection = Substitute.For(); + var client = new FollowersClient(connection); + + AssertEx.Throws(async () => await client.GetAll(null)); + AssertEx.Throws(async () => await client.GetAll("")); + } + } + + public class TheGetFollowingForCurrentMethod + { + [Fact] + public void RequestsTheCorrectUrl() + { + var connection = Substitute.For(); + var client = new FollowersClient(connection); + + client.GetFollowingForCurrent(); + + connection.Received().GetAll(Arg.Is(u => u.ToString() == "user/following")); + } + } + + public class TheGetFollowingMethod + { + [Fact] + public void RequestsTheCorrectUrl() + { + var connection = Substitute.For(); + var client = new FollowersClient(connection); + + client.GetFollowing("alfhenrik"); + + connection.Received().GetAll(Arg.Is(u => u.ToString() == "users/alfhenrik/following")); + } + + [Fact] + public void EnsuresNonNullArguments() + { + var connection = Substitute.For(); + var client = new FollowersClient(connection); + + AssertEx.Throws(async () => await client.GetFollowing(null)); + AssertEx.Throws(async () => await client.GetFollowing("")); + } + } + + public class TheIsFollowingForCurrentMethod + { + [Theory] + [InlineData(HttpStatusCode.NoContent, true)] + [InlineData(HttpStatusCode.NotFound, false)] + public async Task RequestsCorrectValueForStatusCode(HttpStatusCode status, bool expected) + { + var response = Task.Factory.StartNew>(() => + new ApiResponse { StatusCode = status }); + var connection = Substitute.For(); + connection.GetAsync(Arg.Is(u => u.ToString() == "user/following/alfhenrik"), + null, null).Returns(response); + var apiConnection = Substitute.For(); + apiConnection.Connection.Returns(connection); + var client = new FollowersClient(apiConnection); + + var result = await client.IsFollowingForCurrent("alfhenrik"); + + Assert.Equal(expected, result); + } + + [Fact] + public async Task ThrowsExceptionForInvalidStatusCode() + { + var response = Task.Factory.StartNew>(() => + new ApiResponse { StatusCode = HttpStatusCode.Conflict }); + var connection = Substitute.For(); + connection.GetAsync(Arg.Is(u => u.ToString() == "user/following/alfhenrik"), + null, null).Returns(response); + var apiConnection = Substitute.For(); + apiConnection.Connection.Returns(connection); + var client = new FollowersClient(apiConnection); + + AssertEx.Throws(async () => await client.IsFollowingForCurrent("alfhenrik")); + } + + [Fact] + public void EnsuresNonNullArguments() + { + var connection = Substitute.For(); + var client = new FollowersClient(connection); + + AssertEx.Throws(async () => await client.IsFollowingForCurrent(null)); + AssertEx.Throws(async () => await client.IsFollowingForCurrent("")); + } + } + + public class TheIsFollowingMethod + { + [Theory] + [InlineData(HttpStatusCode.NoContent, true)] + [InlineData(HttpStatusCode.NotFound, false)] + public async Task RequestsCorrectValueForStatusCode(HttpStatusCode status, bool expected) + { + var response = Task.Factory.StartNew>(() => + new ApiResponse { StatusCode = status }); + var connection = Substitute.For(); + connection.GetAsync(Arg.Is(u => u.ToString() == "users/alfhenrik/following/alfhenrik-test"), + null, null).Returns(response); + var apiConnection = Substitute.For(); + apiConnection.Connection.Returns(connection); + var client = new FollowersClient(apiConnection); + + var result = await client.IsFollowing("alfhenrik", "alfhenrik-test"); + + Assert.Equal(expected, result); + } + + [Fact] + public async Task ThrowsExceptionForInvalidStatusCode() + { + var response = Task.Factory.StartNew>(() => + new ApiResponse { StatusCode = HttpStatusCode.Conflict }); + var connection = Substitute.For(); + connection.GetAsync(Arg.Is(u => u.ToString() == "users/alfhenrik/following/alfhenrik-test"), + null, null).Returns(response); + var apiConnection = Substitute.For(); + apiConnection.Connection.Returns(connection); + var client = new FollowersClient(apiConnection); + + AssertEx.Throws(async () => await client.IsFollowing("alfhenrik", "alfhenrik-test")); + } + + [Fact] + public void EnsuresNonNullArguments() + { + var connection = Substitute.For(); + var client = new FollowersClient(connection); + + AssertEx.Throws(async () => await client.IsFollowing(null, "alfhenrik-test")); + AssertEx.Throws(async () => await client.IsFollowing("alfhenrik", null)); + AssertEx.Throws(async () => await client.IsFollowing("", "alfhenrik-text")); + AssertEx.Throws(async () => await client.IsFollowing("alfhenrik", "")); + } + + } + + public class TheFollowMethod + { + [Theory] + [InlineData(HttpStatusCode.NoContent, true)] + public async Task RequestsCorrectValueForStatusCode(HttpStatusCode status, bool expected) + { + var response = Task.Factory.StartNew>(() => + new ApiResponse { StatusCode = status }); + var connection = Substitute.For(); + connection.PutAsync(Arg.Is(u => u.ToString() == "user/following/alfhenrik"), + Args.Object).Returns(response); + var apiConnection = Substitute.For(); + apiConnection.Connection.Returns(connection); + var client = new FollowersClient(apiConnection); + + var result = await client.Follow("alfhenrik"); + + Assert.Equal(expected, result); + } + + [Fact] + public async Task ThrowsExceptionForInvalidStatusCode() + { + var response = Task.Factory.StartNew>(() => + new ApiResponse { StatusCode = HttpStatusCode.Conflict }); + var connection = Substitute.For(); + connection.PutAsync(Arg.Is(u => u.ToString() == "user/following/alfhenrik"), + new { }).Returns(response); + var apiConnection = Substitute.For(); + apiConnection.Connection.Returns(connection); + var client = new FollowersClient(apiConnection); + + AssertEx.Throws(async () => await client.Follow("alfhenrik")); + } + + [Fact] + public async Task EnsureNonNullArguments() + { + var connection = Substitute.For(); + var client = new FollowersClient(connection); + + await AssertEx.Throws(async () => await client.Follow(null)); + await AssertEx.Throws(async () => await client.Follow("")); + } + } + + public class TheUnfollowMethod + { + [Fact] + public void RequestsTheCorrectUrl() + { + var connection = Substitute.For(); + var client = new FollowersClient(connection); + + client.Unfollow("alfhenrik"); + + connection.Received().Delete(Arg.Is(u => u.ToString() == "user/following/alfhenrik")); + } + + [Fact] + public async Task EnsureNonNullArguments() + { + var connection = Substitute.For(); + var client = new FollowersClient(connection); + + await AssertEx.Throws(async () => await client.Unfollow(null)); + await AssertEx.Throws(async () => await client.Unfollow("")); + } + } + } +} diff --git a/Octokit.Tests/Octokit.Tests.csproj b/Octokit.Tests/Octokit.Tests.csproj index e0e8b4730c..9b4eda2c2a 100644 --- a/Octokit.Tests/Octokit.Tests.csproj +++ b/Octokit.Tests/Octokit.Tests.csproj @@ -87,6 +87,7 @@ + @@ -136,6 +137,7 @@ + diff --git a/Octokit.Tests/Reactive/ObservableFollowersTest.cs b/Octokit.Tests/Reactive/ObservableFollowersTest.cs new file mode 100644 index 0000000000..145c4dbc05 --- /dev/null +++ b/Octokit.Tests/Reactive/ObservableFollowersTest.cs @@ -0,0 +1,192 @@ +using NSubstitute; +using Octokit; +using Octokit.Internal; +using Octokit.Reactive; +using Octokit.Tests.Helpers; +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Octokit.Tests.Reactive +{ + public class ObservableFollowersTest + { + public class TheGetAllForCurrentMethod + { + [Fact] + public void RequestsTheCorrectUrl() + { + var githubClient = Substitute.For(); + var client = new ObservableFollowersClient(githubClient); + + client.GetAllForCurrent(); + + githubClient.Connection.GetAsync>( + new Uri("user/followers", UriKind.Relative), null, null); + } + } + + public class TheGetAllMethod + { + [Fact] + public void RequestsTheCorrectUrl() + { + var githubClient = Substitute.For(); + var client = new ObservableFollowersClient(githubClient); + + client.GetAll("alfhenrik"); + + githubClient.Connection.GetAsync>( + new Uri("users/alfhenrik/followers", UriKind.Relative), null, null); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new ObservableFollowersClient(Substitute.For()); + + await AssertEx.Throws(async () => await client.GetAll(null)); + await AssertEx.Throws(async () => await client.GetAll("")); + } + } + + public class TheGetFollowingForCurrentMethod + { + [Fact] + public void RequestsTheCorrectUrl() + { + var githubClient = Substitute.For(); + var client = new ObservableFollowersClient(githubClient); + + client.GetFollowingForCurrent(); + + githubClient.Connection.GetAsync>( + new Uri("user/following", UriKind.Relative), null, null); + } + } + + public class TheGetFollowingMethod + { + [Fact] + public void RequestsTheCorrectUrl() + { + var githubClient = Substitute.For(); + var client = new ObservableFollowersClient(githubClient); + + client.GetFollowing("alfhenrik"); + + githubClient.Connection.GetAsync>( + new Uri("users/alfhenrik/following", UriKind.Relative), null, null); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new ObservableFollowersClient(Substitute.For()); + + await AssertEx.Throws(async () => await client.GetFollowing(null)); + await AssertEx.Throws(async () => await client.GetFollowing("")); + } + } + + public class TheIsFollowingForCurrentMethod + { + [Fact] + public void IsFollowingForCurrentFromClientUserFollowers() + { + var githubClient = Substitute.For(); + var client = new ObservableFollowersClient(githubClient); + + client.IsFollowingForCurrent("alfhenrik"); + + githubClient.User.Followers.Received() + .IsFollowingForCurrent("alfhenrik"); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new ObservableFollowersClient(Substitute.For()); + + await AssertEx.Throws(async () => await client.IsFollowingForCurrent(null)); + await AssertEx.Throws(async () => await client.IsFollowingForCurrent("")); + } + } + + public class TheIsFollowingMethod + { + [Fact] + public void IsFollowingFromClientUserFollowers() + { + var githubClient = Substitute.For(); + var client = new ObservableFollowersClient(githubClient); + + client.IsFollowing("alfhenrik", "alfhenrik-test"); + + githubClient.User.Followers.Received() + .IsFollowing("alfhenrik", "alfhenrik-test"); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new ObservableFollowersClient(Substitute.For()); + + await AssertEx.Throws(async () => await client.IsFollowing(null, "alfhenrik-test")); + await AssertEx.Throws(async () => await client.IsFollowing("", "alfhenrik-test")); + await AssertEx.Throws(async () => await client.IsFollowing("alfhenrik", null)); + await AssertEx.Throws(async () => await client.IsFollowing("alfhenrik", "")); + } + } + + public class TheFollowMethod + { + [Fact] + public void FollowFromClientUserFollowers() + { + var githubClient = Substitute.For(); + var client = new ObservableFollowersClient(githubClient); + + client.Follow("alfhenrik"); + + githubClient.User.Followers.Received() + .Follow("alfhenrik"); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new ObservableFollowersClient(Substitute.For()); + + await AssertEx.Throws(async () => await client.Follow(null)); + await AssertEx.Throws(async () => await client.Follow("")); + } + } + + public class TheUnfollowMethod + { + [Fact] + public void UnfollowFromClientUserFollowers() + { + var githubClient = Substitute.For(); + var client = new ObservableFollowersClient(githubClient); + + client.Unfollow("alfhenrik"); + + githubClient.User.Followers.Received() + .Unfollow("alfhenrik"); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new ObservableFollowersClient(Substitute.For()); + + await AssertEx.Throws(async () => await client.Unfollow(null)); + await AssertEx.Throws(async () => await client.Unfollow("")); + } + } + } +} diff --git a/Octokit/Clients/FollowersClient.cs b/Octokit/Clients/FollowersClient.cs new file mode 100644 index 0000000000..76f1851d56 --- /dev/null +++ b/Octokit/Clients/FollowersClient.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// A client for GitHub's User Followers API + /// + /// + /// See the Followers API documentation for more information. + /// + public class FollowersClient : ApiClient, IFollowersClient + { + /// + /// Initializes a new GitHub User Followers API client. + /// + /// An API connection + public FollowersClient(IApiConnection apiConnection) : base(apiConnection) + { + } + + /// + /// List the authenticated user’s followers + /// + /// + /// See the API documentation for more information. + /// + /// A of s that follow the authenticated user. + public Task> GetAllForCurrent() + { + return ApiConnection.GetAll(ApiUrls.Followers()); + } + + /// + /// List a user’s followers + /// + /// The login name for the user + /// + /// See the API documentation for more information. + /// + /// A of s that follow the passed user. + public Task> GetAll(string login) + { + Ensure.ArgumentNotNullOrEmptyString(login, "login"); + + return ApiConnection.GetAll(ApiUrls.Followers(login)); + } + + /// + /// List who the authenticated user is following + /// + /// + /// See the API documentation for more information. + /// + /// A of s that the authenticated user follows. + public Task> GetFollowingForCurrent() + { + return ApiConnection.GetAll(ApiUrls.Following()); + } + + /// + /// List who a user is following + /// + /// The login name of the user + /// + /// See the API documentation for more information. + /// + /// A of s that the passed user follows. + public Task> GetFollowing(string login) + { + Ensure.ArgumentNotNullOrEmptyString(login, "login"); + + return ApiConnection.GetAll(ApiUrls.Following(login)); + } + + /// + /// Check if the authenticated user follows another user + /// + /// The login name of the other user + /// + /// See the API documentation for more information. + /// + /// A bool representing the success of the operation. + public async Task IsFollowingForCurrent(string following) + { + Ensure.ArgumentNotNullOrEmptyString(following, "following"); + + try + { + var response = await Connection.GetAsync(ApiUrls.IsFollowing(following), null, null) + .ConfigureAwait(false); + if(response.StatusCode != HttpStatusCode.NotFound && response.StatusCode != HttpStatusCode.NoContent) + { + throw new ApiException("Invalid Status Code returned. Expected a 204 or a 404", response.StatusCode); + } + return response.StatusCode == HttpStatusCode.NoContent; + } + catch (NotFoundException) + { + return false; + } + } + + /// + /// Check if one user follows another user + /// + /// The login name of the user + /// The login name of the other user + /// + /// See the API documentation for more information. + /// + /// A bool representing the success of the operation. + public async Task IsFollowing(string login, string following) + { + Ensure.ArgumentNotNullOrEmptyString(login, "login"); + Ensure.ArgumentNotNullOrEmptyString(following, "following"); + + try + { + var response = await Connection.GetAsync(ApiUrls.IsFollowing(login, following), null, null) + .ConfigureAwait(false); + if (response.StatusCode != HttpStatusCode.NotFound && response.StatusCode != HttpStatusCode.NoContent) + { + throw new ApiException("Invalid Status Code returned. Expected a 204 or a 404", response.StatusCode); + } + return response.StatusCode == HttpStatusCode.NoContent; + } + catch (NotFoundException) + { + return false; + } + } + + /// + /// Follow a user + /// + /// The login name of the user to follow + /// + /// See the API documentation for more information. + /// + /// A bool representing the success of the operation. + public async Task Follow(string login) + { + Ensure.ArgumentNotNullOrEmptyString(login, "login"); + + try + { + var requestData = new { }; + var response = await Connection.PutAsync(ApiUrls.IsFollowing(login), requestData) + .ConfigureAwait(false); + if (response.StatusCode != HttpStatusCode.NoContent) + { + throw new ApiException("Invalid Status Code returned. Expected a 204", response.StatusCode); + } + return response.StatusCode == HttpStatusCode.NoContent; + } + catch (NotFoundException) + { + return false; + } + } + + /// + /// Unfollow a user + /// + /// The login name of the user to unfollow + /// + /// See the API documentation for more information. + /// + /// + public Task Unfollow(string login) + { + Ensure.ArgumentNotNullOrEmptyString(login, "login"); + + return ApiConnection.Delete(ApiUrls.IsFollowing(login)); + } + } +} diff --git a/Octokit/Clients/IFollowersClient.cs b/Octokit/Clients/IFollowersClient.cs new file mode 100644 index 0000000000..b73dc5833c --- /dev/null +++ b/Octokit/Clients/IFollowersClient.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// A client for GitHub's User Followers API + /// + /// + /// See the Followers API documentation for more information. + /// + public interface IFollowersClient + { + /// + /// List the authenticated user’s followers + /// + /// + /// See the API documentation for more information. + /// + /// A of s that follow the authenticated user. + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + Task> GetAllForCurrent(); + + /// + /// List a user’s followers + /// + /// The login name for the user + /// + /// See the API documentation for more information. + /// + /// A of s that follow the passed user. + Task> GetAll(string login); + + /// + /// List who the authenticated user is following + /// + /// + /// See the API documentation for more information. + /// + /// A of s that the authenticated user follows. + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + Task> GetFollowingForCurrent(); + + /// + /// List who a user is following + /// + /// The login name of the user + /// + /// See the API documentation for more information. + /// + /// A of s that the passed user follows. + Task> GetFollowing(string login); + + /// + /// Check if the authenticated user follows another user + /// + /// The login name of the other user + /// + /// See the API documentation for more information. + /// + /// A bool representing the success of the operation. + Task IsFollowingForCurrent(string following); + + /// + /// Check if one user follows another user + /// + /// The login name of the user + /// The login name of the other user + /// + /// See the API documentation for more information. + /// + /// A bool representing the success of the operation. + Task IsFollowing(string login, string following); + + /// + /// Follow a user + /// + /// The login name of the user to follow + /// + /// See the API documentation for more information. + /// + /// A bool representing the success of the operation. + Task Follow(string login); + + /// + /// Unfollow a user + /// + /// The login name of the user to unfollow + /// + /// See the API documentation for more information. + /// + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Unfollow", + Justification = "Unfollow is consistent with the GitHub website")] + Task Unfollow(string login); + } +} diff --git a/Octokit/Clients/IUsersClient.cs b/Octokit/Clients/IUsersClient.cs index e5c91a88c7..eddc0039ae 100644 --- a/Octokit/Clients/IUsersClient.cs +++ b/Octokit/Clients/IUsersClient.cs @@ -41,5 +41,13 @@ public interface IUsersClient /// [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] Task> GetEmails(); + + /// + /// A client for GitHub's User Followers API + /// + /// + /// See the Followers API documentation for more information. + /// + IFollowersClient Followers { get; } } } diff --git a/Octokit/Clients/UsersClient.cs b/Octokit/Clients/UsersClient.cs index 48ff6746fc..c68ece71f5 100644 --- a/Octokit/Clients/UsersClient.cs +++ b/Octokit/Clients/UsersClient.cs @@ -23,6 +23,7 @@ public class UsersClient : ApiClient, IUsersClient /// An API connection public UsersClient(IApiConnection apiConnection) : base(apiConnection) { + Followers = new FollowersClient(apiConnection); } /// @@ -68,5 +69,13 @@ public Task> GetEmails() { return ApiConnection.GetAll(ApiUrls.Emails(), null); } + + /// + /// A client for GitHub's User Followers API + /// + /// + /// See the Followers API documentation for more information. + /// + public IFollowersClient Followers { get; private set; } } } diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs index acede7739c..de29e397b3 100644 --- a/Octokit/Helpers/ApiUrls.cs +++ b/Octokit/Helpers/ApiUrls.cs @@ -833,5 +833,35 @@ public static Uri SearchCode() { return "search/code".FormatUri(); } + + public static Uri Followers() + { + return "user/followers".FormatUri(); + } + + public static Uri Followers(string login) + { + return "users/{0}/followers".FormatUri(login); + } + + public static Uri Following() + { + return "user/following".FormatUri(); + } + + public static Uri Following(string login) + { + return "users/{0}/following".FormatUri(login); + } + + public static Uri IsFollowing(string following) + { + return "user/following/{0}".FormatUri(following); + } + + public static Uri IsFollowing(string login, string following) + { + return "users/{0}/following/{1}".FormatUri(login, following); + } } } diff --git a/Octokit/Octokit-Mono.csproj b/Octokit/Octokit-Mono.csproj index 9d18a928ab..cc349c2569 100644 --- a/Octokit/Octokit-Mono.csproj +++ b/Octokit/Octokit-Mono.csproj @@ -253,6 +253,8 @@ + + \ No newline at end of file diff --git a/Octokit/Octokit-MonoAndroid.csproj b/Octokit/Octokit-MonoAndroid.csproj index 67fed48458..f196e297bf 100644 --- a/Octokit/Octokit-MonoAndroid.csproj +++ b/Octokit/Octokit-MonoAndroid.csproj @@ -263,6 +263,8 @@ + + \ No newline at end of file diff --git a/Octokit/Octokit-Monotouch.csproj b/Octokit/Octokit-Monotouch.csproj index f34fd75ebd..1908e4b1a4 100644 --- a/Octokit/Octokit-Monotouch.csproj +++ b/Octokit/Octokit-Monotouch.csproj @@ -258,6 +258,8 @@ + + \ No newline at end of file diff --git a/Octokit/Octokit-netcore45.csproj b/Octokit/Octokit-netcore45.csproj index b8c62ad0f6..c855b507ca 100644 --- a/Octokit/Octokit-netcore45.csproj +++ b/Octokit/Octokit-netcore45.csproj @@ -57,6 +57,7 @@ + @@ -66,6 +67,7 @@ + diff --git a/Octokit/Octokit.csproj b/Octokit/Octokit.csproj index a1e1e691c1..6402442c89 100644 --- a/Octokit/Octokit.csproj +++ b/Octokit/Octokit.csproj @@ -58,6 +58,8 @@ + + Code