From ec08efc646201095677239aa020b775ff53a221d Mon Sep 17 00:00:00 2001 From: BensonB12 Date: Fri, 27 Sep 2024 13:14:45 -0600 Subject: [PATCH 1/6] added a test for issue --- .../Controllers/v2/UsersControllerTests.cs | 40 +++++++++++++++++++ src/Api/Api.sln | 25 ++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/Api/Api.sln diff --git a/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs index c0e4f3eb7329..521ffef4ab4b 100644 --- a/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs +++ b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs @@ -236,6 +236,46 @@ public async Task GetList_EmptyResult_Success() AssertHelper.AssertPropertyEqual(expectedResponse, responseModel); } + [Fact] + public async Task GetList_SearchUserNameWithoutOptionalParameters_Success() + { + string filter = "userName eq user2@example.com"; + int? itemsPerPage = null; + int? startIndex = null; + var expectedResponse = new ScimListResponseModel + { + ItemsPerPage = 50, //default value + TotalResults = 1, + StartIndex = 0, //default value + Resources = new List + { + new ScimUserResponseModel + { + Id = ScimApplicationFactory.TestOrganizationUserId2, + DisplayName = "Test User 2", + ExternalId = "UB", + Active = true, + Emails = new List + { + new BaseScimUserModel.EmailModel { Primary = true, Type = "work", Value = "user2@example.com" } + }, + Groups = new List(), + Name = new BaseScimUserModel.NameModel("Test User 2"), + UserName = "user2@example.com", + Schemas = new List { ScimConstants.Scim2SchemaUser } + } + }, + Schemas = new List { ScimConstants.Scim2SchemaListResponse } + }; + + var context = await _factory.UsersGetListAsync(ScimApplicationFactory.TestOrganizationId1, filter, itemsPerPage, startIndex); + + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + + var responseModel = JsonSerializer.Deserialize>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + AssertHelper.AssertPropertyEqual(expectedResponse, responseModel); + } + [Fact] public async Task Post_Success() { diff --git a/src/Api/Api.sln b/src/Api/Api.sln new file mode 100644 index 000000000000..8e35f38dc845 --- /dev/null +++ b/src/Api/Api.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Api", "Api.csproj", "{FF5A5535-4322-4996-9618-A2ACC89E1752}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FF5A5535-4322-4996-9618-A2ACC89E1752}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF5A5535-4322-4996-9618-A2ACC89E1752}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF5A5535-4322-4996-9618-A2ACC89E1752}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF5A5535-4322-4996-9618-A2ACC89E1752}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3FA9E063-94E1-45DF-9A51-0377E9083805} + EndGlobalSection +EndGlobal From eab480aa4993119502ec448bdc0131da9c2fb60d Mon Sep 17 00:00:00 2001 From: BensonB12 Date: Fri, 27 Sep 2024 14:16:17 -0600 Subject: [PATCH 2/6] resolves issue #4043 default values for itemsPerPage and startIndex --- bitwarden_license/src/Scim/Users/GetUsersListQuery.cs | 5 ++++- .../Controllers/v2/UsersControllerTests.cs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs b/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs index 51250250fe92..05dccee6c1db 100644 --- a/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs +++ b/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs @@ -18,6 +18,9 @@ public GetUsersListQuery(IOrganizationUserRepository organizationUserRepository) string emailFilter = null; string usernameFilter = null; string externalIdFilter = null; + count ??= 50; + startIndex ??= 1; + if (!string.IsNullOrWhiteSpace(filter)) { if (filter.StartsWith("userName eq ")) @@ -55,7 +58,7 @@ public GetUsersListQuery(IOrganizationUserRepository organizationUserRepository) } totalResults = userList.Count; } - else if (string.IsNullOrWhiteSpace(filter) && startIndex.HasValue && count.HasValue) + else if (string.IsNullOrWhiteSpace(filter)) { userList = orgUsers.OrderBy(ou => ou.Email) .Skip(startIndex.Value - 1) diff --git a/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs index 521ffef4ab4b..185110fe78ed 100644 --- a/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs +++ b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs @@ -244,9 +244,9 @@ public async Task GetList_SearchUserNameWithoutOptionalParameters_Success() int? startIndex = null; var expectedResponse = new ScimListResponseModel { - ItemsPerPage = 50, //default value + ItemsPerPage = 1, TotalResults = 1, - StartIndex = 0, //default value + StartIndex = 1, //default value Resources = new List { new ScimUserResponseModel From f8d9a7ce4cd2df3f35b480990cfdde5e85eadca3 Mon Sep 17 00:00:00 2001 From: BensonB12 Date: Fri, 27 Sep 2024 14:22:48 -0600 Subject: [PATCH 3/6] got rid of extra sln --- src/Api/Api.sln | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 src/Api/Api.sln diff --git a/src/Api/Api.sln b/src/Api/Api.sln deleted file mode 100644 index 8e35f38dc845..000000000000 --- a/src/Api/Api.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.002.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Api", "Api.csproj", "{FF5A5535-4322-4996-9618-A2ACC89E1752}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FF5A5535-4322-4996-9618-A2ACC89E1752}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF5A5535-4322-4996-9618-A2ACC89E1752}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FF5A5535-4322-4996-9618-A2ACC89E1752}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FF5A5535-4322-4996-9618-A2ACC89E1752}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {3FA9E063-94E1-45DF-9A51-0377E9083805} - EndGlobalSection -EndGlobal From d244dd28ec0bdd6ca99149b74e493ccf56fa8514 Mon Sep 17 00:00:00 2001 From: BensonB12 Date: Mon, 7 Oct 2024 15:13:25 -0600 Subject: [PATCH 4/6] UsersController#Get now uses a queryParamModel Co-authored-by: Ahmad Mustafa Jebran Co-authored-by: Luris Solis --- .../src/Scim/Controllers/v2/UsersController.cs | 10 ++++------ .../src/Scim/Models/GetUserQueryParamModel.cs | 12 ++++++++++++ .../src/Scim/Users/GetUsersListQuery.cs | 12 +++++++----- .../src/Scim/Users/Interfaces/IGetUsersListQuery.cs | 2 +- .../test/Scim.Test/Users/GetUsersListQueryTests.cs | 10 +++++----- 5 files changed, 29 insertions(+), 17 deletions(-) create mode 100644 bitwarden_license/src/Scim/Models/GetUserQueryParamModel.cs diff --git a/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs b/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs index 1429fc3873fc..d7f07ff7e766 100644 --- a/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs +++ b/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs @@ -60,17 +60,15 @@ public async Task Get(Guid organizationId, Guid id) [HttpGet("")] public async Task Get( Guid organizationId, - [FromQuery] string filter, - [FromQuery] int? count, - [FromQuery] int? startIndex) + [FromQuery] GetUsersQueryParamModel model) { - var usersListQueryResult = await _getUsersListQuery.GetUsersListAsync(organizationId, filter, count, startIndex); + var usersListQueryResult = await _getUsersListQuery.GetUsersListAsync(organizationId, model); var scimListResponseModel = new ScimListResponseModel { Resources = usersListQueryResult.userList.Select(u => new ScimUserResponseModel(u)).ToList(), - ItemsPerPage = count.GetValueOrDefault(usersListQueryResult.userList.Count()), + ItemsPerPage = model.Count, TotalResults = usersListQueryResult.totalResults, - StartIndex = startIndex.GetValueOrDefault(1), + StartIndex = model.StartIndex, }; return Ok(scimListResponseModel); } diff --git a/bitwarden_license/src/Scim/Models/GetUserQueryParamModel.cs b/bitwarden_license/src/Scim/Models/GetUserQueryParamModel.cs new file mode 100644 index 000000000000..27d7b6d9a1f5 --- /dev/null +++ b/bitwarden_license/src/Scim/Models/GetUserQueryParamModel.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +public class GetUsersQueryParamModel +{ + public string Filter { get; init; } = string.Empty; + + [Range(1, int.MaxValue)] + public int Count { get; init; } = 50; + + [Range(1, int.MaxValue)] + public int StartIndex { get; init; } = 1; +} diff --git a/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs b/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs index 2b08822f47b8..9bcbcbdafc1d 100644 --- a/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs +++ b/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs @@ -13,13 +13,15 @@ public GetUsersListQuery(IOrganizationUserRepository organizationUserRepository) _organizationUserRepository = organizationUserRepository; } - public async Task<(IEnumerable userList, int totalResults)> GetUsersListAsync(Guid organizationId, string filter, int? count, int? startIndex) + public async Task<(IEnumerable userList, int totalResults)> GetUsersListAsync(Guid organizationId, GetUsersQueryParamModel userQueryParams) { string emailFilter = null; string usernameFilter = null; string externalIdFilter = null; - count ??= 50; - startIndex ??= 1; + + int count = userQueryParams.Count; + int startIndex = userQueryParams.StartIndex; + string filter = userQueryParams.Filter; if (!string.IsNullOrWhiteSpace(filter)) { @@ -62,8 +64,8 @@ public GetUsersListQuery(IOrganizationUserRepository organizationUserRepository) else if (string.IsNullOrWhiteSpace(filter)) { userList = orgUsers.OrderBy(ou => ou.Email) - .Skip(startIndex.Value - 1) - .Take(count.Value) + .Skip(startIndex - 1) + .Take(count) .ToList(); totalResults = orgUsers.Count; } diff --git a/bitwarden_license/src/Scim/Users/Interfaces/IGetUsersListQuery.cs b/bitwarden_license/src/Scim/Users/Interfaces/IGetUsersListQuery.cs index 265c6a8e793d..f584cb8e7b56 100644 --- a/bitwarden_license/src/Scim/Users/Interfaces/IGetUsersListQuery.cs +++ b/bitwarden_license/src/Scim/Users/Interfaces/IGetUsersListQuery.cs @@ -4,5 +4,5 @@ namespace Bit.Scim.Users.Interfaces; public interface IGetUsersListQuery { - Task<(IEnumerable userList, int totalResults)> GetUsersListAsync(Guid organizationId, string filter, int? count, int? startIndex); + Task<(IEnumerable userList, int totalResults)> GetUsersListAsync(Guid organizationId, GetUsersQueryParamModel userQueryParams); } diff --git a/bitwarden_license/test/Scim.Test/Users/GetUsersListQueryTests.cs b/bitwarden_license/test/Scim.Test/Users/GetUsersListQueryTests.cs index b7497e281d7f..9352e5c20287 100644 --- a/bitwarden_license/test/Scim.Test/Users/GetUsersListQueryTests.cs +++ b/bitwarden_license/test/Scim.Test/Users/GetUsersListQueryTests.cs @@ -24,7 +24,7 @@ public async Task GetUsersList_Success(int count, int startIndex, SutProvider().Received(1).GetManyDetailsByOrganizationAsync(organizationId); @@ -49,7 +49,7 @@ public async Task GetUsersList_FilterUserName_Success(string email, SutProvider< .GetManyDetailsByOrganizationAsync(organizationId) .Returns(organizationUserUserDetails); - var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null); + var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter }); await sutProvider.GetDependency().Received(1).GetManyDetailsByOrganizationAsync(organizationId); @@ -71,7 +71,7 @@ public async Task GetUsersList_FilterUserName_Empty(string email, SutProvider().Received(1).GetManyDetailsByOrganizationAsync(organizationId); @@ -96,7 +96,7 @@ public async Task GetUsersList_FilterExternalId_Success(SutProvider().Received(1).GetManyDetailsByOrganizationAsync(organizationId); @@ -120,7 +120,7 @@ public async Task GetUsersList_FilterExternalId_Empty(string externalId, SutProv .GetManyDetailsByOrganizationAsync(organizationId) .Returns(organizationUserUserDetails); - var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null); + var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter }); await sutProvider.GetDependency().Received(1).GetManyDetailsByOrganizationAsync(organizationId); From 3f5d6b36614587d3eb352335d27d60992d90e957 Mon Sep 17 00:00:00 2001 From: BensonB12 Date: Mon, 7 Oct 2024 15:22:01 -0600 Subject: [PATCH 5/6] UsersController#Get now uses a queryParamModel Co-authored-by: Ahmad Mustafa Jebran Co-authored-by: Luris Solis From 33327105fdb4d10664299db954152ae6e9611c1a Mon Sep 17 00:00:00 2001 From: BensonB12 Date: Wed, 9 Oct 2024 10:36:24 -0600 Subject: [PATCH 6/6] Test now passes, default 50 is represented --- .../Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs index 185110fe78ed..1c9b0c1127c2 100644 --- a/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs +++ b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs @@ -244,7 +244,7 @@ public async Task GetList_SearchUserNameWithoutOptionalParameters_Success() int? startIndex = null; var expectedResponse = new ScimListResponseModel { - ItemsPerPage = 1, + ItemsPerPage = 50, //default value TotalResults = 1, StartIndex = 1, //default value Resources = new List