Merge pull request #4980 from appsmithorg/feature/send-email-on-comment

Feature/send email on comment
This commit is contained in:
Shrikant Sharat Kandula 2021-06-10 15:20:34 +05:30 committed by GitHub
commit 8db64e6355
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1415 additions and 42 deletions

View File

@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
@ -41,16 +42,16 @@ public class CommentController extends BaseController<CommentService, Comment, S
@RequestParam String threadId,
ServerWebExchange exchange) {
log.debug("Going to create resource {}", resource.getClass().getName());
return service.create(threadId, resource)
return service.create(threadId, resource, exchange.getRequest().getHeaders().getOrigin())
.map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null));
}
@PostMapping("/threads")
@ResponseStatus(HttpStatus.CREATED)
public Mono<ResponseDTO<CommentThread>> createThread(@Valid @RequestBody CommentThread resource,
ServerWebExchange exchange) {
@RequestHeader(name = "Origin") String originHeader) {
log.debug("Going to create resource {}", resource.getClass().getName());
return service.createThread(resource)
return service.createThread(resource, originHeader)
.map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null));
}
@ -63,10 +64,10 @@ public class CommentController extends BaseController<CommentService, Comment, S
@PutMapping("/threads/{threadId}")
public Mono<ResponseDTO<CommentThread>> updateThread(
@Valid @RequestBody CommentThread resource,
@PathVariable String threadId
@PathVariable String threadId, ServerWebExchange exchange
) {
log.debug("Going to update resource {}", resource.getClass().getName());
return service.updateThread(threadId, resource)
return service.updateThread(threadId, resource, exchange.getRequest().getHeaders().getOrigin())
.map(updated -> new ResponseDTO<>(HttpStatus.ACCEPTED.value(), updated, null));
}

View File

@ -0,0 +1,13 @@
package com.appsmith.server.events;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Organization;
import lombok.Data;
@Data
public abstract class AbstractCommentEvent {
private final String authorUserName;
private final Organization organization;
private final Application application;
private final String originHeader;
}

View File

@ -0,0 +1,16 @@
package com.appsmith.server.events;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Comment;
import com.appsmith.server.domains.Organization;
import lombok.Getter;
@Getter
public class CommentAddedEvent extends AbstractCommentEvent {
private final Comment comment;
public CommentAddedEvent(String authorUserName, Organization organization, Application application, String originHeader, Comment comment) {
super(authorUserName, organization, application, originHeader);
this.comment = comment;
}
}

View File

@ -0,0 +1,16 @@
package com.appsmith.server.events;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.CommentThread;
import com.appsmith.server.domains.Organization;
import lombok.Getter;
@Getter
public class CommentThreadClosedEvent extends AbstractCommentEvent {
private final CommentThread commentThread;
public CommentThreadClosedEvent(String authorUserName, Organization organization, Application application, String originHeader, CommentThread commentThread) {
super(authorUserName, organization, application, originHeader);
this.commentThread = commentThread;
}
}

View File

