From 5a1a2e464fdfc64c0f4a22b97664eeaaf6ebfb9f Mon Sep 17 00:00:00 2001 From: bjk1649 Date: Sat, 28 Oct 2023 17:04:02 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20fcm=EC=9D=84=20=ED=86=B5=ED=95=9C?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC=20=EB=B0=9C=EC=86=A1=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/build.gradle | 3 + .../backend/application/AuthService.java | 4 +- .../application/StudyEventListener.java | 4 +- .../yigongil/backend/domain/BaseEntity.java | 26 +--- .../domain/certification/Certification.java | 7 ++ .../event/CertificationCreatedEvent.java | 8 ++ .../domain/event/FeedPostCreatedEvent.java | 15 +++ .../domain/event/MemberDeleteEvent.java | 5 - .../domain/event/MemberDeletedEvent.java | 5 + .../domain/event/MustDoUpdatedEvent.java | 8 ++ .../domain/event/StudyAppliedEvent.java | 5 + .../domain/event/StudyCreatedEvent.java | 5 + .../domain/event/StudyPermittedEvent.java | 5 + .../domain/event/StudyStartedEvent.java | 5 + .../backend/domain/feedpost/FeedPost.java | 14 +++ .../backend/domain/member/Member.java | 4 +- .../yigongil/backend/domain/round/Round.java | 2 + .../yigongil/backend/domain/study/Study.java | 15 +++ .../MemberNotRegisteredException.java | 10 ++ .../infra/FirebaseCloudMessagingService.java | 113 ++++++++++++++++++ .../backend/infra/FirebaseConfig.java | 35 ++++++ .../backend/infra/FirebaseMember.java | 35 ++++++ .../infra/FirebaseMemberRepository.java | 11 ++ .../backend/infra/LocalMessagingService.java | 25 ++++ .../backend/infra/MessagingService.java | 12 ++ ...nRequest.java => RefreshTokenRequest.java} | 2 +- .../backend/ui/FirebaseTokenRequest.java | 7 ++ .../yigongil/backend/ui/LoginController.java | 20 +++- .../com/yigongil/backend/ui/doc/LoginApi.java | 12 +- .../acceptance/steps/AuthorizationSteps.java | 6 +- 30 files changed, 384 insertions(+), 44 deletions(-) create mode 100644 backend/src/main/java/com/yigongil/backend/domain/event/CertificationCreatedEvent.java create mode 100644 backend/src/main/java/com/yigongil/backend/domain/event/FeedPostCreatedEvent.java delete mode 100644 backend/src/main/java/com/yigongil/backend/domain/event/MemberDeleteEvent.java create mode 100644 backend/src/main/java/com/yigongil/backend/domain/event/MemberDeletedEvent.java create mode 100644 backend/src/main/java/com/yigongil/backend/domain/event/MustDoUpdatedEvent.java create mode 100644 backend/src/main/java/com/yigongil/backend/domain/event/StudyAppliedEvent.java create mode 100644 backend/src/main/java/com/yigongil/backend/domain/event/StudyCreatedEvent.java create mode 100644 backend/src/main/java/com/yigongil/backend/domain/event/StudyPermittedEvent.java create mode 100644 backend/src/main/java/com/yigongil/backend/domain/event/StudyStartedEvent.java create mode 100644 backend/src/main/java/com/yigongil/backend/exception/MemberNotRegisteredException.java create mode 100644 backend/src/main/java/com/yigongil/backend/infra/FirebaseCloudMessagingService.java create mode 100644 backend/src/main/java/com/yigongil/backend/infra/FirebaseConfig.java create mode 100644 backend/src/main/java/com/yigongil/backend/infra/FirebaseMember.java create mode 100644 backend/src/main/java/com/yigongil/backend/infra/FirebaseMemberRepository.java create mode 100644 backend/src/main/java/com/yigongil/backend/infra/LocalMessagingService.java create mode 100644 backend/src/main/java/com/yigongil/backend/infra/MessagingService.java rename backend/src/main/java/com/yigongil/backend/request/{TokenRequest.java => RefreshTokenRequest.java} (67%) create mode 100644 backend/src/main/java/com/yigongil/backend/ui/FirebaseTokenRequest.java diff --git a/backend/build.gradle b/backend/build.gradle index 676c09045..5f70079f6 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -76,6 +76,9 @@ dependencies { annotationProcessor "javax.persistence:javax.persistence-api" annotationProcessor "javax.annotation:javax.annotation-api" annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" + + // fcm + implementation 'com.google.firebase:firebase-admin:9.1.1' } tasks.named('test') { diff --git a/backend/src/main/java/com/yigongil/backend/application/AuthService.java b/backend/src/main/java/com/yigongil/backend/application/AuthService.java index 6f0d559fc..5801f3657 100644 --- a/backend/src/main/java/com/yigongil/backend/application/AuthService.java +++ b/backend/src/main/java/com/yigongil/backend/application/AuthService.java @@ -5,7 +5,7 @@ import com.yigongil.backend.config.oauth.GithubProfileResponse; import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.domain.member.MemberRepository; -import com.yigongil.backend.request.TokenRequest; +import com.yigongil.backend.request.RefreshTokenRequest; import com.yigongil.backend.response.TokenResponse; import java.util.Optional; import org.springframework.stereotype.Service; @@ -40,7 +40,7 @@ public TokenResponse login(String code) { return createTokens(id); } - public TokenResponse refresh(TokenRequest request) { + public TokenResponse refresh(RefreshTokenRequest request) { String refreshToken = request.refreshToken(); jwtTokenProvider.detectTokenTheft(refreshToken); diff --git a/backend/src/main/java/com/yigongil/backend/application/StudyEventListener.java b/backend/src/main/java/com/yigongil/backend/application/StudyEventListener.java index ae1b68d77..cc1c61307 100644 --- a/backend/src/main/java/com/yigongil/backend/application/StudyEventListener.java +++ b/backend/src/main/java/com/yigongil/backend/application/StudyEventListener.java @@ -1,6 +1,6 @@ package com.yigongil.backend.application; -import com.yigongil.backend.domain.event.MemberDeleteEvent; +import com.yigongil.backend.domain.event.MemberDeletedEvent; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; @@ -15,7 +15,7 @@ public StudyEventListener(StudyService studyService) { } @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) - public void listenMemberDeleteEvent(MemberDeleteEvent event) { + public void listenMemberDeleteEvent(MemberDeletedEvent event) { studyService.deleteByMasterId(event.memberId()); } } diff --git a/backend/src/main/java/com/yigongil/backend/domain/BaseEntity.java b/backend/src/main/java/com/yigongil/backend/domain/BaseEntity.java index 90980c463..5e9a54236 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/BaseEntity.java +++ b/backend/src/main/java/com/yigongil/backend/domain/BaseEntity.java @@ -1,22 +1,17 @@ package com.yigongil.backend.domain; -import com.yigongil.backend.domain.event.DomainEvent; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collection; import javax.persistence.Column; import javax.persistence.EntityListeners; import javax.persistence.MappedSuperclass; -import javax.persistence.Transient; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.domain.AfterDomainEventPublication; -import org.springframework.data.domain.DomainEvents; +import org.springframework.data.domain.AbstractAggregateRoot; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @EntityListeners(AuditingEntityListener.class) @MappedSuperclass -public abstract class BaseEntity { +public abstract class BaseEntity extends AbstractAggregateRoot { @CreatedDate @Column(nullable = false) @@ -25,23 +20,6 @@ public abstract class BaseEntity { @LastModifiedDate protected LocalDateTime updatedAt; - @Transient - private Collection domainEvents = new ArrayList<>(); - - @DomainEvents - public Collection events() { - return domainEvents; - } - - @AfterDomainEventPublication - public void clear() { - domainEvents.clear(); - } - - protected void register(DomainEvent event) { - domainEvents.add(event); - } - public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/backend/src/main/java/com/yigongil/backend/domain/certification/Certification.java b/backend/src/main/java/com/yigongil/backend/domain/certification/Certification.java index 265097560..8371d5e6c 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/certification/Certification.java +++ b/backend/src/main/java/com/yigongil/backend/domain/certification/Certification.java @@ -1,6 +1,7 @@ package com.yigongil.backend.domain.certification; import com.yigongil.backend.domain.BaseEntity; +import com.yigongil.backend.domain.event.CertificationCreatedEvent; import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.domain.round.Round; import com.yigongil.backend.domain.study.Study; @@ -11,6 +12,7 @@ import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; +import javax.persistence.PostPersist; import lombok.Builder; import lombok.Getter; @@ -59,4 +61,9 @@ public Certification( this.createdAt = createdAt; this.round = round; } + + @PostPersist + public void registerCreatedEvent() { + registerEvent(new CertificationCreatedEvent(study.getId(), study.getName(), author.getGithubId())); + } } diff --git a/backend/src/main/java/com/yigongil/backend/domain/event/CertificationCreatedEvent.java b/backend/src/main/java/com/yigongil/backend/domain/event/CertificationCreatedEvent.java new file mode 100644 index 000000000..7d049e7e6 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/domain/event/CertificationCreatedEvent.java @@ -0,0 +1,8 @@ +package com.yigongil.backend.domain.event; + +public record CertificationCreatedEvent( + Long studyId, + String studyName, + String authorGithubId +) { +} diff --git a/backend/src/main/java/com/yigongil/backend/domain/event/FeedPostCreatedEvent.java b/backend/src/main/java/com/yigongil/backend/domain/event/FeedPostCreatedEvent.java new file mode 100644 index 000000000..89c250812 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/domain/event/FeedPostCreatedEvent.java @@ -0,0 +1,15 @@ +package com.yigongil.backend.domain.event; + +import java.time.LocalDateTime; + +public record FeedPostCreatedEvent + ( + Long studyId, + String studyName, + String authorGithubId, + String content, + String imageUrl, + LocalDateTime createdAt + ) implements DomainEvent { + +} diff --git a/backend/src/main/java/com/yigongil/backend/domain/event/MemberDeleteEvent.java b/backend/src/main/java/com/yigongil/backend/domain/event/MemberDeleteEvent.java deleted file mode 100644 index 52fa5d122..000000000 --- a/backend/src/main/java/com/yigongil/backend/domain/event/MemberDeleteEvent.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.yigongil.backend.domain.event; - -public record MemberDeleteEvent(Long memberId) implements DomainEvent { - -} diff --git a/backend/src/main/java/com/yigongil/backend/domain/event/MemberDeletedEvent.java b/backend/src/main/java/com/yigongil/backend/domain/event/MemberDeletedEvent.java new file mode 100644 index 000000000..d1f1fa243 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/domain/event/MemberDeletedEvent.java @@ -0,0 +1,5 @@ +package com.yigongil.backend.domain.event; + +public record MemberDeletedEvent(Long memberId) implements DomainEvent { + +} diff --git a/backend/src/main/java/com/yigongil/backend/domain/event/MustDoUpdatedEvent.java b/backend/src/main/java/com/yigongil/backend/domain/event/MustDoUpdatedEvent.java new file mode 100644 index 000000000..a73eea7da --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/domain/event/MustDoUpdatedEvent.java @@ -0,0 +1,8 @@ +package com.yigongil.backend.domain.event; + +public record MustDoUpdatedEvent( + Long studyId, + String studyName, + String mustDo +) implements DomainEvent{ +} diff --git a/backend/src/main/java/com/yigongil/backend/domain/event/StudyAppliedEvent.java b/backend/src/main/java/com/yigongil/backend/domain/event/StudyAppliedEvent.java new file mode 100644 index 000000000..a31555959 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/domain/event/StudyAppliedEvent.java @@ -0,0 +1,5 @@ +package com.yigongil.backend.domain.event; + +public record StudyAppliedEvent(Long studyMasterId, String studyName, String appliedMemberGithubId) implements DomainEvent { + +} diff --git a/backend/src/main/java/com/yigongil/backend/domain/event/StudyCreatedEvent.java b/backend/src/main/java/com/yigongil/backend/domain/event/StudyCreatedEvent.java new file mode 100644 index 000000000..c9ea243e3 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/domain/event/StudyCreatedEvent.java @@ -0,0 +1,5 @@ +package com.yigongil.backend.domain.event; + +public record StudyCreatedEvent(Long studyId, Long masterId) implements DomainEvent { + +} diff --git a/backend/src/main/java/com/yigongil/backend/domain/event/StudyPermittedEvent.java b/backend/src/main/java/com/yigongil/backend/domain/event/StudyPermittedEvent.java new file mode 100644 index 000000000..284177c13 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/domain/event/StudyPermittedEvent.java @@ -0,0 +1,5 @@ +package com.yigongil.backend.domain.event; + +public record StudyPermittedEvent(Long permittedMemberId, Long studyId, String studyName) implements DomainEvent { + +} diff --git a/backend/src/main/java/com/yigongil/backend/domain/event/StudyStartedEvent.java b/backend/src/main/java/com/yigongil/backend/domain/event/StudyStartedEvent.java new file mode 100644 index 000000000..e19d77052 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/domain/event/StudyStartedEvent.java @@ -0,0 +1,5 @@ +package com.yigongil.backend.domain.event; + +public record StudyStartedEvent(Long studyId, String studyName) implements DomainEvent { + +} diff --git a/backend/src/main/java/com/yigongil/backend/domain/feedpost/FeedPost.java b/backend/src/main/java/com/yigongil/backend/domain/feedpost/FeedPost.java index 7278718c3..64f078fd7 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/feedpost/FeedPost.java +++ b/backend/src/main/java/com/yigongil/backend/domain/feedpost/FeedPost.java @@ -1,6 +1,7 @@ package com.yigongil.backend.domain.feedpost; import com.yigongil.backend.domain.BaseEntity; +import com.yigongil.backend.domain.event.FeedPostCreatedEvent; import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.domain.study.Study; import java.time.LocalDateTime; @@ -11,6 +12,7 @@ import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; +import javax.persistence.PostPersist; import lombok.Builder; import lombok.Getter; @@ -54,6 +56,18 @@ public FeedPost( this.createdAt = createdAt; } + @PostPersist + public void registerCreatedEvent() { + registerEvent(new FeedPostCreatedEvent( + study.getId(), + study.getName(), + author.getGithubId(), + content, + imageUrl, + createdAt, + )); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/backend/src/main/java/com/yigongil/backend/domain/member/Member.java b/backend/src/main/java/com/yigongil/backend/domain/member/Member.java index 054eb242d..6b13a397a 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/member/Member.java +++ b/backend/src/main/java/com/yigongil/backend/domain/member/Member.java @@ -1,7 +1,7 @@ package com.yigongil.backend.domain.member; import com.yigongil.backend.domain.BaseEntity; -import com.yigongil.backend.domain.event.MemberDeleteEvent; +import com.yigongil.backend.domain.event.MemberDeletedEvent; import java.util.Objects; import javax.persistence.Column; import javax.persistence.Embedded; @@ -103,7 +103,7 @@ public String getIntroduction() { @PreRemove public void registerDeleteEvent() { - register(new MemberDeleteEvent(id)); + registerEvent(new MemberDeletedEvent(id)); } public void addExperience(int exp) { diff --git a/backend/src/main/java/com/yigongil/backend/domain/round/Round.java b/backend/src/main/java/com/yigongil/backend/domain/round/Round.java index 79795fb48..6458288b9 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/round/Round.java +++ b/backend/src/main/java/com/yigongil/backend/domain/round/Round.java @@ -1,6 +1,7 @@ package com.yigongil.backend.domain.round; import com.yigongil.backend.domain.BaseEntity; +import com.yigongil.backend.domain.event.MustDoUpdatedEvent; import com.yigongil.backend.domain.meetingdayoftheweek.MeetingDayOfTheWeek; import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.domain.roundofmember.RoundOfMember; @@ -106,6 +107,7 @@ public void updateMustDo(Member author, String content) { validateTodoLength(content); validateMaster(author); mustDo = content; + registerEvent(new MustDoUpdatedEvent(study.getId(), study.getName(), content)); } public void validateMaster(Member member) { diff --git a/backend/src/main/java/com/yigongil/backend/domain/study/Study.java b/backend/src/main/java/com/yigongil/backend/domain/study/Study.java index 6e71cf414..c31e02aa5 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/study/Study.java +++ b/backend/src/main/java/com/yigongil/backend/domain/study/Study.java @@ -1,6 +1,10 @@ package com.yigongil.backend.domain.study; import com.yigongil.backend.domain.BaseEntity; +import com.yigongil.backend.domain.event.StudyAppliedEvent; +import com.yigongil.backend.domain.event.StudyCreatedEvent; +import com.yigongil.backend.domain.event.StudyPermittedEvent; +import com.yigongil.backend.domain.event.StudyStartedEvent; import com.yigongil.backend.domain.meetingdayoftheweek.MeetingDayOfTheWeek; import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.domain.round.Round; @@ -33,6 +37,7 @@ import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; +import javax.persistence.PostPersist; import lombok.Builder; import lombok.Getter; import org.hibernate.annotations.Cascade; @@ -151,6 +156,11 @@ public static Study initializeStudyOf( .build(); } + @PostPersist + public void registerCreatedEvent() { + registerEvent(new StudyCreatedEvent(id, getMaster().getId())); + } + private void validateNumberOfMaximumMembers(Integer numberOfMaximumMembers) { if (numberOfMaximumMembers < MIN_MEMBER_SIZE || numberOfMaximumMembers > MAX_MEMBER_SIZE) { throw new InvalidNumberOfMaximumStudyMember( @@ -193,6 +203,8 @@ public void permit(Member applicant, Member master) { .filter(studyMember -> studyMember.getMember().equals(applicant)) .findAny() .ifPresent(StudyMember::participate); + + registerEvent(new StudyPermittedEvent(applicant.getId(), id, name)); } public void validateMemberSize() { @@ -224,6 +236,7 @@ public void start(Member member, List daysOfTheWeek, LocalDateTime st this.processingStatus = ProcessingStatus.PROCESSING; initializeMeetingDaysOfTheWeek(daysOfTheWeek); initializeRounds(startAt.toLocalDate()); + registerEvent(new StudyStartedEvent(id, name)); } private void deleteLeftApplicant() { @@ -398,6 +411,8 @@ public void apply(Member member) { .member(member) .studyResult(StudyResult.NONE) .build()); + + registerEvent(new StudyAppliedEvent(getMaster().getId(), name, member.getGithubId())); } private void validateApplicant(Member member) { diff --git a/backend/src/main/java/com/yigongil/backend/exception/MemberNotRegisteredException.java b/backend/src/main/java/com/yigongil/backend/exception/MemberNotRegisteredException.java new file mode 100644 index 000000000..769162b9e --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/exception/MemberNotRegisteredException.java @@ -0,0 +1,10 @@ +package com.yigongil.backend.exception; + +import org.springframework.http.HttpStatus; + +public class MemberNotRegisteredException extends HttpException { + + public MemberNotRegisteredException(final String message, final String input) { + super(HttpStatus.BAD_REQUEST, message, input); + } +} diff --git a/backend/src/main/java/com/yigongil/backend/infra/FirebaseCloudMessagingService.java b/backend/src/main/java/com/yigongil/backend/infra/FirebaseCloudMessagingService.java new file mode 100644 index 000000000..acd7ee6c0 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/infra/FirebaseCloudMessagingService.java @@ -0,0 +1,113 @@ +package com.yigongil.backend.infra; + +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.Message; +import com.yigongil.backend.domain.event.CertificationCreatedEvent; +import com.yigongil.backend.domain.event.FeedPostCreatedEvent; +import com.yigongil.backend.domain.event.MustDoUpdatedEvent; +import com.yigongil.backend.domain.event.StudyAppliedEvent; +import com.yigongil.backend.domain.event.StudyCreatedEvent; +import com.yigongil.backend.domain.event.StudyPermittedEvent; +import com.yigongil.backend.domain.event.StudyStartedEvent; +import com.yigongil.backend.domain.member.Member; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Profile(value = {"prod", "dev"}) +@Component +public class FirebaseCloudMessagingService implements MessagingService { + + private final FirebaseMessaging firebaseMessaging; + private final FirebaseMemberRepository firebaseMemberRepository; + + public FirebaseCloudMessagingService(FirebaseMessaging firebaseMessaging, final FirebaseMemberRepository firebaseMemberRepository) { + this.firebaseMessaging = firebaseMessaging; + this.firebaseMemberRepository = firebaseMemberRepository; + } + + @Override + public void registerToken(String token, Member member) { + firebaseMemberRepository.save(new FirebaseMember(member, token)); + subscribeToTopic(List.of(token), getMemberTopicById(member.getId())); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void registerStudyMaster(StudyCreatedEvent event) { + List tokens = findAllTokensByMemberId(event.masterId()); + subscribeToTopic(tokens, getStudyTopicById(event.studyId())); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void sendNotificationOfApplicant(StudyAppliedEvent event) { + sendToMember(event.appliedMemberGithubId() + "님이 " + event.studyName() + " 스터디에 신청했어요!", event.studyMasterId()); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void sendNotificationOfPermitted(StudyPermittedEvent event) { + List tokens = findAllTokensByMemberId(event.permittedMemberId()); + subscribeToTopic(tokens, getStudyTopicById(event.studyId())); + sendToMember(event.studyName() + " 스터디 신청이 수락되었어요!", event.permittedMemberId()); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void sendNotificationOfStudyStarted(StudyStartedEvent event) { + sendToStudy(event.studyName() + " 스터디가 시작되었어요!", event.studyId()); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void sendNotificationOfMustDoUpdated(MustDoUpdatedEvent event) { + sendToStudy(event.studyName() + " 스터디의 머스트두가 " + event.mustDo() + "로 변경되었어요!", event.studyId()); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void sendNotificationOfCertificationCreated(CertificationCreatedEvent event) { + sendToStudy(event.studyName() + " 스터디에" + event.authorGithubId() + " 님의 머스트두 인증이 등록되었어요!", event.studyId()); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void sendNotificationOfFeedPostCreated(FeedPostCreatedEvent event) { + sendToStudy(event.studyName() + " 스터디에 " + event.authorGithubId() + " 님이 피드를 등록했어요!", event.studyId()); + } + + private List findAllTokensByMemberId(Long memberId) { + return firebaseMemberRepository.findAllByMemberId(memberId) + .stream() + .map(FirebaseMember::getToken) + .toList(); + } + + public void subscribeToTopic(List tokens, String topic) { + firebaseMessaging.subscribeToTopicAsync(tokens, topic); + } + + @Override + public void sendToMember(String message, Long memberId) { + doSend(message, getMemberTopicById(memberId)); + } + + private String getMemberTopicById(final Long memberId) { + return "member " + memberId; + } + + @Override + public void sendToStudy(String message, Long studyId) { + doSend(message, getStudyTopicById(studyId)); + } + + @NotNull + private static String getStudyTopicById(final Long studyId) { + return "study " + studyId; + } + + private void doSend(String message, String topic) { + Message tokenMessage = Message.builder() + .setTopic(topic) + .putData("message", message) + .build(); + firebaseMessaging.sendAsync(tokenMessage); + } +} diff --git a/backend/src/main/java/com/yigongil/backend/infra/FirebaseConfig.java b/backend/src/main/java/com/yigongil/backend/infra/FirebaseConfig.java new file mode 100644 index 000000000..4a485435b --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/infra/FirebaseConfig.java @@ -0,0 +1,35 @@ +package com.yigongil.backend.infra; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import java.io.IOException; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.io.ClassPathResource; + +@Profile(value = {"prod", "dev"}) +@Configuration +public class FirebaseConfig { + + @Bean + public FirebaseMessaging messagingService(FirebaseApp firebaseApp) { + return FirebaseMessaging.getInstance(firebaseApp); + } + + @Bean + public FirebaseApp firebaseApp(GoogleCredentials credentials) { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(credentials) + .build(); + return FirebaseApp.initializeApp(options); + } + + @Bean + public GoogleCredentials googleCredentials() throws IOException { + final var inputStream = new ClassPathResource("yigongil-private/firebase-adminsdk.json").getInputStream(); + return GoogleCredentials.fromStream(inputStream); + } +} diff --git a/backend/src/main/java/com/yigongil/backend/infra/FirebaseMember.java b/backend/src/main/java/com/yigongil/backend/infra/FirebaseMember.java new file mode 100644 index 000000000..35e88f06d --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/infra/FirebaseMember.java @@ -0,0 +1,35 @@ +package com.yigongil.backend.infra; + +import com.yigongil.backend.domain.BaseEntity; +import com.yigongil.backend.domain.member.Member; +import javax.persistence.Column; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import lombok.Getter; + +@Getter +public class FirebaseMember extends BaseEntity { + + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", updatable = false, nullable = false) + private Member member; + + @Column(name = "token", nullable = false) + private String token; + + protected FirebaseMember() { + } + + public FirebaseMember(Member member, String token) { + this.member = member; + this.token = token; + } +} diff --git a/backend/src/main/java/com/yigongil/backend/infra/FirebaseMemberRepository.java b/backend/src/main/java/com/yigongil/backend/infra/FirebaseMemberRepository.java new file mode 100644 index 000000000..9fbb58ee7 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/infra/FirebaseMemberRepository.java @@ -0,0 +1,11 @@ +package com.yigongil.backend.infra; + +import java.util.List; +import org.springframework.data.repository.Repository; + +public interface FirebaseMemberRepository extends Repository { + + void save(FirebaseMember firebaseMember); + + List findAllByMemberId(Long memberId); +} diff --git a/backend/src/main/java/com/yigongil/backend/infra/LocalMessagingService.java b/backend/src/main/java/com/yigongil/backend/infra/LocalMessagingService.java new file mode 100644 index 000000000..303dfef93 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/infra/LocalMessagingService.java @@ -0,0 +1,25 @@ +package com.yigongil.backend.infra; + +import com.yigongil.backend.domain.member.Member; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +@Profile(value = {"local", "test"}) +@Service +public class LocalMessagingService implements MessagingService { + + @Override + public void registerToken(final String token, final Member member) { + + } + + @Override + public void sendToMember(final String message, final Long memberId) { + + } + + @Override + public void sendToStudy(final String message, final Long studyId) { + + } +} diff --git a/backend/src/main/java/com/yigongil/backend/infra/MessagingService.java b/backend/src/main/java/com/yigongil/backend/infra/MessagingService.java new file mode 100644 index 000000000..438d6edff --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/infra/MessagingService.java @@ -0,0 +1,12 @@ +package com.yigongil.backend.infra; + +import com.yigongil.backend.domain.member.Member; + +public interface MessagingService { + + void registerToken(String token, Member member); + + void sendToMember(String message, Long memberId); + + void sendToStudy(String message, Long studyId); +} diff --git a/backend/src/main/java/com/yigongil/backend/request/TokenRequest.java b/backend/src/main/java/com/yigongil/backend/request/RefreshTokenRequest.java similarity index 67% rename from backend/src/main/java/com/yigongil/backend/request/TokenRequest.java rename to backend/src/main/java/com/yigongil/backend/request/RefreshTokenRequest.java index 1fbfb6c2b..59ced38b9 100644 --- a/backend/src/main/java/com/yigongil/backend/request/TokenRequest.java +++ b/backend/src/main/java/com/yigongil/backend/request/RefreshTokenRequest.java @@ -1,6 +1,6 @@ package com.yigongil.backend.request; -public record TokenRequest( +public record RefreshTokenRequest( String refreshToken ) { diff --git a/backend/src/main/java/com/yigongil/backend/ui/FirebaseTokenRequest.java b/backend/src/main/java/com/yigongil/backend/ui/FirebaseTokenRequest.java new file mode 100644 index 000000000..459140304 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/ui/FirebaseTokenRequest.java @@ -0,0 +1,7 @@ +package com.yigongil.backend.ui; + +public record FirebaseTokenRequest( + String token +) { + +} diff --git a/backend/src/main/java/com/yigongil/backend/ui/LoginController.java b/backend/src/main/java/com/yigongil/backend/ui/LoginController.java index 9124a077c..7a05bc548 100644 --- a/backend/src/main/java/com/yigongil/backend/ui/LoginController.java +++ b/backend/src/main/java/com/yigongil/backend/ui/LoginController.java @@ -1,7 +1,10 @@ package com.yigongil.backend.ui; import com.yigongil.backend.application.AuthService; -import com.yigongil.backend.request.TokenRequest; +import com.yigongil.backend.config.auth.Authorization; +import com.yigongil.backend.domain.member.Member; +import com.yigongil.backend.infra.MessagingService; +import com.yigongil.backend.request.RefreshTokenRequest; import com.yigongil.backend.response.TokenResponse; import com.yigongil.backend.ui.doc.LoginApi; import org.springframework.http.ResponseEntity; @@ -17,9 +20,11 @@ public class LoginController implements LoginApi { private final AuthService authService; + private final MessagingService messagingService; - public LoginController(AuthService authService) { + public LoginController(AuthService authService, final MessagingService messagingService) { this.authService = authService; + this.messagingService = messagingService; } @GetMapping("/github/tokens") @@ -28,13 +33,22 @@ public ResponseEntity createMemberToken(@RequestParam String code return ResponseEntity.ok(response); } + @PostMapping("/firebase/tokens") + public ResponseEntity registerDevice( + @Authorization Member member, + @RequestBody FirebaseTokenRequest request + ) { + messagingService.registerToken(request.token(), member); + return ResponseEntity.ok().build(); + } + @GetMapping("/tokens/validate") public ResponseEntity validateToken() { return ResponseEntity.ok().build(); } @PostMapping("/tokens/refresh") - public ResponseEntity refreshMemberToken(@RequestBody TokenRequest request) { + public ResponseEntity refreshMemberToken(@RequestBody RefreshTokenRequest request) { TokenResponse response = authService.refresh(request); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/com/yigongil/backend/ui/doc/LoginApi.java b/backend/src/main/java/com/yigongil/backend/ui/doc/LoginApi.java index f8d1ba490..636c6f3a8 100644 --- a/backend/src/main/java/com/yigongil/backend/ui/doc/LoginApi.java +++ b/backend/src/main/java/com/yigongil/backend/ui/doc/LoginApi.java @@ -1,7 +1,10 @@ package com.yigongil.backend.ui.doc; -import com.yigongil.backend.request.TokenRequest; +import com.yigongil.backend.config.auth.Authorization; +import com.yigongil.backend.domain.member.Member; +import com.yigongil.backend.request.RefreshTokenRequest; import com.yigongil.backend.response.TokenResponse; +import com.yigongil.backend.ui.FirebaseTokenRequest; import io.swagger.v3.oas.annotations.Hidden; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody; @@ -10,7 +13,12 @@ @Hidden public interface LoginApi { + ResponseEntity registerDevice( + @Authorization Member member, + @RequestBody FirebaseTokenRequest request + ); + ResponseEntity createMemberToken(@RequestParam String code); - ResponseEntity refreshMemberToken(@RequestBody TokenRequest request); + ResponseEntity refreshMemberToken(@RequestBody RefreshTokenRequest request); } diff --git a/backend/src/test/java/com/yigongil/backend/acceptance/steps/AuthorizationSteps.java b/backend/src/test/java/com/yigongil/backend/acceptance/steps/AuthorizationSteps.java index 347039041..38fb9c180 100644 --- a/backend/src/test/java/com/yigongil/backend/acceptance/steps/AuthorizationSteps.java +++ b/backend/src/test/java/com/yigongil/backend/acceptance/steps/AuthorizationSteps.java @@ -7,7 +7,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.yigongil.backend.config.auth.JwtTokenProvider; -import com.yigongil.backend.request.TokenRequest; +import com.yigongil.backend.request.RefreshTokenRequest; import com.yigongil.backend.response.TokenResponse; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; @@ -37,7 +37,7 @@ public AuthorizationSteps(JwtTokenProvider jwtTokenProvider, ObjectMapper object @When("{string}가 토큰이 만료되어 리프레시 토큰을 사용해 새 토큰을 받는다.") public void 토큰_리프레시(String githubId) throws JsonProcessingException { - TokenRequest request = new TokenRequest(sharedContext.getRefresh(githubId)); + RefreshTokenRequest request = new RefreshTokenRequest(sharedContext.getRefresh(githubId)); ExtractableResponse response = given().log().all() .contentType(MediaType.APPLICATION_JSON_VALUE) @@ -66,7 +66,7 @@ public AuthorizationSteps(JwtTokenProvider jwtTokenProvider, ObjectMapper object @Given("{string}가 {string}의 리프레시 토큰을 탈취하여 새 토큰을 받는다.") public void 토큰_탈취(String thief, String victim) throws JsonProcessingException { - TokenRequest request = new TokenRequest(sharedContext.getRefresh(victim)); + RefreshTokenRequest request = new RefreshTokenRequest(sharedContext.getRefresh(victim)); TokenResponse response = given().log().all() .contentType(MediaType.APPLICATION_JSON_VALUE) From e75bf0e1fbce9e28474ed6fee0ccd381b3a6fc17 Mon Sep 17 00:00:00 2001 From: bjk1649 Date: Tue, 31 Oct 2023 14:05:18 +0900 Subject: [PATCH 2/8] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=EC=A4=91=20(=EC=9E=84=EC=8B=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/application/StudyService.java | 6 ++ .../backend/domain/feedpost/FeedPost.java | 2 +- .../infra/FirebaseCloudMessageSender.java | 32 +++++++++ .../infra/FirebaseCloudMessagingService.java | 42 +++++------ .../infra/FirebaseMemberRepository.java | 11 --- ...nfig.java => FirebaseMessagingConfig.java} | 2 +- ...FirebaseMember.java => FirebaseToken.java} | 6 +- .../infra/FirebaseTokenRepository.java | 13 ++++ .../infra/InMemoryTokenRepository.java | 29 ++++++++ .../backend/infra/LocalMessageSender.java | 31 ++++++++ .../backend/infra/LocalMessagingService.java | 25 ------- .../yigongil/backend/infra/MessageSender.java | 10 +++ .../backend/infra/MessagingService.java | 21 +++++- .../backend/infra/TokenRepository.java | 10 +++ .../request/MessagingTokenRequest.java | 7 ++ .../backend/ui/FirebaseTokenRequest.java | 7 -- .../yigongil/backend/ui/LoginController.java | 7 +- .../com/yigongil/backend/ui/doc/LoginApi.java | 4 +- .../acceptance/steps/AcceptanceTest.java | 10 +++ .../backend/acceptance/steps/MemberSteps.java | 9 +++ .../acceptance/steps/MessagingSteps.java | 70 +++++++++++++++++++ .../acceptance/steps/SharedContext.java | 10 +++ .../yigongil/backend/fake/FakeController.java | 18 ++++- .../features/notification-test.feature | 13 ++++ 24 files changed, 313 insertions(+), 82 deletions(-) create mode 100644 backend/src/main/java/com/yigongil/backend/infra/FirebaseCloudMessageSender.java delete mode 100644 backend/src/main/java/com/yigongil/backend/infra/FirebaseMemberRepository.java rename backend/src/main/java/com/yigongil/backend/infra/{FirebaseConfig.java => FirebaseMessagingConfig.java} (97%) rename backend/src/main/java/com/yigongil/backend/infra/{FirebaseMember.java => FirebaseToken.java} (85%) create mode 100644 backend/src/main/java/com/yigongil/backend/infra/FirebaseTokenRepository.java create mode 100644 backend/src/main/java/com/yigongil/backend/infra/InMemoryTokenRepository.java create mode 100644 backend/src/main/java/com/yigongil/backend/infra/LocalMessageSender.java delete mode 100644 backend/src/main/java/com/yigongil/backend/infra/LocalMessagingService.java create mode 100644 backend/src/main/java/com/yigongil/backend/infra/MessageSender.java create mode 100644 backend/src/main/java/com/yigongil/backend/infra/TokenRepository.java create mode 100644 backend/src/main/java/com/yigongil/backend/request/MessagingTokenRequest.java delete mode 100644 backend/src/main/java/com/yigongil/backend/ui/FirebaseTokenRequest.java create mode 100644 backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java create mode 100644 backend/src/test/resources/features/notification-test.feature diff --git a/backend/src/main/java/com/yigongil/backend/application/StudyService.java b/backend/src/main/java/com/yigongil/backend/application/StudyService.java index 94186030b..9400938e2 100644 --- a/backend/src/main/java/com/yigongil/backend/application/StudyService.java +++ b/backend/src/main/java/com/yigongil/backend/application/StudyService.java @@ -33,6 +33,8 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import javax.persistence.EntityManager; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; @@ -47,6 +49,9 @@ public class StudyService { private final FeedService feedService; private final RoundRepository roundRepository; + @Autowired + EntityManager em; + public StudyService( StudyRepository studyRepository, StudyMemberRepository studyMemberRepository, @@ -128,6 +133,7 @@ public List findMyStudies(Member member) { public void apply(Member member, Long studyId) { Study study = findStudyById(studyId); study.apply(member); +// studyRepository.save(study); } @Transactional diff --git a/backend/src/main/java/com/yigongil/backend/domain/feedpost/FeedPost.java b/backend/src/main/java/com/yigongil/backend/domain/feedpost/FeedPost.java index 64f078fd7..3443daca3 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/feedpost/FeedPost.java +++ b/backend/src/main/java/com/yigongil/backend/domain/feedpost/FeedPost.java @@ -64,7 +64,7 @@ public void registerCreatedEvent() { author.getGithubId(), content, imageUrl, - createdAt, + createdAt )); } diff --git a/backend/src/main/java/com/yigongil/backend/infra/FirebaseCloudMessageSender.java b/backend/src/main/java/com/yigongil/backend/infra/FirebaseCloudMessageSender.java new file mode 100644 index 000000000..49ee708de --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/infra/FirebaseCloudMessageSender.java @@ -0,0 +1,32 @@ +package com.yigongil.backend.infra; + +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.Message; +import java.util.List; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Profile(value = {"prod", "dev"}) +@Component +public class FirebaseCloudMessageSender implements MessageSender { + + private final FirebaseMessaging firebaseMessaging; + + public FirebaseCloudMessageSender(FirebaseMessaging firebaseMessaging) { + this.firebaseMessaging = firebaseMessaging; + } + + @Override + public void subscribeToTopicAsync(List tokens, String topic) { + firebaseMessaging.subscribeToTopicAsync(tokens, topic); + } + + @Override + public void send(String message, String topic) { + Message topicMessage = Message.builder() + .setTopic(topic) + .putData("message", message) + .build(); + firebaseMessaging.sendAsync(topicMessage); + } +} diff --git a/backend/src/main/java/com/yigongil/backend/infra/FirebaseCloudMessagingService.java b/backend/src/main/java/com/yigongil/backend/infra/FirebaseCloudMessagingService.java index acd7ee6c0..818e12de9 100644 --- a/backend/src/main/java/com/yigongil/backend/infra/FirebaseCloudMessagingService.java +++ b/backend/src/main/java/com/yigongil/backend/infra/FirebaseCloudMessagingService.java @@ -1,7 +1,5 @@ package com.yigongil.backend.infra; -import com.google.firebase.messaging.FirebaseMessaging; -import com.google.firebase.messaging.Message; import com.yigongil.backend.domain.event.CertificationCreatedEvent; import com.yigongil.backend.domain.event.FeedPostCreatedEvent; import com.yigongil.backend.domain.event.MustDoUpdatedEvent; @@ -12,26 +10,24 @@ import com.yigongil.backend.domain.member.Member; import java.util.List; import org.jetbrains.annotations.NotNull; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; -@Profile(value = {"prod", "dev"}) -@Component +@Service public class FirebaseCloudMessagingService implements MessagingService { - private final FirebaseMessaging firebaseMessaging; - private final FirebaseMemberRepository firebaseMemberRepository; + private final MessageSender messageSender; + private final TokenRepository tokenRepository; - public FirebaseCloudMessagingService(FirebaseMessaging firebaseMessaging, final FirebaseMemberRepository firebaseMemberRepository) { - this.firebaseMessaging = firebaseMessaging; - this.firebaseMemberRepository = firebaseMemberRepository; + public FirebaseCloudMessagingService(MessageSender messageSender, TokenRepository tokenRepository) { + this.messageSender = messageSender; + this.tokenRepository = tokenRepository; } @Override public void registerToken(String token, Member member) { - firebaseMemberRepository.save(new FirebaseMember(member, token)); + tokenRepository.save(new FirebaseToken(member, token)); subscribeToTopic(List.of(token), getMemberTopicById(member.getId())); } @@ -74,40 +70,34 @@ public void sendNotificationOfFeedPostCreated(FeedPostCreatedEvent event) { } private List findAllTokensByMemberId(Long memberId) { - return firebaseMemberRepository.findAllByMemberId(memberId) - .stream() - .map(FirebaseMember::getToken) - .toList(); + return tokenRepository.findAllByMemberId(memberId) + .stream() + .map(FirebaseToken::getToken) + .toList(); } public void subscribeToTopic(List tokens, String topic) { - firebaseMessaging.subscribeToTopicAsync(tokens, topic); + messageSender.subscribeToTopicAsync(tokens, topic); } - @Override public void sendToMember(String message, Long memberId) { doSend(message, getMemberTopicById(memberId)); } - private String getMemberTopicById(final Long memberId) { + private String getMemberTopicById(Long memberId) { return "member " + memberId; } - @Override public void sendToStudy(String message, Long studyId) { doSend(message, getStudyTopicById(studyId)); } @NotNull - private static String getStudyTopicById(final Long studyId) { + private static String getStudyTopicById(Long studyId) { return "study " + studyId; } private void doSend(String message, String topic) { - Message tokenMessage = Message.builder() - .setTopic(topic) - .putData("message", message) - .build(); - firebaseMessaging.sendAsync(tokenMessage); + messageSender.send(message, topic); } } diff --git a/backend/src/main/java/com/yigongil/backend/infra/FirebaseMemberRepository.java b/backend/src/main/java/com/yigongil/backend/infra/FirebaseMemberRepository.java deleted file mode 100644 index 9fbb58ee7..000000000 --- a/backend/src/main/java/com/yigongil/backend/infra/FirebaseMemberRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.yigongil.backend.infra; - -import java.util.List; -import org.springframework.data.repository.Repository; - -public interface FirebaseMemberRepository extends Repository { - - void save(FirebaseMember firebaseMember); - - List findAllByMemberId(Long memberId); -} diff --git a/backend/src/main/java/com/yigongil/backend/infra/FirebaseConfig.java b/backend/src/main/java/com/yigongil/backend/infra/FirebaseMessagingConfig.java similarity index 97% rename from backend/src/main/java/com/yigongil/backend/infra/FirebaseConfig.java rename to backend/src/main/java/com/yigongil/backend/infra/FirebaseMessagingConfig.java index 4a485435b..3ef237d01 100644 --- a/backend/src/main/java/com/yigongil/backend/infra/FirebaseConfig.java +++ b/backend/src/main/java/com/yigongil/backend/infra/FirebaseMessagingConfig.java @@ -12,7 +12,7 @@ @Profile(value = {"prod", "dev"}) @Configuration -public class FirebaseConfig { +public class FirebaseMessagingConfig { @Bean public FirebaseMessaging messagingService(FirebaseApp firebaseApp) { diff --git a/backend/src/main/java/com/yigongil/backend/infra/FirebaseMember.java b/backend/src/main/java/com/yigongil/backend/infra/FirebaseToken.java similarity index 85% rename from backend/src/main/java/com/yigongil/backend/infra/FirebaseMember.java rename to backend/src/main/java/com/yigongil/backend/infra/FirebaseToken.java index 35e88f06d..472ea9d07 100644 --- a/backend/src/main/java/com/yigongil/backend/infra/FirebaseMember.java +++ b/backend/src/main/java/com/yigongil/backend/infra/FirebaseToken.java @@ -12,7 +12,7 @@ import lombok.Getter; @Getter -public class FirebaseMember extends BaseEntity { +public class FirebaseToken extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) @Id @@ -25,10 +25,10 @@ public class FirebaseMember extends BaseEntity { @Column(name = "token", nullable = false) private String token; - protected FirebaseMember() { + protected FirebaseToken() { } - public FirebaseMember(Member member, String token) { + public FirebaseToken(Member member, String token) { this.member = member; this.token = token; } diff --git a/backend/src/main/java/com/yigongil/backend/infra/FirebaseTokenRepository.java b/backend/src/main/java/com/yigongil/backend/infra/FirebaseTokenRepository.java new file mode 100644 index 000000000..6b2c874ea --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/infra/FirebaseTokenRepository.java @@ -0,0 +1,13 @@ +package com.yigongil.backend.infra; + +import java.util.List; +import org.springframework.context.annotation.Profile; +import org.springframework.data.repository.Repository; + +@Profile(value = {"dev", "prod"}) +public interface FirebaseTokenRepository extends Repository, TokenRepository { + + void save(FirebaseToken firebaseToken); + + List findAllByMemberId(Long memberId); +} diff --git a/backend/src/main/java/com/yigongil/backend/infra/InMemoryTokenRepository.java b/backend/src/main/java/com/yigongil/backend/infra/InMemoryTokenRepository.java new file mode 100644 index 000000000..293a379c3 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/infra/InMemoryTokenRepository.java @@ -0,0 +1,29 @@ +package com.yigongil.backend.infra; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; + +@Profile(value = {"test", "local"}) +@Repository +public class InMemoryTokenRepository implements TokenRepository { + + private final List firebaseTokens; + + public InMemoryTokenRepository() { + this.firebaseTokens = new ArrayList<>(); + } + + @Override + public void save(FirebaseToken firebaseToken) { + firebaseTokens.add(firebaseToken); + } + + @Override + public List findAllByMemberId(Long memberId) { + return firebaseTokens.stream() + .filter(firebaseToken -> firebaseToken.getMember().getId().equals(memberId)) + .toList(); + } +} diff --git a/backend/src/main/java/com/yigongil/backend/infra/LocalMessageSender.java b/backend/src/main/java/com/yigongil/backend/infra/LocalMessageSender.java new file mode 100644 index 000000000..79eff42f8 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/infra/LocalMessageSender.java @@ -0,0 +1,31 @@ +package com.yigongil.backend.infra; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Profile(value = {"test", "local"}) +@Component +public class LocalMessageSender implements MessageSender { + + private final Map> tokensByTopic; + + public LocalMessageSender() { + this.tokensByTopic = new HashMap<>(); + } + + @Override + public void subscribeToTopicAsync(List tokens, String topic) { + Set savedTokens = tokensByTopic.computeIfAbsent(topic, key -> new HashSet<>()); + savedTokens.addAll(tokens); + } + + @Override + public void send(String message, String topic) { + System.out.println("LocalMessageSender: " + message + " to " + topic); + } +} diff --git a/backend/src/main/java/com/yigongil/backend/infra/LocalMessagingService.java b/backend/src/main/java/com/yigongil/backend/infra/LocalMessagingService.java deleted file mode 100644 index 303dfef93..000000000 --- a/backend/src/main/java/com/yigongil/backend/infra/LocalMessagingService.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.yigongil.backend.infra; - -import com.yigongil.backend.domain.member.Member; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; - -@Profile(value = {"local", "test"}) -@Service -public class LocalMessagingService implements MessagingService { - - @Override - public void registerToken(final String token, final Member member) { - - } - - @Override - public void sendToMember(final String message, final Long memberId) { - - } - - @Override - public void sendToStudy(final String message, final Long studyId) { - - } -} diff --git a/backend/src/main/java/com/yigongil/backend/infra/MessageSender.java b/backend/src/main/java/com/yigongil/backend/infra/MessageSender.java new file mode 100644 index 000000000..fac09566a --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/infra/MessageSender.java @@ -0,0 +1,10 @@ +package com.yigongil.backend.infra; + +import java.util.List; + +public interface MessageSender { + + void subscribeToTopicAsync(List tokens, String topic); + + void send(String message, String topic); +} diff --git a/backend/src/main/java/com/yigongil/backend/infra/MessagingService.java b/backend/src/main/java/com/yigongil/backend/infra/MessagingService.java index 438d6edff..475c35d47 100644 --- a/backend/src/main/java/com/yigongil/backend/infra/MessagingService.java +++ b/backend/src/main/java/com/yigongil/backend/infra/MessagingService.java @@ -1,12 +1,29 @@ package com.yigongil.backend.infra; +import com.yigongil.backend.domain.event.CertificationCreatedEvent; +import com.yigongil.backend.domain.event.FeedPostCreatedEvent; +import com.yigongil.backend.domain.event.MustDoUpdatedEvent; +import com.yigongil.backend.domain.event.StudyAppliedEvent; +import com.yigongil.backend.domain.event.StudyCreatedEvent; +import com.yigongil.backend.domain.event.StudyPermittedEvent; +import com.yigongil.backend.domain.event.StudyStartedEvent; import com.yigongil.backend.domain.member.Member; public interface MessagingService { void registerToken(String token, Member member); - void sendToMember(String message, Long memberId); + void registerStudyMaster(StudyCreatedEvent event); - void sendToStudy(String message, Long studyId); + void sendNotificationOfApplicant(StudyAppliedEvent event); + + void sendNotificationOfPermitted(StudyPermittedEvent event); + + void sendNotificationOfStudyStarted(StudyStartedEvent event); + + void sendNotificationOfMustDoUpdated(MustDoUpdatedEvent event); + + void sendNotificationOfCertificationCreated(CertificationCreatedEvent event); + + void sendNotificationOfFeedPostCreated(FeedPostCreatedEvent event); } diff --git a/backend/src/main/java/com/yigongil/backend/infra/TokenRepository.java b/backend/src/main/java/com/yigongil/backend/infra/TokenRepository.java new file mode 100644 index 000000000..b43dc3bd1 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/infra/TokenRepository.java @@ -0,0 +1,10 @@ +package com.yigongil.backend.infra; + +import java.util.List; + +public interface TokenRepository { + + void save(FirebaseToken firebaseToken); + + List findAllByMemberId(Long memberId); +} diff --git a/backend/src/main/java/com/yigongil/backend/request/MessagingTokenRequest.java b/backend/src/main/java/com/yigongil/backend/request/MessagingTokenRequest.java new file mode 100644 index 000000000..ee148432e --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/request/MessagingTokenRequest.java @@ -0,0 +1,7 @@ +package com.yigongil.backend.request; + +public record MessagingTokenRequest( + String token +) { + +} diff --git a/backend/src/main/java/com/yigongil/backend/ui/FirebaseTokenRequest.java b/backend/src/main/java/com/yigongil/backend/ui/FirebaseTokenRequest.java deleted file mode 100644 index 459140304..000000000 --- a/backend/src/main/java/com/yigongil/backend/ui/FirebaseTokenRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.yigongil.backend.ui; - -public record FirebaseTokenRequest( - String token -) { - -} diff --git a/backend/src/main/java/com/yigongil/backend/ui/LoginController.java b/backend/src/main/java/com/yigongil/backend/ui/LoginController.java index 7a05bc548..401b608af 100644 --- a/backend/src/main/java/com/yigongil/backend/ui/LoginController.java +++ b/backend/src/main/java/com/yigongil/backend/ui/LoginController.java @@ -4,6 +4,7 @@ import com.yigongil.backend.config.auth.Authorization; import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.infra.MessagingService; +import com.yigongil.backend.request.MessagingTokenRequest; import com.yigongil.backend.request.RefreshTokenRequest; import com.yigongil.backend.response.TokenResponse; import com.yigongil.backend.ui.doc.LoginApi; @@ -22,7 +23,7 @@ public class LoginController implements LoginApi { private final AuthService authService; private final MessagingService messagingService; - public LoginController(AuthService authService, final MessagingService messagingService) { + public LoginController(AuthService authService, MessagingService messagingService) { this.authService = authService; this.messagingService = messagingService; } @@ -33,10 +34,10 @@ public ResponseEntity createMemberToken(@RequestParam String code return ResponseEntity.ok(response); } - @PostMapping("/firebase/tokens") + @PostMapping("/messaging/tokens") public ResponseEntity registerDevice( @Authorization Member member, - @RequestBody FirebaseTokenRequest request + @RequestBody MessagingTokenRequest request ) { messagingService.registerToken(request.token(), member); return ResponseEntity.ok().build(); diff --git a/backend/src/main/java/com/yigongil/backend/ui/doc/LoginApi.java b/backend/src/main/java/com/yigongil/backend/ui/doc/LoginApi.java index 636c6f3a8..08593511b 100644 --- a/backend/src/main/java/com/yigongil/backend/ui/doc/LoginApi.java +++ b/backend/src/main/java/com/yigongil/backend/ui/doc/LoginApi.java @@ -2,9 +2,9 @@ import com.yigongil.backend.config.auth.Authorization; import com.yigongil.backend.domain.member.Member; +import com.yigongil.backend.request.MessagingTokenRequest; import com.yigongil.backend.request.RefreshTokenRequest; import com.yigongil.backend.response.TokenResponse; -import com.yigongil.backend.ui.FirebaseTokenRequest; import io.swagger.v3.oas.annotations.Hidden; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody; @@ -15,7 +15,7 @@ public interface LoginApi { ResponseEntity registerDevice( @Authorization Member member, - @RequestBody FirebaseTokenRequest request + @RequestBody MessagingTokenRequest request ); ResponseEntity createMemberToken(@RequestParam String code); diff --git a/backend/src/test/java/com/yigongil/backend/acceptance/steps/AcceptanceTest.java b/backend/src/test/java/com/yigongil/backend/acceptance/steps/AcceptanceTest.java index e5dc55ba7..0323a892c 100644 --- a/backend/src/test/java/com/yigongil/backend/acceptance/steps/AcceptanceTest.java +++ b/backend/src/test/java/com/yigongil/backend/acceptance/steps/AcceptanceTest.java @@ -1,8 +1,10 @@ package com.yigongil.backend.acceptance.steps; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.firebase.messaging.FirebaseMessaging; import com.yigongil.backend.BackendApplication; import com.yigongil.backend.config.auth.TokenTheftDetector; +import com.yigongil.backend.infra.MessagingService; import io.cucumber.java.Before; import io.cucumber.spring.CucumberContextConfiguration; import io.restassured.RestAssured; @@ -11,6 +13,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.ApplicationContext; import org.springframework.test.context.ContextConfiguration; @@ -32,6 +36,12 @@ public class AcceptanceTest { @Autowired ApplicationContext applicationContext; + @SpyBean + MessagingService messagingService; + + @MockBean + FirebaseMessaging firebaseMessaging; + @Before public void before() { RestAssured.port = port; diff --git a/backend/src/test/java/com/yigongil/backend/acceptance/steps/MemberSteps.java b/backend/src/test/java/com/yigongil/backend/acceptance/steps/MemberSteps.java index 9a7fa3015..cf8a479e9 100644 --- a/backend/src/test/java/com/yigongil/backend/acceptance/steps/MemberSteps.java +++ b/backend/src/test/java/com/yigongil/backend/acceptance/steps/MemberSteps.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.request.ProfileUpdateRequest; import com.yigongil.backend.response.NicknameValidationResponse; import com.yigongil.backend.response.OnboardingCheckResponse; @@ -45,6 +46,14 @@ public MemberSteps( sharedContext.setTokens(githubId, tokenResponse.accessToken()); sharedContext.setId(githubId, tokenResponse.accessToken()); + Member member = Member.builder() + .id(sharedContext.getId(githubId)) + .githubId(githubId) + .nickname(githubId) + .profileImageUrl("this_is_fake_image_url") + .isOnboardingDone(true) + .build(); + sharedContext.setMember(githubId, member); sharedContext.setRefresh(githubId, tokenResponse.refreshToken()); } diff --git a/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java b/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java new file mode 100644 index 000000000..d92d7a337 --- /dev/null +++ b/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java @@ -0,0 +1,70 @@ +package com.yigongil.backend.acceptance.steps; + +import static io.restassured.RestAssured.given; +import static org.mockito.Mockito.verify; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yigongil.backend.domain.event.StudyAppliedEvent; +import com.yigongil.backend.infra.MessagingService; +import com.yigongil.backend.request.MessagingTokenRequest; +import io.cucumber.java.en.Then; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +public class MessagingSteps { + + private final ObjectMapper objectMapper; + private final SharedContext sharedContext; + private final MessagingService messagingService; + + public MessagingSteps(ObjectMapper objectMapper, SharedContext sharedContext, MessagingService messagingService) { + this.objectMapper = objectMapper; + this.sharedContext = sharedContext; + this.messagingService = messagingService; + } + + @Then("{string}가 토큰을 메시지 서비스에 등록한다.") + public void 토큰_등록(String githubId) throws JsonProcessingException { + String token = sharedContext.getToken(githubId); + MessagingTokenRequest request = new MessagingTokenRequest(token); + given().log() + .all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(objectMapper.writeValueAsString(request)) + .header("Authorization", token) + .when() + .post("/login/fake/messaging/tokens") + .then() + .statusCode(HttpStatus.OK.value()) + .log() + .all(); + + verify(messagingService).registerToken(token, sharedContext.getMember(githubId)); + } + + @Then("{string}가 {string}의 {string} 스터디 신청 알림을 받는다.") + public void 신청_알림_발송(String masterGithubId, String applicantGithubId, String studyName) { + String token = sharedContext.getToken(applicantGithubId); + ExtractableResponse response = given().log() + .all() + .header(HttpHeaders.AUTHORIZATION, token) + .when() + .post("/studies/" + sharedContext.getParameter(studyName) + "/applicants") + .then() + .log() + .all() + .extract(); + + sharedContext.setResponse(response); + StudyAppliedEvent appliedEvent = new StudyAppliedEvent( + sharedContext.getId(masterGithubId), + studyName, + applicantGithubId + ); + verify(messagingService).sendNotificationOfApplicant(appliedEvent); + } +} diff --git a/backend/src/test/java/com/yigongil/backend/acceptance/steps/SharedContext.java b/backend/src/test/java/com/yigongil/backend/acceptance/steps/SharedContext.java index ebbbf8b6e..5593210c3 100644 --- a/backend/src/test/java/com/yigongil/backend/acceptance/steps/SharedContext.java +++ b/backend/src/test/java/com/yigongil/backend/acceptance/steps/SharedContext.java @@ -1,6 +1,7 @@ package com.yigongil.backend.acceptance.steps; import com.yigongil.backend.config.auth.JwtTokenProvider; +import com.yigongil.backend.domain.member.Member; import io.cucumber.spring.ScenarioScope; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; @@ -17,6 +18,7 @@ public class SharedContext { private ExtractableResponse response; private final Map parameters = new HashMap<>(); + private final Map members = new HashMap<>(); private final Map tokens = new HashMap<>(); @Autowired @@ -65,4 +67,12 @@ public void setTokens(String userName, String token) { public String getToken(String userName) { return tokens.get(userName); } + + public void setMember(final String githubId, final Member member) { + members.put(githubId, member); + } + + public Member getMember(final String githubId) { + return members.get(githubId); + } } diff --git a/backend/src/test/java/com/yigongil/backend/fake/FakeController.java b/backend/src/test/java/com/yigongil/backend/fake/FakeController.java index bc7b5ed20..c340b7b0e 100644 --- a/backend/src/test/java/com/yigongil/backend/fake/FakeController.java +++ b/backend/src/test/java/com/yigongil/backend/fake/FakeController.java @@ -1,9 +1,12 @@ package com.yigongil.backend.fake; import com.yigongil.backend.application.StudyService; +import com.yigongil.backend.config.auth.Authorization; import com.yigongil.backend.config.auth.JwtTokenProvider; import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.domain.member.MemberRepository; +import com.yigongil.backend.infra.MessagingService; +import com.yigongil.backend.request.MessagingTokenRequest; import com.yigongil.backend.response.TokenResponse; import java.time.LocalDate; import java.util.Optional; @@ -12,7 +15,9 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +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.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -23,11 +28,13 @@ public class FakeController { private final MemberRepository memberRepository; private final JwtTokenProvider jwtTokenProvider; private final StudyService studyService; + private final MessagingService messagingService; - public FakeController(MemberRepository memberRepository, JwtTokenProvider jwtTokenProvider, StudyService studyService) { + public FakeController(MemberRepository memberRepository, JwtTokenProvider jwtTokenProvider, StudyService studyService, final MessagingService messagingService) { this.memberRepository = memberRepository; this.jwtTokenProvider = jwtTokenProvider; this.studyService = studyService; + this.messagingService = messagingService; } @GetMapping("/login/fake/tokens") @@ -45,6 +52,15 @@ public ResponseEntity createFakeToken(@RequestParam String github return ResponseEntity.ok().header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).body(new TokenResponse(jwtTokenProvider.createAccessToken(id), jwtTokenProvider.createRefreshToken(id))); } + @PostMapping("/login/fake/messaging/tokens") + public ResponseEntity registerDevice( + @Authorization Member member, + @RequestBody MessagingTokenRequest request + ) { + messagingService.registerToken(request.token(), member); + return ResponseEntity.ok().build(); + } + @PutMapping("/fake/proceed") public ResponseEntity proceed(@RequestParam Integer days) { for (int i = 1; i <= days; i++) { diff --git a/backend/src/test/resources/features/notification-test.feature b/backend/src/test/resources/features/notification-test.feature new file mode 100644 index 000000000..7ea03331c --- /dev/null +++ b/backend/src/test/resources/features/notification-test.feature @@ -0,0 +1,13 @@ +Feature: 알림을 발송한다. + + Scenario: 스터디의 과정에서 알림을 발송한다. + Given "jinwoo"의 깃허브 아이디로 회원가입을 한다. + Given "jinwoo"가 토큰을 메시지 서비스에 등록한다. + Given "jinwoo"가 제목-"자바1", 정원-"6"명, 최소 주차-"7"주, 주당 진행 횟수-"3"회, 소개-"스터디소개1"로 스터디를 개설한다. + Given "noiman"의 깃허브 아이디로 회원가입을 한다. + Given "noiman"가 토큰을 메시지 서비스에 등록한다. + Then "jinwoo"가 "noiman"의 "자바1" 스터디 신청 알림을 받는다. +# Given "jinwoo"가 "noiman"의 "자바1" 스터디 신청을 수락한다. +# Given "jinwoo"가 이름이 "자바1"인 스터디를 "MONDAY"에 진행되도록 하여 시작한다. +# When "jinwoo"가 홈화면을 조회한다. +# Then 스터디의 남은 날짜가 0이상 6 이하이다. From 46e762ad6fdfbb1dc94bb68814b1481355481487 Mon Sep 17 00:00:00 2001 From: yujamint Date: Tue, 14 Nov 2023 13:12:22 +0900 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20=ED=9B=84=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/MustDoServiceTest.java | 6 ++++++ .../backend/domain/round/RoundTest.java | 20 ++++++++++--------- .../backend/fixture/RoundFixture.java | 11 ++++++++++ .../backend/fixture/StudyFixture.java | 1 + 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/backend/src/test/java/com/yigongil/backend/application/MustDoServiceTest.java b/backend/src/test/java/com/yigongil/backend/application/MustDoServiceTest.java index 1ae6eb943..6b3f349b3 100644 --- a/backend/src/test/java/com/yigongil/backend/application/MustDoServiceTest.java +++ b/backend/src/test/java/com/yigongil/backend/application/MustDoServiceTest.java @@ -8,8 +8,10 @@ import com.yigongil.backend.domain.round.Round; import com.yigongil.backend.domain.round.RoundRepository; import com.yigongil.backend.domain.roundofmember.RoundOfMember; +import com.yigongil.backend.domain.study.Study; import com.yigongil.backend.exception.InvalidTodoLengthException; import com.yigongil.backend.fixture.MemberFixture; +import com.yigongil.backend.fixture.StudyFixture; import com.yigongil.backend.request.MustDoUpdateRequest; import java.util.List; import java.util.Optional; @@ -40,8 +42,12 @@ void setUp() { RoundOfMember roundOfMember = RoundOfMember.builder() .member(member) .build(); + + Study study = StudyFixture.자바_스터디_모집중_ID_1.toStudyWithMaster(member); + round = Round.builder() .id(3L) + .study(study) .roundOfMembers(List.of(roundOfMember)) .master(member) .mustDo(null) diff --git a/backend/src/test/java/com/yigongil/backend/domain/round/RoundTest.java b/backend/src/test/java/com/yigongil/backend/domain/round/RoundTest.java index 9001a9230..c263be084 100644 --- a/backend/src/test/java/com/yigongil/backend/domain/round/RoundTest.java +++ b/backend/src/test/java/com/yigongil/backend/domain/round/RoundTest.java @@ -1,18 +1,19 @@ package com.yigongil.backend.domain.round; +import static com.yigongil.backend.fixture.RoundFixture.아이디_삼_머스트두없는_라운드; +import static com.yigongil.backend.fixture.RoundFixture.아이디없는_라운드; +import static com.yigongil.backend.fixture.StudyFixture.자바_스터디_모집중_ID_1; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.exception.InvalidTodoLengthException; import com.yigongil.backend.exception.NotStudyMasterException; -import com.yigongil.backend.fixture.RoundFixture; import com.yigongil.backend.fixture.RoundOfMemberFixture; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; - class RoundTest { @Nested @@ -21,7 +22,7 @@ class 머스트두_생성 { @Test void 정상생성한다() { //given - Round round = RoundFixture.아이디_삼_머스트두없는_라운드.toRound(); + Round round = 아이디_삼_머스트두없는_라운드.toRoundWithStudy(자바_스터디_모집중_ID_1.toStudy()); //then round.updateMustDo(round.getMaster(), " 머스트두"); @@ -33,7 +34,7 @@ class 머스트두_생성 { @Test void 스터디_마스터가_아니라면_예외를_던진다() { //given - Round round = RoundFixture.아이디_삼_머스트두없는_라운드.toRound(); + Round round = 아이디_삼_머스트두없는_라운드.toRound(); final Member another = Member.builder() .id(3L) .introduction("소개") @@ -50,10 +51,11 @@ class 머스트두_생성 { @Test void 길이가_20자를_넘으면_예외를_던진다() { //given - final Round round = RoundFixture.아이디_삼_머스트두없는_라운드.toRound(); + final Round round = 아이디_삼_머스트두없는_라운드.toRound(); //when - ThrowingCallable throwable = () -> round.updateMustDo(round.getMaster(), "굉장히긴머스트두길이입니다이십자도넘을수도있어"); + ThrowingCallable throwable = () -> round.updateMustDo(round.getMaster(), + "굉장히긴머스트두길이입니다이십자도넘을수도있어"); //then assertThatThrownBy(throwable).isInstanceOf(InvalidTodoLengthException.class); @@ -63,7 +65,7 @@ class 머스트두_생성 { @Test void 멤버의_머스트두_완료여부를_반환한다() { //given - Round round = RoundFixture.아이디없는_라운드.toRound(); + Round round = 아이디없는_라운드.toRound(); Member master = round.getMaster(); //when @@ -79,7 +81,7 @@ class 머스트두_진행률_계산 { @Test void 멤버들의_진행률을_계산한다() { //given - Round round = RoundFixture.아이디없는_라운드.toRoundWithRoundOfMember( + Round round = 아이디없는_라운드.toRoundWithRoundOfMember( RoundOfMemberFixture.노이만_라오멤, RoundOfMemberFixture.노이만_라오멤, RoundOfMemberFixture.노이만_라오멤 @@ -96,7 +98,7 @@ class 머스트두_진행률_계산 { @Test void 머스트두를_완료한_멤버가_없다() { //given - Round round = RoundFixture.아이디없는_라운드.toRoundWithRoundOfMember( + Round round = 아이디없는_라운드.toRoundWithRoundOfMember( RoundOfMemberFixture.노이만_라오멤, RoundOfMemberFixture.노이만_라오멤, RoundOfMemberFixture.노이만_라오멤 diff --git a/backend/src/test/java/com/yigongil/backend/fixture/RoundFixture.java b/backend/src/test/java/com/yigongil/backend/fixture/RoundFixture.java index c892b6249..989e97c19 100644 --- a/backend/src/test/java/com/yigongil/backend/fixture/RoundFixture.java +++ b/backend/src/test/java/com/yigongil/backend/fixture/RoundFixture.java @@ -3,6 +3,7 @@ import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.domain.round.Round; import com.yigongil.backend.domain.roundofmember.RoundOfMember; +import com.yigongil.backend.domain.study.Study; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -39,6 +40,16 @@ public Round toRound() { .build(); } + public Round toRoundWithStudy(Study study) { + return Round.builder() + .id(id) + .study(study) + .mustDo(content) + .master(master) + .roundOfMembers(new ArrayList<>(List.of(RoundOfMemberFixture.김진우_라운드_삼.toRoundOfMember(), RoundOfMemberFixture.노이만_라오멤.toRoundOfMember()))) + .build(); + } + public Round toRoundWithRoundOfMember(RoundOfMemberFixture... roundOfMemberFixtures) { List roundOfMembers = Arrays.stream(roundOfMemberFixtures) .map(RoundOfMemberFixture::toRoundOfMember) diff --git a/backend/src/test/java/com/yigongil/backend/fixture/StudyFixture.java b/backend/src/test/java/com/yigongil/backend/fixture/StudyFixture.java index 8a6745942..a2512a32f 100644 --- a/backend/src/test/java/com/yigongil/backend/fixture/StudyFixture.java +++ b/backend/src/test/java/com/yigongil/backend/fixture/StudyFixture.java @@ -12,6 +12,7 @@ public enum StudyFixture { 자바_스터디_모집중(null, LocalDateTime.now(), "자바", "스터디소개", ProcessingStatus.RECRUITING, 4, 2, 4), + 자바_스터디_모집중_ID_1(1L, LocalDateTime.now(), "자바", "스터디소개", ProcessingStatus.RECRUITING, 4, 2, 4), 자바_스터디_모집중_정원_2(null, LocalDateTime.now(), "자바", "스터디소개", ProcessingStatus.RECRUITING, 2, 2, 4) ; From 8d3e7194e776f8e64df1089d98c85f31f195bbc3 Mon Sep 17 00:00:00 2001 From: yujamint Date: Sat, 18 Nov 2023 02:13:24 +0900 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=9C=ED=96=89=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=86=B5=EA=B3=BC?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/yigongil/backend/application/StudyService.java | 7 +------ .../main/java/com/yigongil/backend/domain/study/Study.java | 2 +- .../src/test/resources/features/notification-test.feature | 6 ++---- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/backend/src/main/java/com/yigongil/backend/application/StudyService.java b/backend/src/main/java/com/yigongil/backend/application/StudyService.java index 9400938e2..e99cf83dd 100644 --- a/backend/src/main/java/com/yigongil/backend/application/StudyService.java +++ b/backend/src/main/java/com/yigongil/backend/application/StudyService.java @@ -33,8 +33,6 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; -import javax.persistence.EntityManager; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; @@ -49,9 +47,6 @@ public class StudyService { private final FeedService feedService; private final RoundRepository roundRepository; - @Autowired - EntityManager em; - public StudyService( StudyRepository studyRepository, StudyMemberRepository studyMemberRepository, @@ -133,7 +128,7 @@ public List findMyStudies(Member member) { public void apply(Member member, Long studyId) { Study study = findStudyById(studyId); study.apply(member); -// studyRepository.save(study); + studyRepository.save(study); } @Transactional diff --git a/backend/src/main/java/com/yigongil/backend/domain/study/Study.java b/backend/src/main/java/com/yigongil/backend/domain/study/Study.java index c31e02aa5..c5686c410 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/study/Study.java +++ b/backend/src/main/java/com/yigongil/backend/domain/study/Study.java @@ -89,7 +89,7 @@ public class Study extends BaseEntity { @Column private Long currentRoundNumber; - @Cascade(CascadeType.PERSIST) + @Cascade({CascadeType.MERGE, CascadeType.PERSIST}) @OneToMany(mappedBy = "study", orphanRemoval = true, fetch = FetchType.LAZY) private List studyMembers = new ArrayList<>(); diff --git a/backend/src/test/resources/features/notification-test.feature b/backend/src/test/resources/features/notification-test.feature index 7ea03331c..9b9537c4f 100644 --- a/backend/src/test/resources/features/notification-test.feature +++ b/backend/src/test/resources/features/notification-test.feature @@ -4,10 +4,8 @@ Feature: 알림을 발송한다. Given "jinwoo"의 깃허브 아이디로 회원가입을 한다. Given "jinwoo"가 토큰을 메시지 서비스에 등록한다. Given "jinwoo"가 제목-"자바1", 정원-"6"명, 최소 주차-"7"주, 주당 진행 횟수-"3"회, 소개-"스터디소개1"로 스터디를 개설한다. + Given "noiman"의 깃허브 아이디로 회원가입을 한다. Given "noiman"가 토큰을 메시지 서비스에 등록한다. + Then "jinwoo"가 "noiman"의 "자바1" 스터디 신청 알림을 받는다. -# Given "jinwoo"가 "noiman"의 "자바1" 스터디 신청을 수락한다. -# Given "jinwoo"가 이름이 "자바1"인 스터디를 "MONDAY"에 진행되도록 하여 시작한다. -# When "jinwoo"가 홈화면을 조회한다. -# Then 스터디의 남은 날짜가 0이상 6 이하이다. From 62acc8dad6f53bc520ae4f385d663441e3615d2b Mon Sep 17 00:00:00 2001 From: yujamint Date: Sat, 18 Nov 2023 02:15:33 +0900 Subject: [PATCH 5/8] =?UTF-8?q?test:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=20=EC=88=98=EB=9D=BD=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/application/StudyService.java | 1 + .../acceptance/steps/MessagingSteps.java | 26 +++++++++++++++++++ .../features/notification-test.feature | 1 + 3 files changed, 28 insertions(+) diff --git a/backend/src/main/java/com/yigongil/backend/application/StudyService.java b/backend/src/main/java/com/yigongil/backend/application/StudyService.java index e99cf83dd..b2652c998 100644 --- a/backend/src/main/java/com/yigongil/backend/application/StudyService.java +++ b/backend/src/main/java/com/yigongil/backend/application/StudyService.java @@ -163,6 +163,7 @@ public void permitApplicant(Member master, Long studyId, Long memberId) { Study study = studyMember.getStudy(); study.permit(studyMember.getMember(), master); + studyRepository.save(study); } @Transactional(readOnly = true) diff --git a/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java b/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java index d92d7a337..179ac105a 100644 --- a/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java +++ b/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.yigongil.backend.domain.event.StudyAppliedEvent; +import com.yigongil.backend.domain.event.StudyPermittedEvent; import com.yigongil.backend.infra.MessagingService; import com.yigongil.backend.request.MessagingTokenRequest; import io.cucumber.java.en.Then; @@ -67,4 +68,29 @@ public MessagingSteps(ObjectMapper objectMapper, SharedContext sharedContext, Me ); verify(messagingService).sendNotificationOfApplicant(appliedEvent); } + + @Then("{string}가 {string}의 신청을 수락하고, {string} 스터디 신청 수락 알림을 받는다.") + public void 신청수락_알림_발송( + String masterGithubId, + String permittedMemberGithubId, + String studyName + ) { + Object studyId = sharedContext.getParameter(studyName); + Object memberId = sharedContext.getParameter(permittedMemberGithubId); + + given().log().all() + .header(HttpHeaders.AUTHORIZATION, sharedContext.getToken(masterGithubId)) + .when() + .patch("/studies/{studyId}/applicants/{memberId}", studyId, memberId) + .then() + .log().all() + .extract(); + + StudyPermittedEvent studyPermittedEvent = new StudyPermittedEvent( + Long.parseLong(String.valueOf(memberId)), + Long.parseLong(String.valueOf(studyId)), + studyName + ); + verify(messagingService).sendNotificationOfPermitted(studyPermittedEvent); + } } diff --git a/backend/src/test/resources/features/notification-test.feature b/backend/src/test/resources/features/notification-test.feature index 9b9537c4f..f5e04d1a3 100644 --- a/backend/src/test/resources/features/notification-test.feature +++ b/backend/src/test/resources/features/notification-test.feature @@ -9,3 +9,4 @@ Feature: 알림을 발송한다. Given "noiman"가 토큰을 메시지 서비스에 등록한다. Then "jinwoo"가 "noiman"의 "자바1" 스터디 신청 알림을 받는다. + Then "jinwoo"가 "noiman"의 신청을 수락하고, "자바1" 스터디 신청 수락 알림을 받는다. From c7809bbd01262808873dd74adc068cb15fb8ce44 Mon Sep 17 00:00:00 2001 From: yujamint Date: Sun, 19 Nov 2023 22:21:30 +0900 Subject: [PATCH 6/8] =?UTF-8?q?test:=20=EB=A8=B8=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=91=90=20=EB=93=B1=EB=A1=9D=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/application/MustDoService.java | 1 + .../backend/application/StudyService.java | 12 ++- .../MeetingDayOfTheWeekRepository.java | 9 +++ .../yigongil/backend/domain/round/Round.java | 2 +- .../backend/domain/round/RoundRepository.java | 2 + .../yigongil/backend/domain/study/Study.java | 2 +- .../acceptance/steps/MessagingSteps.java | 76 +++++++++++++++++++ .../backend/application/StudyServiceTest.java | 4 +- .../features/notification-test.feature | 4 + 9 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 backend/src/main/java/com/yigongil/backend/domain/meetingdayoftheweek/MeetingDayOfTheWeekRepository.java diff --git a/backend/src/main/java/com/yigongil/backend/application/MustDoService.java b/backend/src/main/java/com/yigongil/backend/application/MustDoService.java index b1ed85e52..68dd6f362 100644 --- a/backend/src/main/java/com/yigongil/backend/application/MustDoService.java +++ b/backend/src/main/java/com/yigongil/backend/application/MustDoService.java @@ -23,6 +23,7 @@ public MustDoService( public void updateMustDo(Member member, Long roundId, MustDoUpdateRequest request) { Round round = findRoundById(roundId); round.updateMustDo(member, request.content()); + roundRepository.save(round); } private Round findRoundById(Long roundId) { diff --git a/backend/src/main/java/com/yigongil/backend/application/StudyService.java b/backend/src/main/java/com/yigongil/backend/application/StudyService.java index b2652c998..d2a25499c 100644 --- a/backend/src/main/java/com/yigongil/backend/application/StudyService.java +++ b/backend/src/main/java/com/yigongil/backend/application/StudyService.java @@ -1,6 +1,8 @@ package com.yigongil.backend.application; import com.yigongil.backend.domain.certification.Certification; +import com.yigongil.backend.domain.meetingdayoftheweek.MeetingDayOfTheWeek; +import com.yigongil.backend.domain.meetingdayoftheweek.MeetingDayOfTheWeekRepository; import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.domain.round.Round; import com.yigongil.backend.domain.round.RoundRepository; @@ -46,18 +48,21 @@ public class StudyService { private final CertificationService certificationService; private final FeedService feedService; private final RoundRepository roundRepository; + private final MeetingDayOfTheWeekRepository meetingDayOfTheWeekRepository; public StudyService( StudyRepository studyRepository, StudyMemberRepository studyMemberRepository, CertificationService certificationService, FeedService feedService, - final RoundRepository roundRepository) { + final RoundRepository roundRepository, + MeetingDayOfTheWeekRepository meetingDayOfTheWeekRepository) { this.studyRepository = studyRepository; this.studyMemberRepository = studyMemberRepository; this.certificationService = certificationService; this.feedService = feedService; this.roundRepository = roundRepository; + this.meetingDayOfTheWeekRepository = meetingDayOfTheWeekRepository; } @Transactional @@ -232,9 +237,12 @@ public void proceedRound(LocalDate today) { @Transactional public void start(Member member, Long studyId, StudyStartRequest request) { List meetingDaysOfTheWeek = createDayOfWeek(request.meetingDaysOfTheWeek()); - Study study = findStudyById(studyId); study.start(member, meetingDaysOfTheWeek, LocalDateTime.now()); + for (MeetingDayOfTheWeek meetingDayOfTheWeek : study.getMeetingDaysOfTheWeek()) { + meetingDayOfTheWeekRepository.save(meetingDayOfTheWeek); + } + studyRepository.save(study); } private List createDayOfWeek(List daysOfTheWeek) { diff --git a/backend/src/main/java/com/yigongil/backend/domain/meetingdayoftheweek/MeetingDayOfTheWeekRepository.java b/backend/src/main/java/com/yigongil/backend/domain/meetingdayoftheweek/MeetingDayOfTheWeekRepository.java new file mode 100644 index 000000000..d669bc9b0 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/domain/meetingdayoftheweek/MeetingDayOfTheWeekRepository.java @@ -0,0 +1,9 @@ +package com.yigongil.backend.domain.meetingdayoftheweek; + +import org.springframework.data.repository.Repository; + +public interface MeetingDayOfTheWeekRepository extends Repository { + + MeetingDayOfTheWeek save(MeetingDayOfTheWeek meetingDayOfTheWeek); + +} diff --git a/backend/src/main/java/com/yigongil/backend/domain/round/Round.java b/backend/src/main/java/com/yigongil/backend/domain/round/Round.java index 6458288b9..aeb3ff838 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/round/Round.java +++ b/backend/src/main/java/com/yigongil/backend/domain/round/Round.java @@ -55,7 +55,7 @@ public class Round extends BaseEntity { @JoinColumn(name = "master_id", nullable = false) private Member master; - @Cascade(CascadeType.PERSIST) + @Cascade({CascadeType.PERSIST, CascadeType.MERGE}) @OnDelete(action = OnDeleteAction.CASCADE) @OneToMany @JoinColumn(name = "round_id", nullable = false) diff --git a/backend/src/main/java/com/yigongil/backend/domain/round/RoundRepository.java b/backend/src/main/java/com/yigongil/backend/domain/round/RoundRepository.java index 0138ff6dc..ad71dff0a 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/round/RoundRepository.java +++ b/backend/src/main/java/com/yigongil/backend/domain/round/RoundRepository.java @@ -11,4 +11,6 @@ public interface RoundRepository extends Repository { Optional findById(Long id); List findAllByStudyIdAndWeekNumber(Long studyId, Integer weekNumber); + + Round save(Round round); } diff --git a/backend/src/main/java/com/yigongil/backend/domain/study/Study.java b/backend/src/main/java/com/yigongil/backend/domain/study/Study.java index c5686c410..41218d088 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/study/Study.java +++ b/backend/src/main/java/com/yigongil/backend/domain/study/Study.java @@ -94,7 +94,7 @@ public class Study extends BaseEntity { private List studyMembers = new ArrayList<>(); - @Cascade(CascadeType.PERSIST) + @Cascade({CascadeType.PERSIST, CascadeType.MERGE}) @OnDelete(action = OnDeleteAction.CASCADE) @OneToMany(mappedBy = "study", orphanRemoval = true, fetch = FetchType.LAZY) private List rounds = new ArrayList<>(); diff --git a/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java b/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java index 179ac105a..ccf1cb832 100644 --- a/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java +++ b/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java @@ -5,13 +5,22 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.yigongil.backend.domain.event.MustDoUpdatedEvent; import com.yigongil.backend.domain.event.StudyAppliedEvent; import com.yigongil.backend.domain.event.StudyPermittedEvent; +import com.yigongil.backend.domain.event.StudyStartedEvent; +import com.yigongil.backend.domain.round.RoundStatus; import com.yigongil.backend.infra.MessagingService; import com.yigongil.backend.request.MessagingTokenRequest; +import com.yigongil.backend.request.MustDoUpdateRequest; +import com.yigongil.backend.request.StudyStartRequest; +import com.yigongil.backend.response.MembersCertificationResponse; +import com.yigongil.backend.response.RoundResponse; import io.cucumber.java.en.Then; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import java.util.Arrays; +import java.util.List; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -93,4 +102,71 @@ public MessagingSteps(ObjectMapper objectMapper, SharedContext sharedContext, Me ); verify(messagingService).sendNotificationOfPermitted(studyPermittedEvent); } + + @Then("{string}가 {string} 스터디를 {string}에 진행되도록 하여 시작한다. 스터디 시작 알림이 발송된다.") + public void 스터디시작_알림_발송(String masterGithubId, String studyName, String days) { + String token = sharedContext.getToken(masterGithubId); + String studyId = (String) sharedContext.getParameter(studyName); + StudyStartRequest request = new StudyStartRequest( + Arrays.stream(days.split(",")).map(String::strip).toList()); + + given().log().all() + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .when() + .patch("/studies/" + studyId + "/start") + .then().log().all(); + + StudyStartedEvent studyStartedEvent = new StudyStartedEvent( + Long.parseLong(studyId), + studyName + ); + verify(messagingService).sendNotificationOfStudyStarted(studyStartedEvent); + } + + @Then("{string}가 {string} 스터디의 현재 주차에 {string}라는 머스트두를 추가한다. 머스트두 등록 알림이 발송된다.") + public void 머스트두등록_알림_발송(String masterGithubId, String studyName, String mustDoContent) { + String token = sharedContext.getToken(masterGithubId); + String studyId = (String) sharedContext.getParameter(studyName); + + MembersCertificationResponse membersCertificationResponse = given().log().all() + .header(HttpHeaders.AUTHORIZATION, token) + .when() + .get("/studies/" + studyId + "/certifications") + .then().log().all() + .extract() + .as(MembersCertificationResponse.class); + + List roundResponses = given().log().all() + .header(HttpHeaders.AUTHORIZATION, token) + .when() + .get("/studies/" + studyId + "/rounds?weekNumber=" + membersCertificationResponse.upcomingRound().weekNumber()) + .then().log().all() + .extract() + .response() + .jsonPath().getList(".", RoundResponse.class); + + RoundResponse round = roundResponses.stream() + .filter(roundResponse -> roundResponse.status() == RoundStatus.IN_PROGRESS) + .findAny() + .get(); + + MustDoUpdateRequest request = new MustDoUpdateRequest(mustDoContent); + + given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .header(HttpHeaders.AUTHORIZATION, token) + .when() + .put("/rounds/{roundId}/todos", round.id()) + .then().log().all(); + + MustDoUpdatedEvent mustDoUpdatedEvent = new MustDoUpdatedEvent( + Long.valueOf(studyId), + studyName, + mustDoContent + ); + verify(messagingService).sendNotificationOfMustDoUpdated(mustDoUpdatedEvent); + } } diff --git a/backend/src/test/java/com/yigongil/backend/application/StudyServiceTest.java b/backend/src/test/java/com/yigongil/backend/application/StudyServiceTest.java index 3c6b4ce1a..33328fe77 100644 --- a/backend/src/test/java/com/yigongil/backend/application/StudyServiceTest.java +++ b/backend/src/test/java/com/yigongil/backend/application/StudyServiceTest.java @@ -4,6 +4,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import com.yigongil.backend.domain.meetingdayoftheweek.MeetingDayOfTheWeekRepository; import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.domain.round.RoundRepository; import com.yigongil.backend.domain.study.StudyRepository; @@ -29,7 +30,8 @@ class StudyServiceTest { studyMemberRepository, mock(CertificationService.class), mock(FeedService.class), - mock(RoundRepository.class) + mock(RoundRepository.class), + mock(MeetingDayOfTheWeekRepository.class) ); @Nested diff --git a/backend/src/test/resources/features/notification-test.feature b/backend/src/test/resources/features/notification-test.feature index f5e04d1a3..acb8d7240 100644 --- a/backend/src/test/resources/features/notification-test.feature +++ b/backend/src/test/resources/features/notification-test.feature @@ -10,3 +10,7 @@ Feature: 알림을 발송한다. Then "jinwoo"가 "noiman"의 "자바1" 스터디 신청 알림을 받는다. Then "jinwoo"가 "noiman"의 신청을 수락하고, "자바1" 스터디 신청 수락 알림을 받는다. + + Then "jinwoo"가 "자바1" 스터디를 "MONDAY"에 진행되도록 하여 시작한다. 스터디 시작 알림이 발송된다. + + Then "jinwoo"가 "자바1" 스터디의 현재 주차에 "이번주 머두"라는 머스트두를 추가한다. 머스트두 등록 알림이 발송된다. From ee1ffaf94161adeca8f586a3faf6bf4f53026b5d Mon Sep 17 00:00:00 2001 From: yujamint Date: Sun, 19 Nov 2023 22:59:44 +0900 Subject: [PATCH 7/8] =?UTF-8?q?test:=20=ED=94=BC=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acceptance/steps/MessagingSteps.java | 19 +++++++++++++++++++ .../features/notification-test.feature | 2 ++ 2 files changed, 21 insertions(+) diff --git a/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java b/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java index ccf1cb832..4e3bf4362 100644 --- a/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java +++ b/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java @@ -1,6 +1,7 @@ package com.yigongil.backend.acceptance.steps; import static io.restassured.RestAssured.given; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import com.fasterxml.jackson.core.JsonProcessingException; @@ -11,6 +12,7 @@ import com.yigongil.backend.domain.event.StudyStartedEvent; import com.yigongil.backend.domain.round.RoundStatus; import com.yigongil.backend.infra.MessagingService; +import com.yigongil.backend.request.FeedPostCreateRequest; import com.yigongil.backend.request.MessagingTokenRequest; import com.yigongil.backend.request.MustDoUpdateRequest; import com.yigongil.backend.request.StudyStartRequest; @@ -169,4 +171,21 @@ public MessagingSteps(ObjectMapper objectMapper, SharedContext sharedContext, Me ); verify(messagingService).sendNotificationOfMustDoUpdated(mustDoUpdatedEvent); } + + @Then("{string}가 {string}스터디 피드에 {string}의 글을 작성한다. 피드 등록 알림이 발송된다.") + public void 피드등록_알림_발송(String memberGithubId, String studyName, String feedContent) { + String token = sharedContext.getToken(memberGithubId); + FeedPostCreateRequest request = new FeedPostCreateRequest(feedContent, "https://yigongil.png"); + + given().log().all() + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .when() + .post("/studies/{studyId}/feeds", sharedContext.getParameter(studyName)) + .then().log().all() + .statusCode(HttpStatus.OK.value()); + + verify(messagingService).sendNotificationOfFeedPostCreated(any()); + } } diff --git a/backend/src/test/resources/features/notification-test.feature b/backend/src/test/resources/features/notification-test.feature index acb8d7240..28cec975a 100644 --- a/backend/src/test/resources/features/notification-test.feature +++ b/backend/src/test/resources/features/notification-test.feature @@ -14,3 +14,5 @@ Feature: 알림을 발송한다. Then "jinwoo"가 "자바1" 스터디를 "MONDAY"에 진행되도록 하여 시작한다. 스터디 시작 알림이 발송된다. Then "jinwoo"가 "자바1" 스터디의 현재 주차에 "이번주 머두"라는 머스트두를 추가한다. 머스트두 등록 알림이 발송된다. + + Then "jinwoo"가 "자바1"스터디 피드에 "내용"의 글을 작성한다. 피드 등록 알림이 발송된다. From 1bcb5f63d7b3906bf277dd51a7e41b2bfa26551c Mon Sep 17 00:00:00 2001 From: yujamint Date: Sun, 19 Nov 2023 23:30:14 +0900 Subject: [PATCH 8/8] =?UTF-8?q?test:=20=EB=A8=B8=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=91=90=20=EC=9D=B8=EC=A6=9D=20=EB=93=B1=EB=A1=9D=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acceptance/steps/MessagingSteps.java | 54 ++++++++++++++++--- .../features/notification-test.feature | 4 +- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java b/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java index 4e3bf4362..3e698176c 100644 --- a/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java +++ b/backend/src/test/java/com/yigongil/backend/acceptance/steps/MessagingSteps.java @@ -6,12 +6,14 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.yigongil.backend.domain.event.CertificationCreatedEvent; import com.yigongil.backend.domain.event.MustDoUpdatedEvent; import com.yigongil.backend.domain.event.StudyAppliedEvent; import com.yigongil.backend.domain.event.StudyPermittedEvent; import com.yigongil.backend.domain.event.StudyStartedEvent; import com.yigongil.backend.domain.round.RoundStatus; import com.yigongil.backend.infra.MessagingService; +import com.yigongil.backend.request.CertificationCreateRequest; import com.yigongil.backend.request.FeedPostCreateRequest; import com.yigongil.backend.request.MessagingTokenRequest; import com.yigongil.backend.request.MustDoUpdateRequest; @@ -33,7 +35,8 @@ public class MessagingSteps { private final SharedContext sharedContext; private final MessagingService messagingService; - public MessagingSteps(ObjectMapper objectMapper, SharedContext sharedContext, MessagingService messagingService) { + public MessagingSteps(ObjectMapper objectMapper, SharedContext sharedContext, + MessagingService messagingService) { this.objectMapper = objectMapper; this.sharedContext = sharedContext; this.messagingService = messagingService; @@ -65,7 +68,9 @@ public MessagingSteps(ObjectMapper objectMapper, SharedContext sharedContext, Me .all() .header(HttpHeaders.AUTHORIZATION, token) .when() - .post("/studies/" + sharedContext.getParameter(studyName) + "/applicants") + .post("/studies/" + + sharedContext.getParameter( + studyName) + "/applicants") .then() .log() .all() @@ -133,9 +138,12 @@ public MessagingSteps(ObjectMapper objectMapper, SharedContext sharedContext, Me String studyId = (String) sharedContext.getParameter(studyName); MembersCertificationResponse membersCertificationResponse = given().log().all() - .header(HttpHeaders.AUTHORIZATION, token) + .header(HttpHeaders.AUTHORIZATION, + token) .when() - .get("/studies/" + studyId + "/certifications") + .get("/studies/" + + studyId + + "/certifications") .then().log().all() .extract() .as(MembersCertificationResponse.class); @@ -143,14 +151,18 @@ public MessagingSteps(ObjectMapper objectMapper, SharedContext sharedContext, Me List roundResponses = given().log().all() .header(HttpHeaders.AUTHORIZATION, token) .when() - .get("/studies/" + studyId + "/rounds?weekNumber=" + membersCertificationResponse.upcomingRound().weekNumber()) + .get("/studies/" + studyId + + "/rounds?weekNumber=" + + membersCertificationResponse.upcomingRound() + .weekNumber()) .then().log().all() .extract() .response() .jsonPath().getList(".", RoundResponse.class); RoundResponse round = roundResponses.stream() - .filter(roundResponse -> roundResponse.status() == RoundStatus.IN_PROGRESS) + .filter(roundResponse -> roundResponse.status() + == RoundStatus.IN_PROGRESS) .findAny() .get(); @@ -175,7 +187,8 @@ public MessagingSteps(ObjectMapper objectMapper, SharedContext sharedContext, Me @Then("{string}가 {string}스터디 피드에 {string}의 글을 작성한다. 피드 등록 알림이 발송된다.") public void 피드등록_알림_발송(String memberGithubId, String studyName, String feedContent) { String token = sharedContext.getToken(memberGithubId); - FeedPostCreateRequest request = new FeedPostCreateRequest(feedContent, "https://yigongil.png"); + FeedPostCreateRequest request = new FeedPostCreateRequest(feedContent, + "https://yigongil.png"); given().log().all() .header(HttpHeaders.AUTHORIZATION, token) @@ -188,4 +201,31 @@ public MessagingSteps(ObjectMapper objectMapper, SharedContext sharedContext, Me verify(messagingService).sendNotificationOfFeedPostCreated(any()); } + + @Then("{string}가 {string}스터디 피드에 {string}의 인증 글을 작성한다. 인증 등록 알림이 발송된다.") + public void 인증등록_알림_발송(String memberGithubId, String studyName, String certificationContent) { + String token = sharedContext.getToken(memberGithubId); + String studyId = (String) sharedContext.getParameter(studyName); + + CertificationCreateRequest request = new CertificationCreateRequest( + certificationContent, + "https://yigongil.png" + ); + + given().log().all() + .header(HttpHeaders.AUTHORIZATION, token) + .contentType("application/json") + .body(request) + .when() + .post("/studies/{studyId}/certifications", studyId) + .then().log().all() + .statusCode(HttpStatus.CREATED.value()); + + CertificationCreatedEvent certificationCreatedEvent = new CertificationCreatedEvent( + Long.valueOf(studyId), + studyName, + memberGithubId + ); + verify(messagingService).sendNotificationOfCertificationCreated(certificationCreatedEvent); + } } diff --git a/backend/src/test/resources/features/notification-test.feature b/backend/src/test/resources/features/notification-test.feature index 28cec975a..73204242b 100644 --- a/backend/src/test/resources/features/notification-test.feature +++ b/backend/src/test/resources/features/notification-test.feature @@ -10,9 +10,7 @@ Feature: 알림을 발송한다. Then "jinwoo"가 "noiman"의 "자바1" 스터디 신청 알림을 받는다. Then "jinwoo"가 "noiman"의 신청을 수락하고, "자바1" 스터디 신청 수락 알림을 받는다. - Then "jinwoo"가 "자바1" 스터디를 "MONDAY"에 진행되도록 하여 시작한다. 스터디 시작 알림이 발송된다. - Then "jinwoo"가 "자바1" 스터디의 현재 주차에 "이번주 머두"라는 머스트두를 추가한다. 머스트두 등록 알림이 발송된다. - + Then "jinwoo"가 "자바1"스터디 피드에 "내용"의 인증 글을 작성한다. 인증 등록 알림이 발송된다. Then "jinwoo"가 "자바1"스터디 피드에 "내용"의 글을 작성한다. 피드 등록 알림이 발송된다.