using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Scim.Groups; using Bit.Scim.Models; using Bit.Scim.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; namespace Bit.Scim.Test.Groups; [SutProviderCustomize] public class PatchGroupCommandTests { [Theory] [BitAutoData] public async Task PatchGroup_ReplaceListMembers_Success(SutProvider sutProvider, Organization organization, Group group, IEnumerable userIds) { group.OrganizationId = organization.Id; sutProvider.GetDependency() .GetByIdAsync(group.Id) .Returns(group); var scimPatchModel = new Models.ScimPatchModel { Operations = new List { new ScimPatchModel.OperationModel { Op = "replace", Path = "members", Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement } }, Schemas = new List { ScimConstants.Scim2SchemaUser } }; await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.GetDependency().Received(1).UpdateUsersAsync(group.Id, Arg.Is>(arg => arg.All(id => userIds.Contains(id)))); } [Theory] [BitAutoData] public async Task PatchGroup_ReplaceDisplayNameFromPath_Success(SutProvider sutProvider, Organization organization, Group group, string displayName) { group.OrganizationId = organization.Id; sutProvider.GetDependency() .GetByIdAsync(group.Id) .Returns(group); var scimPatchModel = new Models.ScimPatchModel { Operations = new List { new ScimPatchModel.OperationModel { Op = "replace", Path = "displayname", Value = JsonDocument.Parse($"\"{displayName}\"").RootElement } }, Schemas = new List { ScimConstants.Scim2SchemaUser } }; await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.GetDependency().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM); Assert.Equal(displayName, group.Name); } [Theory] [BitAutoData] public async Task PatchGroup_ReplaceDisplayNameFromValueObject_Success(SutProvider sutProvider, Organization organization, Group group, string displayName) { group.OrganizationId = organization.Id; sutProvider.GetDependency() .GetByIdAsync(group.Id) .Returns(group); var scimPatchModel = new Models.ScimPatchModel { Operations = new List { new ScimPatchModel.OperationModel { Op = "replace", Value = JsonDocument.Parse($"{{\"displayName\":\"{displayName}\"}}").RootElement } }, Schemas = new List { ScimConstants.Scim2SchemaUser } }; await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.GetDependency().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM); Assert.Equal(displayName, group.Name); } [Theory] [BitAutoData] public async Task PatchGroup_AddSingleMember_Success(SutProvider sutProvider, Organization organization, Group group, ICollection existingMembers, Guid userId) { group.OrganizationId = organization.Id; sutProvider.GetDependency() .GetByIdAsync(group.Id) .Returns(group); sutProvider.GetDependency() .GetManyUserIdsByIdAsync(group.Id) .Returns(existingMembers); var scimPatchModel = new Models.ScimPatchModel { Operations = new List { new ScimPatchModel.OperationModel { Op = "add", Path = $"members[value eq \"{userId}\"]", } }, Schemas = new List { ScimConstants.Scim2SchemaUser } }; await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.GetDependency().Received(1).UpdateUsersAsync(group.Id, Arg.Is>(arg => arg.All(id => existingMembers.Append(userId).Contains(id)))); } [Theory] [BitAutoData] public async Task PatchGroup_AddListMembers_Success(SutProvider sutProvider, Organization organization, Group group, ICollection existingMembers, ICollection userIds) { group.OrganizationId = organization.Id; sutProvider.GetDependency() .GetByIdAsync(group.Id) .Returns(group); sutProvider.GetDependency() .GetManyUserIdsByIdAsync(group.Id) .Returns(existingMembers); var scimPatchModel = new Models.ScimPatchModel { Operations = new List { new ScimPatchModel.OperationModel { Op = "add", Path = $"members", Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement } }, Schemas = new List { ScimConstants.Scim2SchemaUser } }; await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.GetDependency().Received(1).UpdateUsersAsync(group.Id, Arg.Is>(arg => arg.All(id => existingMembers.Concat(userIds).Contains(id)))); } [Theory] [BitAutoData] public async Task PatchGroup_RemoveSingleMember_Success(SutProvider sutProvider, Organization organization, Group group, Guid userId) { group.OrganizationId = organization.Id; sutProvider.GetDependency() .GetByIdAsync(group.Id) .Returns(group); var scimPatchModel = new Models.ScimPatchModel { Operations = new List { new ScimPatchModel.OperationModel { Op = "remove", Path = $"members[value eq \"{userId}\"]", } }, Schemas = new List { ScimConstants.Scim2SchemaUser } }; await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.GetDependency().Received(1).DeleteUserAsync(group, userId, EventSystemUser.SCIM); } [Theory] [BitAutoData] public async Task PatchGroup_RemoveListMembers_Success(SutProvider sutProvider, Organization organization, Group group, ICollection existingMembers) { group.OrganizationId = organization.Id; sutProvider.GetDependency() .GetByIdAsync(group.Id) .Returns(group); sutProvider.GetDependency() .GetManyUserIdsByIdAsync(group.Id) .Returns(existingMembers); var scimPatchModel = new Models.ScimPatchModel { Operations = new List { new ScimPatchModel.OperationModel { Op = "remove", Path = $"members", Value = JsonDocument.Parse(JsonSerializer.Serialize(existingMembers.Select(uid => new { value = uid }).ToArray())).RootElement } }, Schemas = new List { ScimConstants.Scim2SchemaUser } }; await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.GetDependency().Received(1).UpdateUsersAsync(group.Id, Arg.Is>(arg => arg.All(id => existingMembers.Contains(id)))); } [Theory] [BitAutoData] public async Task PatchGroup_NoAction_Success(SutProvider sutProvider, Organization organization, Group group) { group.OrganizationId = organization.Id; sutProvider.GetDependency() .GetByIdAsync(group.Id) .Returns(group); var scimPatchModel = new Models.ScimPatchModel { Operations = new List(), Schemas = new List { ScimConstants.Scim2SchemaUser } }; await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default); } [Theory] [BitAutoData] public async Task PatchGroup_NotFound_Throws(SutProvider sutProvider, Organization organization, Guid groupId) { var scimPatchModel = new Models.ScimPatchModel { Operations = new List(), Schemas = new List { ScimConstants.Scim2SchemaUser } }; await Assert.ThrowsAsync(async () => await sutProvider.Sut.PatchGroupAsync(organization, groupId, scimPatchModel)); } [Theory] [BitAutoData] public async Task PatchGroup_MismatchingOrganizationId_Throws(SutProvider sutProvider, Organization organization, Guid groupId) { var scimPatchModel = new Models.ScimPatchModel { Operations = new List(), Schemas = new List { ScimConstants.Scim2SchemaUser } }; sutProvider.GetDependency() .GetByIdAsync(groupId) .Returns(new Group { Id = groupId, OrganizationId = Guid.NewGuid() }); await Assert.ThrowsAsync(async () => await sutProvider.Sut.PatchGroupAsync(organization, groupId, scimPatchModel)); } }