@ -2,10 +2,8 @@ package com.appsmith.server.events;
import com.appsmith.server.domains.User;
import lombok.Data;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor
public class UserChangedEvent {
private final User user;

View File

@ -0,0 +1,43 @@
package com.appsmith.server.helpers;
import com.appsmith.server.domains.Comment;
import java.util.ArrayList;
import java.util.List;
public class CommentUtils {
/**
* Checks whether provided user has been mentioned in the comment. Returns true if yes and false otherwise.
* @param comment Comment objects
* @param userEmail email address of the user
* @return true or false based on the condition
*/
public static boolean isUserMentioned(Comment comment, String userEmail) {
if(comment.getBody() != null && comment.getBody().getEntityMap() != null) {
for(String key : comment.getBody().getEntityMap().keySet()) {
Comment.Entity commentEntity = comment.getBody().getEntityMap().get(key);
if(commentEntity != null && commentEntity.getType() != null
&& commentEntity.getType().equals("mention")) {
// this comment has a mention, check the provided user is mentioned or not
if(commentEntity.getData() != null) {
Comment.EntityData.Mention mention = commentEntity.getData().getMention();
if(mention.getUser().getUsername().equals(userEmail)) {
return true;
}
}
}
}
}
return false;
}
public static List<String> getCommentBody(Comment comment) {
List<String> commentLines = new ArrayList<>();
if(comment.getBody() != null && comment.getBody().getBlocks() != null) {
for (Comment.Block block : comment.getBody().getBlocks()) {
commentLines.add(block.getText());
}
}
return commentLines;
}
}

View File

@ -43,7 +43,7 @@ public class EmailSender {
REPLY_TO = makeReplyTo();
}
public Mono<Boolean> sendMail(String to, String subject, String text, Map<String, String> params) {
public Mono<Boolean> sendMail(String to, String subject, String text, Map<String, ? extends Object> params) {
/**
* Creating a publisher which sends email in a blocking fashion, subscribing on the bounded elastic
@ -122,7 +122,7 @@ public class EmailSender {
* @return Template string with Mustache replacements applied.
* @throws IOException bubbled from Mustache renderer.
*/
private String replaceEmailTemplate(String template, Map<String, String> params) throws IOException {
private String replaceEmailTemplate(String template, Map<String, ? extends Object> params) throws IOException {
MustacheFactory mf = new DefaultMustacheFactory();
StringWriter stringWriter = new StringWriter();
Mustache mustache = mf.compile(template);

View File

@ -8,11 +8,11 @@ import java.util.List;
public interface CommentService extends CrudService<Comment, String> {
Mono<Comment> create(String threadId, Comment organization);
Mono<Comment> create(String threadId, Comment organization, String originHeader);
Mono<CommentThread> createThread(CommentThread commentThread);
Mono<CommentThread> createThread(CommentThread commentThread, String originHeader);
Mono<CommentThread> updateThread(String threadId, CommentThread commentThread);
Mono<CommentThread> updateThread(String threadId, CommentThread commentThread, String originHeader);
Mono<List<CommentThread>> getThreadsByApplicationId(String applicationId);

View File

@ -11,11 +11,14 @@ import com.appsmith.server.domains.CommentThread;
import com.appsmith.server.domains.CommentThreadNotification;
import com.appsmith.server.domains.Notification;
import com.appsmith.server.domains.User;
import com.appsmith.server.events.CommentAddedEvent;
import com.appsmith.server.events.CommentThreadClosedEvent;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.PolicyUtils;
import com.appsmith.server.repositories.CommentRepository;
import com.appsmith.server.repositories.CommentThreadRepository;
import com.appsmith.server.solutions.EmailEventHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
@ -52,6 +55,7 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
private final PolicyGenerator policyGenerator;
private final PolicyUtils policyUtils;
private final EmailEventHandler emailEventHandler;
public CommentServiceImpl(
Scheduler scheduler,
@ -66,8 +70,8 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
ApplicationService applicationService,
NotificationService notificationService,
PolicyGenerator policyGenerator,
PolicyUtils policyUtils
) {
PolicyUtils policyUtils,
EmailEventHandler emailEventHandler) {
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
this.threadRepository = threadRepository;
this.userService = userService;
@ -76,14 +80,15 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
this.notificationService = notificationService;
this.policyGenerator = policyGenerator;
this.policyUtils = policyUtils;
this.emailEventHandler = emailEventHandler;
}
@Override
public Mono<Comment> create(String threadId, Comment comment) {
return create(threadId, comment, true);
public Mono<Comment> create(String threadId, Comment comment, String originHeader) {
return create(threadId, comment, originHeader, true);
}
public Mono<Comment> create(String threadId, Comment comment, boolean shouldCreateNotification) {
private Mono<Comment> create(String threadId, Comment comment, String originHeader, boolean shouldCreateNotification) {
if (StringUtils.isWhitespace(comment.getAuthorName())) {
// Error: User can't explicitly set the author name. It will be the currently logged in user.
return Mono.empty();
@ -125,36 +130,40 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
comment.setAuthorName(authorName);
return Mono.zip(
Mono.just(user),
Mono.just(thread),
repository.save(comment)
);
})
.flatMap(tuple -> {
final User user = tuple.getT1();
final Comment savedComment = tuple.getT2();
CommentThread commentThread = tuple.getT2();
final Comment savedComment = tuple.getT3();
Mono<Boolean> publishEmailMono = emailEventHandler.publish(
comment.getAuthorUsername(), commentThread.getApplicationId(), comment, originHeader
);
if (shouldCreateNotification) {
final Set<String> usernames = policyUtils.findUsernamesWithPermission(
savedComment.getPolicies(), AclPermission.READ_COMMENT);
List<Mono<Notification>> monos = new ArrayList<>();
List<Mono<Notification>> notificationMonos = new ArrayList<>();
for (String username : usernames) {
if (!username.equals(user.getUsername())) {
final CommentNotification notification = new CommentNotification();
notification.setComment(savedComment);
notification.setForUsername(username);
monos.add(notificationService.create(notification));
Mono<Notification> notificationMono = notificationService.create(notification);
notificationMonos.add(notificationMono);
}
}
return Flux.concat(monos).then(Mono.just(savedComment));
return Flux.concat(notificationMonos).then(publishEmailMono).thenReturn(savedComment);
} else {
return Mono.just(savedComment);
return publishEmailMono.thenReturn(savedComment);
}
});
}
@Override
public Mono<CommentThread> createThread(CommentThread commentThread) {
public Mono<CommentThread> createThread(CommentThread commentThread, String originHeader) {
// 1. Check if this user has permission on the application given by `commentThread.applicationId`.
// 2. Save the comment thread and get it's id. This is the `threadId`.
// 3. Pull the comment out of the list of comments, set it's `threadId` and save it separately.
@ -211,7 +220,7 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
boolean isFirst = true;
for (final Comment comment : thread.getComments()) {
comment.setId(null);
commentSaverMonos.add(create(thread.getId(), comment, !isFirst));
commentSaverMonos.add(create(thread.getId(), comment, originHeader, !isFirst));
isFirst = false;
}
}
@ -241,7 +250,6 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
monos.add(notificationService.create(notification));
}
}
return Flux.concat(monos).then(Mono.just(commentThread));
});
}
@ -253,7 +261,7 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
}
@Override
public Mono<CommentThread> updateThread(String threadId, CommentThread commentThread) {
public Mono<CommentThread> updateThread(String threadId, CommentThread commentThread, String originHeader) {
return Mono.zip(
sessionUserService.getCurrentUser(),
// Resolving, pinning and marking as read don't need manage permission on the thread.
@ -301,7 +309,14 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
.updateById(threadId, commentThread, AclPermission.READ_THREAD)
.flatMap(updatedThread -> {
updatedThread.setIsViewed(true);
return Mono.just(updatedThread);
// send email if comment thread is resolved
CommentThread.CommentThreadState resolvedState = commentThread.getResolvedState();
if(resolvedState != null && resolvedState.getActive()) {
return emailEventHandler.publish(user.getUsername(), updatedThread.getApplicationId(),
updatedThread, originHeader).thenReturn(updatedThread);
} else {
return Mono.just(updatedThread);
}
});
});
}

View File

@ -1,6 +1,12 @@
package com.appsmith.server.services;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Comment;
import com.appsmith.server.domains.Notification;
import com.appsmith.server.domains.Organization;
import reactor.core.publisher.Mono;
import java.util.List;
public interface NotificationService extends CrudService<Notification, String> {

View File

@ -29,8 +29,7 @@ public class NotificationServiceImpl
ReactiveMongoTemplate reactiveMongoTemplate,
NotificationRepository repository,
AnalyticsService analyticsService,
SessionUserService sessionUserService
) {
SessionUserService sessionUserService) {
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
this.sessionUserService = sessionUserService;
}
@ -57,5 +56,4 @@ public class NotificationServiceImpl
return sessionUserService.getCurrentUser()
.flatMapMany(user -> repository.findByForUsername(user.getUsername()));
}
}

View File

