Add client for organization outside collaborators (#1639)

* Add client for organization outside collaborators

* Add unit/integration tests

* Add methods for removing an outside collaborator

* Add unit/integration tests

* Add new Put method to Connection which accepts a preview header

* Add methods for converting an org member to an outside collaborator

* Fix copy paste errors in new exceptions

* According to API docs, a 403 should be returned if the member is not a member of the org, but a 404 is actually returned

* Add unit/integration tests

* Remove unused using directives

* Got a bit overzealous with my removal of using directives

* Fix integration tests by using the configured Organization and test username rather than henrik's :)

* Remove ApiOptions overloads as it isn't currently supported

* Fix XML doc grammar

* Fix failing unit tests

* Missed a couple of nameof replacements
This commit is contained in:
Henrik Andersson
2017-08-07 10:20:57 +10:00
committed by Ryan Gribble
parent cda714bef6
commit 1d1ca0a572
18 changed files with 1224 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Octokit.Reactive
{
public interface IObservableOrganizationOutsideCollaboratorsClient
{
/// <summary>
/// List all users who are outside collaborators of an organization. An outside collaborator is a user that
/// is not a member of the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#list-outside-collaborators">API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <returns>The users</returns>
IObservable<User> GetAll(string org);
/// <summary>
/// List all users who are outside collaborators of an organization. An outside collaborator is a user that
/// is not a member of the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#list-outside-collaborators">API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <param name="filter">The filter to use when getting the users, <see cref="OrganizationMembersFilter"/></param>
/// <returns>The users</returns>
IObservable<User> GetAll(string org, OrganizationMembersFilter filter);
/// <summary>
/// Removes a user as an outside collaborator from the organization, this will remove them from all repositories
/// within the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#remove-outside-collaborator">API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <param name="user">The login of the user</param>
/// <returns></returns>
IObservable<bool> Delete(string org, string user);
/// <summary>
/// Converts an organization member to an outside collaborator,
/// when an organization member is converted to an outside collaborator,
/// they'll only have access to the repositories that their current team membership allows.
/// The user will no longer be a member of the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#convert-member-to-outside-collaborator"> API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <param name="user">The login for the user</param>
/// <returns></returns>
IObservable<bool> ConvertFromMember(string org, string user);
}
}

View File

@@ -15,6 +15,11 @@ namespace Octokit.Reactive
/// </summary>
IObservableTeamsClient Team { get; }
/// <summary>
/// Returns a client to manage outside collaborators of an organization.
/// </summary>
IObservableOrganizationOutsideCollaboratorsClient OutsideCollaborator { get; }
/// <summary>
/// Returns the specified organization.
/// </summary>

View File

@@ -0,0 +1,99 @@
using System;
using System.Reactive.Threading.Tasks;
using Octokit.Reactive.Internal;
namespace Octokit.Reactive
{
public class ObservableOrganizationOutsideCollaboratorsClient : IObservableOrganizationOutsideCollaboratorsClient
{
readonly IOrganizationOutsideCollaboratorsClient _client;
readonly IConnection _connection;
/// <summary>
/// Initializes a new Organization Outside Collaborators API client.
/// </summary>
/// <param name="client"></param>
public ObservableOrganizationOutsideCollaboratorsClient(IGitHubClient client)
{
Ensure.ArgumentNotNull(client, nameof(client));
_client = client.Organization.OutsideCollaborator;
_connection = client.Connection;
}
/// <summary>
/// List all users who are outside collaborators of an organization. An outside collaborator is a user that
/// is not a member of the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#list-outside-collaborators">API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <returns>The users</returns>
public IObservable<User> GetAll(string org)
{
Ensure.ArgumentNotNullOrEmptyString(org, nameof(org));
return _connection.GetAndFlattenAllPages<User>(ApiUrls.OutsideCollaborators(org), null, AcceptHeaders.OrganizationMembershipPreview);
}
/// <summary>
/// List all users who are outside collaborators of an organization. An outside collaborator is a user that
/// is not a member of the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#list-outside-collaborators">API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <param name="filter">The filter to use when getting the users, <see cref="OrganizationMembersFilter"/></param>
/// <returns>The users</returns>
public IObservable<User> GetAll(string org, OrganizationMembersFilter filter)
{
Ensure.ArgumentNotNullOrEmptyString(org, nameof(org));
return _connection.GetAndFlattenAllPages<User>(ApiUrls.OutsideCollaborators(org, filter), null, AcceptHeaders.OrganizationMembershipPreview);
}
/// <summary>
/// Removes a user as an outside collaborator from the organization, this will remove them from all repositories
/// within the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#remove-outside-collaborator">API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <param name="user">The login of the user</param>
/// <returns></returns>
public IObservable<bool> Delete(string org, string user)
{
Ensure.ArgumentNotNullOrEmptyString(org, nameof(org));
Ensure.ArgumentNotNullOrEmptyString(user, nameof(user));
return _client.Delete(org, user).ToObservable();
}
/// <summary>
/// Converts an organization member to an outside collaborator,
/// when an organization member is converted to an outside collaborator,
/// they'll only have access to the repositories that their current team membership allows.
/// The user will no longer be a member of the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#convert-member-to-outside-collaborator"> API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <param name="user">The login for the user</param>
/// <returns></returns>
public IObservable<bool> ConvertFromMember(string org, string user)
{
Ensure.ArgumentNotNullOrEmptyString(org, nameof(org));
Ensure.ArgumentNotNullOrEmptyString(user, nameof(user));
return _client.ConvertFromMember(org, user).ToObservable();
}
}
}

View File

@@ -19,6 +19,7 @@ namespace Octokit.Reactive
Member = new ObservableOrganizationMembersClient(client);
Team = new ObservableTeamsClient(client);
OutsideCollaborator = new ObservableOrganizationOutsideCollaboratorsClient(client);
_client = client.Organization;
_connection = client.Connection;
@@ -34,6 +35,11 @@ namespace Octokit.Reactive
/// </summary>
public IObservableTeamsClient Team { get; private set; }
/// <summary>
/// Returns a client to manage outside collaborators of an organization.
/// </summary>
public IObservableOrganizationOutsideCollaboratorsClient OutsideCollaborator { get; private set; }
/// <summary>
/// Returns the specified organization.
/// </summary>

View File

@@ -0,0 +1,184 @@
using System;
using System.Threading.Tasks;
using Octokit.Tests.Integration.Helpers;
using Xunit;
namespace Octokit.Tests.Integration.Clients
{
public class OrganizationOutsideCollaboratorsClientTests
{
public class TheGetAllMethod
{
readonly IGitHubClient _gitHub;
readonly string _fixtureCollaborator = "alfhenrik-test-2";
public TheGetAllMethod()
{
_gitHub = Helper.GetAuthenticatedClient();
}
[IntegrationTest]
public async Task ReturnsNoOutsideCollaborators()
{
var repoName = Helper.MakeNameWithTimestamp("public-repo");
using (var context = await _gitHub.CreateRepositoryContext(Helper.Organization, new NewRepository(repoName)))
{
var outsideCollaborators = await _gitHub.Organization
.OutsideCollaborator
.GetAll(Helper.Organization);
Assert.NotNull(outsideCollaborators);
Assert.Empty(outsideCollaborators);
}
}
[IntegrationTest]
public async Task ReturnsOutsideCollaborators()
{
var repoName = Helper.MakeNameWithTimestamp("public-repo");
using (var context = await _gitHub.CreateRepositoryContext(Helper.Organization, new NewRepository(repoName)))
{
await _gitHub.Repository.Collaborator.Add(context.RepositoryOwner, context.RepositoryName, _fixtureCollaborator);
var outsideCollaborators = await _gitHub.Organization
.OutsideCollaborator
.GetAll(Helper.Organization);
Assert.NotNull(outsideCollaborators);
Assert.Equal(1, outsideCollaborators.Count);
}
}
[IntegrationTest]
public async Task ReturnsCorrectCountOfOutsideCollaboratorsWithAllFilter()
{
var repoName = Helper.MakeNameWithTimestamp("public-repo");
using (var context = await _gitHub.CreateRepositoryContext(Helper.Organization, new NewRepository(repoName)))
{
await _gitHub.Repository.Collaborator.Add(context.RepositoryOwner, context.RepositoryName, _fixtureCollaborator);
await _gitHub.Repository.Collaborator.Add(context.RepositoryOwner, context.RepositoryName, "alfhenrik");
var outsideCollaborators = await _gitHub.Organization
.OutsideCollaborator
.GetAll(Helper.Organization, OrganizationMembersFilter.All);
Assert.NotNull(outsideCollaborators);
Assert.Equal(2, outsideCollaborators.Count);
}
}
[IntegrationTest]
public async Task ReturnsCorrectCountOfOutsideCollaboratorsWithTwoFactorFilter()
{
var repoName = Helper.MakeNameWithTimestamp("public-repo");
using (var context = await _gitHub.CreateRepositoryContext(Helper.Organization, new NewRepository(repoName)))
{
await _gitHub.Repository.Collaborator.Add(context.RepositoryOwner, context.RepositoryName, _fixtureCollaborator);
await _gitHub.Repository.Collaborator.Add(context.RepositoryOwner, context.RepositoryName, "alfhenrik");
var outsideCollaborators = await _gitHub.Organization
.OutsideCollaborator
.GetAll(Helper.Organization, OrganizationMembersFilter.TwoFactorAuthenticationDisabled);
Assert.NotNull(outsideCollaborators);
Assert.Equal(1, outsideCollaborators.Count);
Assert.Equal(_fixtureCollaborator, outsideCollaborators[0].Login);
}
}
}
public class TheDeleteMethod
{
readonly IGitHubClient _gitHub;
readonly string _fixtureCollaborator = "alfhenrik-test-2";
public TheDeleteMethod()
{
_gitHub = Helper.GetAuthenticatedClient();
}
[IntegrationTest]
public async Task CanRemoveOutsideCollaborator()
{
var repoName = Helper.MakeNameWithTimestamp("public-repo");
using (var context = await _gitHub.CreateRepositoryContext(Helper.Organization, new NewRepository(repoName)))
{
await _gitHub.Repository.Collaborator.Add(context.RepositoryOwner, context.RepositoryName, _fixtureCollaborator);
var result = await _gitHub
.Organization
.OutsideCollaborator
.Delete(Helper.Organization, _fixtureCollaborator);
Assert.True(result);
var outsideCollaborators = await _gitHub
.Organization
.OutsideCollaborator
.GetAll(Helper.Organization);
Assert.NotNull(outsideCollaborators);
Assert.Empty(outsideCollaborators);
}
}
[IntegrationTest]
public async Task CannotRemoveMemberOfOrganizationAsOutsideCollaborator()
{
var ex = await Assert.ThrowsAsync<UserIsOrganizationMemberException>(()
=> _gitHub.Organization.OutsideCollaborator.Delete(Helper.Organization, Helper.UserName));
Assert.True(string.Equals(
"You cannot specify an organization member to remove as an outside collaborator.",
ex.Message,
StringComparison.OrdinalIgnoreCase));
}
}
public class TheConvertFromMemberMethod
{
readonly IGitHubClient _gitHub;
readonly string _fixtureCollaborator = "alfhenrik-test-2";
public TheConvertFromMemberMethod()
{
_gitHub = Helper.GetAuthenticatedClient();
}
[IntegrationTest(Skip = "This test relies on https://github.com/octokit/octokit.net/issues/1533 being implemented before being re-enabled as there's currently no way to invite a member to an org")]
public async Task CanConvertOrgMemberToOutsideCollaborator()
{
var result = await _gitHub.Organization.OutsideCollaborator.ConvertFromMember(Helper.Organization, _fixtureCollaborator);
Assert.True(result);
var outsideCollaborators = await _gitHub.Organization.OutsideCollaborator.GetAll(Helper.Organization);
Assert.Equal(1, outsideCollaborators.Count);
Assert.Equal(_fixtureCollaborator, outsideCollaborators[0].Login);
}
[IntegrationTest]
public async Task CannotConvertNonOrgMemberToOutsideCollaborator()
{
var ex = await Assert.ThrowsAsync<UserIsNotMemberOfOrganizationException>(()
=> _gitHub.Organization.OutsideCollaborator.ConvertFromMember(Helper.Organization, _fixtureCollaborator));
Assert.True(string.Equals(
$"{_fixtureCollaborator} is not a member of the {Helper.Organization} organization.",
ex.Message,
StringComparison.OrdinalIgnoreCase));
}
[IntegrationTest]
public async Task CannotConvertLastOrgOwnerToOutsideCollaborator()
{
var ex = await Assert.ThrowsAsync<UserIsLastOwnerOfOrganizationException>(()
=> _gitHub.Organization.OutsideCollaborator.ConvertFromMember(Helper.Organization, Helper.UserName));
Assert.True(string.Equals(
"Cannot convert the last owner to an outside collaborator",
ex.Message,
StringComparison.OrdinalIgnoreCase));
}
}
}
}

View File

@@ -0,0 +1,142 @@
using System.Reactive.Linq;
using System.Threading.Tasks;
using Octokit.Reactive;
using Octokit.Tests.Integration.Helpers;
using Xunit;
namespace Octokit.Tests.Integration.Reactive
{
public class ObservableOrganizationOutsideCollaboratorsClientTests
{
public class TheGetAllMethod
{
readonly IGitHubClient _gitHub;
readonly ObservableOrganizationOutsideCollaboratorsClient _client;
readonly string _fixtureCollaborator = "alfhenrik-test-2";
public TheGetAllMethod()
{
_gitHub = Helper.GetAuthenticatedClient();
_client = new ObservableOrganizationOutsideCollaboratorsClient(_gitHub);
}
[IntegrationTest]
public async Task ReturnsNoOutsideCollaborators()
{
var outsideCollaborators = await _client
.GetAll(Helper.Organization).ToList();
Assert.NotNull(outsideCollaborators);
Assert.Empty(outsideCollaborators);
}
[IntegrationTest]
public async Task ReturnsOutsideCollaborators()
{
var repoName = Helper.MakeNameWithTimestamp("public-repo");
using (var context = await _gitHub.CreateRepositoryContext(Helper.Organization, new NewRepository(repoName)))
{
await _gitHub.Repository.Collaborator.Add(context.RepositoryOwner, context.RepositoryName, _fixtureCollaborator);
var outsideCollaborators = await _client
.GetAll(Helper.Organization).ToList();
Assert.NotNull(outsideCollaborators);
Assert.Equal(1, outsideCollaborators.Count);
}
}
[IntegrationTest]
public async Task ReturnsCorrectCountOfOutsideCollaboratorsWithAllFilter()
{
var repoName = Helper.MakeNameWithTimestamp("public-repo");
using (var context = await _gitHub.CreateRepositoryContext(Helper.Organization, new NewRepository(repoName)))
{
await _gitHub.Repository.Collaborator.Add(context.RepositoryOwner, context.RepositoryName, _fixtureCollaborator);
await _gitHub.Repository.Collaborator.Add(context.RepositoryOwner, context.RepositoryName, "alfhenrik");
var outsideCollaborators = await _client
.GetAll(Helper.Organization, OrganizationMembersFilter.All).ToList();
Assert.NotNull(outsideCollaborators);
Assert.Equal(2, outsideCollaborators.Count);
}
}
[IntegrationTest]
public async Task ReturnsCorrectCountOfOutsideCollaboratorsWithTwoFactorFilter()
{
var repoName = Helper.MakeNameWithTimestamp("public-repo");
using (var context = await _gitHub.CreateRepositoryContext(Helper.Organization, new NewRepository(repoName)))
{
await _gitHub.Repository.Collaborator.Add(context.RepositoryOwner, context.RepositoryName, _fixtureCollaborator);
await _gitHub.Repository.Collaborator.Add(context.RepositoryOwner, context.RepositoryName, "alfhenrik");
var outsideCollaborators = await _client
.GetAll(Helper.Organization, OrganizationMembersFilter.TwoFactorAuthenticationDisabled).ToList();
Assert.NotNull(outsideCollaborators);
Assert.Equal(1, outsideCollaborators.Count);
Assert.Equal("alfhenrik-test-2", outsideCollaborators[0].Login);
}
}
}
public class TheDeleteMethod
{
readonly IGitHubClient _gitHub;
readonly ObservableOrganizationOutsideCollaboratorsClient _client;
readonly string _fixtureCollaborator = "alfhenrik-test-2";
public TheDeleteMethod()
{
_gitHub = Helper.GetAuthenticatedClient();
_client = new ObservableOrganizationOutsideCollaboratorsClient(_gitHub);
}
[IntegrationTest]
public async Task CanRemoveOutsideCollaborator()
{
var repoName = Helper.MakeNameWithTimestamp("public-repo");
using (var context = await _gitHub.CreateRepositoryContext(Helper.Organization, new NewRepository(repoName)))
{
await _gitHub.Repository.Collaborator.Add(context.RepositoryOwner, context.RepositoryName, _fixtureCollaborator);
var result = await _client.Delete(Helper.Organization, _fixtureCollaborator);
Assert.True(result);
var outsideCollaborators = await _client
.GetAll(Helper.Organization).ToList();
Assert.Empty(outsideCollaborators);
}
}
}
public class TheConvertFromMemberMethod
{
readonly IGitHubClient _gitHub;
readonly ObservableOrganizationOutsideCollaboratorsClient _client;
readonly string _fixtureCollaborator = "alfhenrik-test-2";
public TheConvertFromMemberMethod()
{
_gitHub = Helper.GetAuthenticatedClient();
_client = new ObservableOrganizationOutsideCollaboratorsClient(_gitHub);
}
[IntegrationTest(Skip = "This test relies on https://github.com/octokit/octokit.net/issues/1533 being implemented before being re-enabled as there's currently no way to invite a member to an org")]
public async Task CanConvertOrgMemberToOutsideCollaborator()
{
var result = await _client.ConvertFromMember(Helper.Organization, _fixtureCollaborator);
Assert.True(result);
var outsideCollaborators = await _client
.GetAll(Helper.Organization).ToList();
Assert.Equal(1, outsideCollaborators.Count);
Assert.Equal(_fixtureCollaborator, outsideCollaborators[0].Login);
}
}
}
}

View File

@@ -0,0 +1,126 @@
using System;
using System.Threading.Tasks;
using NSubstitute;
using Xunit;
namespace Octokit.Tests.Clients
{
public class OrganizationOutsideCollaboratorsClientTests
{
public class TheCtor
{
[Fact]
public void EnsuresNonNullArguments()
{
Assert.Throws<ArgumentNullException>(() => new OrganizationOutsideCollaboratorsClient(null));
}
}
public class TheGetAllMethod
{
[Fact]
public void RequestsTheCorrectUrl()
{
var connection = Substitute.For<IApiConnection>();
var client = new OrganizationOutsideCollaboratorsClient(connection);
client.GetAll("org");
connection.Received().GetAll<User>(Arg.Is<Uri>(u => u.ToString() == "orgs/org/outside_collaborators"), null, "application/vnd.github.korra-preview+json");
}
[Fact]
public async Task EnsuresNonNullArguments()
{
var client = new OrganizationOutsideCollaboratorsClient(Substitute.For<IApiConnection>());
await Assert.ThrowsAsync<ArgumentNullException>(() => client.GetAll(null));
await Assert.ThrowsAsync<ArgumentNullException>(() => client.GetAll(null, OrganizationMembersFilter.All));
await Assert.ThrowsAsync<ArgumentException>(() => client.GetAll(""));
await Assert.ThrowsAsync<ArgumentException>(() => client.GetAll("", OrganizationMembersFilter.All));
}
[Fact]
public void AllFilterRequestsTheCorrectUrl()
{
var connection = Substitute.For<IApiConnection>();
var client = new OrganizationOutsideCollaboratorsClient(connection);
client.GetAll("org", OrganizationMembersFilter.All);
connection.Received().GetAll<User>(Arg.Is<Uri>(u => u.ToString() == "orgs/org/outside_collaborators?filter=all"), null, "application/vnd.github.korra-preview+json");
}
[Fact]
public void TwoFactorFilterRequestsTheCorrectUrl()
{
var connection = Substitute.For<IApiConnection>();
var client = new OrganizationOutsideCollaboratorsClient(connection);
client.GetAll("org", OrganizationMembersFilter.TwoFactorAuthenticationDisabled);
connection.Received().GetAll<User>(Arg.Is<Uri>(u => u.ToString() == "orgs/org/outside_collaborators?filter=2fa_disabled"), null, "application/vnd.github.korra-preview+json");
}
}
public class TheDeleteMethod
{
[Fact]
public void RequestsTheCorrectUrl()
{
var connection = Substitute.For<IApiConnection>();
var client = new OrganizationOutsideCollaboratorsClient(connection);
client.Delete("org", "user");
connection.Connection.Received().Delete(
Arg.Is<Uri>(u => u.ToString() == "orgs/org/outside_collaborators/user"),
Arg.Any<object>(),
"application/vnd.github.korra-preview+json");
}
[Fact]
public async Task EnsuresNonNullArguments()
{
var client = new OrganizationOutsideCollaboratorsClient(Substitute.For<IApiConnection>());
await Assert.ThrowsAsync<ArgumentNullException>(() => client.Delete(null, "user"));
await Assert.ThrowsAsync<ArgumentNullException>(() => client.Delete("org", null));
await Assert.ThrowsAsync<ArgumentException>(() => client.Delete("", "user"));
await Assert.ThrowsAsync<ArgumentException>(() => client.Delete("org", ""));
}
}
public class TheConvertFromMemberMethod
{
[Fact]
public void RequestsTheCorrectUrl()
{
var connection = Substitute.For<IApiConnection>();
var client = new OrganizationOutsideCollaboratorsClient(connection);
client.ConvertFromMember("org", "user");
connection.Connection.Received().Put(
Arg.Is<Uri>(u => u.ToString() == "orgs/org/outside_collaborators/user"),
"application/vnd.github.korra-preview+json");
}
[Fact]
public async Task EnsuresNonNullArgument()
{
var client = new OrganizationOutsideCollaboratorsClient(Substitute.For<IApiConnection>());
await Assert.ThrowsAsync<ArgumentNullException>(() => client.ConvertFromMember(null, "user"));
await Assert.ThrowsAsync<ArgumentNullException>(() => client.ConvertFromMember("org", null));
await Assert.ThrowsAsync<ArgumentException>(() => client.ConvertFromMember("", "user"));
await Assert.ThrowsAsync<ArgumentException>(() => client.ConvertFromMember("org", ""));
}
}
}
}

View File

@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NSubstitute;
using Octokit.Reactive;
using Xunit;
namespace Octokit.Tests.Reactive
{
public class ObservableOrganizationOutsideCollaboratorsClientTests
{
public class TheCtor
{
[Fact]
public void EnsuresNonNullArguments()
{
Assert.Throws<ArgumentNullException>(()
=> new ObservableOrganizationOutsideCollaboratorsClient(null));
}
}
public class TheGetAllMethod
{
[Fact]
public void RequestsTheCorrectUrl()
{
var gitHubClient = Substitute.For<IGitHubClient>();
var client = new ObservableOrganizationOutsideCollaboratorsClient(gitHubClient);
client.GetAll("org");
gitHubClient.Connection.Received(1).Get<List<User>>(
Arg.Is<Uri>(u => u.ToString() == "orgs/org/outside_collaborators"),
null,
"application/vnd.github.korra-preview+json");
}
[Fact]
public async Task EnsuresNonNullArguments()
{
var client = new ObservableOrganizationOutsideCollaboratorsClient(Substitute.For<IGitHubClient>());
Assert.Throws<ArgumentNullException>(() => client.GetAll(null));
Assert.Throws<ArgumentNullException>(() => client.GetAll(null, OrganizationMembersFilter.All));
Assert.Throws<ArgumentException>(() => client.GetAll(""));
Assert.Throws<ArgumentException>(() => client.GetAll("", OrganizationMembersFilter.All));
}
[Fact]
public void AllFilterRequestsTheCorrectUrl()
{
var gitHubClient = Substitute.For<IGitHubClient>();
var client = new ObservableOrganizationOutsideCollaboratorsClient(gitHubClient);
client.GetAll("org", OrganizationMembersFilter.All);
gitHubClient.Connection.Received(1).Get<List<User>>(
Arg.Is<Uri>(u => u.ToString() == "orgs/org/outside_collaborators?filter=all"),
null,
"application/vnd.github.korra-preview+json");
}
[Fact]
public void TwoFactorFilterRequestsTheCorrectUrl()
{
var gitHubClient = Substitute.For<IGitHubClient>();
var client = new ObservableOrganizationOutsideCollaboratorsClient(gitHubClient);
client.GetAll("org", OrganizationMembersFilter.TwoFactorAuthenticationDisabled);
gitHubClient.Connection.Received(1).Get<List<User>>(
Arg.Is<Uri>(u => u.ToString() == "orgs/org/outside_collaborators?filter=2fa_disabled"),
null,
"application/vnd.github.korra-preview+json");
}
}
public class TheDeleteMethod
{
[Fact]
public void RequestsTheCorrectUrl()
{
var gitHubClient = Substitute.For<IGitHubClient>();
var client = new ObservableOrganizationOutsideCollaboratorsClient(gitHubClient);
client.Delete("org", "user");
gitHubClient.Organization.OutsideCollaborator.Received().Delete(
Arg.Is("org"),
Arg.Is("user"));
}
[Fact]
public void EnsuresNonNullArguments()
{
var client = new ObservableOrganizationOutsideCollaboratorsClient(Substitute.For<IGitHubClient>());
Assert.Throws<ArgumentNullException>(() => client.Delete(null, "user"));
Assert.Throws<ArgumentNullException>(() => client.Delete("org", null));
Assert.Throws<ArgumentException>(() => client.Delete("", "user"));
Assert.Throws<ArgumentException>(() => client.Delete("org", ""));
}
}
public class TheConvertFromMemberMethod
{
[Fact]
public void RequestsTheCorrectUrl()
{
var gitHubClient = Substitute.For<IGitHubClient>();
var client = new ObservableOrganizationOutsideCollaboratorsClient(gitHubClient);
client.ConvertFromMember("org", "user");
gitHubClient.Organization.OutsideCollaborator.Received().ConvertFromMember(
Arg.Is("org"),
Arg.Is("user"));
}
[Fact]
public void EnsuresNonNullArgument()
{
var client = new ObservableOrganizationOutsideCollaboratorsClient(Substitute.For<IGitHubClient>());
Assert.Throws<ArgumentNullException>(() => client.ConvertFromMember(null, "user"));
Assert.Throws<ArgumentNullException>(() => client.ConvertFromMember("org", null));
Assert.Throws<ArgumentException>(() => client.ConvertFromMember("", "user"));
Assert.Throws<ArgumentException>(() => client.ConvertFromMember("org", ""));
}
}
}
}

View File

@@ -0,0 +1,61 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Octokit
{
public interface IOrganizationOutsideCollaboratorsClient
{
/// <summary>
/// List all users who are outside collaborators of an organization. An outside collaborator is a user that
/// is not a member of the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#list-outside-collaborators">API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <returns>The users</returns>
Task<IReadOnlyList<User>> GetAll(string org);
/// <summary>
/// List all users who are outside collaborators of an organization. An outside collaborator is a user that
/// is not a member of the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#list-outside-collaborators">API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <param name="filter">The filter to use when getting the users, <see cref="OrganizationMembersFilter"/></param>
/// <returns>The users</returns>
Task<IReadOnlyList<User>> GetAll(string org, OrganizationMembersFilter filter);
/// <summary>
/// Removes a user as an outside collaborator from the organization, this will remove them from all repositories
/// within the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#remove-outside-collaborator">API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <param name="user">The login of the user</param>
/// <returns></returns>
Task<bool> Delete(string org, string user);
/// <summary>
/// Converts an organization member to an outside collaborator,
/// when an organization member is converted to an outside collaborator,
/// they'll only have access to the repositories that their current team membership allows.
/// The user will no longer be a member of the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#convert-member-to-outside-collaborator"> API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <param name="user">The login for the user</param>
/// <returns></returns>
Task<bool> ConvertFromMember(string org, string user);
}
}

View File

@@ -24,6 +24,11 @@ namespace Octokit
/// </summary>
ITeamsClient Team { get; }
/// <summary>
/// Returns a client to manage outside collaborators of an organization.
/// </summary>
IOrganizationOutsideCollaboratorsClient OutsideCollaborator { get; }
/// <summary>
/// Returns the specified <see cref="Organization"/>.
/// </summary>

View File

@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
namespace Octokit
{
/// <summary>
/// A client for GitHub's Organization Outside Collaborators API.
/// </summary>
/// <remarks>
/// See the <a href="http://developer.github.com/v3/orgs/outside_collaborators/">Orgs API documentation</a> for more information.
/// </remarks>
public class OrganizationOutsideCollaboratorsClient : ApiClient, IOrganizationOutsideCollaboratorsClient
{
/// <summary>
/// Initializes a new Organization Outside Collaborators API client.
/// </summary>
/// <param name="apiConnection">An API connection</param>
public OrganizationOutsideCollaboratorsClient(IApiConnection apiConnection)
: base(apiConnection)
{
}
/// <summary>
/// List all users who are outside collaborators of an organization. An outside collaborator is a user that
/// is not a member of the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#list-outside-collaborators">API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <returns>The users</returns>
public Task<IReadOnlyList<User>> GetAll(string org)
{
Ensure.ArgumentNotNullOrEmptyString(org, nameof(org));
return ApiConnection.GetAll<User>(ApiUrls.OutsideCollaborators(org), null, AcceptHeaders.OrganizationMembershipPreview);
}
/// <summary>
/// List all users who are outside collaborators of an organization. An outside collaborator is a user that
/// is not a member of the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#list-outside-collaborators">API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <param name="filter">The filter to use when getting the users, <see cref="OrganizationMembersFilter"/></param>
/// <returns>The users</returns>
public Task<IReadOnlyList<User>> GetAll(string org, OrganizationMembersFilter filter)
{
Ensure.ArgumentNotNullOrEmptyString(org, nameof(org));
return ApiConnection.GetAll<User>(ApiUrls.OutsideCollaborators(org, filter), null, AcceptHeaders.OrganizationMembershipPreview);
}
/// <summary>
/// Removes a user as an outside collaborator from the organization, this will remove them from all repositories
/// within the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#remove-outside-collaborator">API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <param name="user">The login of the user</param>
/// <returns></returns>
public async Task<bool> Delete(string org, string user)
{
Ensure.ArgumentNotNullOrEmptyString(org, nameof(org));
Ensure.ArgumentNotNullOrEmptyString(user, nameof(user));
try
{
var statusCode = await Connection.Delete(ApiUrls.OutsideCollaborator(org, user), null, AcceptHeaders.OrganizationMembershipPreview).ConfigureAwait(false);
if (statusCode != HttpStatusCode.NoContent
&& statusCode != (HttpStatusCode)422)
{
throw new ApiException("Invalid Status Code returned. Expected a 204 or a 422", statusCode);
}
return statusCode == HttpStatusCode.NoContent;
}
catch (ApiException ex)
{
if (ex.StatusCode == (HttpStatusCode)422)
{
throw new UserIsOrganizationMemberException(ex.HttpResponse);
}
throw;
}
}
/// <summary>
/// Converts an organization member to an outside collaborator,
/// when an organization member is converted to an outside collaborator,
/// they'll only have access to the repositories that their current team membership allows.
/// The user will no longer be a member of the organization.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/orgs/outside_collaborators/#convert-member-to-outside-collaborator"> API documentation</a>
/// for more information.
/// </remarks>
/// <param name="org">The login for the organization</param>
/// <param name="user">The login for the user</param>
/// <returns></returns>
public async Task<bool> ConvertFromMember(string org, string user)
{
Ensure.ArgumentNotNullOrEmptyString(org, nameof(org));
Ensure.ArgumentNotNullOrEmptyString(user, nameof(user));
try
{
var statusCode = await Connection.Put(ApiUrls.OutsideCollaborator(org, user), AcceptHeaders.OrganizationMembershipPreview);
if (statusCode != HttpStatusCode.NoContent
&& statusCode != HttpStatusCode.Forbidden)
{
throw new ApiException("Invalid Status Code returned. Expected a 204 or a 403", statusCode);
}
return statusCode == HttpStatusCode.NoContent;
}
catch (ForbiddenException fex)
{
if (string.Equals(
"Cannot convert the last owner to an outside collaborator",
fex.Message,
StringComparison.OrdinalIgnoreCase))
{
throw new UserIsLastOwnerOfOrganizationException(fex.HttpResponse);
}
throw;
}
catch (NotFoundException nfex)
{
if (string.Equals(
$"{user} is not a member of the {org} organization.",
nfex.Message,
StringComparison.OrdinalIgnoreCase))
{
throw new UserIsNotMemberOfOrganizationException(nfex.HttpResponse);
}
throw;
}
}
}
}

View File

@@ -20,6 +20,7 @@ namespace Octokit
{
Member = new OrganizationMembersClient(apiConnection);
Team = new TeamsClient(apiConnection);
OutsideCollaborator = new OrganizationOutsideCollaboratorsClient(apiConnection);
}
/// <summary>
@@ -32,6 +33,11 @@ namespace Octokit
/// </summary>
public ITeamsClient Team { get; private set; }
/// <summary>
/// Returns a client to manage outside collaborators of an organization.
/// </summary>
public IOrganizationOutsideCollaboratorsClient OutsideCollaborator { get; private set; }
/// <summary>
/// Returns the specified <see cref="Organization"/>.
/// </summary>

View File

@@ -0,0 +1,63 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net;
#if !NO_SERIALIZABLE
using System.Runtime.Serialization;
#endif
namespace Octokit
{
/// <summary>
/// Represents an error that occurs when trying to convert the
/// last owner of the organization to an outside collaborator
/// </summary>
#if !NO_SERIALIZABLE
[Serializable]
#endif
[SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors",
Justification = "These exceptions are specific to the GitHub API and not general purpose exceptions")]
public class UserIsLastOwnerOfOrganizationException : ApiException
{
/// <summary>
/// Constructs an instance of the <see cref="UserIsLastOwnerOfOrganizationException"/> class.
/// </summary>
/// <param name="response">The HTTP payload from the server</param>
public UserIsLastOwnerOfOrganizationException(IResponse response) : this(response, null)
{
}
/// <summary>
/// Constructs an instance of the <see cref="UserIsLastOwnerOfOrganizationException"/> class.
/// </summary>
/// <param name="response">The HTTP payload from the server</param>
/// <param name="innerException">The inner exception</param>
public UserIsLastOwnerOfOrganizationException(IResponse response, ApiException innerException)
: base(response, innerException)
{
Debug.Assert(response != null && response.StatusCode == HttpStatusCode.Forbidden,
"UserIsLastOwnerOfOrganizationException created with the wrong HTTP status code");
}
// https://developer.github.com/v3/orgs/outside_collaborators/#response-if-user-is-the-last-owner-of-the-organization
public override string Message => ApiErrorMessageSafe ?? "User is the last owner of the organization.";
#if !NO_SERIALIZABLE
/// <summary>
/// Constructs an instance of <see cref="UserIsLastOwnerOfOrganizationException"/>.
/// </summary>
/// <param name="info">
/// The <see cref="SerializationInfo"/> that holds the
/// serialized object data about the exception being thrown.
/// </param>
/// <param name="context">
/// The <see cref="StreamingContext"/> that contains
/// contextual information about the source or destination.
/// </param>
protected UserIsLastOwnerOfOrganizationException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
#endif
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net;
#if !NO_SERIALIZABLE
using System.Runtime.Serialization;
#endif
namespace Octokit
{
/// <summary>
/// Represents an error that occurs when trying to convert a user
/// that is not a member of the organization to an outside collaborator
/// </summary>
#if !NO_SERIALIZABLE
[Serializable]
#endif
[SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors",
Justification = "These exceptions are specific to the GitHub API and not general purpose exceptions")]
public class UserIsNotMemberOfOrganizationException : ApiException
{
/// <summary>
/// Constructs an instance of the <see cref="UserIsNotMemberOfOrganizationException"/> class.
/// </summary>
/// <param name="response">The HTTP payload from the server</param>
public UserIsNotMemberOfOrganizationException(IResponse response) : this(response, null)
{
}
/// <summary>
/// Constructs an instance of the <see cref="UserIsNotMemberOfOrganizationException"/> class.
/// </summary>
/// <param name="response">The HTTP payload from the server</param>
/// <param name="innerException">The inner exception</param>
public UserIsNotMemberOfOrganizationException(IResponse response, ApiException innerException)
: base(response, innerException)
{
Debug.Assert(response != null && response.StatusCode == HttpStatusCode.NotFound,
"UserIsNotMemberOfOrganizationException created with the wrong HTTP status code");
}
// https://developer.github.com/v3/orgs/outside_collaborators/#response-if-user-is-not-a-member-of-the-organization
public override string Message => ApiErrorMessageSafe ?? "User is not a member of the organization.";
#if !NO_SERIALIZABLE
/// <summary>
/// Constructs an instance of ForbiddenException
/// </summary>
/// <param name="info">
/// The <see cref="SerializationInfo"/> that holds the
/// serialized object data about the exception being thrown.
/// </param>
/// <param name="context">
/// The <see cref="StreamingContext"/> that contains
/// contextual information about the source or destination.
/// </param>
protected UserIsNotMemberOfOrganizationException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
#endif
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net;
#if !NO_SERIALIZABLE
using System.Runtime.Serialization;
#endif
namespace Octokit
{
/// <summary>
/// Represents an error that occurs when trying to remove an
/// outside collaborator that is a member of the organization
/// </summary>
#if !NO_SERIALIZABLE
[Serializable]
#endif
[SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors",
Justification = "These exceptions are specific to the GitHub API and not general purpose exceptions")]
public class UserIsOrganizationMemberException : ApiException
{
/// <summary>
/// Constructs an instance of the <see cref="UserIsOrganizationMemberException"/> class.
/// </summary>
/// <param name="response">The HTTP payload from the server</param>
public UserIsOrganizationMemberException(IResponse response) : this(response, null)
{
}
/// <summary>
/// Constructs an instance of the <see cref="UserIsOrganizationMemberException"/> class.
/// </summary>
/// <param name="response">The HTTP payload from the server</param>
/// <param name="innerException">The inner exception</param>
public UserIsOrganizationMemberException(IResponse response, ApiException innerException)
: base(response, innerException)
{
Debug.Assert(response != null && response.StatusCode == (HttpStatusCode)422,
"UserIsOrganizationMemberException created with the wrong HTTP status code");
}
// https://developer.github.com/v3/orgs/outside_collaborators/#response-if-user-is-a-member-of-the-organization
public override string Message => ApiErrorMessageSafe ?? "User could not be removed as an outside collaborator.";
#if !NO_SERIALIZABLE
/// <summary>
/// Constructs an instance of <see cref="UserIsOrganizationMemberException"/>.
/// </summary>
/// <param name="info">
/// The <see cref="SerializationInfo"/> that holds the
/// serialized object data about the exception being thrown.
/// </param>
/// <param name="context">
/// The <see cref="StreamingContext"/> that contains
/// contextual information about the source or destination.
/// </param>
protected UserIsOrganizationMemberException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
#endif
}
}

View File

@@ -661,6 +661,32 @@ namespace Octokit
return "orgs/{0}/public_members/{1}".FormatUri(org, name);
}
/// <summary>
/// Returns the <see cref="Uri"/> that returns all of the outside collaborators of the organization
/// </summary>
/// <param name="org">The organization</param>
/// <returns></returns>
public static Uri OutsideCollaborators(string org)
{
return "orgs/{0}/outside_collaborators".FormatUri(org);
}
/// <summary>
/// Returns the <see cref="Uri"/> that returns all of the outside collaborators of the organization
/// </summary>
/// <param name="org">The organization</param>
/// <param name="filter">The collaborator filter, <see cref="OrganizationMembersFilter"/></param>
/// <returns>The correct uri</returns>
public static Uri OutsideCollaborators(string org, OrganizationMembersFilter filter)
{
return "orgs/{0}/outside_collaborators?filter={1}".FormatUri(org, filter.ToParameter());
}
public static Uri OutsideCollaborator(string org, string user)
{
return "orgs/{0}/outside_collaborators/{1}".FormatUri(org, user);
}
/// <summary>
/// Returns the <see cref="Uri"/> that returns the issue/pull request event and issue info for the specified repository.
/// </summary>

View File

@@ -430,6 +430,21 @@ namespace Octokit
return response.HttpResponse.StatusCode;
}
/// <summary>
/// Performs an asynchronous HTTP PUT request that expects an empty response.
/// </summary>
/// <param name="uri">URI endpoint to send request to</param>
/// <param name="accepts">Specifies accepted response media types.</param>
/// <returns>The returned <seealso cref="HttpStatusCode"/></returns>
public async Task<HttpStatusCode> Put(Uri uri, string accepts)
{
Ensure.ArgumentNotNull(uri, nameof(uri));
Ensure.ArgumentNotNull(accepts, nameof(accepts));
var response = await SendData<object>(uri, HttpMethod.Put, null, accepts, null, CancellationToken.None).ConfigureAwait(false);
return response.HttpResponse.StatusCode;
}
/// <summary>
/// Performs an asynchronous HTTP DELETE request that expects an empty response.
/// </summary>

View File

@@ -214,6 +214,14 @@ namespace Octokit
/// <returns>The returned <seealso cref="HttpStatusCode"/></returns>
Task<HttpStatusCode> Put(Uri uri);
/// <summary>
/// Performs an asynchronous HTTP PUT request that expects an empty response.
/// </summary>
/// <param name="uri">URI endpoint to send request to</param>
/// <param name="accepts">Specifies accepted response media types.</param>
/// <returns>The returned <seealso cref="HttpStatusCode"/></returns>
Task<HttpStatusCode> Put(Uri uri, string accepts);
/// <summary>
/// Performs an asynchronous HTTP DELETE request that expects an empty response.
/// </summary>