Skip to content

Commit

Permalink
Add async functions to IMembersClient and other changes (#652)
Browse files Browse the repository at this point in the history
- Improve Create Group and Create User mock to better emulate GitLab.
- Add support for explicit Group Path in mock helpers.
- Add `RemoveMemberFrom*Async` and Implement async versions of the other Group and Project methods.
- Add `IUserClient.GetByUserNameAsync`
- Add unit and integrations tests to verify behavior of both the GitHub and Mock `IMembersClient`.
  • Loading branch information
belcher-rok authored Mar 27, 2024
1 parent 1378a00 commit e46467d
Show file tree
Hide file tree
Showing 18 changed files with 664 additions and 96 deletions.
98 changes: 97 additions & 1 deletion NGitLab.Mock.Tests/MembersMockTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Linq;
using System;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using NGitLab.Mock.Config;
using NUnit.Framework;

Expand Down Expand Up @@ -82,4 +85,97 @@ public void Test_members_project_all_inherited()

Assert.That(members.Count(), Is.EqualTo(3), "Membership found are invalid");
}

[Test]
public async Task Test_members_async_methods_simulate_gitlab_behavior()
{
// This test emulates GitLab's behavior. See `MembersClientTests.AsyncMethodsBehaveAsExpected()`

// Arrange
var user1Name = "user1";
var ownerName = "owner";

using var server = new GitLabConfig()
.WithUser(ownerName, isDefault: true)
.WithUser(user1Name)
.WithGroupOfFullPath("G1", configure: g =>
g.WithUserPermission(ownerName, Models.AccessLevel.Owner)
.WithUserPermission(user1Name, Models.AccessLevel.Maintainer))
.WithGroupOfFullPath("G1/G2", configure: g =>
g.WithUserPermission(ownerName, Models.AccessLevel.Owner))
.WithProject("Project", 1, @namespace: "G1")
.BuildServer();

var client = server.CreateClient(ownerName);

var user1 = await client.Users.GetByUserNameAsync(user1Name);
var user1Id = user1.Id.ToString();

// Act
// Assert
const string projectId = "G1/Project";
const string groupId = "G1/G2";

// Does NOT search inherited permission by default...
AssertThrowsGitLabException(() => client.Members.GetMemberOfProjectAsync(projectId, user1.Id), System.Net.HttpStatusCode.NotFound);
AssertThrowsGitLabException(() => client.Members.GetMemberOfGroupAsync(groupId, user1.Id), System.Net.HttpStatusCode.NotFound);
client.Members.OfProjectAsync(projectId).Select(m => m.UserName).Should().BeEmpty();
client.Members.OfGroupAsync(groupId).Select(m => m.UserName).Should().BeEquivalentTo(new[] { ownerName });

// Does search inherited permission when asked...
(await client.Members.GetMemberOfProjectAsync(projectId, user1.Id, includeInheritedMembers: true)).UserName.Should().Be(user1Name);
(await client.Members.GetMemberOfGroupAsync(groupId, user1.Id, includeInheritedMembers: true)).UserName.Should().Be(user1Name);
client.Members.OfProjectAsync(projectId, includeInheritedMembers: true).Select(m => m.UserName).Should().BeEquivalentTo(new[] { ownerName, user1Name });
client.Members.OfGroupAsync(groupId, includeInheritedMembers: true).Select(m => m.UserName).Should().BeEquivalentTo(new[] { ownerName, user1Name });

// Cannot update non-existent membership...
AssertThrowsGitLabException(() => client.Members.UpdateMemberOfProjectAsync(projectId, new() { UserId = user1Id, AccessLevel = Models.AccessLevel.Owner }), System.Net.HttpStatusCode.NotFound);
AssertThrowsGitLabException(() => client.Members.UpdateMemberOfGroupAsync(groupId, new() { UserId = user1Id, AccessLevel = Models.AccessLevel.Owner }), System.Net.HttpStatusCode.NotFound);

// Cannot add membership with an access-level lower than inherited...
AssertThrowsGitLabException(() => client.Members.AddMemberToProjectAsync(projectId, new() { UserId = user1Id, AccessLevel = Models.AccessLevel.Reporter }), System.Net.HttpStatusCode.BadRequest);
AssertThrowsGitLabException(() => client.Members.AddMemberToGroupAsync(groupId, new() { UserId = user1Id, AccessLevel = Models.AccessLevel.Reporter }), System.Net.HttpStatusCode.BadRequest);

// Can add membership with greater than or equal access-level...
await AssertReturnsMembership(() => client.Members.AddMemberToProjectAsync(projectId, new() { UserId = user1Id, AccessLevel = Models.AccessLevel.Maintainer }), Models.AccessLevel.Maintainer);
await AssertReturnsMembership(() => client.Members.AddMemberToGroupAsync(groupId, new() { UserId = user1Id, AccessLevel = Models.AccessLevel.Maintainer }), Models.AccessLevel.Maintainer);

// Cannot add duplicate membership...
AssertThrowsGitLabException(() => client.Members.AddMemberToProjectAsync(projectId, new() { UserId = user1Id, AccessLevel = Models.AccessLevel.Owner }), System.Net.HttpStatusCode.Conflict);
AssertThrowsGitLabException(() => client.Members.AddMemberToGroupAsync(groupId, new() { UserId = user1Id, AccessLevel = Models.AccessLevel.Owner }), System.Net.HttpStatusCode.Conflict);

// Can raise access-level above inherited...
await AssertReturnsMembership(() => client.Members.UpdateMemberOfProjectAsync(projectId, new() { UserId = user1Id, AccessLevel = Models.AccessLevel.Owner }), Models.AccessLevel.Owner);
await AssertReturnsMembership(() => client.Members.UpdateMemberOfGroupAsync(groupId, new() { UserId = user1Id, AccessLevel = Models.AccessLevel.Owner }), Models.AccessLevel.Owner);

// Can decrease access-level to inherited...
await AssertReturnsMembership(() => client.Members.UpdateMemberOfProjectAsync(projectId, new() { UserId = user1Id, AccessLevel = Models.AccessLevel.Maintainer }), Models.AccessLevel.Maintainer);
await AssertReturnsMembership(() => client.Members.UpdateMemberOfGroupAsync(groupId, new() { UserId = user1Id, AccessLevel = Models.AccessLevel.Maintainer }), Models.AccessLevel.Maintainer);

// Cannot decrease access-level lower than inherited...
AssertThrowsGitLabException(() => client.Members.UpdateMemberOfProjectAsync(projectId, new() { UserId = user1Id, AccessLevel = Models.AccessLevel.Reporter }), System.Net.HttpStatusCode.BadRequest);
AssertThrowsGitLabException(() => client.Members.UpdateMemberOfGroupAsync(groupId, new() { UserId = user1Id, AccessLevel = Models.AccessLevel.Reporter }), System.Net.HttpStatusCode.BadRequest);

// Can delete...
await client.Members.RemoveMemberFromProjectAsync(projectId, user1.Id);
await client.Members.RemoveMemberFromGroupAsync(groupId, user1.Id);

// Delete fails when not exist...
AssertThrowsGitLabException(() => client.Members.RemoveMemberFromProjectAsync(projectId, user1.Id), System.Net.HttpStatusCode.NotFound);
AssertThrowsGitLabException(() => client.Members.RemoveMemberFromGroupAsync(groupId, user1.Id), System.Net.HttpStatusCode.NotFound);
}