@ -48,7 +48,8 @@ import static com.appsmith.server.acl.AclPermission.USER_MANAGE_ORGANIZATIONS;
@Slf4j
@Service
public class OrganizationServiceImpl extends BaseService<OrganizationRepository, Organization, String> implements OrganizationService {
public class OrganizationServiceImpl extends BaseService<OrganizationRepository, Organization, String>
implements OrganizationService {
private final PluginRepository pluginRepository;
private final SessionUserService sessionUserService;

View File

@ -0,0 +1,152 @@
package com.appsmith.server.solutions;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Comment;
import com.appsmith.server.domains.CommentThread;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.UserRole;
import com.appsmith.server.events.CommentAddedEvent;
import com.appsmith.server.events.CommentThreadClosedEvent;
import com.appsmith.server.helpers.CommentUtils;
import com.appsmith.server.notifications.EmailSender;
import com.appsmith.server.repositories.ApplicationRepository;
import com.appsmith.server.repositories.OrganizationRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
@RequiredArgsConstructor
@Slf4j
public class EmailEventHandler {
private static final String COMMENT_ADDED_EMAIL_TEMPLATE = "email/commentAddedTemplate.html";
private static final String USER_MENTIONED_EMAIL_TEMPLATE = "email/userTaggedInCommentTemplate.html";
private static final String THREAD_RESOLVED_EMAIL_TEMPLATE = "email/commentResolvedTemplate.html";
private final ApplicationEventPublisher applicationEventPublisher;
private final EmailSender emailSender;
private final OrganizationRepository organizationRepository;
private final ApplicationRepository applicationRepository;
public Mono<Boolean> publish(String authorUserName, String applicationId, Comment comment, String originHeader) {
return applicationRepository.findById(applicationId).flatMap(application -> {
return organizationRepository.findById(application.getOrganizationId()).flatMap(organization -> {
applicationEventPublisher.publishEvent(
new CommentAddedEvent(authorUserName, organization, application, originHeader, comment)
);
return Mono.just(organization);
});
}).thenReturn(Boolean.TRUE);
}
public Mono<Boolean> publish(String authorUserName, String applicationId, CommentThread thread, String originHeader) {
return applicationRepository.findById(applicationId).flatMap(application -> {
return organizationRepository.findById(application.getOrganizationId()).flatMap(organization -> {
applicationEventPublisher.publishEvent(
new CommentThreadClosedEvent(authorUserName, organization, application, originHeader, thread)
);
return Mono.just(organization);
});
}).thenReturn(Boolean.TRUE);
}
@Async
@EventListener
public void handle(CommentAddedEvent event) {
this.sendEmailForComment(
event.getAuthorUserName(), event.getApplication(), event.getOrganization(),
event.getComment(), event.getOriginHeader()
).subscribeOn(Schedulers.elastic())
.subscribe();
}
@Async
@EventListener
public void handle(CommentThreadClosedEvent event) {
this.sendEmailForComment(
event.getAuthorUserName(), event.getApplication(), event.getOrganization(),
event.getCommentThread(), event.getOriginHeader()
)
.subscribeOn(Schedulers.elastic())
.subscribe();
}
private Mono<Boolean> getEmailSenderMono(UserRole receiverUserRole, CommentThread commentThread,
String originHeader, Application application, Organization organization) {
String receiverName = StringUtils.isEmpty(receiverUserRole.getName()) ? "User" : receiverUserRole.getName();
String receiverEmail = receiverUserRole.getUsername();
CommentThread.CommentThreadState resolvedState = commentThread.getResolvedState();
Map<String, Object> templateParams = new HashMap<>();
templateParams.put("App_User_Name", receiverName);
templateParams.put("Commenter_Name", resolvedState.getAuthorName());
templateParams.put("Application_Name", application.getName());
templateParams.put("Organization_Name", organization.getName());
templateParams.put("inviteUrl", originHeader);
String emailSubject = String.format(
"%s has resolved comment in %s", resolvedState.getAuthorName(), application.getName()
);
return emailSender.sendMail(receiverEmail, emailSubject, THREAD_RESOLVED_EMAIL_TEMPLATE, templateParams);
}
private Mono<Boolean> getEmailSenderMono(UserRole receiverUserRole, Comment comment, String originHeader,
Application application, Organization organization) {
String receiverName = StringUtils.isEmpty(receiverUserRole.getName()) ? "User" : receiverUserRole.getName();
String receiverEmail = receiverUserRole.getUsername();
Map<String, Object> templateParams = new HashMap<>();
templateParams.put("App_User_Name", receiverName);
templateParams.put("Commenter_Name", comment.getAuthorName());
templateParams.put("Application_Name", application.getName());
templateParams.put("Organization_Name", organization.getName());
templateParams.put("Comment_Body", CommentUtils.getCommentBody(comment));
templateParams.put("inviteUrl", originHeader);
String emailTemplate = COMMENT_ADDED_EMAIL_TEMPLATE;
String emailSubject = String.format(
"New comment from %s in %s", comment.getAuthorName(), application.getName()
);
// check if user has been mentioned in the comment
if(CommentUtils.isUserMentioned(comment, receiverEmail)) {
emailTemplate = USER_MENTIONED_EMAIL_TEMPLATE;
emailSubject = String.format("New comment for you from %s", comment.getAuthorName());
}
return emailSender.sendMail(receiverEmail, emailSubject, emailTemplate, templateParams);
}
private <E> Mono<Boolean> sendEmailForComment(String authorUserName, Application application, Organization organization,
E commentDomain, String originHeader) {
List<Mono<Boolean>> emailMonos = new ArrayList<>();
for (UserRole userRole : organization.getUserRoles()) {
if(!authorUserName.equals(userRole.getUsername())) {
if(commentDomain instanceof Comment) {
Comment comment = (Comment)commentDomain;
emailMonos.add(
getEmailSenderMono(userRole, comment, originHeader, application, organization)
);
} else if(commentDomain instanceof CommentThread) {
CommentThread commentThread = (CommentThread) commentDomain;
emailMonos.add(
getEmailSenderMono(userRole, commentThread, originHeader, application, organization)
);
}
}
}
return Flux.concat(emailMonos).then(Mono.just(Boolean.TRUE));
}
}

View File

@ -0,0 +1,278 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html data-editor-version="2" class="sg-campaigns" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<!--<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<style type="text/css">
body {
width: 600px;
margin: 0 auto;
}
table {
border-collapse: collapse;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
</style>
<![endif]-->
<style type="text/css">
body, p, div {
font-family: arial, helvetica, sans-serif;
font-size: 14px;
}
body {
color: #000000;
}
body a {
color: #1188E6;
text-decoration: none;
}
p {
margin: 0;
padding: 0;
}
table.wrapper {
width: 100% !important;
table-layout: fixed;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
img.max-width {
max-width: 100% !important;
}
.column.of-2 {
width: 50%;
}
.column.of-3 {
width: 33.333%;
}
.column.of-4 {
width: 25%;
}
@media screen and (max-width: 480px) {
.preheader .rightColumnContent,
.footer .rightColumnContent {
text-align: left !important;
}
.preheader .rightColumnContent div,
.preheader .rightColumnContent span,
.footer .rightColumnContent div,
.footer .rightColumnContent span {
text-align: left !important;
}
.preheader .rightColumnContent,
.preheader .leftColumnContent {
font-size: 80% !important;
padding: 5px 0;
}
table.wrapper-mobile {
width: 100% !important;
table-layout: fixed;
}
img.max-width {
height: auto !important;
max-width: 100% !important;
}
a.bulletproof-button {
display: block !important;
width: auto !important;
font-size: 80%;
padding-left: 0 !important;
padding-right: 0 !important;
}
.columns {
width: 100% !important;
}
.column {
display: block !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
}
</style>
<!--user entered Head Start--><!--End Head user entered-->
</head>
<body>
<center class="wrapper" data-link-color="#1188E6"
data-body-style="font-size:14px; font-family:arial,helvetica,sans-serif; color:#000000; background-color:#FFFFFF;">
<div class="webkit">
<table cellpadding="0" cellspacing="0" border="0" width="100%" class="wrapper" bgcolor="#FFFFFF">
<tbody>
<tr>
<td valign="top" bgcolor="#FFFFFF" width="100%">
<table width="100%" role="content-container" class="outer" align="center" cellpadding="0"
cellspacing="0" border="0">
<tbody>
<tr>
<td width="100%">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td>
<!--[if mso]>
<center>
<table>
<tr>
<td width="600">
<![endif]-->
<table width="100%" cellpadding="0" cellspacing="0" border="0"
style="width:100%; max-width:600px;" align="center">
<tbody>
<tr>
<td role="modules-container"
style="padding:0px 0px 0px 0px; color:#000000; text-align:left;"
bgcolor="#ffffff" width="100%" align="left">
<table class="module preheader preheader-hide" role="module"
data-type="preheader" border="0" cellpadding="0"
cellspacing="0" width="100%"
style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;">
<tbody>
<tr>
<td role="module-content">
<p></p>
</td>
</tr>
</tbody>
</table>
<table class="wrapper" role="module" data-type="image"
border="0" cellpadding="0" cellspacing="0" width="100%"
style="table-layout: fixed;">
<tbody>
<tr>
<td style="font-size:6px; line-height:10px; padding:0px 0px 0px 0px;"
valign="top" align="center">
<a href="https://www.appsmith.com/"><img
class="max-width" border="0"
style="display:block; color:#000000; text-decoration:none; font-family:Helvetica, arial, sans-serif; font-size:16px; max-width:25% !important; width:25%; height:auto !important;"
width="150" alt=""
data-proportionally-constrained="true"
data-responsive="true"
src="http://cdn.mcauto-images-production.sendgrid.net/4bbae2fffe647858/b21738f2-3a49-4774-aae9-c8e80ad9c26e/924x284.png"></a>
</td>
</tr>
</tbody>
</table>
<table class="module" role="module" data-type="text" border="0"
cellpadding="0" cellspacing="0" width="100%"
style="table-layout: fixed;">
<tbody>
<tr>
<td style="padding:0px 0px 18px 0px; line-height:22px; text-align:inherit; background-color:#ffffff;"
height="100%" valign="top" bgcolor="#ffffff"
role="module-content">
<div>
<div style="font-family: inherit; text-align: center">
<br></div>
<div style="margin-left: 0px">
<span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">Hi {{App_User_Name}},</span>
</div>
<div style="margin-left: 0px; margin-top: 10px">
<span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">{{Commenter_Name}} just added a new comment in {{Application_Name}} in the {{Organization_Name}} organization.</span>
</div>
<div style="margin-left: 0px; margin-top: 10px;padding:10px;background-color:#EEE">
{{#Comment_Body}}
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">{{ . }}</div>
{{/Comment_Body}}
</div>
<div style="margin-left: 0px; margin-top: 20px;">
<span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">Please follow this link to view and respond to the comment. </span>
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table border="0" cellpadding="0" cellspacing="0" style="table-layout:fixed;" width="100%">
<tbody>
<tr>
<td align="center" bgcolor="" class="outer-td"
style="padding:0px 0px 0px 0px;">
<table border="0" cellpadding="0" cellspacing="0"
class="wrapper-mobile"
style="text-align:center;">
<tbody>
<tr>
<td align="center" bgcolor="#ff6d2d"
class="inner-td"
style="border-radius:6px; font-size:16px; text-align:center; background-color:inherit;">
<a href="{{inviteUrl}}"
style="background-color:#ff6d2d; border:1px solid #ff6d2d; border-color:#ff6d2d; border-radius:6px; border-width:1px; color:#ffffff; display:inline-block; font-weight:400; letter-spacing:0px; line-height:6px; padding:12px 18px 12px 18px; text-align:center; text-decoration:none; border-style:solid; font-family:tahoma,geneva,sans-serif; font-size:16px;"
target="_blank">Go to Comment</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso]>
</td>
</tr>
</table>
</center>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
</center>
</body>
</html>

View File

@ -0,0 +1,274 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html data-editor-version="2" class="sg-campaigns" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<!--<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<style type="text/css">
body {
width: 600px;
margin: 0 auto;
}
table {
border-collapse: collapse;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
</style>
<![endif]-->
<style type="text/css">
body, p, div {
font-family: arial, helvetica, sans-serif;
font-size: 14px;
}
body {
color: #000000;
}
body a {
color: #1188E6;
text-decoration: none;
}
p {
margin: 0;
padding: 0;
}
table.wrapper {
width: 100% !important;
table-layout: fixed;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
img.max-width {
max-width: 100% !important;
}
.column.of-2 {
width: 50%;
}
.column.of-3 {
width: 33.333%;
}
.column.of-4 {
width: 25%;
}
@media screen and (max-width: 480px) {
.preheader .rightColumnContent,
.footer .rightColumnContent {
text-align: left !important;
}
.preheader .rightColumnContent div,
.preheader .rightColumnContent span,
.footer .rightColumnContent div,
.footer .rightColumnContent span {
text-align: left !important;
}
.preheader .rightColumnContent,
.preheader .leftColumnContent {
font-size: 80% !important;
padding: 5px 0;
}
table.wrapper-mobile {
width: 100% !important;
table-layout: fixed;
}
img.max-width {
height: auto !important;
max-width: 100% !important;
}
a.bulletproof-button {
display: block !important;
width: auto !important;
font-size: 80%;
padding-left: 0 !important;
padding-right: 0 !important;
}
.columns {
width: 100% !important;
}
.column {
display: block !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
}
</style>
<!--user entered Head Start--><!--End Head user entered-->
</head>
<body>
<center class="wrapper" data-link-color="#1188E6"
data-body-style="font-size:14px; font-family:arial,helvetica,sans-serif; color:#000000; background-color:#FFFFFF;">
<div class="webkit">
<table cellpadding="0" cellspacing="0" border="0" width="100%" class="wrapper" bgcolor="#FFFFFF">
<tbody>
<tr>
<td valign="top" bgcolor="#FFFFFF" width="100%">
<table width="100%" role="content-container" class="outer" align="center" cellpadding="0"
cellspacing="0" border="0">
<tbody>
<tr>
<td width="100%">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td>
<!--[if mso]>
<center>
<table>
<tr>
<td width="600">
<![endif]-->
<table width="100%" cellpadding="0" cellspacing="0" border="0"
style="width:100%; max-width:600px;" align="center">
<tbody>
<tr>
<td role="modules-container"
style="padding:0px 0px 0px 0px; color:#000000; text-align:left;"
bgcolor="#ffffff" width="100%" align="left">
<table class="module preheader preheader-hide" role="module"
data-type="preheader" border="0" cellpadding="0"
cellspacing="0" width="100%"
style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;">
<tbody>
<tr>
<td role="module-content">
<p></p>
</td>
</tr>
</tbody>
</table>
<table class="wrapper" role="module" data-type="image"
border="0" cellpadding="0" cellspacing="0" width="100%"
style="table-layout: fixed;">
<tbody>
<tr>
<td style="font-size:6px; line-height:10px; padding:0px 0px 0px 0px;"
valign="top" align="center">
<a href="https://www.appsmith.com/"><img
class="max-width" border="0"
style="display:block; color:#000000; text-decoration:none; font-family:Helvetica, arial, sans-serif; font-size:16px; max-width:25% !important; width:25%; height:auto !important;"
width="150" alt=""
data-proportionally-constrained="true"
data-responsive="true"
src="http://cdn.mcauto-images-production.sendgrid.net/4bbae2fffe647858/b21738f2-3a49-4774-aae9-c8e80ad9c26e/924x284.png"></a>
</td>
</tr>
</tbody>
</table>
<table class="module" role="module" data-type="text" border="0"
cellpadding="0" cellspacing="0" width="100%"
style="table-layout: fixed;">
<tbody>
<tr>
<td style="padding:0px 0px 18px 0px; line-height:22px; text-align:inherit; background-color:#ffffff;"
height="100%" valign="top" bgcolor="#ffffff"
role="module-content">
<div>
<div style="font-family: inherit; text-align: center">
<br></div>
<div style="margin-left: 0px">
<span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">Hi {{App_User_Name}},</span>
</div>
<div style="margin-left: 0px; margin-top: 10px">
<span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">{{Commenter_Name}} has resolved the comment in {{Application_Name}} in the {{Organization_Name}} organization.</span>
</div>
<div style="margin-left: 0px; margin-top: 20px;">
<span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">Please follow this link to view and re-open the comment. </span>
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table border="0" cellpadding="0" cellspacing="0" style="table-layout:fixed;" width="100%">
<tbody>
<tr>
<td align="center" bgcolor="" class="outer-td"
style="padding:0px 0px 0px 0px;">
<table border="0" cellpadding="0" cellspacing="0"
class="wrapper-mobile"
style="text-align:center;">
<tbody>
<tr>
<td align="center" bgcolor="#ff6d2d"
class="inner-td"
style="border-radius:6px; font-size:16px; text-align:center; background-color:inherit;">
<a href="{{inviteUrl}}"
style="background-color:#ff6d2d; border:1px solid #ff6d2d; border-color:#ff6d2d; border-radius:6px; border-width:1px; color:#ffffff; display:inline-block; font-weight:400; letter-spacing:0px; line-height:6px; padding:12px 18px 12px 18px; text-align:center; text-decoration:none; border-style:solid; font-family:tahoma,geneva,sans-serif; font-size:16px;"
target="_blank">Go to Comment</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso]>
</td>
</tr>
</table>
</center>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
</center>
</body>
</html>

View File

@ -0,0 +1,278 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html data-editor-version="2" class="sg-campaigns" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<!--<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<style type="text/css">
body {
width: 600px;
margin: 0 auto;
}
table {
border-collapse: collapse;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
</style>
<![endif]-->
<style type="text/css">
body, p, div {
font-family: arial, helvetica, sans-serif;
font-size: 14px;
}
body {
color: #000000;
}
body a {
color: #1188E6;
text-decoration: none;
}
p {
margin: 0;
padding: 0;
}
table.wrapper {
width: 100% !important;
table-layout: fixed;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
img.max-width {
max-width: 100% !important;
}
.column.of-2 {
width: 50%;
}
.column.of-3 {
width: 33.333%;
}
.column.of-4 {
width: 25%;
}
@media screen and (max-width: 480px) {
.preheader .rightColumnContent,
.footer .rightColumnContent {
text-align: left !important;
}
.preheader .rightColumnContent div,
.preheader .rightColumnContent span,
.footer .rightColumnContent div,
.footer .rightColumnContent span {
text-align: left !important;
}
.preheader .rightColumnContent,
.preheader .leftColumnContent {
font-size: 80% !important;
padding: 5px 0;
}
table.wrapper-mobile {
width: 100% !important;
table-layout: fixed;
}
img.max-width {
height: auto !important;
max-width: 100% !important;
}
a.bulletproof-button {
display: block !important;
width: auto !important;
font-size: 80%;
padding-left: 0 !important;
padding-right: 0 !important;
}
.columns {
width: 100% !important;
}
.column {
display: block !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
}
</style>
<!--user entered Head Start--><!--End Head user entered-->
</head>
<body>
<center class="wrapper" data-link-color="#1188E6"
data-body-style="font-size:14px; font-family:arial,helvetica,sans-serif; color:#000000; background-color:#FFFFFF;">
<div class="webkit">
<table cellpadding="0" cellspacing="0" border="0" width="100%" class="wrapper" bgcolor="#FFFFFF">
<tbody>
<tr>
<td valign="top" bgcolor="#FFFFFF" width="100%">
<table width="100%" role="content-container" class="outer" align="center" cellpadding="0"
cellspacing="0" border="0">
<tbody>
<tr>
<td width="100%">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td>
<!--[if mso]>
<center>
<table>
<tr>
<td width="600">
<![endif]-->
<table width="100%" cellpadding="0" cellspacing="0" border="0"
style="width:100%; max-width:600px;" align="center">
<tbody>
<tr>
<td role="modules-container"
style="padding:0px 0px 0px 0px; color:#000000; text-align:left;"
bgcolor="#ffffff" width="100%" align="left">
<table class="module preheader preheader-hide" role="module"
data-type="preheader" border="0" cellpadding="0"
cellspacing="0" width="100%"
style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;">
<tbody>
<tr>
<td role="module-content">
<p></p>
</td>
</tr>
</tbody>
</table>
<table class="wrapper" role="module" data-type="image"
border="0" cellpadding="0" cellspacing="0" width="100%"
style="table-layout: fixed;">
<tbody>
<tr>
<td style="font-size:6px; line-height:10px; padding:0px 0px 0px 0px;"
valign="top" align="center">
<a href="https://www.appsmith.com/"><img
class="max-width" border="0"
style="display:block; color:#000000; text-decoration:none; font-family:Helvetica, arial, sans-serif; font-size:16px; max-width:25% !important; width:25%; height:auto !important;"
width="150" alt=""
data-proportionally-constrained="true"
data-responsive="true"
src="http://cdn.mcauto-images-production.sendgrid.net/4bbae2fffe647858/b21738f2-3a49-4774-aae9-c8e80ad9c26e/924x284.png"></a>
</td>
</tr>
</tbody>
</table>
<table class="module" role="module" data-type="text" border="0"
cellpadding="0" cellspacing="0" width="100%"
style="table-layout: fixed;">
<tbody>
<tr>
<td style="padding:0px 0px 18px 0px; line-height:22px; text-align:inherit; background-color:#ffffff;"
height="100%" valign="top" bgcolor="#ffffff"
role="module-content">
<div>
<div style="font-family: inherit; text-align: center">
<br></div>
<div style="margin-left: 0px">
<span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">Hi {{App_User_Name}},</span>
</div>
<div style="margin-left: 0px; margin-top: 10px">
<span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">{{Commenter_Name}} tagged you in a comment in {{Application_Name}} in the {{Organization_Name}} organization.</span>
</div>
<div style="margin-left: 0px; margin-top: 10px;padding:10px;background-color:#EEE">
{{#Comment_Body}}
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">{{ . }}</div>
{{/Comment_Body}}
</div>
<div style="margin-left: 0px; margin-top: 20px;">
<span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">Please follow this link to view and respond to the comment. </span>
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table border="0" cellpadding="0" cellspacing="0" style="table-layout:fixed;" width="100%">
<tbody>
<tr>
<td align="center" bgcolor="" class="outer-td"
style="padding:0px 0px 0px 0px;">
<table border="0" cellpadding="0" cellspacing="0"
class="wrapper-mobile"
style="text-align:center;">
<tbody>
<tr>
<td align="center" bgcolor="#ff6d2d"
class="inner-td"
style="border-radius:6px; font-size:16px; text-align:center; background-color:inherit;">
<a href="{{inviteUrl}}"
style="background-color:#ff6d2d; border:1px solid #ff6d2d; border-color:#ff6d2d; border-radius:6px; border-width:1px; color:#ffffff; display:inline-block; font-weight:400; letter-spacing:0px; line-height:6px; padding:12px 18px 12px 18px; text-align:center; text-decoration:none; border-style:solid; font-family:tahoma,geneva,sans-serif; font-size:16px;"
target="_blank">Go to Comment</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso]>
</td>
</tr>
</table>
</center>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
</center>
</body>
</html>

View File

@ -0,0 +1,91 @@
package com.appsmith.server.helpers;
import com.appsmith.server.domains.Comment;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class CommentUtilsTest {
@Test
void getCommentBody_WhenBodyIsNull_ReturnsEmptyList() {
Comment comment = new Comment();
Assert.assertEquals(0, CommentUtils.getCommentBody(comment).size());
}
@Test
void getCommentBody_WhenBodyHasMultipleBlocks_ReturnsValidBodies() {
Comment.Block commentBlock1 = new Comment.Block();
commentBlock1.setText("First line");
Comment.Block commentBlock2 = new Comment.Block();
commentBlock1.setText("Second line");
Comment.Body commentBody = new Comment.Body();
commentBody.setBlocks(List.of(commentBlock1, commentBlock2));
Comment comment = new Comment();
comment.setBody(commentBody);
Assert.assertEquals(2, CommentUtils.getCommentBody(comment).size());
Assert.assertEquals(commentBlock1.getText(), CommentUtils.getCommentBody(comment).get(0));
Assert.assertEquals(commentBlock2.getText(), CommentUtils.getCommentBody(comment).get(1));
}
@Test
public void isUserMentioned_WhenBodyIsNull_ReturnsFalse() {
Comment comment = new Comment();
Assert.assertFalse(CommentUtils.isUserMentioned(comment, "user@abc.com"));
}
@Test
public void isUserMentioned_WhenBodyHasNoMention_ReturnsFalse() {
Comment.Body body = new Comment.Body();
Comment comment = new Comment();
comment.setBody(body);
Assert.assertFalse(CommentUtils.isUserMentioned(comment, "user@abc.com"));
Comment.Entity entity = new Comment.Entity();
Map<String, Comment.Entity> entityMap = new HashMap<>();
entityMap.put("abc", entity);
body.setEntityMap(entityMap);
comment.setBody(body);
Assert.assertFalse(CommentUtils.isUserMentioned(comment, "user@abc.com"));
}
private Map<String, Comment.Entity> createEntityMapForUsers(List<String> mentionedUserNames) {
Map<String, Comment.Entity> entityMap = new HashMap<>();
for (String username: mentionedUserNames) {
Comment.EntityData.EntityUser entityUser = new Comment.EntityData.EntityUser();
entityUser.setUsername(username);
Comment.EntityData.Mention mention = new Comment.EntityData.Mention();
mention.setUser(entityUser);
Comment.EntityData entityData = new Comment.EntityData();
entityData.setMention(mention);
Comment.Entity entity = new Comment.Entity();
entity.setType("mention");
entity.setData(entityData);
entityMap.put(username, entity);
}
return entityMap;
}
@Test
public void isUserMentioned_WhenSomeoneIsMentioned_ReturnsCorrectValue() {
Map<String, Comment.Entity> entityMap = createEntityMapForUsers(
List.of("1", "2", "3")
);
Comment.Body body = new Comment.Body();
body.setEntityMap(entityMap);
Comment comment = new Comment();
comment.setBody(body);
Assert.assertTrue(CommentUtils.isUserMentioned(comment, "1"));
Assert.assertTrue(CommentUtils.isUserMentioned(comment, "2"));
Assert.assertTrue(CommentUtils.isUserMentioned(comment, "3"));
Assert.assertFalse(CommentUtils.isUserMentioned(comment, "4"));
}
}

View File

@ -57,7 +57,7 @@ public class CommentServiceTest {
thread.setComments(List.of(
makePlainTextComment("comment one")
));
return commentService.createThread(thread);
return commentService.createThread(thread, "https://app.appsmith.com");
})
.zipWhen(thread -> commentService.getThreadsByApplicationId(thread.getApplicationId()));
@ -128,7 +128,7 @@ public class CommentServiceTest {
thread.setComments(List.of(
makePlainTextComment("Test Comment")
));
return commentService.createThread(thread);
return commentService.createThread(thread, "https://app.appsmith.com");
})
.flatMap(commentThread -> Mono.just(commentThread.getComments().get(0)))
.cache();
@ -163,7 +163,7 @@ public class CommentServiceTest {
final CommentThread thread = new CommentThread();
thread.setApplicationId(application.getId());
thread.setComments(List.of(makePlainTextComment("Test Comment")));
return commentService.createThread(thread);
return commentService.createThread(thread, "https://app.appsmith.com");
})
.flatMap(commentThread -> Mono.just(commentThread.getComments().get(0)))
.flatMap(comment -> {
@ -202,7 +202,7 @@ public class CommentServiceTest {
final CommentThread thread = new CommentThread();
thread.setApplicationId(application.getId());
thread.setComments(List.of(makePlainTextComment("Test Comment")));
return commentService.createThread(thread);
return commentService.createThread(thread, "https://app.appsmith.com");
})
.flatMap(commentThread -> Mono.just(commentThread.getComments().get(0)))
.flatMap(comment -> {

View File

@ -3,7 +3,6 @@ package com.appsmith.server.services;
import com.appsmith.server.acl.RoleGraph;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.User;
import com.appsmith.server.domains.UserRole;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.repositories.AssetRepository;
@ -15,18 +14,15 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.test.StepVerifier;
import javax.validation.Validator;
import java.util.ArrayList;
import java.util.List;
import static com.appsmith.server.acl.AclPermission.ORGANIZATION_INVITE_USERS;
@ -56,7 +52,6 @@ public class OrganizationServiceUnitTest {
organizationRepository, analyticsService, pluginRepository, sessionUserService, userOrganizationService,
userRepository, roleGraph, assetRepository, assetService
);
MockitoAnnotations.initMocks(this);
}
@Test

View File

@ -0,0 +1,198 @@
package com.appsmith.server.solutions;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Comment;
import com.appsmith.server.domains.CommentThread;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.UserRole;
import com.appsmith.server.events.CommentAddedEvent;
import com.appsmith.server.events.CommentThreadClosedEvent;
import com.appsmith.server.notifications.EmailSender;
import com.appsmith.server.repositories.ApplicationRepository;
import com.appsmith.server.repositories.OrganizationRepository;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.mockito.ArgumentMatchers.eq;
@RunWith(SpringJUnit4ClassRunner.class)
public class EmailEventHandlerTest {
private static final String COMMENT_ADDED_EMAIL_TEMPLATE = "email/commentAddedTemplate.html";
private static final String USER_MENTIONED_EMAIL_TEMPLATE = "email/userTaggedInCommentTemplate.html";
private static final String THREAD_RESOLVED_EMAIL_TEMPLATE = "email/commentResolvedTemplate.html";
@MockBean
private ApplicationEventPublisher applicationEventPublisher;
@MockBean
private EmailSender emailSender;
@MockBean
private OrganizationRepository organizationRepository;
@MockBean
private ApplicationRepository applicationRepository;
EmailEventHandler emailEventHandler;
private Application application;
private Organization organization;
String authorUserName = "abc";
String originHeader = "efg";
String applicationId = "application-id";
String organizationId = "organization-id";
String emailReceiverUsername = "email-receiver";
@Before
public void setUp() {
emailEventHandler = new EmailEventHandler(
applicationEventPublisher, emailSender, organizationRepository, applicationRepository
);
application = new Application();
application.setName("Test application for comment");
application.setOrganizationId(organizationId);
organization = new Organization();
// add a role with email receiver username
UserRole userRole = new UserRole();
userRole.setUsername(emailReceiverUsername);
organization.setUserRoles(List.of(userRole));
Mockito.when(applicationRepository.findById(applicationId)).thenReturn(Mono.just(application));
Mockito.when(organizationRepository.findById(organizationId)).thenReturn(Mono.just(organization));
}
@Test
public void publish_WhenValidCommentProvided_ReturnsTrue() {
Comment comment = new Comment();
CommentAddedEvent commentAddedEvent = new CommentAddedEvent(
authorUserName, organization, application, originHeader, comment
);
Mockito.doNothing().when(applicationEventPublisher).publishEvent(commentAddedEvent);
Mono<Boolean> booleanMono = emailEventHandler.publish(authorUserName, applicationId, comment, originHeader);
StepVerifier.create(booleanMono).assertNext(aBoolean -> {
Assert.assertEquals(Boolean.TRUE, aBoolean);
}).verifyComplete();
}
@Test
public void publish_WhenValidCommentThreadProvided_ReturnsTrue() {
CommentThread commentThread = new CommentThread();
CommentThreadClosedEvent commentThreadClosedEvent = new CommentThreadClosedEvent(
authorUserName, organization, application, originHeader, commentThread
);
Mockito.doNothing().when(applicationEventPublisher).publishEvent(commentThreadClosedEvent);
Mono<Boolean> booleanMono = emailEventHandler.publish(authorUserName, applicationId, commentThread, originHeader);
StepVerifier.create(booleanMono).assertNext(aBoolean -> {
Assert.assertEquals(Boolean.TRUE, aBoolean);
}).verifyComplete();
}
@Test
public void handle_WhenValidCommentAddedEvent_ReturnsTrue() {
Comment sampleComment = new Comment();
sampleComment.setAuthorUsername(authorUserName);
sampleComment.setAuthorName("Test Author");
// send the event
CommentAddedEvent commentAddedEvent = new CommentAddedEvent(
authorUserName, organization, application, originHeader, sampleComment
);
emailEventHandler.handle(commentAddedEvent);
String expectedEmailSubject = String.format(
"New comment from %s in %s", sampleComment.getAuthorName(), application.getName()
);
// check email sender was called with expected template and subject
Mockito.verify(emailSender, Mockito.times(1)).sendMail(
eq(emailReceiverUsername), eq(expectedEmailSubject), eq(COMMENT_ADDED_EMAIL_TEMPLATE), Mockito.anyMap()
);
}
private Map<String, Comment.Entity> createEntityMapForUsers(List<String> mentionedUserNames) {
Map<String, Comment.Entity> entityMap = new HashMap<>();
for (String username: mentionedUserNames) {
Comment.EntityData.EntityUser entityUser = new Comment.EntityData.EntityUser();
entityUser.setUsername(username);
Comment.EntityData.Mention mention = new Comment.EntityData.Mention();
mention.setUser(entityUser);
Comment.EntityData entityData = new Comment.EntityData();
entityData.setMention(mention);
Comment.Entity entity = new Comment.Entity();
entity.setType("mention");
entity.setData(entityData);
entityMap.put(username, entity);
}
return entityMap;
}
@Test
public void handle_WhenUserMentionedEvent_ReturnsTrue() {
Comment sampleComment = new Comment();
sampleComment.setAuthorUsername(authorUserName);
sampleComment.setAuthorName("Test Author");
// mention the emailReceiverUsername in the sample comment
Map<String, Comment.Entity> entityMap = createEntityMapForUsers(List.of(emailReceiverUsername));
Comment.Body body = new Comment.Body();
body.setEntityMap(entityMap);
sampleComment.setBody(body);
// send the event
CommentAddedEvent commentAddedEvent = new CommentAddedEvent(
authorUserName, organization, application, originHeader, sampleComment
);
emailEventHandler.handle(commentAddedEvent);
// check if expectation meets
String expectedEmailSubject = String.format("New comment for you from %s", sampleComment.getAuthorName());
// check email sender was called with expected template and subject
Mockito.verify(emailSender, Mockito.times(1)).sendMail(
eq(emailReceiverUsername), eq(expectedEmailSubject), eq(USER_MENTIONED_EMAIL_TEMPLATE), Mockito.anyMap()
);
}
@Test
public void handle_WhenThreadClosed_ReturnsTrue() {
// add comment thread with a resolved state where resolver is `authorUserName`
String resolverName = "Test Author";
CommentThread.CommentThreadState resolveState = new CommentThread.CommentThreadState();
resolveState.setAuthorUsername(authorUserName);
resolveState.setAuthorName(resolverName);
resolveState.setActive(true);
CommentThread commentThread = new CommentThread();
commentThread.setResolvedState(resolveState);
// send the event
CommentThreadClosedEvent commentAddedEvent = new CommentThreadClosedEvent(
authorUserName, organization, application, originHeader, commentThread
);
emailEventHandler.handle(commentAddedEvent);
// check if expectation meets
String expectedEmailSubject = String.format(
"%s has resolved comment in %s", resolveState.getAuthorName(), application.getName()
);
// check email sender was called with expected template and subject
Mockito.verify(emailSender, Mockito.times(1)).sendMail(
eq(emailReceiverUsername), eq(expectedEmailSubject), eq(THREAD_RESOLVED_EMAIL_TEMPLATE), Mockito.anyMap()
);
}
}