diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/ApiWorkflowStateServiceImpl.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/ApiWorkflowStateServiceImpl.java index b8bd1574c9c..0ebc3051374 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/ApiWorkflowStateServiceImpl.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/ApiWorkflowStateServiceImpl.java @@ -30,6 +30,8 @@ import io.gravitee.rest.api.model.UserEntity; import io.gravitee.rest.api.model.WorkflowReferenceType; import io.gravitee.rest.api.model.WorkflowState; +import io.gravitee.rest.api.model.parameters.Key; +import io.gravitee.rest.api.model.parameters.ParameterReferenceType; import io.gravitee.rest.api.model.permissions.ApiPermission; import io.gravitee.rest.api.model.permissions.RolePermissionAction; import io.gravitee.rest.api.model.permissions.RoleScope; @@ -39,6 +41,7 @@ import io.gravitee.rest.api.service.EmailService; import io.gravitee.rest.api.service.MembershipService; import io.gravitee.rest.api.service.NotifierService; +import io.gravitee.rest.api.service.ParameterService; import io.gravitee.rest.api.service.RoleService; import io.gravitee.rest.api.service.UserService; import io.gravitee.rest.api.service.WorkflowService; @@ -55,6 +58,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Predicate; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; @@ -75,6 +79,7 @@ public class ApiWorkflowStateServiceImpl implements ApiWorkflowStateService { private final MembershipService membershipService; private final EmailService emailService; private final ApiSearchService apiSearchService; + private final ParameterService parameterService; public ApiWorkflowStateServiceImpl( final AuditService auditService, @@ -85,7 +90,8 @@ public ApiWorkflowStateServiceImpl( final NotifierService notifierService, final MembershipService membershipService, final EmailService emailService, - final ApiSearchService apiSearchService + final ApiSearchService apiSearchService, + final ParameterService parameterService ) { this.auditService = auditService; this.apiMetadataService = apiMetadataService; @@ -96,6 +102,7 @@ public ApiWorkflowStateServiceImpl( this.membershipService = membershipService; this.emailService = emailService; this.apiSearchService = apiSearchService; + this.parameterService = parameterService; } @Override @@ -164,14 +171,16 @@ private GenericApiEntity updateWorkflowReview( // Find all reviewers of the API and send them a notification email if (hook.equals(ApiHook.ASK_FOR_REVIEW)) { List reviewersEmail = findAllReviewersEmail(executionContext, genericApiEntity); - this.emailService.sendAsyncEmailNotification( - executionContext, - new EmailNotificationBuilder() - .params(new NotificationParamsBuilder().api(genericApiEntity).user(user).build()) - .to(reviewersEmail.toArray(new String[reviewersEmail.size()])) - .template(EmailNotificationBuilder.EmailTemplate.API_ASK_FOR_REVIEW) - .build() - ); + if (reviewersEmail.size() > 0) { + this.emailService.sendAsyncEmailNotification( + executionContext, + new EmailNotificationBuilder() + .params(new NotificationParamsBuilder().api(genericApiEntity).user(user).build()) + .to(reviewersEmail.toArray(new String[reviewersEmail.size()])) + .template(EmailNotificationBuilder.EmailTemplate.API_ASK_FOR_REVIEW) + .build() + ); + } } Map properties = new HashMap<>(); @@ -197,6 +206,8 @@ private GenericApiEntity updateWorkflowReview( private List findAllReviewersEmail(ExecutionContext executionContext, GenericApiEntity genericApiEntity) { final RolePermissionAction[] acls = { RolePermissionAction.UPDATE }; + final boolean isTrialInstance = parameterService.findAsBoolean(executionContext, Key.TRIAL_INSTANCE, ParameterReferenceType.SYSTEM); + final Predicate excludeIfTrialAndNotOptedIn = userEntity -> !isTrialInstance || userEntity.optedIn(); // find direct members of the API Set reviewerEmails = roleService @@ -211,6 +222,7 @@ private List findAllReviewersEmail(ExecutionContext executionContext, Ge .map(MembershipEntity::getMemberId) .distinct() .map(id -> this.userService.findById(executionContext, id)) + .filter(excludeIfTrialAndNotOptedIn) .map(UserEntity::getEmail) .filter(Objects::nonNull) .collect(toSet()); @@ -232,6 +244,7 @@ private List findAllReviewersEmail(ExecutionContext executionContext, Ge .map(MembershipEntity::getMemberId) .distinct() .map(id -> this.userService.findById(executionContext, id)) + .filter(excludeIfTrialAndNotOptedIn) .map(UserEntity::getEmail) .filter(Objects::nonNull) .collect(toSet()) diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/ApiWorkflowStateServiceImplTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/ApiWorkflowStateServiceImplTest.java index ee41911ed1b..55d960c3330 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/ApiWorkflowStateServiceImplTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/ApiWorkflowStateServiceImplTest.java @@ -16,26 +16,37 @@ package io.gravitee.rest.api.service.v4.impl; import static io.gravitee.rest.api.model.WorkflowType.REVIEW; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import io.gravitee.repository.management.model.Workflow; +import io.gravitee.rest.api.model.MembershipEntity; +import io.gravitee.rest.api.model.MembershipMemberType; +import io.gravitee.rest.api.model.MembershipReferenceType; import io.gravitee.rest.api.model.ReviewEntity; +import io.gravitee.rest.api.model.RoleEntity; import io.gravitee.rest.api.model.UserEntity; import io.gravitee.rest.api.model.WorkflowReferenceType; import io.gravitee.rest.api.model.WorkflowState; import io.gravitee.rest.api.model.api.ApiEntity; +import io.gravitee.rest.api.model.parameters.Key; +import io.gravitee.rest.api.model.parameters.ParameterReferenceType; +import io.gravitee.rest.api.model.permissions.ApiPermission; import io.gravitee.rest.api.model.v4.api.GenericApiEntity; import io.gravitee.rest.api.service.ApiMetadataService; import io.gravitee.rest.api.service.AuditService; +import io.gravitee.rest.api.service.EmailNotification; import io.gravitee.rest.api.service.EmailService; import io.gravitee.rest.api.service.MembershipService; import io.gravitee.rest.api.service.NotifierService; +import io.gravitee.rest.api.service.ParameterService; import io.gravitee.rest.api.service.RoleService; import io.gravitee.rest.api.service.UserService; import io.gravitee.rest.api.service.WorkflowService; @@ -43,10 +54,15 @@ import io.gravitee.rest.api.service.v4.ApiNotificationService; import io.gravitee.rest.api.service.v4.ApiSearchService; import io.gravitee.rest.api.service.v4.ApiWorkflowStateService; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import org.assertj.core.api.Assertions; import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.security.core.Authentication; @@ -92,6 +108,9 @@ public class ApiWorkflowStateServiceImplTest { @Mock private EmailService emailService; + @Mock + private ParameterService parameterService; + private ApiWorkflowStateService apiWorkflowStateService; @AfterClass @@ -122,7 +141,8 @@ public void setUp() { notifierService, membershipService, emailService, - apiSearchService + apiSearchService, + parameterService ); } @@ -156,6 +176,122 @@ public void shouldAskForReview() { verify(apiNotificationService, times(0)).triggerUpdateNotification(eq(GraviteeContext.getExecutionContext()), any(ApiEntity.class)); } + @Test + public void shouldAskForReviewAndSendMail() { + final GenericApiEntity genericApiEntity = new io.gravitee.rest.api.model.v4.api.ApiEntity(); + genericApiEntity.setId(API_ID); + + final ReviewEntity reviewEntity = new ReviewEntity(); + reviewEntity.setMessage("Test Review msg"); + + when(workflowService.create(WorkflowReferenceType.API, API_ID, REVIEW, USER_ID, WorkflowState.IN_REVIEW, reviewEntity.getMessage())) + .thenReturn(null); + when(apiSearchService.findGenericById(GraviteeContext.getExecutionContext(), API_ID)).thenReturn(genericApiEntity); + when(userService.findById(eq(GraviteeContext.getExecutionContext()), any())) + .thenReturn(UserEntity.builder().email("test@gio.gio").status("ACTIVE").password("password").build()); + when(roleService.findByScope(any(), any())).thenReturn(List.of(new RoleEntity())); + when(roleService.hasPermission(any(), eq(ApiPermission.REVIEWS), any())).thenReturn(true); + when(membershipService.getMembershipsByReferenceAndRole(eq(MembershipReferenceType.API), eq(API_ID), any())) + .thenReturn(Set.of(MembershipEntity.builder().id(USER_ID).memberType(MembershipMemberType.USER).build())); + when(parameterService.findAsBoolean(any(), eq(Key.TRIAL_INSTANCE), eq(ParameterReferenceType.SYSTEM))).thenReturn(false); + apiWorkflowStateService.askForReview(GraviteeContext.getExecutionContext(), API_ID, USER_ID, reviewEntity); + + verify(workflowService) + .create(WorkflowReferenceType.API, API_ID, REVIEW, USER_ID, WorkflowState.IN_REVIEW, reviewEntity.getMessage()); + verify(auditService) + .createApiAuditLog( + eq(GraviteeContext.getExecutionContext()), + argThat(apiId -> apiId.equals(API_ID)), + anyMap(), + argThat(evt -> Workflow.AuditEvent.API_REVIEW_ASKED.equals(evt)), + any(), + any(), + any() + ); + verify(apiNotificationService, times(0)).triggerUpdateNotification(eq(GraviteeContext.getExecutionContext()), any(ApiEntity.class)); + final ArgumentCaptor emailNotificationArgumentCaptor = ArgumentCaptor.forClass(EmailNotification.class); + verify(emailService) + .sendAsyncEmailNotification(eq(GraviteeContext.getExecutionContext()), emailNotificationArgumentCaptor.capture()); + assertThat(emailNotificationArgumentCaptor.getValue()) + .satisfies(emailNotification -> assertThat(emailNotification.getTo()).containsExactly("test@gio.gio")); + } + + @Test + public void shouldAskForReviewAndSendMailForOptedInUserInTrialInstance() { + final GenericApiEntity genericApiEntity = new io.gravitee.rest.api.model.v4.api.ApiEntity(); + genericApiEntity.setId(API_ID); + + final ReviewEntity reviewEntity = new ReviewEntity(); + reviewEntity.setMessage("Test Review msg"); + + when(workflowService.create(WorkflowReferenceType.API, API_ID, REVIEW, USER_ID, WorkflowState.IN_REVIEW, reviewEntity.getMessage())) + .thenReturn(null); + when(apiSearchService.findGenericById(GraviteeContext.getExecutionContext(), API_ID)).thenReturn(genericApiEntity); + when(userService.findById(eq(GraviteeContext.getExecutionContext()), any())) + .thenReturn(UserEntity.builder().email("test@gio.gio").status("ACTIVE").password("password").build()); + when(roleService.findByScope(any(), any())).thenReturn(List.of(new RoleEntity())); + when(roleService.hasPermission(any(), eq(ApiPermission.REVIEWS), any())).thenReturn(true); + when(membershipService.getMembershipsByReferenceAndRole(eq(MembershipReferenceType.API), eq(API_ID), any())) + .thenReturn(Set.of(MembershipEntity.builder().id(USER_ID).memberType(MembershipMemberType.USER).build())); + when(parameterService.findAsBoolean(any(), eq(Key.TRIAL_INSTANCE), eq(ParameterReferenceType.SYSTEM))).thenReturn(true); + apiWorkflowStateService.askForReview(GraviteeContext.getExecutionContext(), API_ID, USER_ID, reviewEntity); + + verify(workflowService) + .create(WorkflowReferenceType.API, API_ID, REVIEW, USER_ID, WorkflowState.IN_REVIEW, reviewEntity.getMessage()); + verify(auditService) + .createApiAuditLog( + eq(GraviteeContext.getExecutionContext()), + argThat(apiId -> apiId.equals(API_ID)), + anyMap(), + argThat(evt -> Workflow.AuditEvent.API_REVIEW_ASKED.equals(evt)), + any(), + any(), + any() + ); + verify(apiNotificationService, times(0)).triggerUpdateNotification(eq(GraviteeContext.getExecutionContext()), any(ApiEntity.class)); + final ArgumentCaptor emailNotificationArgumentCaptor = ArgumentCaptor.forClass(EmailNotification.class); + verify(emailService) + .sendAsyncEmailNotification(eq(GraviteeContext.getExecutionContext()), emailNotificationArgumentCaptor.capture()); + assertThat(emailNotificationArgumentCaptor.getValue()) + .satisfies(emailNotification -> assertThat(emailNotification.getTo()).containsExactly("test@gio.gio")); + } + + @Test + public void shouldAskForReviewAndNotSendMailForNonOptedInUserInTrialInstance() { + final GenericApiEntity genericApiEntity = new io.gravitee.rest.api.model.v4.api.ApiEntity(); + genericApiEntity.setId(API_ID); + + final ReviewEntity reviewEntity = new ReviewEntity(); + reviewEntity.setMessage("Test Review msg"); + + when(workflowService.create(WorkflowReferenceType.API, API_ID, REVIEW, USER_ID, WorkflowState.IN_REVIEW, reviewEntity.getMessage())) + .thenReturn(null); + when(apiSearchService.findGenericById(GraviteeContext.getExecutionContext(), API_ID)).thenReturn(genericApiEntity); + when(userService.findById(eq(GraviteeContext.getExecutionContext()), any())) + .thenReturn(UserEntity.builder().email("test@gio.gio").status("PENDING").password("password").build()); + when(roleService.findByScope(any(), any())).thenReturn(List.of(new RoleEntity())); + when(roleService.hasPermission(any(), eq(ApiPermission.REVIEWS), any())).thenReturn(true); + when(membershipService.getMembershipsByReferenceAndRole(eq(MembershipReferenceType.API), eq(API_ID), any())) + .thenReturn(Set.of(MembershipEntity.builder().id(USER_ID).memberType(MembershipMemberType.USER).build())); + when(parameterService.findAsBoolean(any(), eq(Key.TRIAL_INSTANCE), eq(ParameterReferenceType.SYSTEM))).thenReturn(true); + apiWorkflowStateService.askForReview(GraviteeContext.getExecutionContext(), API_ID, USER_ID, reviewEntity); + + verify(workflowService) + .create(WorkflowReferenceType.API, API_ID, REVIEW, USER_ID, WorkflowState.IN_REVIEW, reviewEntity.getMessage()); + verify(auditService) + .createApiAuditLog( + eq(GraviteeContext.getExecutionContext()), + argThat(apiId -> apiId.equals(API_ID)), + anyMap(), + argThat(evt -> Workflow.AuditEvent.API_REVIEW_ASKED.equals(evt)), + any(), + any(), + any() + ); + verify(apiNotificationService, times(0)).triggerUpdateNotification(eq(GraviteeContext.getExecutionContext()), any(ApiEntity.class)); + verify(emailService, never()).sendAsyncEmailNotification(eq(GraviteeContext.getExecutionContext()), any()); + } + @Test public void shouldAskForReviewWithNoMessage() { final GenericApiEntity genericApiEntity = new io.gravitee.rest.api.model.v4.api.ApiEntity();