private static async Task AssertReturnsMembership(Func<Task<Models.Membership>> code, Models.AccessLevel expectedAccessLevel)
{
var membership = await code.Invoke().ConfigureAwait(false);
Assert.That(membership, Is.Not.Null);
Assert.That(membership.AccessLevel, Is.EqualTo((int)expectedAccessLevel));
}

private static void AssertThrowsGitLabException(AsyncTestDelegate code, System.Net.HttpStatusCode expectedStatusCode)
{
var ex = Assert.CatchAsync(typeof(GitLabException), code) as GitLabException;
Assert.That(ex, Is.Not.Null);
Assert.That(ex.StatusCode, Is.EqualTo(expectedStatusCode));
}
}
5 changes: 5 additions & 0 deletions NGitLab.Mock/Clients/ClientBase.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Net;
using NGitLab.Models;

namespace NGitLab.Mock.Clients;

Expand Down Expand Up @@ -31,6 +32,8 @@ protected Group GetGroup(object id, GroupPermission permissions)
{
int idInt => Server.AllGroups.FindById(idInt),
string idStr => Server.AllGroups.FindGroup(idStr),
IIdOrPathAddressable gid when gid.Path != null => Server.AllGroups.FindByNamespacedPath(gid.Path),
IIdOrPathAddressable gid => Server.AllGroups.FindById(gid.Id),
_ => throw new ArgumentException($"Id of type '{id.GetType()}' is not supported"),
};

Expand Down Expand Up @@ -77,6 +80,8 @@ protected Project GetProject(object id, ProjectPermission permissions)
{
int idInt => Server.AllProjects.FindById(idInt),
string idStr => Server.AllProjects.FindProject(idStr),
IIdOrPathAddressable pid when pid.Path != null => Server.AllProjects.FindByNamespacedPath(pid.Path),
IIdOrPathAddressable pid => Server.AllProjects.FindById(pid.Id),
_ => throw new ArgumentException($"Id of type '{id.GetType()}' is not supported"),
};

Expand Down
5 changes: 5 additions & 0 deletions NGitLab.Mock/Clients/GroupClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,13 @@ public Models.Group Create(GroupCreate group)
var newGroup = new Group
{
Name = group.Name,
Path = group.Path,
Description = group.Description,
Visibility = group.Visibility,
LfsEnabled = group.LfsEnabled,
RequestAccessEnabled = group.RequestAccessEnabled,
SharedRunnersLimit = TimeSpan.FromMinutes(group.SharedRunnersMinutesLimit ?? 0),

Permissions =
{
new Permission(Context.User, AccessLevel.Owner),
Expand Down
Loading

0 comments on commit e46467d

Please sign in to comment.