diff --git a/src/main/java/io/oduck/api/domain/contact/dto/ContactReq.java b/src/main/java/io/oduck/api/domain/contact/dto/ContactReq.java index 9467dbf0..f504454b 100644 --- a/src/main/java/io/oduck/api/domain/contact/dto/ContactReq.java +++ b/src/main/java/io/oduck/api/domain/contact/dto/ContactReq.java @@ -1,6 +1,6 @@ package io.oduck.api.domain.contact.dto; -import io.oduck.api.domain.contact.entity.InquiryType; +import io.oduck.api.domain.contact.entity.ContactType; import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Getter; @@ -10,10 +10,7 @@ public class ContactReq { @Getter @AllArgsConstructor public static class PostReq { - @NotBlank - @Length(min = 1, max = 50, - message = "글자 수는 1~50을 허용합니다.") - private InquiryType type; + private ContactType type; @NotBlank @Length(min = 1, max = 50, diff --git a/src/main/java/io/oduck/api/domain/contact/entity/Contact.java b/src/main/java/io/oduck/api/domain/contact/entity/Contact.java index 4c232745..75c292fc 100644 --- a/src/main/java/io/oduck/api/domain/contact/entity/Contact.java +++ b/src/main/java/io/oduck/api/domain/contact/entity/Contact.java @@ -40,7 +40,7 @@ public class Contact extends BaseEntity { private String content; @Enumerated(value = EnumType.STRING) - private InquiryType type; + private ContactType type; private boolean answered = false; diff --git a/src/main/java/io/oduck/api/domain/contact/entity/ContactType.java b/src/main/java/io/oduck/api/domain/contact/entity/ContactType.java new file mode 100644 index 00000000..64db1a90 --- /dev/null +++ b/src/main/java/io/oduck/api/domain/contact/entity/ContactType.java @@ -0,0 +1,15 @@ +package io.oduck.api.domain.contact.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum ContactType { + ADD_REQUEST("기능 추가 건의"), + BUG_REPORT("버그 신고"), + ETC_REQUEST("기타 문의"), + ; + + String description; +} diff --git a/src/main/java/io/oduck/api/domain/contact/entity/InquiryType.java b/src/main/java/io/oduck/api/domain/contact/entity/InquiryType.java deleted file mode 100644 index 81e1035c..00000000 --- a/src/main/java/io/oduck/api/domain/contact/entity/InquiryType.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.oduck.api.domain.contact.entity; - -public enum InquiryType { - ADD_REQUEST, - BUG_REPORT, - ETC_REQUEST -} diff --git a/src/main/java/io/oduck/api/domain/contact/infra/event/ContactEvent.java b/src/main/java/io/oduck/api/domain/contact/infra/event/ContactEvent.java new file mode 100644 index 00000000..ee727e0f --- /dev/null +++ b/src/main/java/io/oduck/api/domain/contact/infra/event/ContactEvent.java @@ -0,0 +1,26 @@ +package io.oduck.api.domain.contact.infra.event; + +import io.oduck.api.domain.contact.dto.ContactReq.PostReq; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder(access = AccessLevel.PRIVATE) +public class ContactEvent { + private String title; + private String content; + private String type; + private String name; + + public static ContactEvent from(PostReq request, String nickname) { + return ContactEvent.builder() + .type(request.getType().getDescription()) + .title(request.getTitle()) + .content(request.getContent()) + .name(nickname) + .build(); + } +} diff --git a/src/main/java/io/oduck/api/domain/contact/infra/event/ContactEventPublisher.java b/src/main/java/io/oduck/api/domain/contact/infra/event/ContactEventPublisher.java new file mode 100644 index 00000000..bf40acc3 --- /dev/null +++ b/src/main/java/io/oduck/api/domain/contact/infra/event/ContactEventPublisher.java @@ -0,0 +1,16 @@ +package io.oduck.api.domain.contact.infra.event; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ContactEventPublisher { + + private final ApplicationEventPublisher eventPublisher; + + public void contact(ContactEvent contactEvent) { + eventPublisher.publishEvent(contactEvent); + } +} diff --git a/src/main/java/io/oduck/api/domain/contact/infra/notifier/ContactNotifier.java b/src/main/java/io/oduck/api/domain/contact/infra/notifier/ContactNotifier.java new file mode 100644 index 00000000..121bedab --- /dev/null +++ b/src/main/java/io/oduck/api/domain/contact/infra/notifier/ContactNotifier.java @@ -0,0 +1,36 @@ +package io.oduck.api.domain.contact.infra.notifier; + +import io.oduck.api.global.notification.Notifier; +import io.oduck.api.global.notification.dto.Message; +import io.oduck.api.global.webHook.DiscordWebhook; +import io.oduck.api.global.webHook.DiscordWebhook.EmbedObject; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +@Primary +@RequiredArgsConstructor +@Component +@Slf4j +public class ContactNotifier implements Notifier { + + @Value("${config.webhook.qna}") + private String url; + + @Override + public void sendNotification(Message message) { + DiscordWebhook webhook = new DiscordWebhook(url); + + EmbedObject content = message.getContent(); + webhook.addEmbed(content); + + try{ + webhook.execute(); + } catch (IOException exception) { + log.error("Discord WebHook Error"); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/oduck/api/domain/contact/service/ContactService.java b/src/main/java/io/oduck/api/domain/contact/service/ContactService.java index 0868c855..3314590b 100644 --- a/src/main/java/io/oduck/api/domain/contact/service/ContactService.java +++ b/src/main/java/io/oduck/api/domain/contact/service/ContactService.java @@ -9,7 +9,7 @@ import io.oduck.api.global.common.PageResponse; public interface ContactService { - void inquiry(Long memberId, PostReq request); + void contact(Long memberId, PostReq request); PageResponse getAllByMemberId(Long memberId, int page, int size); diff --git a/src/main/java/io/oduck/api/domain/contact/service/ContactServiceImpl.java b/src/main/java/io/oduck/api/domain/contact/service/ContactServiceImpl.java index c570c83e..fcc8a1cf 100644 --- a/src/main/java/io/oduck/api/domain/contact/service/ContactServiceImpl.java +++ b/src/main/java/io/oduck/api/domain/contact/service/ContactServiceImpl.java @@ -7,12 +7,14 @@ import io.oduck.api.domain.admin.repository.AdminRepository; import io.oduck.api.domain.contact.dto.AnswerFeedback; import io.oduck.api.domain.contact.dto.ContactId; +import io.oduck.api.domain.contact.infra.event.ContactEvent; import io.oduck.api.domain.contact.dto.ContactReq.AnswerReq; import io.oduck.api.domain.contact.dto.ContactReq.AnswerUpdateReq; import io.oduck.api.domain.contact.dto.ContactRequestHolder; import io.oduck.api.domain.contact.dto.ContactRes.DetailRes; import io.oduck.api.domain.contact.entity.Contact; import io.oduck.api.domain.contact.entity.FeedbackType; +import io.oduck.api.domain.contact.infra.event.ContactEventPublisher; import io.oduck.api.domain.contact.repository.ContactRepository; import io.oduck.api.domain.member.entity.Member; import io.oduck.api.domain.member.repository.MemberRepository; @@ -31,17 +33,22 @@ public class ContactServiceImpl implements ContactService { private final MemberRepository memberRepository; private final ContactRepository contactRepository; + private final AdminRepository adminRepository; private final ContactPolicy contactPolicy; - private final AdminRepository adminRepository; + private final ContactEventPublisher eventPublisher; + @Override @Transactional - public void inquiry(Long memberId, PostReq request) { - Member member = memberRepository.findById(memberId) + public void contact(Long memberId, PostReq request) { + Member member = memberRepository.findWithProfileById(memberId) .orElseThrow(() -> new NotFoundException("member")); - member.inquiry(ContactRequestHolder.from(request, member)); + member.contact(ContactRequestHolder.from(request, member)); + + String nickname = member.getMemberProfile().getName(); + eventPublisher.contact(ContactEvent.from(request, nickname)); } @Override diff --git a/src/main/java/io/oduck/api/domain/member/entity/Member.java b/src/main/java/io/oduck/api/domain/member/entity/Member.java index 5f78db2e..36d78a03 100644 --- a/src/main/java/io/oduck/api/domain/member/entity/Member.java +++ b/src/main/java/io/oduck/api/domain/member/entity/Member.java @@ -122,7 +122,7 @@ public void delete() { this.role = Role.WITHDRAWAL; } - public void inquiry(ContactRequestHolder holder) { + public void contact(ContactRequestHolder holder) { Contact contact = holder.getContact(); this.contacts.add(contact); diff --git a/src/main/java/io/oduck/api/domain/member/repository/MemberRepository.java b/src/main/java/io/oduck/api/domain/member/repository/MemberRepository.java index 28e99a55..557f2932 100644 --- a/src/main/java/io/oduck/api/domain/member/repository/MemberRepository.java +++ b/src/main/java/io/oduck/api/domain/member/repository/MemberRepository.java @@ -3,9 +3,14 @@ import io.oduck.api.domain.member.entity.Member; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface MemberRepository extends JpaRepository, MemberRepositoryCustom{ Optional findByIdAndDeletedAtIsNull(Long id); + + @Query("select m from Member m join fetch m.memberProfile p where m.id = :id") + Optional findWithProfileById(@Param("id") Long id); } diff --git a/src/main/java/io/oduck/api/domain/notifier/service/ContactEventHandler.java b/src/main/java/io/oduck/api/domain/notifier/service/ContactEventHandler.java new file mode 100644 index 00000000..6e68569d --- /dev/null +++ b/src/main/java/io/oduck/api/domain/notifier/service/ContactEventHandler.java @@ -0,0 +1,40 @@ +package io.oduck.api.domain.notifier.service; + +import io.oduck.api.domain.contact.infra.event.ContactEvent; +import io.oduck.api.global.notification.Notifier; +import io.oduck.api.global.notification.dto.Message; +import io.oduck.api.global.webHook.DiscordWebhook.EmbedObject; +import java.awt.Color; +import java.util.Date; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +public class ContactEventHandler { + private final Notifier notifier; + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleContactEvent(ContactEvent contactEvent) { + String title = contactEvent.getTitle(); + String content = contactEvent.getContent(); + String category = contactEvent.getType(); + String name = contactEvent.getName(); + + String requestTime = new Date().toString(); + + EmbedObject embed = new EmbedObject() + .setTitle(title) + .setDescription(String.format("[문의사항] %s 님의 문의사항", name)) + .setColor(new Color(000, 100, 255)) + .addField("CONTENT", content, false) + .addField("CATEGORY", category, false) + .addField("TIME", requestTime, false); + + notifier.sendNotification(Message.from(embed)); + } +} diff --git a/src/main/java/io/oduck/api/global/advice/ExceptionHandlerAdvice.java b/src/main/java/io/oduck/api/global/advice/ExceptionHandlerAdvice.java index 88535c79..34d420ff 100644 --- a/src/main/java/io/oduck/api/global/advice/ExceptionHandlerAdvice.java +++ b/src/main/java/io/oduck/api/global/advice/ExceptionHandlerAdvice.java @@ -1,5 +1,7 @@ package io.oduck.api.global.advice; +import static io.oduck.api.global.utils.HttpHeaderUtils.getClientIP; + import io.oduck.api.global.common.ErrorResponse; import io.oduck.api.global.exception.BadRequestException; import io.oduck.api.global.exception.ConflictException; @@ -7,11 +9,15 @@ import io.oduck.api.global.exception.ForbiddenException; import io.oduck.api.global.exception.NotFoundException; import io.oduck.api.global.exception.UnauthorizedException; -import io.oduck.api.global.webHook.WebHookService; +import io.oduck.api.global.notification.Notifier; +import io.oduck.api.global.notification.dto.Message; +import io.oduck.api.global.webHook.DiscordWebhook.EmbedObject; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolationException; -import lombok.RequiredArgsConstructor; +import java.awt.Color; +import java.util.Date; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -22,14 +28,16 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.client.HttpClientErrorException.Forbidden; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -@RequiredArgsConstructor @Slf4j @RestControllerAdvice public class ExceptionHandlerAdvice { - private final WebHookService webHookService; + private final Notifier notifier; + + public ExceptionHandlerAdvice(@Qualifier("exceptionNotifier") Notifier notifier) { + this.notifier = notifier; + } // 요청 바디 필드 유효성 검증 예외 처리 @ExceptionHandler @@ -113,9 +121,15 @@ public ResponseEntity handleCustomException(CustomException e) { @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) protected ErrorResponse handleNullPointerException(HttpServletRequest req, NullPointerException e) { log.error("handleNullPointerException", e); + // Discord WebHook에 보낼 때 "가 있으면 json 파싱 에러가 발생함. // NPE 메시지에서 "를 사용함. -> Discord WebHook에 보낼 때 " -> \" 로 치환 - webHookService.sendMsg(new NullPointerException(e.getMessage().replace("\"", "\\\"")), req); + String message = e.getMessage().replace("\"", "\\\""); + NullPointerException npe = new NullPointerException(message); + + EmbedObject content = getMessageContent(npe.getMessage(), getStackTraceInfo(npe), req); + + notifier.sendNotification(Message.from(content)); return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR); } @@ -127,7 +141,44 @@ protected ErrorResponse handleNullPointerException(HttpServletRequest req, NullP @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse handleException(HttpServletRequest req, Exception e) { log.error("# Uncaught exceptions, which can be fatal to the server", e); - webHookService.sendMsg(e, req); + + EmbedObject content = getMessageContent(e.getMessage(), getStackTraceInfo(e), req); + + notifier.sendNotification(Message.from(content)); return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR); } + + + private EmbedObject getMessageContent(String message, String stackTraceInfo, HttpServletRequest req) { + String description = message; + String stackTrace = stackTraceInfo; + String clientIP = getClientIP(req); + String requestURL = req.getRequestURL().toString(); + String requestMethod = req.getMethod(); + String requestTime = new Date().toString(); + String requestUserAgent = req.getHeader("User-Agent"); + + EmbedObject content = new EmbedObject() + .setTitle("** Error Stack **") + .setDescription(description) + .setColor(new Color(16711680)) + .addField("HTTP_METHOD", requestMethod, false) + .addField("REQUEST_ENDPOINT", requestURL, false) + .addField("CLIENT_IP", clientIP, false) + .addField("ERROR_STACK", stackTrace, false) + .addField("TIME", requestTime, false) + .addField("USER_AGENT", requestUserAgent, false); + return content; + } + + private String getStackTraceInfo(Exception e) { + StackTraceElement[] stackTrace = e.getStackTrace(); + + String stackTraceInfo = null; + if (stackTrace.length > 0) { + StackTraceElement firstElement = stackTrace[0]; + stackTraceInfo = firstElement.getClassName() + "." + firstElement.getMethodName() + "(" + firstElement.getFileName() + ":" + firstElement.getLineNumber() + ")"; + } + return stackTraceInfo; + } } diff --git a/src/main/java/io/oduck/api/global/advice/infra/ExceptionNotifier.java b/src/main/java/io/oduck/api/global/advice/infra/ExceptionNotifier.java new file mode 100644 index 00000000..50d215a4 --- /dev/null +++ b/src/main/java/io/oduck/api/global/advice/infra/ExceptionNotifier.java @@ -0,0 +1,36 @@ +package io.oduck.api.global.advice.infra; + +import io.oduck.api.global.notification.Notifier; +import io.oduck.api.global.notification.dto.Message; +import io.oduck.api.global.webHook.DiscordWebhook; +import io.oduck.api.global.webHook.DiscordWebhook.EmbedObject; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@Qualifier("exceptionNotifier") +@RequiredArgsConstructor +@Slf4j +public class ExceptionNotifier implements Notifier { + + @Value("${config.webhook.url}") + private String url; + + @Override + public void sendNotification(Message message) { + DiscordWebhook webhook = new DiscordWebhook(url); + + EmbedObject content = message.getContent(); + webhook.addEmbed(content); + + try{ + webhook.execute(); + } catch (IOException exception) { + log.error("Discord WebHook Error"); + } + } +} diff --git a/src/main/java/io/oduck/api/global/notification/Notifier.java b/src/main/java/io/oduck/api/global/notification/Notifier.java new file mode 100644 index 00000000..430696a5 --- /dev/null +++ b/src/main/java/io/oduck/api/global/notification/Notifier.java @@ -0,0 +1,7 @@ +package io.oduck.api.global.notification; + +import io.oduck.api.global.notification.dto.Message; + +public interface Notifier { + void sendNotification(Message message); +} \ No newline at end of file diff --git a/src/main/java/io/oduck/api/global/notification/dto/Message.java b/src/main/java/io/oduck/api/global/notification/dto/Message.java new file mode 100644 index 00000000..4cdbe64b --- /dev/null +++ b/src/main/java/io/oduck/api/global/notification/dto/Message.java @@ -0,0 +1,20 @@ +package io.oduck.api.global.notification.dto; + +import io.oduck.api.global.webHook.DiscordWebhook.EmbedObject; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder(access = AccessLevel.PRIVATE) +public class Message { + private EmbedObject content; + + public static Message from(EmbedObject content) { + return Message.builder() + .content(content) + .build(); + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index b2542842..9ef89dd0 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -125,3 +125,4 @@ config: url: ${.base.url} webhook: url: ${.webhook.url} + qna: ${.webhook.qna} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 60c7858a..568db071 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -125,3 +125,4 @@ config: url: ${.base.url} webhook: url: ${.webhook.url} + qna: ${.webhook.qna} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index ed17f07e..fbfb8c1f 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -127,3 +127,4 @@ config: url: ${.base.url} webhook: url: ${.webhook.url} + qna: ${.webhook.qna} diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 803c8788..f8c64cf1 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -77,4 +77,5 @@ config: base: url: http://localhost:5173 webhook: - url: adsfasdfasdfasdf \ No newline at end of file + url: adsfasdfasdfasdf + qna: adsfasdfasdfasdf \ No newline at end of file diff --git a/src/test/java/io/oduck/api/unit/contact/domain/ContactTest.java b/src/test/java/io/oduck/api/unit/contact/domain/ContactTest.java index 1ecd25d7..47571f64 100644 --- a/src/test/java/io/oduck/api/unit/contact/domain/ContactTest.java +++ b/src/test/java/io/oduck/api/unit/contact/domain/ContactTest.java @@ -10,7 +10,7 @@ import io.oduck.api.domain.contact.entity.Answer; import io.oduck.api.domain.contact.entity.Contact; import io.oduck.api.domain.contact.entity.FeedbackType; -import io.oduck.api.domain.contact.entity.InquiryType; +import io.oduck.api.domain.contact.entity.ContactType; import io.oduck.api.domain.member.entity.Member; import java.util.ArrayList; import java.util.List; @@ -26,13 +26,13 @@ public class ContactTest { .contacts(new ArrayList<>()) .build(); - PostReq postReq = new PostReq(InquiryType.ADD_REQUEST, "이거 왜 안 됨?", "왜"); + PostReq postReq = new PostReq(ContactType.ADD_REQUEST, "이거 왜 안 됨?", "왜"); //when - target.inquiry(ContactRequestHolder.from(postReq, target)); + target.contact(ContactRequestHolder.from(postReq, target)); //then - assertThat(target.getContacts().size()).isEqualTo(1); + assertThat(target.getContacts()).hasSize(1); assertThat(target.getContacts().get(0).getTitle()).isEqualTo("이거 왜 안 됨?"); assertThat(target.getContacts().get(0).getContent()).isEqualTo("왜"); } diff --git a/src/test/java/io/oduck/api/unit/contact/repository/ContactRepositoryTest.java b/src/test/java/io/oduck/api/unit/contact/repository/ContactRepositoryTest.java index db478b8d..40ba93ea 100644 --- a/src/test/java/io/oduck/api/unit/contact/repository/ContactRepositoryTest.java +++ b/src/test/java/io/oduck/api/unit/contact/repository/ContactRepositoryTest.java @@ -4,7 +4,7 @@ import io.oduck.api.domain.contact.dto.ContactReq.PostReq; import io.oduck.api.domain.contact.dto.ContactRequestHolder; -import io.oduck.api.domain.contact.entity.InquiryType; +import io.oduck.api.domain.contact.entity.ContactType; import io.oduck.api.domain.contact.repository.ContactRepository; import io.oduck.api.domain.member.entity.Member; import io.oduck.api.domain.member.repository.MemberRepository; @@ -36,16 +36,16 @@ public class ContactRepositoryTest { .build(); Member target = memberRepository.save(member); - PostReq postReq = new PostReq(InquiryType.ADD_REQUEST, "이거 왜 안 됨?", "왜"); + PostReq postReq = new PostReq(ContactType.ADD_REQUEST, "이거 왜 안 됨?", "왜"); //when - target.inquiry(ContactRequestHolder.from(postReq, target)); + target.contact(ContactRequestHolder.from(postReq, target)); //then assertThat(target.getContacts().isEmpty()).isFalse(); assertThat(target.getContacts().size()).isEqualTo(1L); assertThat(target.getContacts().get(0).getTitle()).isEqualTo("이거 왜 안 됨?"); assertThat(target.getContacts().get(0).getContent()).isEqualTo("왜"); - assertThat(target.getContacts().get(0).getType()).isEqualTo(InquiryType.ADD_REQUEST); + assertThat(target.getContacts().get(0).getType()).isEqualTo(ContactType.ADD_REQUEST); } }