Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/user-group-ms/src/main/docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,10 @@
"type" : "string",
"description" : "Users group's name"
},
"parentInstitutionId" : {
"type" : "string",
"description" : "Users group's parent institutionId"
},
"productId" : {
"type" : "string",
"description" : "Users group's productId"
Expand Down Expand Up @@ -1036,6 +1040,10 @@
"type" : "string",
"description" : "Users group's name"
},
"parentInstitutionId" : {
"type" : "string",
"description" : "Users group's parent institutionId"
},
"productId" : {
"type" : "string",
"description" : "Users group's productId"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ public class CreateUserGroupDto {
@NotBlank
private String institutionId;

@ApiModelProperty(value = "${swagger.user-group.model.parentInstitutionId}")
private String parentInstitutionId;

@ApiModelProperty(value = "${swagger.user-group.model.productId}", required = true)
@JsonProperty(required = true)
@NotBlank
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public class UserGroupResource {
@NotBlank
private String institutionId;

@ApiModelProperty(value = "${swagger.user-group.model.parentInstitutionId}")
private String parentInstitutionId;

@ApiModelProperty(value = "${swagger.user-group.model.productId}", required = true)
@NotBlank
private String productId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class UserGroupServiceImpl implements UserGroupService {
private static final String MEMBERS_REQUIRED = "Members are required";
private static final String MEMBER_ID_REQUIRED = "A member id is required";
private static final String GROUP_NAME_ALREADY_EXISTS = "A group with the same name already exists in ACTIVE or SUSPENDED state";
private static final String GROUP_PARENT_ALREADY_EXISTS = "A group with the same institutionId-parentInstitutionId-productId already exists in ACTIVE or SUSPENDED state";
private final List<String> allowedSortingParams;
private final UserGroupRepository repository;
private final MongoTemplate mongoTemplate;
Expand All @@ -69,7 +70,7 @@ public UserGroupOperations createGroup(UserGroupOperations group) {
Assert.state(authentication.getPrincipal() instanceof SelfCareUser, "Not SelfCareUser principal");
Assert.notNull(group, "A group is required");

checkNameUniqueness(group.getId(), group.getName(), group.getProductId(), group.getInstitutionId());
checkGroupUniqueness(group.getId(), group.getName(), group.getProductId(), group.getInstitutionId(), group.getParentInstitutionId());
return insertUserGroupEntity(group);
}

Expand Down Expand Up @@ -194,7 +195,7 @@ public UserGroupOperations updateGroup(String id, UserGroupOperations group) {
if (UserGroupStatus.SUSPENDED.equals(foundGroup.getStatus())) {
throw new ResourceUpdateException(TRYING_TO_MODIFY_SUSPENDED_GROUP);
}
checkNameUniqueness(id, group.getName(), foundGroup.getProductId(), foundGroup.getInstitutionId());
checkGroupUniqueness(id, group.getName(), foundGroup.getProductId(), foundGroup.getInstitutionId(), foundGroup.getParentInstitutionId());

foundGroup.setMembers(group.getMembers());
foundGroup.setName(group.getName());
Expand All @@ -219,20 +220,35 @@ private UserGroupEntity insertUserGroupEntity(UserGroupOperations group) {
return insert;
}

private void checkNameUniqueness(String currentGroupId, String groupName, String productId, String institutionId) {
private void checkGroupUniqueness(String currentGroupId, String groupName, String productId, String institutionId, String parentInstitutionId) {
UserGroupFilter filter = new UserGroupFilter();
filter.setProductId(productId);
filter.setInstitutionId(institutionId);
filter.setStatus(Arrays.asList(UserGroupStatus.ACTIVE, UserGroupStatus.SUSPENDED));

Page<UserGroupOperations> existingGroups = findAll(filter, Pageable.unpaged());
boolean isDuplicate = existingGroups.stream()
.anyMatch(g -> g.getName().equals(groupName) && !g.getId().equals(currentGroupId));

if (isDuplicate) {
// verify if there's no other group with the same name (but different groupId)
boolean isSameName = existingGroups.stream().anyMatch(g ->
g.getName().equals(groupName) && !g.getId().equals(currentGroupId));

// when parentInsitutionId is not null, verify if there's no other group with same institutionId-parentInstitutionId-productId (but different groupId)
boolean isDuplicate = parentInstitutionId != null && existingGroups.stream().anyMatch(g ->
Objects.equals(g.getInstitutionId(), institutionId)
&& Objects.equals(g.getProductId(), productId)
&& Objects.equals(g.getParentInstitutionId(), parentInstitutionId)
&& !g.getId().equals(currentGroupId));

if (isSameName) {
log.warn("Attempted to create/update group with duplicate name: {}", groupName);
throw new ResourceAlreadyExistsException(GROUP_NAME_ALREADY_EXISTS);
}

if (isDuplicate) {
log.warn("Attempted to create/update group with duplicate institutionId: {}, parentInstitutionId: {} and productId: {}",
institutionId, parentInstitutionId, productId);
throw new ResourceAlreadyExistsException(GROUP_PARENT_ALREADY_EXISTS);
}
}

private Query createActiveGroupQuery(String id) {
Expand Down Expand Up @@ -336,7 +352,9 @@ private Optional<UserGroupOperations> findById(String id) {

private Page<UserGroupOperations> findAll(UserGroupFilter filter, Pageable pageable) {
log.trace("findAll start");
log.debug("findAll institutionId= {} , productId = {}, userId = {}, pageable = {}", filter.getInstitutionId(), filter.getProductId(), filter.getUserId(), pageable);
log.debug("findAll institutionId = {}, parentInstitutionId = {}, productId = {}, userId = {}, pageable = {}",
Encode.forJava(filter.getInstitutionId()), Encode.forJava(filter.getParentInstitutionId()),
Encode.forJava(filter.getProductId()), Encode.forJava(filter.getUserId()), pageable);
if (pageable.getSort().isSorted() && !StringUtils.hasText(filter.getProductId()) && !StringUtils.hasText(filter.getInstitutionId())) {
throw new ValidationException("Sorting not allowed without productId or institutionId");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public void verifyErrorMessage(String expectedErrorMessage) {
public UserGroupEntity convertRequest(Map<String, String> entry) {
UserGroupEntity userGroupEntity = new UserGroupEntity();
userGroupEntity.setInstitutionId(entry.get("institutionId"));
userGroupEntity.setParentInstitutionId(entry.get("parentInstitutionId"));
userGroupEntity.setProductId(entry.get("productId"));
userGroupEntity.setName(entry.get("name"));
userGroupEntity.setDescription(entry.get("description"));
Expand Down Expand Up @@ -108,6 +109,11 @@ public void verifyInstitutionId(String expectedInstitutionId) {
Assertions.assertEquals(expectedInstitutionId, userGroupEntityResponse.getInstitutionId());
}

@Then("the response should contain the parent institutionId {string}")
public void verifyParentInstitutionId(String expectedParentInstitutionId) {
Assertions.assertEquals(expectedParentInstitutionId, userGroupEntityResponse.getParentInstitutionId());
}

@And("the response should contain the status {string}")
public void theResponseShouldContainTheStatus(String expectedStatus) {
Assertions.assertEquals(expectedStatus, userGroupEntityResponse.getStatus().name());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,22 @@ public void the_response_should_contain_the_group_details() {
Assertions.assertNull(userGroupEntityResponse.getModifiedBy());
}

@Then("the response should contain the group details with parent institution")
public void the_response_should_contain_the_group_details_with_parent_institution() {
Assertions.assertEquals(userGroupId, userGroupEntityResponse.getId());
Assertions.assertEquals("io group with parent", userGroupEntityResponse.getName());
Assertions.assertEquals("io group with parent description", userGroupEntityResponse.getDescription());
Assertions.assertEquals("9c7ae123-d990-4400-b043-67a60aff31bc", userGroupEntityResponse.getInstitutionId());
Assertions.assertEquals("5d1ae124-d870-4400-b043-67a60aff32cb", userGroupEntityResponse.getParentInstitutionId());
Assertions.assertEquals("prod-test", userGroupEntityResponse.getProductId());
Assertions.assertEquals("ACTIVE", userGroupEntityResponse.getStatus().name());
Assertions.assertEquals(1, userGroupEntityResponse.getMembers().size());
Assertions.assertEquals("75003d64-7b8c-4768-b20c-cf66467d44c7", userGroupEntityResponse.getMembers().iterator().next());
Assertions.assertNotNull(userGroupEntityResponse.getCreatedAt());
Assertions.assertEquals("4ba2832d-9c4c-40f3-9126-e1c72905ef14", userGroupEntityResponse.getCreatedBy());
Assertions.assertNull(userGroupEntityResponse.getModifiedBy());
}

@And("the response should contain a paginated list of user groups of {int} items on page {int}")
public void theResponseShouldContainAPaginatedListOfUserGroups(int count, int page) {
Assertions.assertEquals(count, userGroupEntityResponsePage.getContent().size());
Expand All @@ -185,6 +201,20 @@ public void theResponseShouldContainAPaginatedListOfUserGroups(int count, int pa
Assertions.assertEquals(page, userGroupEntityResponsePage.getNumber());
}

@Then("the response page should contain a group with parent institution id")
public void the_response_page_should_contain_a_group_with_parent_institution_id() {
List<UserGroupEntity> groups = userGroupEntityResponsePage.getContent();

Assertions.assertFalse(groups.isEmpty(), "Expected response page to contain at least one group");

boolean hasParentInstitutionId = groups.stream()
.anyMatch(group -> group.getParentInstitutionId() != null);

Assertions.assertTrue(hasParentInstitutionId,
"Expected at least one group to have a parentInstitutionId");
}


@Given("I have valid filters institutionId {string} productId {string} and status {string}")
public void iHaveValidFiltersAndAnd(String institutionId, String productId, String status) {
userGroupEntityFilter = new UserGroupEntity();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,6 @@ void getUserGroups_validSortParameter() {
filter.setProductId("productId"); // Aggiungi un productId per soddisfare la condizione
Pageable pageable = PageRequest.of(0, 10, Sort.by("name"));
List<UserGroupEntity> userGroups = Collections.singletonList(mock(UserGroupEntity.class));
Page<UserGroupEntity> page = new PageImpl<>(userGroups, pageable, userGroups.size());
when(mongoTemplateMock.find(any(Query.class), eq(UserGroupEntity.class))).thenReturn(userGroups);
when(mongoTemplateMock.count(any(Query.class), eq(UserGroupEntity.class))).thenReturn((long) userGroups.size());
//when
Expand Down Expand Up @@ -817,6 +816,43 @@ void createGroup_duplicateName() {
verify(mongoTemplateMock).count(any(Query.class), eq(UserGroupEntity.class));
}

@Test
void createGroup_duplicateParent() {
//given
SelfCareUser selfCareUser = SelfCareUser.builder("userId")
.email("test@example.com")
.name("name")
.surname("surname")
.build();
TestingAuthenticationToken authenticationToken = new TestingAuthenticationToken(selfCareUser, null);
TestSecurityContextHolder.setAuthentication(authenticationToken);
UserGroupOperations input = TestUtils.mockInstance(new DummyGroup(), "setId", "setCreateAt", "setModifiedAt");
input.setName("name 2");
input.setProductId("productId");
input.setInstitutionId("institutionId");
input.setParentInstitutionId("parentInstitutionId");

UserGroupEntity existingGroup = new UserGroupEntity();
existingGroup.setId("existingGroupId");
existingGroup.setName("name 1");
existingGroup.setProductId("productId");
existingGroup.setInstitutionId("institutionId");
existingGroup.setParentInstitutionId("parentInstitutionId");
existingGroup.setStatus(UserGroupStatus.ACTIVE);

when(mongoTemplateMock.find(any(Query.class), eq(UserGroupEntity.class))).thenReturn(Collections.singletonList(existingGroup));
when(mongoTemplateMock.count(any(Query.class), eq(UserGroupEntity.class))).thenReturn(1L);

//when
Executable executable = () -> groupService.createGroup(input);

//then
ResourceAlreadyExistsException exception = assertThrows(ResourceAlreadyExistsException.class, executable);
assertEquals("A group with the same institutionId-parentInstitutionId-productId already exists in ACTIVE or SUSPENDED state", exception.getMessage());
verify(mongoTemplateMock).find(any(Query.class), eq(UserGroupEntity.class));
verify(mongoTemplateMock).count(any(Query.class), eq(UserGroupEntity.class));
}

@Test
void updateGroup_duplicateName() {
//given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,25 @@ Feature: Create User Group
And the response should contain the createdAt notNull
And the response should contain the modified data null

@CreateNewGroup
Scenario: Successfully create a new user group with parentInstitutionId
Given [CREATE] user login with username "j.doe" and password "test"
And the following user group details:
| name | description | productId | institutionId | parentInstitutionId | status | members |
| Group Name Parent Institution | TestGroupParentInstitution | product123 | inst123 | inst234 | ACTIVE | 525db33f-967f-4a82-8984-c606225e714a,a1b7c86b-d195-41d8-8291-7c3467abfd30 |
When I send a POST request to "/v1/user-groups" with the given details
Then [CREATE] the response status should be 201
And the response should contain a valid user group resource with name "Group Name Parent Institution"
And the response should contain the description "TestGroupParentInstitution"
And the response should contain the productId "product123"
And the response should contain the institutionId "inst123"
And the response should contain the parent institutionId "inst234"
And the response should contain the status "ACTIVE"
And the response should contain 2 members
And the response should contain the createdBy "97a511a7-2acc-47b9-afed-2f3c65753b4a"
And the response should contain the createdAt notNull
And the response should contain the modified data null

# Scenario negativo: Nome del gruppo già esistente (conflitto)
@DuplicateGroupName
Scenario: Attempt to create a user group with a duplicate name
Expand All @@ -30,6 +49,17 @@ Feature: Create User Group
Then [CREATE] the response status should be 409
And [CREATE] the response should contain an error message "A group with the same name already exists in ACTIVE or SUSPENDED state"

# Scenario negativo: Gruppo già esistente con stesso institutionId-parentInstitutionId-productId (conflitto)
@DuplicateGroupName
Scenario: Attempt to create a user group with a duplicate institutionId-parentInstitutionId-productId
Given [CREATE] user login with username "j.doe" and password "test"
And the following user group details:
| name | description | productId | institutionId | parentInstitutionId | status | members |
| io group with parent 2 | TestGroup | prod-test | 9c7ae123-d990-4400-b043-67a60aff31bc | 5d1ae124-d870-4400-b043-67a60aff32cb |ACTIVE | 525db33f-967f-4a82-8984-c606225e714a,a1b7c86b-d195-41d8-8291-7c3467abfd30 |
When I send a POST request to "/v1/user-groups" with the given details
Then [CREATE] the response status should be 409
And [CREATE] the response should contain an error message "A group with the same institutionId-parentInstitutionId-productId already exists in ACTIVE or SUSPENDED state"

# Scenario negativo: Dettagli del gruppo mancanti (name non fornito)
Scenario: Attempt to create a user group with missing required fields (name)
Given [CREATE] user login with username "j.doe" and password "test"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ Feature: Get User Group
Then [RETRIEVE] the response status should be 200
And the response should contain the group details

Scenario: Successfully retrieve a group with a valid ID with parent institution
Given [RETRIEVE] user login with username "j.doe" and password "test"
And I have a valid group ID to retrieve: "6759f8df78b6af202b222d2c"
When I send a GET request to "/v1/user-groups/{id}"
Then [RETRIEVE] the response status should be 200
And the response should contain the group details with parent institution

Scenario: Attempt to retrieve a non-existent group
Given [RETRIEVE] user login with username "j.doe" and password "test"
And I have a non-existent group ID to retrieve "99999"
Expand Down Expand Up @@ -68,6 +75,7 @@ Feature: Get User Group
When I send a GET request to "/v1/user-groups" to retrieve userGroups
Then [RETRIEVE] the response status should be 200
And the response should contain a paginated list of user groups of 2 items on page 1
And the response page should contain a group with parent institution id

@LastRetrieveGroupScenario
Scenario: No user groups found for the provided filters
Expand Down
4 changes: 4 additions & 0 deletions infra/container_apps/user-group-ms/mongo_db.tf
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ module "mongodb_collection_user-groups" {
keys = ["institutionId"]
unique = false
},
{
keys = ["parentInstitutionId"]
unique = false
},
{
keys = ["productId"]
unique = false
Expand Down
Loading