From 9a60c871807c88e5865e05a586f6c4c623483dc3 Mon Sep 17 00:00:00 2001 From: gdrffg Date: Tue, 14 Jan 2025 17:32:19 +0900 Subject: [PATCH 1/8] =?UTF-8?q?Refactor:=20MemberRepository=EC=97=90=20Spr?= =?UTF-8?q?ing=20Data=20Jpa=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MemberRepository에 Spring Data Jpa 도입 - Member 엔티티 관련 Service, Controller, Repository 테스트 - Member, MemberResponse에 Builder 패턴 제거 - DTO들 네이밍 변경 -> xxxRequest, xxxResponse --- .../domain/member/MemberController.java | 39 ++++------ .../domain/member/MemberRepository.java | 50 ++----------- .../example/domain/member/MemberService.java | 29 ++++--- ...erEditForm.java => MemberEditRequest.java} | 3 +- ...erJoinForm.java => MemberJoinRequest.java} | 2 +- .../domain/member/dto/response/MemberDTO.java | 41 ---------- .../member/dto/response/MemberResponse.java | 41 ++++++++++ .../example/domain/member/entity/Member.java | 37 ++++----- app/src/main/resources/application.yml | 2 +- .../domain/member/MemberControllerTest.java | 75 +++++++++++++++++++ .../domain/member/MemberRepositoryTest.java | 51 +++++++++++++ .../domain/member/MemberServiceTest.java | 63 ++++++++++++++++ .../domain/member/MemberTestFixture.java | 29 +++---- 13 files changed, 295 insertions(+), 167 deletions(-) rename app/src/main/java/org/example/domain/member/dto/request/{MemberEditForm.java => MemberEditRequest.java} (94%) rename app/src/main/java/org/example/domain/member/dto/request/{MemberJoinForm.java => MemberJoinRequest.java} (98%) delete mode 100644 app/src/main/java/org/example/domain/member/dto/response/MemberDTO.java create mode 100644 app/src/main/java/org/example/domain/member/dto/response/MemberResponse.java create mode 100644 app/src/test/java/org/example/domain/member/MemberControllerTest.java create mode 100644 app/src/test/java/org/example/domain/member/MemberRepositoryTest.java create mode 100644 app/src/test/java/org/example/domain/member/MemberServiceTest.java diff --git a/app/src/main/java/org/example/domain/member/MemberController.java b/app/src/main/java/org/example/domain/member/MemberController.java index c50d9b2..73c6387 100644 --- a/app/src/main/java/org/example/domain/member/MemberController.java +++ b/app/src/main/java/org/example/domain/member/MemberController.java @@ -2,9 +2,9 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.example.domain.member.dto.request.MemberEditForm; -import org.example.domain.member.dto.request.MemberJoinForm; -import org.example.domain.member.dto.response.MemberDTO; +import org.example.domain.member.dto.request.MemberEditRequest; +import org.example.domain.member.dto.request.MemberJoinRequest; +import org.example.domain.member.dto.response.MemberResponse; import org.example.domain.member.entity.Member; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -15,34 +15,27 @@ public class MemberController { private final MemberService memberService; - x @PostMapping("/add") - public ResponseEntity memberAdd(@Valid @RequestBody MemberJoinForm memberForm) { - Member savedMember = memberService.addMember(memberForm); - MemberDTO memberDTO = MemberDTO.createMemberDTO(savedMember); - return ResponseEntity.status(201).body(memberDTO); + public ResponseEntity memberAdd(@Valid @RequestBody MemberJoinRequest MemberJoinRequest) { + Member savedMember = memberService.addMember(MemberJoinRequest); + MemberResponse memberResponse = MemberResponse.createMemberResponse(savedMember); + return ResponseEntity.status(201).body(memberResponse); + } - // 사용자 프로필 조회 + // 사용자 프로필 조회, 사용자 기본 정보 제공 @GetMapping("/{user_id}") - public ResponseEntity memberDetails(@PathVariable(value = "user_id") Long userId) { + public ResponseEntity memberDetails(@PathVariable(value = "user_id") Long userId) { Member member = memberService.findMember(userId); - MemberDTO memberDTO = MemberDTO.createMemberDTO(member); - return ResponseEntity.ok().body(memberDTO); + MemberResponse memberResponse = MemberResponse.createMemberResponse(member); + return ResponseEntity.ok().body(memberResponse); } - // 사용자 프로필 수정 폼에 초기 데이터 제공 - @GetMapping("/{user_id}/edit") - public ResponseEntity memberModify(@PathVariable(value = "user_id") Long userId) { - Member member = memberService.findMember(userId); - MemberDTO memberDTO = MemberDTO.createMemberDTO(member); - return ResponseEntity.ok().body(memberDTO); - } // 사용자 프로필 수정 @PutMapping("/{user_id}/edit") - public ResponseEntity memberModify(@PathVariable(value = "user_id") Long user_id, @Valid @RequestBody MemberEditForm memberEditForm) { - Member updateMember = memberService.modifyMember(user_id, memberEditForm); - MemberDTO memberDTO = MemberDTO.createMemberDTO(updateMember); - return ResponseEntity.ok().body(memberDTO); + public ResponseEntity memberModify(@PathVariable(value = "user_id") Long user_id, @Valid @RequestBody MemberEditRequest memberEditRequest) { + Member updateMember = memberService.modifyMember(user_id, memberEditRequest); + MemberResponse memberResponse = MemberResponse.createMemberResponse(updateMember); + return ResponseEntity.ok().body(memberResponse); } } diff --git a/app/src/main/java/org/example/domain/member/MemberRepository.java b/app/src/main/java/org/example/domain/member/MemberRepository.java index 487aa0c..f9b0a3d 100644 --- a/app/src/main/java/org/example/domain/member/MemberRepository.java +++ b/app/src/main/java/org/example/domain/member/MemberRepository.java @@ -1,50 +1,16 @@ package org.example.domain.member; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import lombok.extern.slf4j.Slf4j; import org.example.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; +import java.util.Optional; -@Slf4j @Repository -public class MemberRepository { - - @PersistenceContext - private EntityManager em; - - public Member save(Member member) { - em.persist(member); - return member; - } - - public Member findById(Long id) { - return em.find(Member.class, id); - } - - public List findAll() { - List members = em.createQuery("select m from Member m", Member.class).getResultList(); - return members; - } - public Optional findByEmail(String email) { - Member member = em.createQuery("select m from Member m where m.email = :email", Member.class) - .setParameter("email", email).getSingleResult(); - return Optional.ofNullable(member); - } - - public Optional findByEmailAndPassword(String email, String password) { - Optional member = findAll().stream().filter(m -> m.getEmail().equals(email)).findFirst(); - - if(member.isPresent()) { - if(password.equals(member.get().getPassword())) { - return member; - } - } - - return Optional.empty(); - } +public interface MemberRepository extends JpaRepository { + // save() + // findById() + // findAll() + Optional findByEmail(String email); + Optional findByEmailAndPassword(String email, String password); } diff --git a/app/src/main/java/org/example/domain/member/MemberService.java b/app/src/main/java/org/example/domain/member/MemberService.java index 68cf34e..90291c6 100644 --- a/app/src/main/java/org/example/domain/member/MemberService.java +++ b/app/src/main/java/org/example/domain/member/MemberService.java @@ -3,13 +3,11 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.example.domain.language.Language; -import org.example.domain.member.dto.request.MemberEditForm; -import org.example.domain.member.dto.request.MemberJoinForm; -import org.example.domain.member.dto.response.MemberDTO; +import org.example.domain.member.dto.request.MemberEditRequest; +import org.example.domain.member.dto.request.MemberJoinRequest; import org.example.domain.member.entity.Member; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.PutMapping; @Service @RequiredArgsConstructor @@ -21,22 +19,21 @@ public class MemberService { // 회원가입 @Transactional - public Member addMember(MemberJoinForm memberForm){ - Member member = Member.createMember(memberForm); + public Member addMember(MemberJoinRequest MemberJoinRequest){ + Member member = Member.createMember(MemberJoinRequest); - for(String lang : memberForm.getLearning()) { + for(String lang : MemberJoinRequest.getLearning()) { Language language = Language.createLanguage(lang); member.addLearning(language); } - memberRepository.save(member); // language 도 함께 저장 - return member; + return memberRepository.save(member); } // 사용자 프로필 조회 public Member findMember(Long user_id){ - Member member = memberRepository.findById(user_id); + Member member = memberRepository.findById(user_id).get(); // 사용자 조회 실패 if(member == null) { @@ -47,22 +44,24 @@ public Member findMember(Long user_id){ } // 사용자 프로필 수정 - @PutMapping("/{user_id}/edit") @Transactional - public Member modifyMember(Long user_id, MemberEditForm memberEditForm){ + public Member modifyMember(Long user_id, MemberEditRequest memberEditRequest){ - Member member = memberRepository.findById(user_id); + Member member = memberRepository.findById(user_id).get(); if(member == null) { throw new RuntimeException("존재하지 않는 사용자입니다."); } + member.clearLearnings(); - for (String lang : memberEditForm.getLearning()) { + for (String lang : memberEditRequest.getLearning()) { Language language = Language.createLanguage(lang); member.addLearning(language); } - return member.editMember(memberEditForm); + + + return member.editMember(memberEditRequest); } } diff --git a/app/src/main/java/org/example/domain/member/dto/request/MemberEditForm.java b/app/src/main/java/org/example/domain/member/dto/request/MemberEditRequest.java similarity index 94% rename from app/src/main/java/org/example/domain/member/dto/request/MemberEditForm.java rename to app/src/main/java/org/example/domain/member/dto/request/MemberEditRequest.java index d94643d..8e1db95 100644 --- a/app/src/main/java/org/example/domain/member/dto/request/MemberEditForm.java +++ b/app/src/main/java/org/example/domain/member/dto/request/MemberEditRequest.java @@ -1,6 +1,5 @@ package org.example.domain.member.dto.request; -import jakarta.validation.Valid; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; import lombok.Builder; @@ -10,7 +9,7 @@ @Data @Builder -public class MemberEditForm { +public class MemberEditRequest { @Size(max = 20, message = "사용자 이름은 최대 20글자 입니다.") private String username; diff --git a/app/src/main/java/org/example/domain/member/dto/request/MemberJoinForm.java b/app/src/main/java/org/example/domain/member/dto/request/MemberJoinRequest.java similarity index 98% rename from app/src/main/java/org/example/domain/member/dto/request/MemberJoinForm.java rename to app/src/main/java/org/example/domain/member/dto/request/MemberJoinRequest.java index 8428e6e..646dcad 100644 --- a/app/src/main/java/org/example/domain/member/dto/request/MemberJoinForm.java +++ b/app/src/main/java/org/example/domain/member/dto/request/MemberJoinRequest.java @@ -11,7 +11,7 @@ @Data @Builder -public class MemberJoinForm { +public class MemberJoinRequest { @NotBlank(message = "이메일은 반드시 입력해야 합니다.") @Email(message = "유효한 이메일 형식이 아닙니다.") diff --git a/app/src/main/java/org/example/domain/member/dto/response/MemberDTO.java b/app/src/main/java/org/example/domain/member/dto/response/MemberDTO.java deleted file mode 100644 index c2f4a6a..0000000 --- a/app/src/main/java/org/example/domain/member/dto/response/MemberDTO.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.example.domain.member.dto.response; - -import lombok.Builder; -import lombok.Data; -import org.example.domain.language.Language; -import org.example.domain.member.entity.Member; - -import java.util.List; - -@Data -@Builder -public class MemberDTO { - private Long id; - private String email; - private String username; - private String nationality; - private String nativeLang; - private String introduction; - private List learnings; - private int follower; - private int following; - private int point; - - public static MemberDTO createMemberDTO(Member member) { - List learnings = member.getLearnings(); - List languages = learnings.stream().map(Language::getLanguage).toList(); - - return MemberDTO.builder() - .id(member.getId()) - .email(member.getEmail()) - .username(member.getUsername()) - .nationality(member.getNationality()) - .nativeLang(member.getNativeLang()) - .introduction(member.getIntroduction()) - .learnings(languages) - .follower(member.getFollower()) - .following(member.getFollowing()) - .point(member.getPoint()) - .build(); - } -} diff --git a/app/src/main/java/org/example/domain/member/dto/response/MemberResponse.java b/app/src/main/java/org/example/domain/member/dto/response/MemberResponse.java new file mode 100644 index 0000000..c77c147 --- /dev/null +++ b/app/src/main/java/org/example/domain/member/dto/response/MemberResponse.java @@ -0,0 +1,41 @@ +package org.example.domain.member.dto.response; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.example.domain.language.Language; +import org.example.domain.member.entity.Member; + +import java.util.List; + +@Data +@NoArgsConstructor +public class MemberResponse { + private Long id; + private String email; + private String username; + private String nationality; + private String nativeLang; + private String introduction; + private List learnings; + private int follower; + private int following; + private int point; + + public static MemberResponse createMemberResponse(Member member) { + List learnings = member.getLearnings(); + List languages = learnings.stream().map(Language::getLanguage).toList(); + + MemberResponse dto = new MemberResponse(); + dto.id = member.getId(); + dto.email = member.getEmail(); + dto.username = member.getUsername(); + dto.nationality = member.getNationality(); + dto.nativeLang = member.getNativeLang(); + dto.introduction = member.getIntroduction(); + dto.learnings = languages; + dto.follower = member.getFollower(); + dto.following = member.getFollowing(); + dto.point = member.getPoint(); + return dto; + } +} diff --git a/app/src/main/java/org/example/domain/member/entity/Member.java b/app/src/main/java/org/example/domain/member/entity/Member.java index 11d875a..45e1f6c 100644 --- a/app/src/main/java/org/example/domain/member/entity/Member.java +++ b/app/src/main/java/org/example/domain/member/entity/Member.java @@ -3,8 +3,8 @@ import jakarta.persistence.*; import lombok.*; import org.example.domain.language.Language; -import org.example.domain.member.dto.request.MemberEditForm; -import org.example.domain.member.dto.request.MemberJoinForm; +import org.example.domain.member.dto.request.MemberEditRequest; +import org.example.domain.member.dto.request.MemberJoinRequest; import org.example.domain.question.entity.Question; import org.example.domain.wordbook.entity.WordBook; @@ -15,7 +15,6 @@ @Getter @Setter @NoArgsConstructor @AllArgsConstructor -@Builder public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -32,35 +31,31 @@ public class Member { private int point; @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) - @Builder.Default private List learnings = new ArrayList(); @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) - @Builder.Default private List questions = new ArrayList(); @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) - @Builder.Default private List wordbooks = new ArrayList(); // 생성 메서드 - static public Member createMember(MemberJoinForm memberForm) { - Member member = Member.builder() - .email(memberForm.getEmail()) - .username(memberForm.getUsername()) - .password(memberForm.getPassword()) - .nationality(memberForm.getNationality()) - .nativeLang(memberForm.getNative_lang()) - .introduction(memberForm.getIntroduction()) - .follower(0) - .following(0) - .point(0) - .build(); + static public Member createMember(MemberJoinRequest MemberJoinRequest) { + Member member = new Member(); + member.email = MemberJoinRequest.getEmail(); + member.username = MemberJoinRequest.getUsername(); + member.password = MemberJoinRequest.getPassword(); + member.nationality = MemberJoinRequest.getNationality(); + member.nativeLang = MemberJoinRequest.getNative_lang(); + member.introduction = MemberJoinRequest.getIntroduction(); + member.follower = 0; + member.following = 0; + member.point = 0; return member; } // 임시 수정 메서드 - public Member editMember(MemberEditForm form) { + public Member editMember(MemberEditRequest form) { if (form.getUsername() != null) this.username = form.getUsername(); if (form.getNationality() != null) this.nationality = form.getNationality(); if (form.getNativeLang() != null) this.nativeLang = form.getNativeLang(); @@ -81,10 +76,6 @@ public void clearLearnings() { } this.learnings.clear(); // 리스트 비우기 } - public void removeLearning(Language language) { - this.learnings.remove(language); - language.setMember(null); // 연관관계 제거 - } public void addQuestion(Question question) { this.questions.add(question); diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml index 3ed1c5d..65fe61b 100644 --- a/app/src/main/resources/application.yml +++ b/app/src/main/resources/application.yml @@ -8,7 +8,7 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: - ddl-auto: update + ddl-auto: create show-sql: true database-platform: org.hibernate.dialect.MySQLDialect server: diff --git a/app/src/test/java/org/example/domain/member/MemberControllerTest.java b/app/src/test/java/org/example/domain/member/MemberControllerTest.java new file mode 100644 index 0000000..020c961 --- /dev/null +++ b/app/src/test/java/org/example/domain/member/MemberControllerTest.java @@ -0,0 +1,75 @@ +package org.example.domain.member; + +import org.example.domain.member.dto.request.MemberEditRequest; +import org.example.domain.member.dto.request.MemberJoinRequest; +import org.example.domain.member.dto.response.MemberResponse; +import org.example.domain.member.entity.Member; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.Rollback; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.*; + +@SpringBootTest +@Rollback +class MemberControllerTest { + @Autowired + MemberController memberController; + + @Autowired + MemberRepository memberRepository; + @Test + void memberAdd() { + //Given + MemberJoinRequest MemberJoinRequest = MemberTestFixture.createMemberJoinRequest(); + + //When + ResponseEntity response = memberController.memberAdd(MemberJoinRequest); + + //Then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(response.getBody().getUsername()).isEqualTo("validUsername"); + assertThat(response.getBody().getEmail()).isEqualTo("valid@example.com"); + } + + @Test + @Transactional + // Member와 Language가 1:N 연관관계를 맺고 있고 지연 로딩으로 설정되어 있음. + // 테스트 코드를 호출하는 동안에는 영속성 컨텍스트가 닫혀있으므로 지연 로딩 을 사용할 수 없어 @Transactional 이용 + void memberDetails() { + //Given + Member member = MemberTestFixture.createMember(); + Member saveMember = memberRepository.save(member); + + //When + ResponseEntity response = memberController.memberDetails(saveMember.getId()); + + //Then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody().getUsername()).isEqualTo("validUsername"); + assertThat(response.getBody().getEmail()).isEqualTo("valid@example.com"); + } + + + @Test + void memberModify() { + //Given + Member member = MemberTestFixture.createMember(); + Member saveMember = memberRepository.save(member); + + MemberEditRequest validMemberEditRequest = MemberTestFixture.createValidMemberEditForm(); + + //When + ResponseEntity response = memberController.memberModify(member.getId(), validMemberEditRequest); + + //Then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody().getUsername()).isEqualTo("updatedUsername"); + assertThat(response.getBody().getIntroduction()).isEqualTo("This is my updated introduction."); + } + +} \ No newline at end of file diff --git a/app/src/test/java/org/example/domain/member/MemberRepositoryTest.java b/app/src/test/java/org/example/domain/member/MemberRepositoryTest.java new file mode 100644 index 0000000..136205e --- /dev/null +++ b/app/src/test/java/org/example/domain/member/MemberRepositoryTest.java @@ -0,0 +1,51 @@ +package org.example.domain.member; + +import jakarta.persistence.EntityManager; +import org.example.domain.member.entity.Member; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.Rollback; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Transactional +@Rollback +class MemberRepositoryTest { + @Autowired + MemberRepository memberRepository; + @Autowired + EntityManager em; + // 테스타가 실했다가 성공했다가 반복 -> 테스트에 사용하는 데이터가 같아서 중복된 데이터가 삽입될 가능성이 있음. + // 각 테스트가 독립적으로 실행되도록 해야 함 -> 테스트 데이터를 다르게 구성? + @Test + void findByEmailTest() { + //Given + Member member = Member.createMember(MemberTestFixture.createMemberJoinRequest()); + memberRepository.save(member); + + //When + Member findMember = memberRepository.findByEmail("valid@example.com").orElseThrow(); + //Then + // 통과 실패 Assertions.assertThat(findMember).isEqualTo(member); + assertThat(findMember.getEmail()).isEqualTo(member.getEmail()); + + } + + @Test + void findByEmailAndPasswordTeat() { + //Given + Member member = Member.createMember(MemberTestFixture.createMemberJoinRequest()); + memberRepository.save(member); + + //When + Member findMember = memberRepository.findByEmailAndPassword("valid@example.com", "validPassword123").orElseThrow(); + + //Then + assertThat(findMember.getEmail()).isEqualTo("valid@example.com"); + assertThat(findMember.getPassword()).isEqualTo("validPassword123"); + + } +} \ No newline at end of file diff --git a/app/src/test/java/org/example/domain/member/MemberServiceTest.java b/app/src/test/java/org/example/domain/member/MemberServiceTest.java new file mode 100644 index 0000000..0e78dc9 --- /dev/null +++ b/app/src/test/java/org/example/domain/member/MemberServiceTest.java @@ -0,0 +1,63 @@ +package org.example.domain.member; + +import org.example.domain.language.Language; +import org.example.domain.member.dto.request.MemberJoinRequest; +import org.example.domain.member.entity.Member; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.Rollback; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Transactional +@Rollback +class MemberServiceTest { + @Autowired + MemberService memberService; + @Test + void addMember() { + //Given + MemberJoinRequest MemberJoinRequest = MemberTestFixture.createMemberJoinRequest(); + + //When + Member member = memberService.addMember(MemberJoinRequest); + + //Then + assertThat(member.getEmail()).isEqualTo(MemberJoinRequest.getEmail()); + assertThat(member.getPassword()).isEqualTo(MemberJoinRequest.getPassword()); + } + + @Test + void findMember() { + //Given + MemberJoinRequest MemberJoinRequest = MemberTestFixture.createMemberJoinRequest(); + Member member = memberService.addMember(MemberJoinRequest); + + //When + Member findMember = memberService.findMember(member.getId()); + + //Then + assertThat(findMember).isEqualTo(member); + } + + @Test + void modifyMember() { + //Given + MemberJoinRequest MemberJoinRequest = MemberTestFixture.createMemberJoinRequest(); + Member member = memberService.addMember(MemberJoinRequest); + + //When + Member editMember = memberService.modifyMember(member.getId(), MemberTestFixture.createValidMemberEditForm()); + + //Then + assertThat(editMember).isEqualTo(member); + + // 배우는 언어가 "fr", "ja"에서 "es", "cn"으로 바뀌는지 확인 + assertThat(editMember.getLearnings()) + .extracting(Language::getLanguage) // Language 객체에서 이름 추출 + .containsExactlyInAnyOrder("es", "cn"); + } +} \ No newline at end of file diff --git a/app/src/test/java/org/example/domain/member/MemberTestFixture.java b/app/src/test/java/org/example/domain/member/MemberTestFixture.java index 1eb709b..b909481 100644 --- a/app/src/test/java/org/example/domain/member/MemberTestFixture.java +++ b/app/src/test/java/org/example/domain/member/MemberTestFixture.java @@ -1,7 +1,8 @@ package org.example.domain.member; -import org.example.domain.member.dto.request.MemberEditForm; -import org.example.domain.member.dto.request.MemberJoinForm; +import org.example.domain.member.dto.request.MemberEditRequest; +import org.example.domain.member.dto.request.MemberJoinRequest; +import org.example.domain.member.entity.Member; import java.util.List; @@ -9,8 +10,8 @@ public class MemberTestFixture { // 회원 가입 성공 - public static MemberJoinForm createMemberForm() { - return MemberJoinForm.builder() + public static MemberJoinRequest createMemberJoinRequest() { + return MemberJoinRequest.builder() .email("valid@example.com") // 유효한 이메일 .username("validUsername") // 20자 이하 .password("validPassword123") // 8자 이상 @@ -22,8 +23,8 @@ public static MemberJoinForm createMemberForm() { } // 사용자 프로필 수정 - public static MemberEditForm createValidMemberEditForm() { - return MemberEditForm.builder() + public static MemberEditRequest createValidMemberEditForm() { + return MemberEditRequest.builder() .username("updatedUsername") .nationality("CAN") .nativeLang("fr") @@ -32,18 +33,8 @@ public static MemberEditForm createValidMemberEditForm() { .build(); } - /* - public static Member fakeMember() { - return Member.builder() - .id(1L) - .email("valid@example.com") - .username("validUsername") - .password("validPassword123") - .nationality("USA") - .nativeLang("en") - .learning(List.of("fr", "ja")) - .introduction("I am learning languages!") - .build(); + // 사용자 생성 + public static Member createMember( ){ + return Member.createMember(MemberTestFixture.createMemberJoinRequest()); } - */ } From 66904e8d7bc512478ea7ffcb5b938728107774bf Mon Sep 17 00:00:00 2001 From: gdrffg Date: Tue, 14 Jan 2025 18:10:29 +0900 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20sonarqube=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/MemberController.java | 8 ++--- .../example/domain/member/MemberService.java | 27 ++++++-------- .../member/dto/request/MemberJoinRequest.java | 2 +- .../example/domain/member/entity/Member.java | 36 ++++++++++--------- .../domain/member/MemberControllerTest.java | 6 ++-- .../domain/member/MemberRepositoryTest.java | 2 +- .../domain/member/MemberServiceTest.java | 16 ++++----- .../domain/member/MemberTestFixture.java | 2 +- 8 files changed, 47 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/org/example/domain/member/MemberController.java b/app/src/main/java/org/example/domain/member/MemberController.java index 73c6387..09e2ffd 100644 --- a/app/src/main/java/org/example/domain/member/MemberController.java +++ b/app/src/main/java/org/example/domain/member/MemberController.java @@ -16,8 +16,8 @@ public class MemberController { private final MemberService memberService; @PostMapping("/add") - public ResponseEntity memberAdd(@Valid @RequestBody MemberJoinRequest MemberJoinRequest) { - Member savedMember = memberService.addMember(MemberJoinRequest); + public ResponseEntity memberAdd(@Valid @RequestBody MemberJoinRequest memberJoinRequest) { + Member savedMember = memberService.addMember(memberJoinRequest); MemberResponse memberResponse = MemberResponse.createMemberResponse(savedMember); return ResponseEntity.status(201).body(memberResponse); @@ -33,8 +33,8 @@ public ResponseEntity memberDetails(@PathVariable(value = "user_ // 사용자 프로필 수정 @PutMapping("/{user_id}/edit") - public ResponseEntity memberModify(@PathVariable(value = "user_id") Long user_id, @Valid @RequestBody MemberEditRequest memberEditRequest) { - Member updateMember = memberService.modifyMember(user_id, memberEditRequest); + public ResponseEntity memberModify(@PathVariable(value = "user_id") Long userId, @Valid @RequestBody MemberEditRequest memberEditRequest) { + Member updateMember = memberService.modifyMember(userId, memberEditRequest); MemberResponse memberResponse = MemberResponse.createMemberResponse(updateMember); return ResponseEntity.ok().body(memberResponse); } diff --git a/app/src/main/java/org/example/domain/member/MemberService.java b/app/src/main/java/org/example/domain/member/MemberService.java index 90291c6..c0348e0 100644 --- a/app/src/main/java/org/example/domain/member/MemberService.java +++ b/app/src/main/java/org/example/domain/member/MemberService.java @@ -9,6 +9,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -19,10 +21,10 @@ public class MemberService { // 회원가입 @Transactional - public Member addMember(MemberJoinRequest MemberJoinRequest){ - Member member = Member.createMember(MemberJoinRequest); + public Member addMember(MemberJoinRequest memberJoinRequest){ + Member member = Member.createMember(memberJoinRequest); - for(String lang : MemberJoinRequest.getLearning()) { + for(String lang : memberJoinRequest.getLearning()) { Language language = Language.createLanguage(lang); member.addLearning(language); } @@ -31,27 +33,18 @@ public Member addMember(MemberJoinRequest MemberJoinRequest){ } // 사용자 프로필 조회 - public Member findMember(Long user_id){ - - Member member = memberRepository.findById(user_id).get(); - - // 사용자 조회 실패 - if(member == null) { - throw new RuntimeException("존재하지 않는 사용자입니다."); - } + public Member findMember(Long userId){ + Member member = memberRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("존재하지 않는 사용자입니다.")); return member; } // 사용자 프로필 수정 @Transactional - public Member modifyMember(Long user_id, MemberEditRequest memberEditRequest){ - - Member member = memberRepository.findById(user_id).get(); + public Member modifyMember(Long userId, MemberEditRequest memberEditRequest){ - if(member == null) { - throw new RuntimeException("존재하지 않는 사용자입니다."); - } + Member member = memberRepository.findById(userId).orElseThrow(() -> new RuntimeException("존재하지 않는 사용자입니다.")); member.clearLearnings(); diff --git a/app/src/main/java/org/example/domain/member/dto/request/MemberJoinRequest.java b/app/src/main/java/org/example/domain/member/dto/request/MemberJoinRequest.java index 646dcad..e485012 100644 --- a/app/src/main/java/org/example/domain/member/dto/request/MemberJoinRequest.java +++ b/app/src/main/java/org/example/domain/member/dto/request/MemberJoinRequest.java @@ -34,7 +34,7 @@ public class MemberJoinRequest { regexp = "^(ko|en|ja|cn|fr|ar|es|ru)$", message = "허용되지 않은 언어 코드입니다. (ko, en, ja, cn, fr, ar, es, ru만 허용)" ) - private String native_lang; + private String nativeLang; @Size(min = 1, max = 5, message = "학습 언어는 1~5개까지 선택 가능합니다.") private List<@Pattern( diff --git a/app/src/main/java/org/example/domain/member/entity/Member.java b/app/src/main/java/org/example/domain/member/entity/Member.java index 45e1f6c..9a8b6ed 100644 --- a/app/src/main/java/org/example/domain/member/entity/Member.java +++ b/app/src/main/java/org/example/domain/member/entity/Member.java @@ -39,27 +39,29 @@ public class Member { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List wordbooks = new ArrayList(); + private Member(final MemberJoinRequest memberJoinRequest) { + this.email = memberJoinRequest.getEmail(); + this.username = memberJoinRequest.getUsername(); + this.password = memberJoinRequest.getPassword(); + this.nationality = memberJoinRequest.getNationality(); + this.nativeLang = memberJoinRequest.getNativeLang(); + this.introduction = memberJoinRequest.getIntroduction(); + this.follower = 0; + this.following = 0; + this.point = 50; + } + // 생성 메서드 - static public Member createMember(MemberJoinRequest MemberJoinRequest) { - Member member = new Member(); - member.email = MemberJoinRequest.getEmail(); - member.username = MemberJoinRequest.getUsername(); - member.password = MemberJoinRequest.getPassword(); - member.nationality = MemberJoinRequest.getNationality(); - member.nativeLang = MemberJoinRequest.getNative_lang(); - member.introduction = MemberJoinRequest.getIntroduction(); - member.follower = 0; - member.following = 0; - member.point = 0; - return member; + static public Member createMember(final MemberJoinRequest memberJoinRequest) { + return new Member(memberJoinRequest); } // 임시 수정 메서드 - public Member editMember(MemberEditRequest form) { - if (form.getUsername() != null) this.username = form.getUsername(); - if (form.getNationality() != null) this.nationality = form.getNationality(); - if (form.getNativeLang() != null) this.nativeLang = form.getNativeLang(); - if (form.getIntroduction() != null) this.introduction = form.getIntroduction(); + public Member editMember(final MemberEditRequest request) { + if (request.getUsername() != null) this.username = request.getUsername(); + if (request.getNationality() != null) this.nationality = request.getNationality(); + if (request.getNativeLang() != null) this.nativeLang = request.getNativeLang(); + if (request.getIntroduction() != null) this.introduction = request.getIntroduction(); return this; } diff --git a/app/src/test/java/org/example/domain/member/MemberControllerTest.java b/app/src/test/java/org/example/domain/member/MemberControllerTest.java index 020c961..516d125 100644 --- a/app/src/test/java/org/example/domain/member/MemberControllerTest.java +++ b/app/src/test/java/org/example/domain/member/MemberControllerTest.java @@ -25,10 +25,10 @@ class MemberControllerTest { @Test void memberAdd() { //Given - MemberJoinRequest MemberJoinRequest = MemberTestFixture.createMemberJoinRequest(); + MemberJoinRequest memberJoinRequest = MemberTestFixture.createMemberJoinRequest(); //When - ResponseEntity response = memberController.memberAdd(MemberJoinRequest); + ResponseEntity response = memberController.memberAdd(memberJoinRequest); //Then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); @@ -59,7 +59,7 @@ void memberDetails() { void memberModify() { //Given Member member = MemberTestFixture.createMember(); - Member saveMember = memberRepository.save(member); + memberRepository.save(member); MemberEditRequest validMemberEditRequest = MemberTestFixture.createValidMemberEditForm(); diff --git a/app/src/test/java/org/example/domain/member/MemberRepositoryTest.java b/app/src/test/java/org/example/domain/member/MemberRepositoryTest.java index 136205e..a75c51f 100644 --- a/app/src/test/java/org/example/domain/member/MemberRepositoryTest.java +++ b/app/src/test/java/org/example/domain/member/MemberRepositoryTest.java @@ -29,7 +29,7 @@ void findByEmailTest() { //When Member findMember = memberRepository.findByEmail("valid@example.com").orElseThrow(); //Then - // 통과 실패 Assertions.assertThat(findMember).isEqualTo(member); + //Note: 통과 실패 Assertions.assertThat(findMember).isEqualTo(member); assertThat(findMember.getEmail()).isEqualTo(member.getEmail()); } diff --git a/app/src/test/java/org/example/domain/member/MemberServiceTest.java b/app/src/test/java/org/example/domain/member/MemberServiceTest.java index 0e78dc9..9fa69b9 100644 --- a/app/src/test/java/org/example/domain/member/MemberServiceTest.java +++ b/app/src/test/java/org/example/domain/member/MemberServiceTest.java @@ -20,21 +20,21 @@ class MemberServiceTest { @Test void addMember() { //Given - MemberJoinRequest MemberJoinRequest = MemberTestFixture.createMemberJoinRequest(); + MemberJoinRequest memberJoinRequest = MemberTestFixture.createMemberJoinRequest(); //When - Member member = memberService.addMember(MemberJoinRequest); + Member member = memberService.addMember(memberJoinRequest); //Then - assertThat(member.getEmail()).isEqualTo(MemberJoinRequest.getEmail()); - assertThat(member.getPassword()).isEqualTo(MemberJoinRequest.getPassword()); + assertThat(member.getEmail()).isEqualTo(memberJoinRequest.getEmail()); + assertThat(member.getPassword()).isEqualTo(memberJoinRequest.getPassword()); } @Test void findMember() { //Given - MemberJoinRequest MemberJoinRequest = MemberTestFixture.createMemberJoinRequest(); - Member member = memberService.addMember(MemberJoinRequest); + MemberJoinRequest memberJoinRequest = MemberTestFixture.createMemberJoinRequest(); + Member member = memberService.addMember(memberJoinRequest); //When Member findMember = memberService.findMember(member.getId()); @@ -46,8 +46,8 @@ void findMember() { @Test void modifyMember() { //Given - MemberJoinRequest MemberJoinRequest = MemberTestFixture.createMemberJoinRequest(); - Member member = memberService.addMember(MemberJoinRequest); + MemberJoinRequest memberJoinRequest = MemberTestFixture.createMemberJoinRequest(); + Member member = memberService.addMember(memberJoinRequest); //When Member editMember = memberService.modifyMember(member.getId(), MemberTestFixture.createValidMemberEditForm()); diff --git a/app/src/test/java/org/example/domain/member/MemberTestFixture.java b/app/src/test/java/org/example/domain/member/MemberTestFixture.java index b909481..822cf70 100644 --- a/app/src/test/java/org/example/domain/member/MemberTestFixture.java +++ b/app/src/test/java/org/example/domain/member/MemberTestFixture.java @@ -16,7 +16,7 @@ public static MemberJoinRequest createMemberJoinRequest() { .username("validUsername") // 20자 이하 .password("validPassword123") // 8자 이상 .nationality("USA") - .native_lang("en") // 허용된 언어 코드 + .nativeLang("en") // 허용된 언어 코드 .learning(List.of("fr", "ja")) // 허용된 언어 코드 리스트 .introduction("I am learning languages!") // 50자 이하 .build(); From da903dcdc681aaef2de57d836f7f2b8eeae3fb8b Mon Sep 17 00:00:00 2001 From: gdrffg Date: Wed, 15 Jan 2025 16:26:30 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20JWT=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 세션 기반 인증을 JWT 기반 인증으로 바꾸었습니다. --- app/build.gradle | 9 +++ .../domain/global/config/SecurityConfig.java | 53 ++++++++++++++++ .../domain/global/config/WebMvcConfig.java | 24 ------- .../interceptor/LoginCheckInterceptor.java | 37 ----------- .../org/example/domain/jwt/JWTFilter.java | 51 +++++++++++++++ .../java/org/example/domain/jwt/JWTUtil.java | 42 +++++++++++++ .../org/example/domain/jwt/LoginFilter.java | 63 +++++++++++++++++++ .../domain/login/CustomUserDetails.java | 58 +++++++++++++++++ .../login/CustomUserDetailsService.java | 27 ++++++++ .../example/domain/login/LoginController.java | 39 ------------ .../example/domain/login/LoginService.java | 63 ------------------- .../{LoginForm.java => LoginRequest.java} | 8 +-- .../login/dto/response/LoginResponse.java | 16 ----- .../login/dto/response/LogoutResponse.java | 11 ---- .../domain/member/MemberRepository.java | 2 + .../example/domain/member/MemberService.java | 8 ++- .../member/dto/request/MemberJoinRequest.java | 1 - .../example/domain/member/entity/Member.java | 3 + app/src/main/resources/application.yml | 2 + 19 files changed, 321 insertions(+), 196 deletions(-) create mode 100644 app/src/main/java/org/example/domain/global/config/SecurityConfig.java delete mode 100644 app/src/main/java/org/example/domain/global/config/WebMvcConfig.java delete mode 100644 app/src/main/java/org/example/domain/global/config/interceptor/LoginCheckInterceptor.java create mode 100644 app/src/main/java/org/example/domain/jwt/JWTFilter.java create mode 100644 app/src/main/java/org/example/domain/jwt/JWTUtil.java create mode 100644 app/src/main/java/org/example/domain/jwt/LoginFilter.java create mode 100644 app/src/main/java/org/example/domain/login/CustomUserDetails.java create mode 100644 app/src/main/java/org/example/domain/login/CustomUserDetailsService.java delete mode 100644 app/src/main/java/org/example/domain/login/LoginController.java delete mode 100644 app/src/main/java/org/example/domain/login/LoginService.java rename app/src/main/java/org/example/domain/login/dto/request/{LoginForm.java => LoginRequest.java} (80%) delete mode 100644 app/src/main/java/org/example/domain/login/dto/response/LoginResponse.java delete mode 100644 app/src/main/java/org/example/domain/login/dto/response/LogoutResponse.java diff --git a/app/build.gradle b/app/build.gradle index d42bfcb..47047fb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,6 +25,15 @@ dependencies { testImplementation 'com.h2database:h2' // H2를 테스트용 DB로 사용 시 + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' + + // spring security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + } // Apply a specific Java toolchain to ease working on different environments. diff --git a/app/src/main/java/org/example/domain/global/config/SecurityConfig.java b/app/src/main/java/org/example/domain/global/config/SecurityConfig.java new file mode 100644 index 0000000..8c8ab93 --- /dev/null +++ b/app/src/main/java/org/example/domain/global/config/SecurityConfig.java @@ -0,0 +1,53 @@ +package org.example.domain.global.config; + +import lombok.RequiredArgsConstructor; +import org.example.domain.jwt.JWTFilter; +import org.example.domain.jwt.JWTUtil; +import org.example.domain.jwt.LoginFilter; +import org.example.domain.member.MemberRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + private final AuthenticationConfiguration authenticationConfiguration; + private final JWTUtil jwtUtil; + private final MemberRepository memberRepository; + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + + http.csrf((auth) -> auth.disable()); + http.formLogin((auth) -> auth.disable()); + http.httpBasic((auth) -> auth.disable()); + + http.authorizeHttpRequests((auth) -> + auth.requestMatchers("/login","/profile/add").permitAll() + .anyRequest().authenticated()); + + http.addFilterBefore(new JWTFilter(jwtUtil,memberRepository), LoginFilter.class); + http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration),jwtUtil), UsernamePasswordAuthenticationFilter.class); + + http.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + return http.build(); + } +} diff --git a/app/src/main/java/org/example/domain/global/config/WebMvcConfig.java b/app/src/main/java/org/example/domain/global/config/WebMvcConfig.java deleted file mode 100644 index 9ce0d8f..0000000 --- a/app/src/main/java/org/example/domain/global/config/WebMvcConfig.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.example.domain.global.config; - -import org.example.domain.global.config.interceptor.LoginCheckInterceptor; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -@Profile("!test") -public class WebMvcConfig implements WebMvcConfigurer { - /* - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new LoginCheckInterceptor()) - .order(1) - .addPathPatterns("/**") - .excludePathPatterns( - "/profile/add", - "/login", "/logout"); - } - - */ -} diff --git a/app/src/main/java/org/example/domain/global/config/interceptor/LoginCheckInterceptor.java b/app/src/main/java/org/example/domain/global/config/interceptor/LoginCheckInterceptor.java deleted file mode 100644 index 339605a..0000000 --- a/app/src/main/java/org/example/domain/global/config/interceptor/LoginCheckInterceptor.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.example.domain.global.config.interceptor; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpSession; -import lombok.extern.slf4j.Slf4j; -import org.example.domain.session.SessionConst; -import org.springframework.context.annotation.Profile; -import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.ModelAndView; - -@Slf4j -@Profile("!test") -public class LoginCheckInterceptor implements HandlerInterceptor { - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - - HttpSession session = request.getSession(false); - log.debug("인터셉터 활성화"); - // 미인증 사용자 이면 - if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) { - return false; - } - - return true; - } - - @Override - public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { - - } - - @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { - - } -} diff --git a/app/src/main/java/org/example/domain/jwt/JWTFilter.java b/app/src/main/java/org/example/domain/jwt/JWTFilter.java new file mode 100644 index 0000000..ed1cb56 --- /dev/null +++ b/app/src/main/java/org/example/domain/jwt/JWTFilter.java @@ -0,0 +1,51 @@ +package org.example.domain.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.example.domain.login.CustomUserDetails; +import org.example.domain.member.MemberRepository; +import org.example.domain.member.entity.Member; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +@Slf4j +public class JWTFilter extends OncePerRequestFilter { + private final JWTUtil jwtUtil; + private final MemberRepository memberRepository; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String authorization = request.getHeader("Authorization"); + + if(authorization == null || !authorization.startsWith("Bearer ")) { + log.debug("token is null"); + filterChain.doFilter(request,response); + return; + } + + log.debug("token exists"); + String token = authorization.split(" ")[1]; + + if(jwtUtil.isExpired(token)){ + filterChain.doFilter(request,response); + return; + } + + String username = jwtUtil.getUsername(token); + String role = jwtUtil.getRole(token); + + Member member = memberRepository.findByUsername(username).get(); + CustomUserDetails customUserDetails = new CustomUserDetails(member); + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authToken); + filterChain.doFilter(request,response); + } +} diff --git a/app/src/main/java/org/example/domain/jwt/JWTUtil.java b/app/src/main/java/org/example/domain/jwt/JWTUtil.java new file mode 100644 index 0000000..d83af13 --- /dev/null +++ b/app/src/main/java/org/example/domain/jwt/JWTUtil.java @@ -0,0 +1,42 @@ +package org.example.domain.jwt; + +import io.jsonwebtoken.Jwts; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +//JWT 0.12.3 +@Component +public class JWTUtil { + private SecretKey secretKey; + + public JWTUtil(@Value("${spring.jwt.secret}")String secret) { + secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm()); + } + + public String getUsername(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class); + } + + public String getRole(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class); + } + + public Boolean isExpired(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date()); + } + + public String createJwt(String username, String role, Long expiredMs) { + return Jwts.builder() + .claim("username", username) + .claim("role", role) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiredMs)) + .signWith(secretKey) + .compact(); + } +} diff --git a/app/src/main/java/org/example/domain/jwt/LoginFilter.java b/app/src/main/java/org/example/domain/jwt/LoginFilter.java new file mode 100644 index 0000000..a284dbe --- /dev/null +++ b/app/src/main/java/org/example/domain/jwt/LoginFilter.java @@ -0,0 +1,63 @@ +package org.example.domain.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.example.domain.login.CustomUserDetails; +import org.example.domain.login.dto.request.LoginRequest; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import java.io.IOException; +import java.util.Collection; +import java.util.Iterator; + +@RequiredArgsConstructor +@Slf4j +public class LoginFilter extends UsernamePasswordAuthenticationFilter { + + private final AuthenticationManager authenticationManager; + private final JWTUtil jwtUtil; + + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + ObjectMapper objectMapper = new ObjectMapper(); + + try { + LoginRequest loginRequest = objectMapper.readValue(request.getInputStream(), LoginRequest.class); + String username = loginRequest.getUsername(); + String password = loginRequest.getPassword(); + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null); + log.debug("authToken = {}", authToken); + return authenticationManager.authenticate(authToken); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { + CustomUserDetails customUserDetails = (CustomUserDetails)authResult.getPrincipal(); // 인증된 사용자 정보 + String username = customUserDetails.getUsername(); + Collection authorities = authResult.getAuthorities(); + Iterator iterator = authorities.iterator(); + GrantedAuthority auth = iterator.next(); + String role = auth.getAuthority(); + String token = jwtUtil.createJwt(username, role, 60 * 60 * 10000L); + response.addHeader("Authorization", "Bearer " + token); + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { + response.setStatus(401); + } +} diff --git a/app/src/main/java/org/example/domain/login/CustomUserDetails.java b/app/src/main/java/org/example/domain/login/CustomUserDetails.java new file mode 100644 index 0000000..fb07d0d --- /dev/null +++ b/app/src/main/java/org/example/domain/login/CustomUserDetails.java @@ -0,0 +1,58 @@ +package org.example.domain.login; + +import org.example.domain.member.entity.Member; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.ArrayList; +import java.util.Collection; + +public class CustomUserDetails implements UserDetails { + private final Member member; + + public CustomUserDetails(Member member) { + this.member = member; + } + + @Override + public Collection getAuthorities() { + Collection collection = new ArrayList<>(); + collection.add(new GrantedAuthority() { + @Override + public String getAuthority() { + return member.getRole(); + } + }); + return collection; + } + + @Override + public String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getUsername(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/app/src/main/java/org/example/domain/login/CustomUserDetailsService.java b/app/src/main/java/org/example/domain/login/CustomUserDetailsService.java new file mode 100644 index 0000000..baa5ce4 --- /dev/null +++ b/app/src/main/java/org/example/domain/login/CustomUserDetailsService.java @@ -0,0 +1,27 @@ +package org.example.domain.login; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.example.domain.member.MemberRepository; +import org.example.domain.member.entity.Member; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class CustomUserDetailsService implements UserDetailsService { + private final MemberRepository memberRepository; + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + Member member = memberRepository.findByUsername(username).get(); + if(member != null) { + return new CustomUserDetails(member); + } + return null; + } + + +} diff --git a/app/src/main/java/org/example/domain/login/LoginController.java b/app/src/main/java/org/example/domain/login/LoginController.java deleted file mode 100644 index 378a1ab..0000000 --- a/app/src/main/java/org/example/domain/login/LoginController.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.example.domain.login; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.example.domain.login.dto.request.LoginForm; -import org.example.domain.login.dto.response.LoginResponse; -import org.example.domain.login.dto.response.LogoutResponse; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - -@Slf4j -@RestController -@RequiredArgsConstructor -public class LoginController { - private LoginService loginService; - - @Autowired - public LoginController(LoginService loginService) { - this.loginService = loginService; - } - - @PostMapping("/login") - public ResponseEntity login(@Valid @RequestBody LoginForm loginForm, HttpServletRequest request) { - LoginResponse loginResponse = loginService.createSession(loginForm, request); - return ResponseEntity.ok().body(loginResponse); - } - - @PostMapping("/logout") - public ResponseEntity logout(HttpServletRequest request) { - loginService.invalidateSession(request); - return ResponseEntity.status(204).build(); - } - -} diff --git a/app/src/main/java/org/example/domain/login/LoginService.java b/app/src/main/java/org/example/domain/login/LoginService.java deleted file mode 100644 index 33f5b64..0000000 --- a/app/src/main/java/org/example/domain/login/LoginService.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.example.domain.login; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; -import lombok.extern.slf4j.Slf4j; -import org.example.domain.login.dto.request.LoginForm; -import org.example.domain.login.dto.response.LoginResponse; -import org.example.domain.member.MemberRepository; -import org.example.domain.member.entity.Member; -import org.example.domain.session.SessionConst; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Slf4j -@Service -public class LoginService { - - private MemberRepository memberRepository; - - @Autowired - public LoginService(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } - - public LoginResponse createSession(LoginForm loginForm, HttpServletRequest request) { - - Optional optionalMember = memberRepository.findByEmailAndPassword(loginForm.getEmail(), loginForm.getPassword()); - - if (optionalMember.isPresent()) { - Member member = optionalMember.get(); - HttpSession session = request.getSession(); - session.setAttribute(SessionConst.LOGIN_MEMBER, member); - - return LoginResponse.builder() - .sessionId(session.getId()) - .userId(member.getId()) - .username(member.getUsername()) - .email(member.getEmail()) - .message("login success") - .build(); - } - - // 로그인 실패 "login fail" - else { - throw new RuntimeException("로그인 실패"); - } - - } - - public void invalidateSession(HttpServletRequest request) { - HttpSession session = request.getSession(false); - log.debug("session= {}", session); - - if (session == null) { - throw new RuntimeException("세션이 존재하지 않음"); - } - - log.debug("로그아웃 시도"); - session.invalidate(); - } -} diff --git a/app/src/main/java/org/example/domain/login/dto/request/LoginForm.java b/app/src/main/java/org/example/domain/login/dto/request/LoginRequest.java similarity index 80% rename from app/src/main/java/org/example/domain/login/dto/request/LoginForm.java rename to app/src/main/java/org/example/domain/login/dto/request/LoginRequest.java index 8aed224..dc12a90 100644 --- a/app/src/main/java/org/example/domain/login/dto/request/LoginForm.java +++ b/app/src/main/java/org/example/domain/login/dto/request/LoginRequest.java @@ -2,18 +2,18 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @AllArgsConstructor -public class LoginForm { +@NoArgsConstructor +public class LoginRequest { @NotBlank(message = "이메일은 반드시 입력해야 합니다.") @Email(message = "유효한 이메일 형식이 아닙니다.") - private String email; + private String username; @NotBlank(message = "비밀번호는 반드시 입력해야 합니다.") private String password; diff --git a/app/src/main/java/org/example/domain/login/dto/response/LoginResponse.java b/app/src/main/java/org/example/domain/login/dto/response/LoginResponse.java deleted file mode 100644 index 3dc5ccb..0000000 --- a/app/src/main/java/org/example/domain/login/dto/response/LoginResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.example.domain.login.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -public class LoginResponse { - private String sessionId; - private Long userId; - private String username; - private String email; - private String message; -} diff --git a/app/src/main/java/org/example/domain/login/dto/response/LogoutResponse.java b/app/src/main/java/org/example/domain/login/dto/response/LogoutResponse.java deleted file mode 100644 index 9f2178f..0000000 --- a/app/src/main/java/org/example/domain/login/dto/response/LogoutResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.example.domain.login.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -public class LogoutResponse { - private String message; -} diff --git a/app/src/main/java/org/example/domain/member/MemberRepository.java b/app/src/main/java/org/example/domain/member/MemberRepository.java index f9b0a3d..1f7a71c 100644 --- a/app/src/main/java/org/example/domain/member/MemberRepository.java +++ b/app/src/main/java/org/example/domain/member/MemberRepository.java @@ -12,5 +12,7 @@ public interface MemberRepository extends JpaRepository { // findById() // findAll() Optional findByEmail(String email); + Optional findByUsername(String username); + Boolean existsByEmail(String email); Optional findByEmailAndPassword(String email, String password); } diff --git a/app/src/main/java/org/example/domain/member/MemberService.java b/app/src/main/java/org/example/domain/member/MemberService.java index c0348e0..d140063 100644 --- a/app/src/main/java/org/example/domain/member/MemberService.java +++ b/app/src/main/java/org/example/domain/member/MemberService.java @@ -6,6 +6,7 @@ import org.example.domain.member.dto.request.MemberEditRequest; import org.example.domain.member.dto.request.MemberJoinRequest; import org.example.domain.member.entity.Member; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,10 +19,15 @@ public class MemberService { private final MemberRepository memberRepository; - + private final BCryptPasswordEncoder bCryptPasswordEncoder; // 회원가입 @Transactional public Member addMember(MemberJoinRequest memberJoinRequest){ + + // 암호화 + String password = memberJoinRequest.getPassword(); + memberJoinRequest.setPassword(bCryptPasswordEncoder.encode(password)); + Member member = Member.createMember(memberJoinRequest); for(String lang : memberJoinRequest.getLearning()) { diff --git a/app/src/main/java/org/example/domain/member/dto/request/MemberJoinRequest.java b/app/src/main/java/org/example/domain/member/dto/request/MemberJoinRequest.java index e485012..7266921 100644 --- a/app/src/main/java/org/example/domain/member/dto/request/MemberJoinRequest.java +++ b/app/src/main/java/org/example/domain/member/dto/request/MemberJoinRequest.java @@ -8,7 +8,6 @@ import lombok.Data; import java.util.List; - @Data @Builder public class MemberJoinRequest { diff --git a/app/src/main/java/org/example/domain/member/entity/Member.java b/app/src/main/java/org/example/domain/member/entity/Member.java index 9a8b6ed..dd3bea9 100644 --- a/app/src/main/java/org/example/domain/member/entity/Member.java +++ b/app/src/main/java/org/example/domain/member/entity/Member.java @@ -26,6 +26,7 @@ public class Member { private String nationality; private String nativeLang; private String introduction; + private String role; private int follower; private int following; private int point; @@ -51,11 +52,13 @@ private Member(final MemberJoinRequest memberJoinRequest) { this.point = 50; } + // 생성 메서드 static public Member createMember(final MemberJoinRequest memberJoinRequest) { return new Member(memberJoinRequest); } + // 임시 수정 메서드 public Member editMember(final MemberEditRequest request) { if (request.getUsername() != null) this.username = request.getUsername(); diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml index 65fe61b..f883a96 100644 --- a/app/src/main/resources/application.yml +++ b/app/src/main/resources/application.yml @@ -11,6 +11,8 @@ spring: ddl-auto: create show-sql: true database-platform: org.hibernate.dialect.MySQLDialect + jwt: + secret: 636E385229FF3D2E43E4362356D1fggQweRT123 #HS256 (key >= 32*8) server: port: 8081 logging: From f6ae392259d627345ef52f54d55aca48aba3d319 Mon Sep 17 00:00:00 2001 From: gdrffg Date: Wed, 15 Jan 2025 16:43:19 +0900 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20#17=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +-- app/build.gradle | 2 -- compose.yml | 14 ++++++++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 compose.yml diff --git a/.gitignore b/.gitignore index 041958d..aa261d5 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,4 @@ out/ .vscode/ ## ADD -mysql_data -compose.yml \ No newline at end of file +mysql_data \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 47047fb..753f5fd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,6 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' developmentOnly 'org.springframework.boot:spring-boot-devtools' - testImplementation 'com.h2database:h2' // H2를 테스트용 DB로 사용 시 - // jwt implementation 'io.jsonwebtoken:jjwt-api:0.12.3' implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..c401873 --- /dev/null +++ b/compose.yml @@ -0,0 +1,14 @@ +services: + lingo-db: + image: mysql + environment: + MYSQL_ROOT_PASSWORD: pwd1234 + MYSQL_DATABASE: lingodb + volumes: + - ./mysql_data:/var/lib/mysql + ports: + - 3306:3306 + healthcheck: + test: [ "CMD", "mysqladmin", "ping" ] # MySQL이 healthy 한 지 판단할 수 있는 명령어 + interval: 5s # 5초 간격으로 체크 + retries: 10 # 10번까지 재시도 \ No newline at end of file From c0e064d53cf19ad3c086885c74dc6752aefe043e Mon Sep 17 00:00:00 2001 From: gdrffg Date: Wed, 22 Jan 2025 16:49:19 +0900 Subject: [PATCH 5/8] =?UTF-8?q?refactor:=20spring=20security=20=EC=97=86?= =?UTF-8?q?=EC=9D=B4=20jwt=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 5 - .../example/domain/auth/AuthController.java | 51 +++++++ .../example/domain/auth/AuthRepository.java | 24 +++ .../org/example/domain/auth/AuthService.java | 91 +++++++++++ .../domain/auth/AuthenticationContext.java | 32 ++++ .../domain/auth/annotation/LoginMember.java | 11 ++ .../dto/request/LoginRequest.java | 14 +- .../auth/dto/request/RefreshRequest.java | 14 ++ .../auth/dto/response/TokenResponse.java | 12 ++ .../domain/auth/entity/AuthEntity.java | 33 ++++ .../interceptor/LoginCheckInterceptor.java | 31 ++++ .../domain/auth/jwt/JWTLoginFilter.java | 54 +++++++ .../domain/{ => auth}/jwt/JWTUtil.java | 23 ++- .../resolver/LoginMemberArgumentResolver.java | 29 ++++ .../org/example/domain/comment/Comment.java | 10 +- .../global/config/AuthFilterConfig.java | 24 +++ .../domain/global/config/SecurityConfig.java | 53 ------- .../domain/global/config/WebMvcConfig.java | 34 +++++ .../org/example/domain/jwt/JWTFilter.java | 51 ------- .../org/example/domain/jwt/LoginFilter.java | 63 -------- .../domain/login/CustomUserDetails.java | 58 ------- .../login/CustomUserDetailsService.java | 27 ---- .../domain/member/MemberController.java | 10 +- .../domain/member/MemberRepository.java | 1 + .../example/domain/member/MemberService.java | 14 +- .../member/dto/response/MemberResponse.java | 2 + .../example/domain/member/entity/Member.java | 4 + .../domain/question/QuestionController.java | 67 ++++---- .../domain/question/QuestionRepository.java | 57 ++----- .../domain/question/QuestionService.java | 43 +++--- .../domain/question/dto/QuestionResponse.java | 32 ++++ .../{CommentForm.java => CommentRequest.java} | 2 +- ...teForm.java => QuestionCreateRequest.java} | 8 +- ...EditForm.java => QuestionEditRequest.java} | 4 +- .../domain/question/entity/Question.java | 40 ++--- .../example/domain/session/SessionConst.java | 6 - .../domain/session/SessionController.java | 24 --- .../auth/AuthControllerIntegrationTest.java | 103 +++++++++++++ .../domain/auth/AuthControllerWebMvcTest.java | 96 ++++++++++++ .../domain/auth/AuthRepositoryTest.java | 44 ++++++ .../example/domain/auth/AuthServiceTest.java | 143 ++++++++++++++++++ .../auth/filter/JWTLoginFilterTest.java | 112 ++++++++++++++ .../domain/auth/fixture/AuthTestFixture.java | 34 +++++ .../LoginCheckInterceptorTest.java | 56 +++++++ .../LoginMemberArgumentResolverTest.java | 68 +++++++++ 45 files changed, 1275 insertions(+), 439 deletions(-) create mode 100644 app/src/main/java/org/example/domain/auth/AuthController.java create mode 100644 app/src/main/java/org/example/domain/auth/AuthRepository.java create mode 100644 app/src/main/java/org/example/domain/auth/AuthService.java create mode 100644 app/src/main/java/org/example/domain/auth/AuthenticationContext.java create mode 100644 app/src/main/java/org/example/domain/auth/annotation/LoginMember.java rename app/src/main/java/org/example/domain/{login => auth}/dto/request/LoginRequest.java (64%) create mode 100644 app/src/main/java/org/example/domain/auth/dto/request/RefreshRequest.java create mode 100644 app/src/main/java/org/example/domain/auth/dto/response/TokenResponse.java create mode 100644 app/src/main/java/org/example/domain/auth/entity/AuthEntity.java create mode 100644 app/src/main/java/org/example/domain/auth/interceptor/LoginCheckInterceptor.java create mode 100644 app/src/main/java/org/example/domain/auth/jwt/JWTLoginFilter.java rename app/src/main/java/org/example/domain/{ => auth}/jwt/JWTUtil.java (59%) create mode 100644 app/src/main/java/org/example/domain/auth/resolver/LoginMemberArgumentResolver.java create mode 100644 app/src/main/java/org/example/domain/global/config/AuthFilterConfig.java delete mode 100644 app/src/main/java/org/example/domain/global/config/SecurityConfig.java create mode 100644 app/src/main/java/org/example/domain/global/config/WebMvcConfig.java delete mode 100644 app/src/main/java/org/example/domain/jwt/JWTFilter.java delete mode 100644 app/src/main/java/org/example/domain/jwt/LoginFilter.java delete mode 100644 app/src/main/java/org/example/domain/login/CustomUserDetails.java delete mode 100644 app/src/main/java/org/example/domain/login/CustomUserDetailsService.java create mode 100644 app/src/main/java/org/example/domain/question/dto/QuestionResponse.java rename app/src/main/java/org/example/domain/question/dto/request/{CommentForm.java => CommentRequest.java} (92%) rename app/src/main/java/org/example/domain/question/dto/request/{QuestionCreateForm.java => QuestionCreateRequest.java} (87%) rename app/src/main/java/org/example/domain/question/dto/request/{QuestionEditForm.java => QuestionEditRequest.java} (92%) delete mode 100644 app/src/main/java/org/example/domain/session/SessionConst.java delete mode 100644 app/src/main/java/org/example/domain/session/SessionController.java create mode 100644 app/src/test/java/org/example/domain/auth/AuthControllerIntegrationTest.java create mode 100644 app/src/test/java/org/example/domain/auth/AuthControllerWebMvcTest.java create mode 100644 app/src/test/java/org/example/domain/auth/AuthRepositoryTest.java create mode 100644 app/src/test/java/org/example/domain/auth/AuthServiceTest.java create mode 100644 app/src/test/java/org/example/domain/auth/filter/JWTLoginFilterTest.java create mode 100644 app/src/test/java/org/example/domain/auth/fixture/AuthTestFixture.java create mode 100644 app/src/test/java/org/example/domain/auth/interceptor/LoginCheckInterceptorTest.java create mode 100644 app/src/test/java/org/example/domain/auth/resolver/LoginMemberArgumentResolverTest.java diff --git a/app/build.gradle b/app/build.gradle index 753f5fd..93fff5f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,11 +27,6 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.12.3' implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' - - // spring security - implementation 'org.springframework.boot:spring-boot-starter-security' - testImplementation 'org.springframework.security:spring-security-test' - } // Apply a specific Java toolchain to ease working on different environments. diff --git a/app/src/main/java/org/example/domain/auth/AuthController.java b/app/src/main/java/org/example/domain/auth/AuthController.java new file mode 100644 index 0000000..adf8f59 --- /dev/null +++ b/app/src/main/java/org/example/domain/auth/AuthController.java @@ -0,0 +1,51 @@ +package org.example.domain.auth; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.example.domain.auth.dto.request.LoginRequest; +import org.example.domain.auth.dto.request.RefreshRequest; +import org.example.domain.auth.dto.response.TokenResponse; +import org.example.domain.member.MemberService; +import org.example.domain.member.dto.request.MemberJoinRequest; +import org.example.domain.member.dto.response.MemberResponse; +import org.example.domain.member.entity.Member; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Slf4j +@RequiredArgsConstructor +@RequestMapping("/auth") +public class AuthController { + + private final AuthService authService; + private final MemberService memberService; + @PostMapping("/join") + public ResponseEntity memberAdd(@Valid @RequestBody MemberJoinRequest memberJoinRequest) { + log.debug("join"); + Member savedMember = memberService.addMember(memberJoinRequest); + MemberResponse memberResponse = MemberResponse.createMemberResponse(savedMember); + return ResponseEntity.status(201).body(memberResponse); + + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest loginRequest) { + TokenResponse tokenResponse = authService.issueToken(loginRequest); + return ResponseEntity.ok() + .header("Authorization","Bearer " + tokenResponse.getAccessToken()) + .body(tokenResponse); + + } + + @PostMapping("/refresh") + public ResponseEntity refresh(@RequestBody RefreshRequest refreshRequest) { + TokenResponse tokenResponse = authService.reissueRefreshToken(refreshRequest); + return ResponseEntity.ok().header("Authorization", "Bearer " + tokenResponse.getAccessToken()) + .body(tokenResponse); + } +} diff --git a/app/src/main/java/org/example/domain/auth/AuthRepository.java b/app/src/main/java/org/example/domain/auth/AuthRepository.java new file mode 100644 index 0000000..dfa7885 --- /dev/null +++ b/app/src/main/java/org/example/domain/auth/AuthRepository.java @@ -0,0 +1,24 @@ +package org.example.domain.auth; + +import jakarta.persistence.LockModeType; +import org.example.domain.auth.entity.AuthEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AuthRepository extends JpaRepository { + + Optional findByRefreshToken(String refreshToken); + @Query("DELETE FROM AuthEntity a WHERE a.id = :auth_id") + @Modifying + void deleteByAuthId(@Param("auth_id") Long auth_id); + + @Modifying + void deleteByRefreshToken(String refreshToken); +} diff --git a/app/src/main/java/org/example/domain/auth/AuthService.java b/app/src/main/java/org/example/domain/auth/AuthService.java new file mode 100644 index 0000000..a1b94cd --- /dev/null +++ b/app/src/main/java/org/example/domain/auth/AuthService.java @@ -0,0 +1,91 @@ +package org.example.domain.auth; + +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.antlr.v4.runtime.Token; +import org.example.domain.auth.dto.request.LoginRequest; +import org.example.domain.auth.dto.request.RefreshRequest; +import org.example.domain.auth.dto.response.TokenResponse; +import org.example.domain.auth.entity.AuthEntity; +import org.example.domain.auth.jwt.JWTUtil; +import org.example.domain.member.MemberRepository; +import org.example.domain.member.entity.Member; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Slf4j +@RequiredArgsConstructor +@Transactional +public class AuthService { + private final MemberRepository memberRepository; + private final AuthRepository authRepository; + private final JWTUtil jwtUtil; + private final EntityManager em; + public TokenResponse issueToken(final LoginRequest loginRequest) { + String email = loginRequest.getEmail(); + + // 이메일로 Member 조회 + Member member = memberRepository.findByEmail(email).orElseThrow(() -> new RuntimeException("회원가입 되어 있지 않습니다.")); + Long member_id = member.getId(); + String username = member.getUsername(); + log.debug("member_id = {}",member_id); + log.debug("username = {}",username); + + + // AccessToken, RefreshToken 생성 + String accessToken = jwtUtil.createAccessToken(member_id, username, "USER"); + String refreshToken = jwtUtil.createRefreshToken(member_id, username, "USER"); + + // refreshToken DB 저장 + AuthEntity authEntity = AuthEntity.createWith(refreshToken); + authEntity.setMember(member); + authRepository.save(authEntity); + + // AccessToken, RefreshToken을 담은 LoginResponse를 컨트롤러에 전달 + return TokenResponse.builder().accessToken(accessToken).refreshToken(refreshToken).build(); + } + + public TokenResponse reissueRefreshToken(final RefreshRequest refreshRequest) { + + String accessToken = refreshRequest.getAccessToken(); + String refreshToken = refreshRequest.getRefreshToken(); + + // refresh 만료 확인 -> 만료 되었다면 refreshToken을 DB에서 삭제하고 예외를 발생시킴 + try{ + jwtUtil.isExpired(refreshToken); + } catch (ExpiredJwtException e) { + authRepository.deleteByRefreshToken(refreshToken); + throw new RuntimeException("다시 로그인 하세요."); + } + + // Access Token, Refresh Token의 사용자 ID가 일치하는지 확인 -> 일치하지 않으면 예외를 발생시킴 + if (!jwtUtil.getId(accessToken).equals(jwtUtil.getId(refreshToken))) { + throw new RuntimeException("Access Token, Refresh Token의 사용자 ID가 일치하지 않습니다."); + } + + // refreshToken으로 AuthEntity를 찾는다 + AuthEntity authEntity = authRepository.findByRefreshToken(refreshToken).orElseThrow(() -> new RuntimeException("refresh token이 없습니다. 로그인 하세요")); + + // AuthEntity로 Member를 찾아 리턴한다 + Member member = memberRepository.findById(authEntity.getId()).orElseThrow(() -> new RuntimeException("존재하지 않는 사용자입니다.")); + + // Member 정보로 accessToken을 재발급한다. + String newAccessToken = jwtUtil.createAccessToken(member.getId(), member.getUsername(), member.getRole()); + + // refreshToken을 파기한 후 다시 만들고 저정한다. + authRepository.deleteByAuthId(authEntity.getId()); + em.flush(); + em.clear(); + String newRefreshToken = jwtUtil.createRefreshToken(member.getId(), member.getUsername(), member.getRole()); + AuthEntity newAuthEntity = AuthEntity.createWith(newRefreshToken); + newAuthEntity.setMember(member); + + authRepository.save(newAuthEntity); + + return TokenResponse.builder().accessToken(newAccessToken).refreshToken(newRefreshToken).build(); + + } +} diff --git a/app/src/main/java/org/example/domain/auth/AuthenticationContext.java b/app/src/main/java/org/example/domain/auth/AuthenticationContext.java new file mode 100644 index 0000000..358e93f --- /dev/null +++ b/app/src/main/java/org/example/domain/auth/AuthenticationContext.java @@ -0,0 +1,32 @@ +package org.example.domain.auth; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.web.context.annotation.RequestScope; + +import java.util.Objects; + +@Component +@RequestScope +@Slf4j +public class AuthenticationContext { + private static final String ANONYMOUS_USERNAME = "UNKNOWN"; + private String username; + + public void setAuthentication(String username) { + this.username = username; + } + + public void setAnonymousUsername(){ + this.username = ANONYMOUS_USERNAME; + } + + public String getPrincipal() { + if (Objects.isNull(this.username)) { + throw new RuntimeException("username is null"); + } + log.debug("authcontext username = {}", username); + return username; + } +} diff --git a/app/src/main/java/org/example/domain/auth/annotation/LoginMember.java b/app/src/main/java/org/example/domain/auth/annotation/LoginMember.java new file mode 100644 index 0000000..c7ae5e9 --- /dev/null +++ b/app/src/main/java/org/example/domain/auth/annotation/LoginMember.java @@ -0,0 +1,11 @@ +package org.example.domain.auth.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface LoginMember { +} diff --git a/app/src/main/java/org/example/domain/login/dto/request/LoginRequest.java b/app/src/main/java/org/example/domain/auth/dto/request/LoginRequest.java similarity index 64% rename from app/src/main/java/org/example/domain/login/dto/request/LoginRequest.java rename to app/src/main/java/org/example/domain/auth/dto/request/LoginRequest.java index dc12a90..7fefa68 100644 --- a/app/src/main/java/org/example/domain/login/dto/request/LoginRequest.java +++ b/app/src/main/java/org/example/domain/auth/dto/request/LoginRequest.java @@ -1,19 +1,17 @@ -package org.example.domain.login.dto.request; +package org.example.domain.auth.dto.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.Builder; +import lombok.Getter; -@Data -@AllArgsConstructor -@NoArgsConstructor +@Builder +@Getter public class LoginRequest { @NotBlank(message = "이메일은 반드시 입력해야 합니다.") @Email(message = "유효한 이메일 형식이 아닙니다.") - private String username; + private String email; @NotBlank(message = "비밀번호는 반드시 입력해야 합니다.") private String password; diff --git a/app/src/main/java/org/example/domain/auth/dto/request/RefreshRequest.java b/app/src/main/java/org/example/domain/auth/dto/request/RefreshRequest.java new file mode 100644 index 0000000..ec2be49 --- /dev/null +++ b/app/src/main/java/org/example/domain/auth/dto/request/RefreshRequest.java @@ -0,0 +1,14 @@ +package org.example.domain.auth.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class RefreshRequest { + private String accessToken; + private String refreshToken; +} diff --git a/app/src/main/java/org/example/domain/auth/dto/response/TokenResponse.java b/app/src/main/java/org/example/domain/auth/dto/response/TokenResponse.java new file mode 100644 index 0000000..71277ec --- /dev/null +++ b/app/src/main/java/org/example/domain/auth/dto/response/TokenResponse.java @@ -0,0 +1,12 @@ +package org.example.domain.auth.dto.response; + + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class TokenResponse { + private String accessToken; + private String refreshToken; +} diff --git a/app/src/main/java/org/example/domain/auth/entity/AuthEntity.java b/app/src/main/java/org/example/domain/auth/entity/AuthEntity.java new file mode 100644 index 0000000..8edcc28 --- /dev/null +++ b/app/src/main/java/org/example/domain/auth/entity/AuthEntity.java @@ -0,0 +1,33 @@ +package org.example.domain.auth.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.example.domain.member.entity.Member; + +@Entity +@Getter +@NoArgsConstructor +public class AuthEntity { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "auth_id") + public Long id; + public String refreshToken; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + public Member member; + + private AuthEntity(String refreshToken) { + this.refreshToken = refreshToken; + } + + public static AuthEntity createWith(final String refreshToken){ + return new AuthEntity(refreshToken); + } + public void setMember(Member member) { + this.member = member; + member.setAuthEntity(this); + } +} diff --git a/app/src/main/java/org/example/domain/auth/interceptor/LoginCheckInterceptor.java b/app/src/main/java/org/example/domain/auth/interceptor/LoginCheckInterceptor.java new file mode 100644 index 0000000..1d6c422 --- /dev/null +++ b/app/src/main/java/org/example/domain/auth/interceptor/LoginCheckInterceptor.java @@ -0,0 +1,31 @@ +package org.example.domain.auth.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.example.domain.auth.AuthenticationContext; +import org.example.domain.auth.jwt.JWTUtil; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + + +@Component +@RequiredArgsConstructor +@Slf4j +public class LoginCheckInterceptor implements HandlerInterceptor { + private final JWTUtil jwtUtil; + private final AuthenticationContext authenticationContext; + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + log.debug("interceptor"); + + String token = request.getHeader("Authorization"); + token = token.substring(7); + String username = jwtUtil.getUsername(token); + log.debug("interceptor -> token = {}", token); + + authenticationContext.setAuthentication(username); + return true; + } +} diff --git a/app/src/main/java/org/example/domain/auth/jwt/JWTLoginFilter.java b/app/src/main/java/org/example/domain/auth/jwt/JWTLoginFilter.java new file mode 100644 index 0000000..69f0200 --- /dev/null +++ b/app/src/main/java/org/example/domain/auth/jwt/JWTLoginFilter.java @@ -0,0 +1,54 @@ +package org.example.domain.auth.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JWTLoginFilter extends OncePerRequestFilter { + + private final JWTUtil jwtUtil; + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String requestURI = request.getRequestURI(); + + if(requestURI.startsWith("/auth/")) { + filterChain.doFilter(request,response); + return; + } + + String authorization = request.getHeader("Authorization"); + + log.debug("v = {}", authorization); + // "/auth/* 제외 인증 정보 없이 다른 경로 접근시 401 응답 처리 + if(authorization == null || !authorization.startsWith("Bearer ")) { + // 이 부분 예외처리로 + response.setStatus(401); + log.debug("/api/* 경로는 인증 정보 필요"); + return; + } + + String token = authorization.split(" ")[1]; + + // AccessToken 만료시 401 응답 처리 + if (jwtUtil.isExpired(token)) { + // 이 부분 예외처리로 + response.setStatus(401); + log.debug("토큰 만료"); + return; + } + + // 인증 정보가 유효함. 다음 필터로 go + filterChain.doFilter(request,response); + + } +} diff --git a/app/src/main/java/org/example/domain/jwt/JWTUtil.java b/app/src/main/java/org/example/domain/auth/jwt/JWTUtil.java similarity index 59% rename from app/src/main/java/org/example/domain/jwt/JWTUtil.java rename to app/src/main/java/org/example/domain/auth/jwt/JWTUtil.java index d83af13..80f9c74 100644 --- a/app/src/main/java/org/example/domain/jwt/JWTUtil.java +++ b/app/src/main/java/org/example/domain/auth/jwt/JWTUtil.java @@ -1,4 +1,4 @@ -package org.example.domain.jwt; +package org.example.domain.auth.jwt; import io.jsonwebtoken.Jwts; import org.springframework.beans.factory.annotation.Value; @@ -13,11 +13,16 @@ @Component public class JWTUtil { private SecretKey secretKey; + private final long accessTokenValidity = 1000 * 60 * 15; // 15분 + private final long refreshTokenValidity = 1000 * 60 * 60; // 1시간 public JWTUtil(@Value("${spring.jwt.secret}")String secret) { secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm()); } + public Long getId(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("member_id", Long.class); + } public String getUsername(String token) { return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class); } @@ -30,12 +35,24 @@ public Boolean isExpired(String token) { return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date()); } - public String createJwt(String username, String role, Long expiredMs) { + public String createAccessToken(Long member_id, String username, String role) { + return Jwts.builder() + .claim("member_id", member_id) + .claim("username", username) + .claim("role", role) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + accessTokenValidity)) + .signWith(secretKey) + .compact(); + } + + public String createRefreshToken(Long member_id, String username, String role) { return Jwts.builder() + .claim("member_id", member_id) .claim("username", username) .claim("role", role) .issuedAt(new Date(System.currentTimeMillis())) - .expiration(new Date(System.currentTimeMillis() + expiredMs)) + .expiration(new Date(System.currentTimeMillis() + refreshTokenValidity)) .signWith(secretKey) .compact(); } diff --git a/app/src/main/java/org/example/domain/auth/resolver/LoginMemberArgumentResolver.java b/app/src/main/java/org/example/domain/auth/resolver/LoginMemberArgumentResolver.java new file mode 100644 index 0000000..4b5a1b3 --- /dev/null +++ b/app/src/main/java/org/example/domain/auth/resolver/LoginMemberArgumentResolver.java @@ -0,0 +1,29 @@ +package org.example.domain.auth.resolver; + +import lombok.RequiredArgsConstructor; +import org.example.domain.auth.AuthenticationContext; +import org.example.domain.auth.annotation.LoginMember; +import org.example.domain.auth.jwt.JWTUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + private final AuthenticationContext authenticationContext; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(LoginMember.class) && parameter.getParameterType().equals(String.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + return authenticationContext.getPrincipal(); + } +} diff --git a/app/src/main/java/org/example/domain/comment/Comment.java b/app/src/main/java/org/example/domain/comment/Comment.java index 2e1c5bb..84c9c30 100644 --- a/app/src/main/java/org/example/domain/comment/Comment.java +++ b/app/src/main/java/org/example/domain/comment/Comment.java @@ -3,7 +3,7 @@ import jakarta.persistence.*; import lombok.*; import org.example.domain.member.entity.Member; -import org.example.domain.question.dto.request.CommentForm; +import org.example.domain.question.dto.request.CommentRequest; import org.example.domain.question.entity.Question; import java.time.LocalDateTime; @@ -29,14 +29,14 @@ public class Comment { @JoinColumn(name = "question_id") private Question question; - public static Comment createComment(CommentForm commentForm, Member member) { + public static Comment createComment(CommentRequest commentRequest, Member member) { return Comment.builder().id(++sequence) - .comment(commentForm.getComment()) + .comment(commentRequest.getComment()) .build(); } - public Comment editComment(CommentForm commentForm) { - this.comment = commentForm.getComment(); + public Comment editComment(CommentRequest commentRequest) { + this.comment = commentRequest.getComment(); this.updatedAt = LocalDateTime.now(); return this; } diff --git a/app/src/main/java/org/example/domain/global/config/AuthFilterConfig.java b/app/src/main/java/org/example/domain/global/config/AuthFilterConfig.java new file mode 100644 index 0000000..ffd415a --- /dev/null +++ b/app/src/main/java/org/example/domain/global/config/AuthFilterConfig.java @@ -0,0 +1,24 @@ +package org.example.domain.global.config; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.example.domain.auth.jwt.JWTLoginFilter; +import org.example.domain.auth.jwt.JWTUtil; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +@Slf4j +public class AuthFilterConfig { + private final JWTUtil jwtUtil; + @Bean + public FilterRegistrationBean jwtLoginFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new JWTLoginFilter(jwtUtil)); + registrationBean.addUrlPatterns("/*"); + registrationBean.setOrder(1); + return registrationBean; + } +} diff --git a/app/src/main/java/org/example/domain/global/config/SecurityConfig.java b/app/src/main/java/org/example/domain/global/config/SecurityConfig.java deleted file mode 100644 index 8c8ab93..0000000 --- a/app/src/main/java/org/example/domain/global/config/SecurityConfig.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.example.domain.global.config; - -import lombok.RequiredArgsConstructor; -import org.example.domain.jwt.JWTFilter; -import org.example.domain.jwt.JWTUtil; -import org.example.domain.jwt.LoginFilter; -import org.example.domain.member.MemberRepository; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -@Configuration -@EnableWebSecurity -@RequiredArgsConstructor -public class SecurityConfig { - private final AuthenticationConfiguration authenticationConfiguration; - private final JWTUtil jwtUtil; - private final MemberRepository memberRepository; - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { - return configuration.getAuthenticationManager(); - } - @Bean - public BCryptPasswordEncoder bCryptPasswordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - - http.csrf((auth) -> auth.disable()); - http.formLogin((auth) -> auth.disable()); - http.httpBasic((auth) -> auth.disable()); - - http.authorizeHttpRequests((auth) -> - auth.requestMatchers("/login","/profile/add").permitAll() - .anyRequest().authenticated()); - - http.addFilterBefore(new JWTFilter(jwtUtil,memberRepository), LoginFilter.class); - http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration),jwtUtil), UsernamePasswordAuthenticationFilter.class); - - http.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); - - return http.build(); - } -} diff --git a/app/src/main/java/org/example/domain/global/config/WebMvcConfig.java b/app/src/main/java/org/example/domain/global/config/WebMvcConfig.java new file mode 100644 index 0000000..cdffb23 --- /dev/null +++ b/app/src/main/java/org/example/domain/global/config/WebMvcConfig.java @@ -0,0 +1,34 @@ +package org.example.domain.global.config; + +import lombok.RequiredArgsConstructor; +import org.example.domain.auth.interceptor.LoginCheckInterceptor; +import org.example.domain.auth.resolver.LoginMemberArgumentResolver; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@RequiredArgsConstructor +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + private final LoginMemberArgumentResolver loginMemberArgumentResolver; + private final LoginCheckInterceptor loginCheckInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(loginCheckInterceptor) + .order(1) + .addPathPatterns("/api/**") + .excludePathPatterns("/auth/**"); + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(loginMemberArgumentResolver); + } + + +} diff --git a/app/src/main/java/org/example/domain/jwt/JWTFilter.java b/app/src/main/java/org/example/domain/jwt/JWTFilter.java deleted file mode 100644 index ed1cb56..0000000 --- a/app/src/main/java/org/example/domain/jwt/JWTFilter.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.example.domain.jwt; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.example.domain.login.CustomUserDetails; -import org.example.domain.member.MemberRepository; -import org.example.domain.member.entity.Member; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -@RequiredArgsConstructor -@Slf4j -public class JWTFilter extends OncePerRequestFilter { - private final JWTUtil jwtUtil; - private final MemberRepository memberRepository; - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String authorization = request.getHeader("Authorization"); - - if(authorization == null || !authorization.startsWith("Bearer ")) { - log.debug("token is null"); - filterChain.doFilter(request,response); - return; - } - - log.debug("token exists"); - String token = authorization.split(" ")[1]; - - if(jwtUtil.isExpired(token)){ - filterChain.doFilter(request,response); - return; - } - - String username = jwtUtil.getUsername(token); - String role = jwtUtil.getRole(token); - - Member member = memberRepository.findByUsername(username).get(); - CustomUserDetails customUserDetails = new CustomUserDetails(member); - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities()); - SecurityContextHolder.getContext().setAuthentication(authToken); - filterChain.doFilter(request,response); - } -} diff --git a/app/src/main/java/org/example/domain/jwt/LoginFilter.java b/app/src/main/java/org/example/domain/jwt/LoginFilter.java deleted file mode 100644 index a284dbe..0000000 --- a/app/src/main/java/org/example/domain/jwt/LoginFilter.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.example.domain.jwt; - -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.example.domain.login.CustomUserDetails; -import org.example.domain.login.dto.request.LoginRequest; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -import java.io.IOException; -import java.util.Collection; -import java.util.Iterator; - -@RequiredArgsConstructor -@Slf4j -public class LoginFilter extends UsernamePasswordAuthenticationFilter { - - private final AuthenticationManager authenticationManager; - private final JWTUtil jwtUtil; - - - @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { - ObjectMapper objectMapper = new ObjectMapper(); - - try { - LoginRequest loginRequest = objectMapper.readValue(request.getInputStream(), LoginRequest.class); - String username = loginRequest.getUsername(); - String password = loginRequest.getPassword(); - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null); - log.debug("authToken = {}", authToken); - return authenticationManager.authenticate(authToken); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { - CustomUserDetails customUserDetails = (CustomUserDetails)authResult.getPrincipal(); // 인증된 사용자 정보 - String username = customUserDetails.getUsername(); - Collection authorities = authResult.getAuthorities(); - Iterator iterator = authorities.iterator(); - GrantedAuthority auth = iterator.next(); - String role = auth.getAuthority(); - String token = jwtUtil.createJwt(username, role, 60 * 60 * 10000L); - response.addHeader("Authorization", "Bearer " + token); - } - - @Override - protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { - response.setStatus(401); - } -} diff --git a/app/src/main/java/org/example/domain/login/CustomUserDetails.java b/app/src/main/java/org/example/domain/login/CustomUserDetails.java deleted file mode 100644 index fb07d0d..0000000 --- a/app/src/main/java/org/example/domain/login/CustomUserDetails.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.example.domain.login; - -import org.example.domain.member.entity.Member; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.ArrayList; -import java.util.Collection; - -public class CustomUserDetails implements UserDetails { - private final Member member; - - public CustomUserDetails(Member member) { - this.member = member; - } - - @Override - public Collection getAuthorities() { - Collection collection = new ArrayList<>(); - collection.add(new GrantedAuthority() { - @Override - public String getAuthority() { - return member.getRole(); - } - }); - return collection; - } - - @Override - public String getPassword() { - return member.getPassword(); - } - - @Override - public String getUsername() { - return member.getUsername(); - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } -} diff --git a/app/src/main/java/org/example/domain/login/CustomUserDetailsService.java b/app/src/main/java/org/example/domain/login/CustomUserDetailsService.java deleted file mode 100644 index baa5ce4..0000000 --- a/app/src/main/java/org/example/domain/login/CustomUserDetailsService.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.example.domain.login; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.example.domain.member.MemberRepository; -import org.example.domain.member.entity.Member; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -@Slf4j -public class CustomUserDetailsService implements UserDetailsService { - private final MemberRepository memberRepository; - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - Member member = memberRepository.findByUsername(username).get(); - if(member != null) { - return new CustomUserDetails(member); - } - return null; - } - - -} diff --git a/app/src/main/java/org/example/domain/member/MemberController.java b/app/src/main/java/org/example/domain/member/MemberController.java index 09e2ffd..d8d0b81 100644 --- a/app/src/main/java/org/example/domain/member/MemberController.java +++ b/app/src/main/java/org/example/domain/member/MemberController.java @@ -10,19 +10,11 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/profile") +@RequestMapping("/api/profile") @RequiredArgsConstructor public class MemberController { private final MemberService memberService; - @PostMapping("/add") - public ResponseEntity memberAdd(@Valid @RequestBody MemberJoinRequest memberJoinRequest) { - Member savedMember = memberService.addMember(memberJoinRequest); - MemberResponse memberResponse = MemberResponse.createMemberResponse(savedMember); - return ResponseEntity.status(201).body(memberResponse); - - } - // 사용자 프로필 조회, 사용자 기본 정보 제공 @GetMapping("/{user_id}") public ResponseEntity memberDetails(@PathVariable(value = "user_id") Long userId) { diff --git a/app/src/main/java/org/example/domain/member/MemberRepository.java b/app/src/main/java/org/example/domain/member/MemberRepository.java index 1f7a71c..e4121c1 100644 --- a/app/src/main/java/org/example/domain/member/MemberRepository.java +++ b/app/src/main/java/org/example/domain/member/MemberRepository.java @@ -13,6 +13,7 @@ public interface MemberRepository extends JpaRepository { // findAll() Optional findByEmail(String email); Optional findByUsername(String username); + Long findIdByUsername(String username); Boolean existsByEmail(String email); Optional findByEmailAndPassword(String email, String password); } diff --git a/app/src/main/java/org/example/domain/member/MemberService.java b/app/src/main/java/org/example/domain/member/MemberService.java index d140063..144ca41 100644 --- a/app/src/main/java/org/example/domain/member/MemberService.java +++ b/app/src/main/java/org/example/domain/member/MemberService.java @@ -6,12 +6,9 @@ import org.example.domain.member.dto.request.MemberEditRequest; import org.example.domain.member.dto.request.MemberJoinRequest; import org.example.domain.member.entity.Member; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -19,14 +16,21 @@ public class MemberService { private final MemberRepository memberRepository; - private final BCryptPasswordEncoder bCryptPasswordEncoder; + //private final BCryptPasswordEncoder bCryptPasswordEncoder; // 회원가입 @Transactional public Member addMember(MemberJoinRequest memberJoinRequest){ - // 암호화 + /* 암호화 String password = memberJoinRequest.getPassword(); memberJoinRequest.setPassword(bCryptPasswordEncoder.encode(password)); + */ + + // 중복 로그인 체크 + String username = memberJoinRequest.getUsername(); + if(memberRepository.findByUsername(username).isPresent()) { + throw new RuntimeException("중복 사용자"); + } Member member = Member.createMember(memberJoinRequest); diff --git a/app/src/main/java/org/example/domain/member/dto/response/MemberResponse.java b/app/src/main/java/org/example/domain/member/dto/response/MemberResponse.java index c77c147..2afe881 100644 --- a/app/src/main/java/org/example/domain/member/dto/response/MemberResponse.java +++ b/app/src/main/java/org/example/domain/member/dto/response/MemberResponse.java @@ -20,6 +20,7 @@ public class MemberResponse { private int follower; private int following; private int point; + private String role; public static MemberResponse createMemberResponse(Member member) { List learnings = member.getLearnings(); @@ -36,6 +37,7 @@ public static MemberResponse createMemberResponse(Member member) { dto.follower = member.getFollower(); dto.following = member.getFollowing(); dto.point = member.getPoint(); + dto.role = member.getRole(); return dto; } } diff --git a/app/src/main/java/org/example/domain/member/entity/Member.java b/app/src/main/java/org/example/domain/member/entity/Member.java index dd3bea9..82f00e3 100644 --- a/app/src/main/java/org/example/domain/member/entity/Member.java +++ b/app/src/main/java/org/example/domain/member/entity/Member.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.*; +import org.example.domain.auth.entity.AuthEntity; import org.example.domain.language.Language; import org.example.domain.member.dto.request.MemberEditRequest; import org.example.domain.member.dto.request.MemberJoinRequest; @@ -40,6 +41,8 @@ public class Member { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List wordbooks = new ArrayList(); + @OneToOne(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) + private AuthEntity authEntity; private Member(final MemberJoinRequest memberJoinRequest) { this.email = memberJoinRequest.getEmail(); this.username = memberJoinRequest.getUsername(); @@ -47,6 +50,7 @@ private Member(final MemberJoinRequest memberJoinRequest) { this.nationality = memberJoinRequest.getNationality(); this.nativeLang = memberJoinRequest.getNativeLang(); this.introduction = memberJoinRequest.getIntroduction(); + this.role = "USER"; this.follower = 0; this.following = 0; this.point = 50; diff --git a/app/src/main/java/org/example/domain/question/QuestionController.java b/app/src/main/java/org/example/domain/question/QuestionController.java index 8ca8980..543dcec 100644 --- a/app/src/main/java/org/example/domain/question/QuestionController.java +++ b/app/src/main/java/org/example/domain/question/QuestionController.java @@ -2,15 +2,14 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.example.domain.auth.annotation.LoginMember; +import org.example.domain.auth.jwt.JWTUtil; +import org.example.domain.member.MemberRepository; import org.example.domain.member.entity.Member; -import org.example.domain.question.dto.request.CommentForm; -import org.example.domain.question.dto.request.QuestionCreateForm; -import org.example.domain.question.dto.request.QuestionEditForm; -import org.example.domain.comment.Comment; -import org.example.domain.question.entity.Question; -import org.example.domain.session.SessionConst; -import org.springframework.beans.factory.annotation.Autowired; +import org.example.domain.question.dto.QuestionResponse; +import org.example.domain.question.dto.request.QuestionCreateRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -18,42 +17,49 @@ @Slf4j @RestController -@RequestMapping("/question") +@RequestMapping("/api/question") +@RequiredArgsConstructor public class QuestionController { private final QuestionService questionService; + private final JWTUtil jwtUtil; + private final MemberRepository memberRepository; + + public Member loginCheck(HttpServletRequest request) { + String token = request.getHeader("Authorization").substring(7); + String username = jwtUtil.getUsername(token); + return memberRepository.findByUsername(username).get(); - @Autowired - public QuestionController(QuestionService questionService) { - this.questionService = questionService; } // 질문 생성 @PostMapping("/create") - public ResponseEntity questionAdd(@Valid @RequestBody QuestionCreateForm createForm, HttpServletRequest request) { - Member member = (Member)request.getSession().getAttribute(SessionConst.LOGIN_MEMBER); - Question newQuestion = questionService.addQuestion(createForm, member.getUsername()); - return ResponseEntity.ok().body(newQuestion); + public ResponseEntity questionAdd(@LoginMember String username, @Valid @RequestBody QuestionCreateRequest questionCreateRequest, HttpServletRequest request) { + log.debug("username = {}", username); + QuestionResponse response = questionService.addQuestion(questionCreateRequest, username); + return ResponseEntity.ok().body(response); } - // 질문 조회 - @GetMapping("/{q_id}") - public ResponseEntity questionDetails(@PathVariable(value = "q_id") Long questionId) { - Question question = questionService.findQuestion(questionId); - return ResponseEntity.ok().body(question); + // 사용자 생성 질문 조회 + @GetMapping("/{member_id}") + public ResponseEntity> questionDetails(@PathVariable(value = "member_id") Long memberId) { + List response = questionService.findQuestion(memberId); + return ResponseEntity.ok().body(response); } + /* // 질문 수정 - @PutMapping("/{q_id}/edit") - public ResponseEntity questionModify(@PathVariable(value = "q_id") Long questionId, - @RequestBody QuestionEditForm updatedQuestion) { + @PutMapping("/{question_id}/edit") + public ResponseEntity questionModify(@PathVariable(value = "question_Id") Long questionId, + @RequestBody QuestionEditRequest updatedQuestion, + HttpServletRequest request) { Question modifiedQuestion = questionService.modifyQuestion(questionId, updatedQuestion); return ResponseEntity.ok().body(modifiedQuestion); } // 질문 삭제 - @DeleteMapping("/{q_id}") - public ResponseEntity questionRemove(@PathVariable(value = "q_id") Long questionId) { + @DeleteMapping("/{question_id}") + public ResponseEntity questionRemove(@PathVariable(value = "question_id") Long questionId) { Question question = questionService.removeQuestion(questionId); return ResponseEntity.ok().body(question); } @@ -74,12 +80,12 @@ public ResponseEntity> questionSearch(@RequestParam("keyword") St } // 질문 댓글 추가 - @PostMapping("/{q_id}/comments") - public ResponseEntity commentAdd(@PathVariable(value = "q_id") Long questionId, - @Valid @RequestBody CommentForm commentForm, HttpServletRequest request) + @PostMapping("/{question_id}/comments") + public ResponseEntity commentAdd(@PathVariable(value = "question_id") Long questionId, + @Valid @RequestBody CommentRequest commentRequest, HttpServletRequest request) { Member member = (Member) request.getSession().getAttribute(SessionConst.LOGIN_MEMBER); - Question question = questionService.addComment(questionId, commentForm, member); + Question question = questionService.addComment(questionId, commentRequest, member); return ResponseEntity.ok().body(question); } @@ -87,7 +93,7 @@ public ResponseEntity commentAdd(@PathVariable(value = "q_id") Long qu @PutMapping("/{q_id}/comments/{c_id}") public ResponseEntity commentModify(@PathVariable(value = "q_id") Long questionId, @PathVariable(value = "c_id") Long commentId, - @Valid @RequestBody CommentForm commentEditForm, + @Valid @RequestBody CommentRequest commentEditForm, HttpServletRequest request) { log.debug("here"); Member member = (Member)request.getSession().getAttribute(SessionConst.LOGIN_MEMBER); @@ -105,4 +111,5 @@ public ResponseEntity commentRemove(@PathVariable(value = "q_id") Long return ResponseEntity.ok().body(comment); } + */ } diff --git a/app/src/main/java/org/example/domain/question/QuestionRepository.java b/app/src/main/java/org/example/domain/question/QuestionRepository.java index 3c63450..7efd27f 100644 --- a/app/src/main/java/org/example/domain/question/QuestionRepository.java +++ b/app/src/main/java/org/example/domain/question/QuestionRepository.java @@ -1,57 +1,24 @@ package org.example.domain.question; -import lombok.extern.slf4j.Slf4j; -import org.example.domain.question.dto.request.QuestionCreateForm; +import org.example.domain.member.entity.Member; import org.example.domain.question.entity.Question; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ConcurrentHashMap; +import java.util.Optional; -import static org.example.domain.question.entity.Question.createQuestion; - -@Slf4j @Repository -public class QuestionRepository { - - private static ConcurrentHashMap store = new ConcurrentHashMap<>(); - - public Question save(QuestionCreateForm createForm, String loggedUsername) { - Question newQuestion = createQuestion(createForm, loggedUsername); - store.put(newQuestion.getId(), newQuestion); - return newQuestion; - } - - public Question findById(Long q_id) { - return store.get(q_id); - } - - public List findAll() { - return new ArrayList<>(store.values()); - } - - public Question deleteById(Long q_id) { - return store.remove(q_id); - } - - public List findByKeyword(String keyword) { - ArrayList questions = new ArrayList<>(store.values()); - ArrayList searchedQuestion = new ArrayList<>(); - for (Question question : questions) { - String content = question.getContent(); - String title = question.getTitle(); - - if(content.contains(keyword) || title.contains(keyword)){ - searchedQuestion.add(question); - } - } - return searchedQuestion; - } +public interface QuestionRepository extends JpaRepository { - public void cleanStore() { - store.clear(); - } + @Query("SELECT q FROM Question q WHERE q.title LIKE %:keyword% OR q.content LIKE %:keyword%") + List findByKeyword(@Param("keyword") String keyword); + @Query("SELECT q FROM Question q WHERE q.member.id = :memberId") + List findByMemberId(@Param("memberId") Long memberId); + @Query("SELECT m FROM Member m WHERE m.username = :username") + Optional findMemberByUsername(@Param("username") String username); } diff --git a/app/src/main/java/org/example/domain/question/QuestionService.java b/app/src/main/java/org/example/domain/question/QuestionService.java index cd08d36..09316d4 100644 --- a/app/src/main/java/org/example/domain/question/QuestionService.java +++ b/app/src/main/java/org/example/domain/question/QuestionService.java @@ -2,21 +2,26 @@ import lombok.extern.slf4j.Slf4j; import org.example.domain.member.entity.Member; -import org.example.domain.question.dto.request.CommentForm; -import org.example.domain.question.dto.request.QuestionCreateForm; -import org.example.domain.question.dto.request.QuestionEditForm; +import org.example.domain.question.dto.QuestionResponse; +import org.example.domain.question.dto.request.CommentRequest; +import org.example.domain.question.dto.request.QuestionCreateRequest; +import org.example.domain.question.dto.request.QuestionEditRequest; import org.example.domain.comment.Comment; import org.example.domain.question.entity.Question; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; import static org.example.domain.comment.Comment.*; @Slf4j @Service +@Transactional(readOnly = true) public class QuestionService { private final QuestionRepository questionRepository; @Autowired @@ -32,22 +37,24 @@ private void isNullException(Question question) { } // 질문 생성 - public Question addQuestion(QuestionCreateForm createForm, String loggedUsername) { - Question question = questionRepository.save(createForm, loggedUsername); - return question; + @Transactional + public QuestionResponse addQuestion(QuestionCreateRequest request, String username) { + Question question = Question.createQuestion(request); + Member member = questionRepository.findMemberByUsername(username) + .orElseThrow(() -> new RuntimeException("member를 찾지 못함")); + member.addQuestion(question); + return QuestionResponse.create(question); } - // 질문 조회 - public Question findQuestion(Long questionId) { - - Question question = questionRepository.findById(questionId); - isNullException(question); - return question; + // 사용자 생성 질문 조회 + public List findQuestion(Long memberId) { + List questions = questionRepository.findByMemberId(memberId); + return questions.stream().map(QuestionResponse::create).collect(Collectors.toList()); } - + /* // 질문 수정 - public Question modifyQuestion(Long questionId, QuestionEditForm updatedQuestion) { + public Question modifyQuestion(Long questionId, QuestionEditRequest updatedQuestion) { Question findQuestion = questionRepository.findById(questionId); isNullException(findQuestion); @@ -80,20 +87,20 @@ public List searchQuestion(@RequestParam("keyword") String keyword) { } // 질문 댓글 추가 - public Question addComment(Long questionId, CommentForm commentForm, Member member) { + public Question addComment(Long questionId, CommentRequest commentRequest, Member member) { // 댓글 달릴 질문 조회 Question question = questionRepository.findById(questionId); isNullException(question); // 댓글 생성 - Comment comment = createComment(commentForm, member); + Comment comment = createComment(commentRequest, member); question.addComment(comment); return question; } // 질문 댓글 수정 - public Question modifyComment(Long questionId, Long commentId, CommentForm commentEditForm, Member member) { + public Question modifyComment(Long questionId, Long commentId, CommentRequest commentEditForm, Member member) { Question question = questionRepository.findById(questionId); isNullException(question); @@ -110,4 +117,6 @@ public Comment removeComment(Long questionId, Long commentId) { return findQuestion.deleteComment(commentId); } + + */ } diff --git a/app/src/main/java/org/example/domain/question/dto/QuestionResponse.java b/app/src/main/java/org/example/domain/question/dto/QuestionResponse.java new file mode 100644 index 0000000..6c07536 --- /dev/null +++ b/app/src/main/java/org/example/domain/question/dto/QuestionResponse.java @@ -0,0 +1,32 @@ +package org.example.domain.question.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.example.domain.question.entity.Question; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +public class QuestionResponse { + private String questionLanguage; + private String title; + private String content; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private int point; + + private QuestionResponse(final Question question) { + this.questionLanguage = question.getQuestionLanguage(); + this.title = question.getTitle(); + this.content = question.getContent(); + this.createdAt = question.getCreatedAt(); + this.updatedAt = question.getUpdatedAt(); + this.point = question.getPoint(); + + } + public static QuestionResponse create(Question question){ + return new QuestionResponse(question); + } + +} diff --git a/app/src/main/java/org/example/domain/question/dto/request/CommentForm.java b/app/src/main/java/org/example/domain/question/dto/request/CommentRequest.java similarity index 92% rename from app/src/main/java/org/example/domain/question/dto/request/CommentForm.java rename to app/src/main/java/org/example/domain/question/dto/request/CommentRequest.java index 0d751db..5972d3c 100644 --- a/app/src/main/java/org/example/domain/question/dto/request/CommentForm.java +++ b/app/src/main/java/org/example/domain/question/dto/request/CommentRequest.java @@ -7,7 +7,7 @@ @Data @AllArgsConstructor -public class CommentForm { +public class CommentRequest { @NotBlank(message = "댓글을 입력하셔야 합니다.") @Size(max = 500, message = "댓글은 최대 500자입니다.") private String comment; diff --git a/app/src/main/java/org/example/domain/question/dto/request/QuestionCreateForm.java b/app/src/main/java/org/example/domain/question/dto/request/QuestionCreateRequest.java similarity index 87% rename from app/src/main/java/org/example/domain/question/dto/request/QuestionCreateForm.java rename to app/src/main/java/org/example/domain/question/dto/request/QuestionCreateRequest.java index a730f03..d59f5d4 100644 --- a/app/src/main/java/org/example/domain/question/dto/request/QuestionCreateForm.java +++ b/app/src/main/java/org/example/domain/question/dto/request/QuestionCreateRequest.java @@ -2,15 +2,11 @@ import jakarta.validation.constraints.*; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; -import java.time.LocalDateTime; -import java.util.List; - @Data @AllArgsConstructor -public class QuestionCreateForm { +public class QuestionCreateRequest { //private String username; @@ -19,7 +15,7 @@ public class QuestionCreateForm { regexp = "^(ko|en|ja|cn|fr|ar|es|ru)$", message = "허용되지 않은 언어 코드입니다. (ko, en, ja, cn, fr, ar, es, ru만 허용)" ) - private String question_language; + private String questionLanguage; @NotBlank(message = "제목은 반드시 입력해야 합니다.") @Size(max = 50, message = "제목은 최대 50자입니다.") diff --git a/app/src/main/java/org/example/domain/question/dto/request/QuestionEditForm.java b/app/src/main/java/org/example/domain/question/dto/request/QuestionEditRequest.java similarity index 92% rename from app/src/main/java/org/example/domain/question/dto/request/QuestionEditForm.java rename to app/src/main/java/org/example/domain/question/dto/request/QuestionEditRequest.java index ec57f6a..5093917 100644 --- a/app/src/main/java/org/example/domain/question/dto/request/QuestionEditForm.java +++ b/app/src/main/java/org/example/domain/question/dto/request/QuestionEditRequest.java @@ -4,11 +4,9 @@ import lombok.Builder; import lombok.Data; -import java.time.LocalDateTime; - @Data @Builder -public class QuestionEditForm { +public class QuestionEditRequest { @Pattern( regexp = "^(ko|en|ja|cn|fr|ar|es|ru)$", message = "허용되지 않은 언어 코드입니다. (ko, en, ja, cn, fr, ar, es, ru만 허용)" diff --git a/app/src/main/java/org/example/domain/question/entity/Question.java b/app/src/main/java/org/example/domain/question/entity/Question.java index f9cb196..ffdb92d 100644 --- a/app/src/main/java/org/example/domain/question/entity/Question.java +++ b/app/src/main/java/org/example/domain/question/entity/Question.java @@ -4,30 +4,24 @@ import lombok.*; import org.example.domain.comment.Comment; import org.example.domain.member.entity.Member; -import org.example.domain.question.dto.request.CommentForm; -import org.example.domain.question.dto.request.QuestionCreateForm; -import org.example.domain.question.dto.request.QuestionEditForm; +import org.example.domain.question.dto.request.CommentRequest; +import org.example.domain.question.dto.request.QuestionCreateRequest; +import org.example.domain.question.dto.request.QuestionEditRequest; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; @Entity @Data @NoArgsConstructor @AllArgsConstructor -@Builder public class Question { - private static final AtomicLong sequence = new AtomicLong(); - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "question_id") private Long id; - private String username; - private String question_language; + private String questionLanguage; private String title; private String content; private LocalDateTime createdAt; @@ -39,27 +33,33 @@ public class Question { private Member member; @OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true) - @Builder.Default private List comments = new ArrayList(); + private Question(final QuestionCreateRequest request){ + this.questionLanguage = request.getQuestionLanguage(); + this.title = request.getTitle(); + this.content = request.getContent(); + this.point = request.getPoint(); + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + + } + // 연관관계 편의 메서드 public void addComment(Comment comment) { this.comments.add(comment); comment.setQuestion(this); } - // 생성 메서드 - public static Question createQuestion(QuestionCreateForm questionCreateForm, String loggedUsername) { - return Question.builder() - .id(sequence.incrementAndGet()) // ID 자동 증가 - .username(loggedUsername) - .build(); + // 생성 메서드 + public static Question createQuestion(QuestionCreateRequest questionCreateRequest) { + return new Question(questionCreateRequest); } - public Question editQuestion(QuestionEditForm updatedQuestion) { + public Question editQuestion(QuestionEditRequest updatedQuestion) { if (updatedQuestion.getQuestion_language() != null) { - this.question_language = updatedQuestion.getQuestion_language(); + this.questionLanguage = updatedQuestion.getQuestion_language(); } if (updatedQuestion.getTitle() != null) { this.title = updatedQuestion.getTitle(); @@ -79,7 +79,7 @@ public Question editQuestion(QuestionEditForm updatedQuestion) { // 질문 댓글 수정 - public void editComment(CommentForm commentEditForm, Long comment_id) { + public void editComment(CommentRequest commentEditForm, Long comment_id) { } diff --git a/app/src/main/java/org/example/domain/session/SessionConst.java b/app/src/main/java/org/example/domain/session/SessionConst.java deleted file mode 100644 index 62bd7d3..0000000 --- a/app/src/main/java/org/example/domain/session/SessionConst.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.example.domain.session; - -public class SessionConst { - public static final String LOGIN_MEMBER = "loginMember"; - -} diff --git a/app/src/main/java/org/example/domain/session/SessionController.java b/app/src/main/java/org/example/domain/session/SessionController.java deleted file mode 100644 index 10ec43c..0000000 --- a/app/src/main/java/org/example/domain/session/SessionController.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.example.domain.session; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@Slf4j -@RestController -public class SessionController { - - @GetMapping("/session-info") - public String sessionInfo(HttpServletRequest request) { - HttpSession session = request.getSession(false); - - if(session == null) { - return "세션이 존재하지 않습니다."; - } - - session.getAttributeNames().asIterator().forEachRemaining(name -> log.info("name = {}, value = {}", name, session.getAttribute(name))); - return "세션 정보 로그 출력 완료"; - } -} diff --git a/app/src/test/java/org/example/domain/auth/AuthControllerIntegrationTest.java b/app/src/test/java/org/example/domain/auth/AuthControllerIntegrationTest.java new file mode 100644 index 0000000..bf14c96 --- /dev/null +++ b/app/src/test/java/org/example/domain/auth/AuthControllerIntegrationTest.java @@ -0,0 +1,103 @@ +package org.example.domain.auth; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityManager; +import org.assertj.core.api.Assertions; +import org.example.domain.auth.dto.request.LoginRequest; +import org.example.domain.auth.dto.response.TokenResponse; +import org.example.domain.auth.fixture.AuthTestFixture; +import org.example.domain.member.MemberRepository; +import org.example.domain.member.MemberTestFixture; +import org.example.domain.member.dto.request.MemberJoinRequest; +import org.example.domain.member.dto.response.MemberResponse; +import org.example.domain.member.entity.Member; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.*; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Transactional +class AuthControllerIntegrationTest { + + @LocalServerPort + private int port; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private EntityManager em; + + @Autowired + private MemberRepository memberRepository; + + private final RestTemplate restTemplate = new RestTemplate(); + @BeforeEach + public void setUp() { + memberRepository.deleteAll(); + em.flush(); + em.clear(); + } + + @Test + void 회원가입_성공을_확인한다() throws Exception { + // given + MemberJoinRequest memberJoinRequest = MemberTestFixture.createMemberJoinRequest(); + String url = "http://localhost:" + port + "/auth/join"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity request = new HttpEntity<>(objectMapper.writeValueAsString(memberJoinRequest), headers); + + // When + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, request, MemberResponse.class); + + // Then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + MemberResponse body = response.getBody(); + assertThat(body).isNotNull(); + assertThat(body.getNationality()).isEqualTo("USA"); + assertThat(body.getNativeLang()).isEqualTo("en"); + assertThat(body.getLearnings()).containsExactly("fr", "ja"); + assertThat(body.getIntroduction()).isEqualTo("I am learning languages!"); + assertThat(body.getFollower()).isEqualTo(0); + assertThat(body.getFollowing()).isEqualTo(0); + assertThat(body.getPoint()).isEqualTo(50); + assertThat(body.getRole()).isEqualTo("USER"); + } + + //@Test + void 로그인_성공을_확인한다() throws Exception{ + // given + + // 회원 저장 org.springframework.orm.ObjectOptimisticLockingFailureException + Member member = MemberTestFixture.createMember(); + memberRepository.saveAndFlush(member); + + LoginRequest loginRequest = AuthTestFixture.createLoginRequest(); + TokenResponse tokenResponse = AuthTestFixture.createTokenResponse(); + String url = "http://localhost:" + port + "/auth/login"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity request = new HttpEntity<>(objectMapper.writeValueAsString(loginRequest), headers); + + // When + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, request, TokenResponse.class); + + // Then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + TokenResponse body = response.getBody(); + + assertThat(body).isNotNull(); + + } + + +} + diff --git a/app/src/test/java/org/example/domain/auth/AuthControllerWebMvcTest.java b/app/src/test/java/org/example/domain/auth/AuthControllerWebMvcTest.java new file mode 100644 index 0000000..39d0f01 --- /dev/null +++ b/app/src/test/java/org/example/domain/auth/AuthControllerWebMvcTest.java @@ -0,0 +1,96 @@ +package org.example.domain.auth; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.example.domain.auth.dto.request.LoginRequest; +import org.example.domain.auth.dto.request.RefreshRequest; +import org.example.domain.auth.dto.response.TokenResponse; +import org.example.domain.auth.fixture.AuthTestFixture; +import org.example.domain.member.MemberTestFixture; +import org.example.domain.member.dto.request.MemberJoinRequest; +import org.example.domain.member.entity.Member; +import org.example.helper.MockBeanInjection; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(AuthController.class) +public class AuthControllerWebMvcTest extends MockBeanInjection { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Test + void 회원가입을_한다()throws Exception{ + // given + MemberJoinRequest memberJoinRequest = MemberTestFixture.createMemberJoinRequest(); + Member member = MemberTestFixture.createMember(); + when(memberService.addMember(any(MemberJoinRequest.class))).thenReturn(member); + + // When & Then + mockMvc.perform(post("/auth/join") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(memberJoinRequest))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.email").value("valid@example.com")) + .andExpect(jsonPath("$.username").value("validUsername")) + .andExpect(jsonPath("$.nationality").value("USA")) + .andExpect(jsonPath("$.nativeLang").value("en")) + .andExpect(jsonPath("$.learnings[0]").value("fr")) + .andExpect(jsonPath("$.learnings[1]").value("ja")) + .andExpect(jsonPath("$.introduction").value("I am learning languages!")) + .andExpect(jsonPath("$.follower").value(0)) + .andExpect(jsonPath("$.following").value(0)) + .andExpect(jsonPath("$.point").value(50)) + .andExpect(jsonPath("$.role").value("USER")); + + } + + @Test + void 로그인을_한다() throws Exception{ + // given + LoginRequest loginRequest = AuthTestFixture.createLoginRequest(); + TokenResponse tokenResponse = AuthTestFixture.createTokenResponse(); + when(authService.issueToken(any(LoginRequest.class))).thenReturn(tokenResponse); + + // When & Then + mockMvc.perform(post("/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginRequest))) + .andExpect(status().isOk()) + .andExpect(header().string("Authorization", "Bearer " + tokenResponse.getAccessToken())) + .andExpect(jsonPath("$.accessToken").value(tokenResponse.getAccessToken())) + .andExpect(jsonPath("$.refreshToken").value(tokenResponse.getRefreshToken())); + } + + @Test + void accessToken을_재발급_받는다() throws Exception{ + + //Given + RefreshRequest refreshRequest = AuthTestFixture.createRefreshRequest(); + TokenResponse tokenResponse = AuthTestFixture.createTokenResponse(); + when(authService.reissueRefreshToken(refreshRequest)).thenReturn(tokenResponse); + //When & Then + mockMvc.perform(post("/auth/refresh") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(refreshRequest))) + .andExpect(status().isOk()) + .andExpect(header().string("Authorization", "Bearer " + tokenResponse.getAccessToken())) + .andExpect(jsonPath("$.accessToken").value(tokenResponse.getAccessToken())) + .andExpect(jsonPath("$.refreshToken").value(tokenResponse.getRefreshToken())); + + + } + + +} diff --git a/app/src/test/java/org/example/domain/auth/AuthRepositoryTest.java b/app/src/test/java/org/example/domain/auth/AuthRepositoryTest.java new file mode 100644 index 0000000..12281fd --- /dev/null +++ b/app/src/test/java/org/example/domain/auth/AuthRepositoryTest.java @@ -0,0 +1,44 @@ +package org.example.domain.auth; + +import org.example.domain.auth.entity.AuthEntity; +import org.example.domain.auth.fixture.AuthTestFixture; +import org.example.domain.member.MemberRepository; +import org.example.domain.member.MemberTestFixture; +import org.example.domain.member.entity.Member; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class AuthRepositoryTest { + + @Autowired + private AuthRepository authRepository; + + @Autowired + MemberRepository memberRepository; + + @BeforeEach + void setUp() { + Member member = MemberTestFixture.createMember(); + memberRepository.save(member); + // org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [org.example.domain.member.entity.Member#1] + AuthEntity authEntity = AuthEntity.createWith(AuthTestFixture.createRefreshToken()); + authEntity.setMember(member); + authRepository.save(authEntity); + } + + @Test + void testFindByRefreshToken() { + String refreshToken = "mockRefreshToken"; + Optional authEntityOptional = authRepository.findByRefreshToken(refreshToken); + assertTrue(authEntityOptional.isPresent()); + } +} \ No newline at end of file diff --git a/app/src/test/java/org/example/domain/auth/AuthServiceTest.java b/app/src/test/java/org/example/domain/auth/AuthServiceTest.java new file mode 100644 index 0000000..36ea9cf --- /dev/null +++ b/app/src/test/java/org/example/domain/auth/AuthServiceTest.java @@ -0,0 +1,143 @@ +package org.example.domain.auth; + +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.persistence.EntityManager; +import lombok.extern.slf4j.Slf4j; +import org.assertj.core.api.Assert; +import org.assertj.core.api.Assertions; +import org.example.domain.auth.dto.request.LoginRequest; +import org.example.domain.auth.dto.request.RefreshRequest; +import org.example.domain.auth.dto.response.TokenResponse; +import org.example.domain.auth.entity.AuthEntity; +import org.example.domain.auth.fixture.AuthTestFixture; +import org.example.domain.auth.jwt.JWTUtil; +import org.example.domain.member.MemberRepository; +import org.example.domain.member.MemberTestFixture; +import org.example.domain.member.entity.Member; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +//@SpringBootTest +//@Transactional +@Slf4j +@ExtendWith(MockitoExtension.class) +class AuthServiceTest { + + private AuthService authService; + @Mock + MemberRepository memberRepository; + @Mock + private AuthRepository authRepository; + @Mock + private JWTUtil jwtUtil; + @Mock + private EntityManager em; + @BeforeEach + public void setUp() { + authService = new AuthService(memberRepository,authRepository,jwtUtil,em); + } + @Test + void 로그인_했을_때_refreshToken이_DB에_저장되었는지_확인한다() { + // Given + Member member = MemberTestFixture.createMember(); + LoginRequest loginRequest = AuthTestFixture.createLoginRequest(); + String generatedAccessToken = AuthTestFixture.createAccessToken(); + String generatedRefreshToken = AuthTestFixture.createRefreshToken(); + AuthEntity authEntity = AuthTestFixture.createAuthEntity(); + + + // mocking + // when(memberRepository.save(any(Member.class))).thenReturn(member); + when(memberRepository.findByEmail(loginRequest.getEmail())).thenReturn(Optional.of(member)); + when(jwtUtil.createAccessToken(any(), eq(member.getUsername()), eq(member.getRole()))) + .thenReturn(generatedAccessToken); + when(jwtUtil.createRefreshToken(any(), eq(member.getUsername()), eq(member.getRole()))) + .thenReturn(generatedRefreshToken); + when(authRepository.save(any(AuthEntity.class))).thenReturn(authEntity); + when(authRepository.findByRefreshToken(generatedRefreshToken)).thenReturn(Optional.of(authEntity)); + + + // When + TokenResponse tokenResponse = authService.issueToken(loginRequest); + // Then + String refreshToken = tokenResponse.getRefreshToken(); + assertThat(authRepository.findByRefreshToken(refreshToken).get()).isNotNull(); + } + @Test + void refreshToken과_accessToken이_유효하고_사용자ID가_일치하면_새로운_refresh토큰을_발급한다 () { + // Given + Member member = MemberTestFixture.createMember(); + LoginRequest loginRequest = AuthTestFixture.createLoginRequest(); + String generatedAccessToken = AuthTestFixture.createAccessToken(); + String generatedRefreshToken = AuthTestFixture.createRefreshToken(); + RefreshRequest refreshRequest = AuthTestFixture.createRefreshRequest(); + AuthEntity authEntity = AuthTestFixture.createAuthEntity(); + + String newAccessToken = "newAccessToken"; + String newRefreshToken = "newRefreshToken"; + + //mocking + when(jwtUtil.isExpired(refreshRequest.getRefreshToken())).thenReturn(false); + when(jwtUtil.getId(generatedAccessToken)).thenReturn(1L); + when(jwtUtil.getId(generatedRefreshToken)).thenReturn(1L); + when(authRepository.findByRefreshToken(generatedRefreshToken)).thenReturn(Optional.of(authEntity)); + when(memberRepository.findById(any())).thenReturn(Optional.of(member)); + when(jwtUtil.createAccessToken(any(),eq(member.getUsername()),eq(member.getRole()))).thenReturn(newAccessToken); + doNothing().when(authRepository).deleteByAuthId(any()); + when(jwtUtil.createRefreshToken(any(),eq(member.getUsername()),eq(member.getRole()))).thenReturn(newRefreshToken); + when(authRepository.save(any(AuthEntity.class))).thenReturn(authEntity); + + + //when + TokenResponse tokenResponse = authService.reissueRefreshToken(refreshRequest); + + //Then + Assertions.assertThat(tokenResponse.getRefreshToken()).isEqualTo(newRefreshToken); + } + + @Test + void refreshToken이_만료되었다면__401에러를_발생시킨다() { + // Given + RefreshRequest refreshRequest = AuthTestFixture.createRefreshRequest(); + + // mocking + when(jwtUtil.isExpired(refreshRequest.getRefreshToken())).thenThrow(ExpiredJwtException.class); + doNothing().when(authRepository).deleteByRefreshToken(refreshRequest.getRefreshToken()); + + // When + RuntimeException exception = assertThrows(RuntimeException.class, () -> authService.reissueRefreshToken(refreshRequest)); + assertEquals(exception.getMessage(),"다시 로그인 하세요."); + + } + + @Test + void accssToken과_refreshToken의_사용자ID가_일치하지_않는다면_401에러를_발생시킨다(){ + // Given + RefreshRequest refreshRequest = AuthTestFixture.createRefreshRequest(); + + // mocking + when(jwtUtil.isExpired(refreshRequest.getRefreshToken())).thenReturn(false); + when(jwtUtil.getId(refreshRequest.getAccessToken())).thenReturn(1L); + when(jwtUtil.getId(refreshRequest.getRefreshToken())).thenReturn(2L); + // When + RuntimeException exception = assertThrows(RuntimeException.class, () -> authService.reissueRefreshToken(refreshRequest)); + + // Then + assertEquals("Access Token, Refresh Token의 사용자 ID가 일치하지 않습니다.", exception.getMessage()); + + } +} \ No newline at end of file diff --git a/app/src/test/java/org/example/domain/auth/filter/JWTLoginFilterTest.java b/app/src/test/java/org/example/domain/auth/filter/JWTLoginFilterTest.java new file mode 100644 index 0000000..b362f6f --- /dev/null +++ b/app/src/test/java/org/example/domain/auth/filter/JWTLoginFilterTest.java @@ -0,0 +1,112 @@ +package org.example.domain.auth.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import org.example.domain.auth.jwt.JWTLoginFilter; +import org.example.domain.auth.jwt.JWTUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class JWTLoginFilterTest { + + private JWTLoginFilter jwtLoginFilter; + + @Mock + private JWTUtil jwtUtil; + + @Mock + private FilterChain filterChain; + + private MockHttpServletRequest request; + private MockHttpServletResponse response; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + jwtLoginFilter = new JWTLoginFilter(jwtUtil); + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + } + + + @Test + void auth경로는_JWTLoginFilter를_통과한다 () throws ServletException, IOException { + + //Given + request.setRequestURI("/auth/login"); + + //When + jwtLoginFilter.doFilter(request,response,filterChain); + + //Then + verify(filterChain, times(1)).doFilter(request,response); + + } + + @Test + void api경로는_인증_정보가_없으면_JWTLoginFilter에서_걸리고_401을_리턴한다 () throws ServletException, IOException { + + //Given + request.setRequestURI("/api/resource"); + + //When + jwtLoginFilter.doFilter(request,response,filterChain); + + //Then + verify(filterChain, never()).doFilter(request,response); + assertEquals(401, response.getStatus()); + } + + @Test + void api경로의_authorization이_Bearer로_시작하지_않으면_JWTLoginFilter에서_걸리고_401을_리턴한다 () throws ServletException, IOException { + //Given - Bearer로 시작하지 않은 Authorization 헤더 + request.setRequestURI("/api/resource"); + request.addHeader("Authorization", "Bear"); + + //When + jwtLoginFilter.doFilter(request,response,filterChain); + + //Then + verify(filterChain, never()).doFilter(request,response); + assertEquals(401, response.getStatus()); + } + + @Test + void api경로의_토큰이_만료되었다면_JWTLoginFilter를_통과하지_못한다 () throws ServletException, IOException { + //Given + request.setRequestURI("/api/resource"); + request.addHeader("Authorization", "Bearer expired"); + when(jwtUtil.isExpired("expired")).thenReturn(true); + + //When + jwtLoginFilter.doFilter(request,response,filterChain); + + //Then + verify(filterChain, never()).doFilter(request,response); + assertEquals(401, response.getStatus()); + } + + @Test + void api경로의_토큰이_유효하면_JWTLoginFilter를_통과한다 () throws ServletException, IOException { + // 유효한 토큰 + request.setRequestURI("/api/resource"); + request.addHeader("Authorization", "Bearer validToken"); + when(jwtUtil.isExpired("validToken")).thenReturn(false); + + jwtLoginFilter.doFilter(request, response, filterChain); + + // 필터 체인이 실행되었는지 확인 + verify(filterChain, times(1)).doFilter(request, response); + assertEquals(200, response.getStatus()); + + } +} \ No newline at end of file diff --git a/app/src/test/java/org/example/domain/auth/fixture/AuthTestFixture.java b/app/src/test/java/org/example/domain/auth/fixture/AuthTestFixture.java new file mode 100644 index 0000000..a732786 --- /dev/null +++ b/app/src/test/java/org/example/domain/auth/fixture/AuthTestFixture.java @@ -0,0 +1,34 @@ +package org.example.domain.auth.fixture; + +import org.example.domain.auth.dto.request.LoginRequest; +import org.example.domain.auth.dto.request.RefreshRequest; +import org.example.domain.auth.dto.response.TokenResponse; +import org.example.domain.auth.entity.AuthEntity; +import org.example.domain.member.MemberTestFixture; + +public class AuthTestFixture { + public static LoginRequest createLoginRequest() { + return LoginRequest.builder().email("valid@example.com").password("validPassword123").build(); + } + + public static String createAccessToken() { + return "mockAccessToken"; + } + + public static String createRefreshToken() { + return "mockRefreshToken"; + } + + public static RefreshRequest createRefreshRequest() { + return new RefreshRequest(createAccessToken(),createRefreshToken()); + } + public static TokenResponse createTokenResponse( ){ + return TokenResponse.builder().accessToken(createAccessToken()).refreshToken(createRefreshToken()).build(); + } + + public static AuthEntity createAuthEntity() { + return AuthEntity.createWith(createRefreshToken()); + } + + +} diff --git a/app/src/test/java/org/example/domain/auth/interceptor/LoginCheckInterceptorTest.java b/app/src/test/java/org/example/domain/auth/interceptor/LoginCheckInterceptorTest.java new file mode 100644 index 0000000..d683958 --- /dev/null +++ b/app/src/test/java/org/example/domain/auth/interceptor/LoginCheckInterceptorTest.java @@ -0,0 +1,56 @@ +package org.example.domain.auth.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.AssertTrue; +import org.assertj.core.api.Assertions; +import org.example.domain.auth.AuthenticationContext; +import org.example.domain.auth.jwt.JWTUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class LoginCheckInterceptorTest { + private LoginCheckInterceptor loginCheckInterceptor; + private AuthenticationContext authenticationContext; + @Mock + JWTUtil jwtUtil; + + @Mock + HttpServletRequest request; + + @Mock + HttpServletResponse response; + + @Mock + Object handler; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + authenticationContext = new AuthenticationContext(); + loginCheckInterceptor = new LoginCheckInterceptor(jwtUtil, authenticationContext); + } + + @Test + void 인증된_사용자의_이름이_authenticationContext에_저장된다( ) throws Exception { + //Given + String validToken = "Bearer valid"; + String username = "test"; + + when(request.getHeader("Authorization")).thenReturn(validToken); + when(jwtUtil.getUsername("valid")).thenReturn(username); + + //When + boolean result = loginCheckInterceptor.preHandle(request, response, handler); + + //Then + assertTrue(result); + Assertions.assertThat(authenticationContext.getPrincipal()).isEqualTo("test"); + } +} \ No newline at end of file diff --git a/app/src/test/java/org/example/domain/auth/resolver/LoginMemberArgumentResolverTest.java b/app/src/test/java/org/example/domain/auth/resolver/LoginMemberArgumentResolverTest.java new file mode 100644 index 0000000..0c118dd --- /dev/null +++ b/app/src/test/java/org/example/domain/auth/resolver/LoginMemberArgumentResolverTest.java @@ -0,0 +1,68 @@ +package org.example.domain.auth.resolver; + +import jakarta.validation.constraints.AssertTrue; +import lombok.extern.slf4j.Slf4j; +import org.assertj.core.api.Assertions; +import org.example.domain.auth.AuthenticationContext; +import org.example.domain.auth.annotation.LoginMember; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; + +import static org.mockito.Mockito.when; + +@Slf4j +class LoginMemberArgumentResolverTest { + private LoginMemberArgumentResolver resolver; + + @Mock + AuthenticationContext authenticationContext; + @Mock + MethodParameter methodParameter; + + @Mock + ModelAndViewContainer modelAndViewContainer; + + @Mock + NativeWebRequest nativeWebRequest; + @Mock + WebDataBinderFactory webDataBinderFactory; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + resolver = new LoginMemberArgumentResolver(authenticationContext); + } + @Test + void LoginMember_어노테이션과_문자열_타입을_만족하는지_확인한다() { + // Given + when(methodParameter.hasParameterAnnotation(LoginMember.class)).thenReturn(true); + when(methodParameter.getParameterType()).thenReturn((Class) String.class); + + // When + boolean result = resolver.supportsParameter(methodParameter); + + // Then + Assertions.assertThat(result).isTrue(); + + + + } + @Test + void resolveArgument는_authentication_context의_principal값을_리턴한다() throws Exception { + //Given + when(authenticationContext.getPrincipal()).thenReturn("user"); + + //When + String result = (String)resolver.resolveArgument(methodParameter, modelAndViewContainer, nativeWebRequest, webDataBinderFactory); + + //Then + Assertions.assertThat(result).isEqualTo("user"); + } + +} \ No newline at end of file From 2df6c025f64c785b6842ac32f73176019ab7ddac Mon Sep 17 00:00:00 2001 From: gdrffg Date: Wed, 22 Jan 2025 16:50:22 +0900 Subject: [PATCH 6/8] Create MockBeanInjection.java --- .../org/example/helper/MockBeanInjection.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/src/test/java/org/example/helper/MockBeanInjection.java diff --git a/app/src/test/java/org/example/helper/MockBeanInjection.java b/app/src/test/java/org/example/helper/MockBeanInjection.java new file mode 100644 index 0000000..bc3da67 --- /dev/null +++ b/app/src/test/java/org/example/helper/MockBeanInjection.java @@ -0,0 +1,22 @@ +package org.example.helper; + +import org.example.domain.auth.AuthService; +import org.example.domain.auth.AuthenticationContext; +import org.example.domain.auth.jwt.JWTUtil; +import org.example.domain.member.MemberService; +import org.mockito.junit.jupiter.MockitoSettings; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +public class MockBeanInjection { + //auth + @MockitoBean + protected JWTUtil jwtUtil; + @MockitoBean + protected AuthenticationContext authenticationContext; + @MockitoBean + protected AuthService authService; + @MockitoBean + protected MemberService memberService; + +} From 8d3106322fdd5174addfe87cc72f44d8b30af1ed Mon Sep 17 00:00:00 2001 From: gdrffg Date: Wed, 22 Jan 2025 16:52:56 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20member=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/MemberControllerTest.java | 75 ------------------- .../member/MemberControllerWebMvcTest.java | 65 ++++++++++++++++ .../domain/member/MemberRepositoryTest.java | 14 ++-- .../domain/member/MemberServiceTest.java | 58 ++++++++------ .../domain/member/MemberTestFixture.java | 20 ++++- 5 files changed, 123 insertions(+), 109 deletions(-) delete mode 100644 app/src/test/java/org/example/domain/member/MemberControllerTest.java create mode 100644 app/src/test/java/org/example/domain/member/MemberControllerWebMvcTest.java diff --git a/app/src/test/java/org/example/domain/member/MemberControllerTest.java b/app/src/test/java/org/example/domain/member/MemberControllerTest.java deleted file mode 100644 index 516d125..0000000 --- a/app/src/test/java/org/example/domain/member/MemberControllerTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.example.domain.member; - -import org.example.domain.member.dto.request.MemberEditRequest; -import org.example.domain.member.dto.request.MemberJoinRequest; -import org.example.domain.member.dto.response.MemberResponse; -import org.example.domain.member.entity.Member; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.test.annotation.Rollback; -import org.springframework.transaction.annotation.Transactional; - -import static org.assertj.core.api.Assertions.*; - -@SpringBootTest -@Rollback -class MemberControllerTest { - @Autowired - MemberController memberController; - - @Autowired - MemberRepository memberRepository; - @Test - void memberAdd() { - //Given - MemberJoinRequest memberJoinRequest = MemberTestFixture.createMemberJoinRequest(); - - //When - ResponseEntity response = memberController.memberAdd(memberJoinRequest); - - //Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); - assertThat(response.getBody().getUsername()).isEqualTo("validUsername"); - assertThat(response.getBody().getEmail()).isEqualTo("valid@example.com"); - } - - @Test - @Transactional - // Member와 Language가 1:N 연관관계를 맺고 있고 지연 로딩으로 설정되어 있음. - // 테스트 코드를 호출하는 동안에는 영속성 컨텍스트가 닫혀있으므로 지연 로딩 을 사용할 수 없어 @Transactional 이용 - void memberDetails() { - //Given - Member member = MemberTestFixture.createMember(); - Member saveMember = memberRepository.save(member); - - //When - ResponseEntity response = memberController.memberDetails(saveMember.getId()); - - //Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody().getUsername()).isEqualTo("validUsername"); - assertThat(response.getBody().getEmail()).isEqualTo("valid@example.com"); - } - - - @Test - void memberModify() { - //Given - Member member = MemberTestFixture.createMember(); - memberRepository.save(member); - - MemberEditRequest validMemberEditRequest = MemberTestFixture.createValidMemberEditForm(); - - //When - ResponseEntity response = memberController.memberModify(member.getId(), validMemberEditRequest); - - //Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody().getUsername()).isEqualTo("updatedUsername"); - assertThat(response.getBody().getIntroduction()).isEqualTo("This is my updated introduction."); - } - -} \ No newline at end of file diff --git a/app/src/test/java/org/example/domain/member/MemberControllerWebMvcTest.java b/app/src/test/java/org/example/domain/member/MemberControllerWebMvcTest.java new file mode 100644 index 0000000..066fced --- /dev/null +++ b/app/src/test/java/org/example/domain/member/MemberControllerWebMvcTest.java @@ -0,0 +1,65 @@ +package org.example.domain.member; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.example.domain.member.dto.request.MemberEditRequest; +import org.example.domain.member.dto.response.MemberResponse; +import org.example.domain.member.entity.Member; +import org.example.helper.MockBeanInjection; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.MockMvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +@WebMvcTest(MemberController.class) +class MemberControllerWebMvcTest extends MockBeanInjection { + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + private final String token = "validToken"; + @Test + void 프로필을_조회한다() throws Exception { + //Given & When + Member member = MemberTestFixture.createMember(); + MemberResponse memberResponse = MemberTestFixture.createMemberResponse(); + when(memberService.findMember(any(Long.class))).thenReturn(member); + + //Then + mockMvc.perform(get("/api/profile/1").header("Authorization", "Bearer " + token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.email").value("valid@example.com")) + .andExpect(jsonPath("$.username").value("validUsername")); + } + + + @Test + void 프로필을_수정한다() throws Exception { + //Given & When + Member member = MemberTestFixture.createMember(); + MemberEditRequest memberEditRequest = MemberTestFixture.createMemberEditRequest(); + Member updatedMember = member.editMember(memberEditRequest); + when(memberService.modifyMember(any(Long.class), any(MemberEditRequest.class))).thenReturn(updatedMember); + + //Then + mockMvc.perform(put("/api/profile/1/edit") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(memberEditRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username").value("updatedUsername")); + } + +} \ No newline at end of file diff --git a/app/src/test/java/org/example/domain/member/MemberRepositoryTest.java b/app/src/test/java/org/example/domain/member/MemberRepositoryTest.java index a75c51f..6da66bd 100644 --- a/app/src/test/java/org/example/domain/member/MemberRepositoryTest.java +++ b/app/src/test/java/org/example/domain/member/MemberRepositoryTest.java @@ -2,24 +2,24 @@ import jakarta.persistence.EntityManager; import org.example.domain.member.entity.Member; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import static org.assertj.core.api.Assertions.assertThat; -@SpringBootTest -@Transactional -@Rollback +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class MemberRepositoryTest { @Autowired MemberRepository memberRepository; @Autowired - EntityManager em; - // 테스타가 실했다가 성공했다가 반복 -> 테스트에 사용하는 데이터가 같아서 중복된 데이터가 삽입될 가능성이 있음. - // 각 테스트가 독립적으로 실행되도록 해야 함 -> 테스트 데이터를 다르게 구성? + EntityManager entityManager; + @Test void findByEmailTest() { //Given diff --git a/app/src/test/java/org/example/domain/member/MemberServiceTest.java b/app/src/test/java/org/example/domain/member/MemberServiceTest.java index 9fa69b9..1bcc709 100644 --- a/app/src/test/java/org/example/domain/member/MemberServiceTest.java +++ b/app/src/test/java/org/example/domain/member/MemberServiceTest.java @@ -1,59 +1,69 @@ package org.example.domain.member; +import jakarta.persistence.EntityManager; +import lombok.extern.slf4j.Slf4j; import org.example.domain.language.Language; +import org.example.domain.member.dto.request.MemberEditRequest; import org.example.domain.member.dto.request.MemberJoinRequest; import org.example.domain.member.entity.Member; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; -@SpringBootTest -@Transactional -@Rollback +@Slf4j +@ExtendWith(MockitoExtension.class) class MemberServiceTest { - @Autowired - MemberService memberService; - @Test - void addMember() { - //Given - MemberJoinRequest memberJoinRequest = MemberTestFixture.createMemberJoinRequest(); - //When - Member member = memberService.addMember(memberJoinRequest); + private MemberService memberService; + @Mock + private MemberRepository memberRepository; - //Then - assertThat(member.getEmail()).isEqualTo(memberJoinRequest.getEmail()); - assertThat(member.getPassword()).isEqualTo(memberJoinRequest.getPassword()); + @BeforeEach + public void setUp() { + memberService = new MemberService(memberRepository); } @Test - void findMember() { + void userId로_사용자를_조회한다() { //Given MemberJoinRequest memberJoinRequest = MemberTestFixture.createMemberJoinRequest(); - Member member = memberService.addMember(memberJoinRequest); + Member mockMember = MemberTestFixture.createMember(); + + //Mocking + when(memberRepository.findById(any(Long.class))).thenReturn(Optional.of(mockMember)); //When - Member findMember = memberService.findMember(member.getId()); + Member findMember = memberService.findMember(mockMember.getId()); //Then - assertThat(findMember).isEqualTo(member); + assertThat(findMember.getId()).isEqualTo(mockMember.getId()); } @Test - void modifyMember() { + void userId와_MemberEditRequest로_사용자_프로필을_수정한다() { //Given - MemberJoinRequest memberJoinRequest = MemberTestFixture.createMemberJoinRequest(); - Member member = memberService.addMember(memberJoinRequest); + MemberEditRequest memberEditRequest = MemberTestFixture.createMemberEditRequest(); + Member mockMember = MemberTestFixture.createMember(); + + //Mocking + when(memberRepository.findById(any(Long.class))).thenReturn(Optional.of(mockMember)); //When - Member editMember = memberService.modifyMember(member.getId(), MemberTestFixture.createValidMemberEditForm()); + Member editMember = memberService.modifyMember(mockMember.getId(), memberEditRequest); //Then - assertThat(editMember).isEqualTo(member); + assertThat(editMember).isEqualTo(editMember); // 배우는 언어가 "fr", "ja"에서 "es", "cn"으로 바뀌는지 확인 assertThat(editMember.getLearnings()) diff --git a/app/src/test/java/org/example/domain/member/MemberTestFixture.java b/app/src/test/java/org/example/domain/member/MemberTestFixture.java index 822cf70..e921622 100644 --- a/app/src/test/java/org/example/domain/member/MemberTestFixture.java +++ b/app/src/test/java/org/example/domain/member/MemberTestFixture.java @@ -1,15 +1,18 @@ package org.example.domain.member; +import org.example.domain.language.Language; import org.example.domain.member.dto.request.MemberEditRequest; import org.example.domain.member.dto.request.MemberJoinRequest; +import org.example.domain.member.dto.response.MemberResponse; import org.example.domain.member.entity.Member; +import java.util.ArrayList; import java.util.List; public class MemberTestFixture { - // 회원 가입 성공 + // 회원 가입 요청 public static MemberJoinRequest createMemberJoinRequest() { return MemberJoinRequest.builder() .email("valid@example.com") // 유효한 이메일 @@ -22,8 +25,13 @@ public static MemberJoinRequest createMemberJoinRequest() { .build(); } + // 회원 가입 성공 + public static MemberResponse createMemberResponse() { + return MemberResponse.createMemberResponse(createMember()); + } + // 사용자 프로필 수정 - public static MemberEditRequest createValidMemberEditForm() { + public static MemberEditRequest createMemberEditRequest() { return MemberEditRequest.builder() .username("updatedUsername") .nationality("CAN") @@ -33,8 +41,14 @@ public static MemberEditRequest createValidMemberEditForm() { .build(); } + public static Member createByMysqlMember() { + return Member.createMember(MemberTestFixture.createMemberJoinRequest()); + } // 사용자 생성 public static Member createMember( ){ - return Member.createMember(MemberTestFixture.createMemberJoinRequest()); + Member member = Member.createMember(MemberTestFixture.createMemberJoinRequest()); + member.setId(1L); + member.setLearnings(new ArrayList<>(List.of(Language.createLanguage("fr"), Language.createLanguage("ja")))); + return member; } } From c3816ae1acb5cb685cfcdcd8cae98f8e9c112372 Mon Sep 17 00:00:00 2001 From: gdrffg Date: Wed, 22 Jan 2025 17:21:27 +0900 Subject: [PATCH 8/8] fix: sonarqube --- .../main/java/org/example/domain/auth/AuthRepository.java | 6 ++---- .../main/java/org/example/domain/auth/AuthService.java | 8 ++++---- .../example/domain/auth/dto/request/RefreshRequest.java | 1 - .../java/org/example/domain/member/MemberController.java | 8 ++++++++ 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/example/domain/auth/AuthRepository.java b/app/src/main/java/org/example/domain/auth/AuthRepository.java index dfa7885..89f6e9c 100644 --- a/app/src/main/java/org/example/domain/auth/AuthRepository.java +++ b/app/src/main/java/org/example/domain/auth/AuthRepository.java @@ -1,9 +1,7 @@ package org.example.domain.auth; -import jakarta.persistence.LockModeType; import org.example.domain.auth.entity.AuthEntity; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -15,9 +13,9 @@ public interface AuthRepository extends JpaRepository { Optional findByRefreshToken(String refreshToken); - @Query("DELETE FROM AuthEntity a WHERE a.id = :auth_id") + @Query("DELETE FROM AuthEntity a WHERE a.id = :autId") @Modifying - void deleteByAuthId(@Param("auth_id") Long auth_id); + void deleteByAuthId(@Param("auth_id") Long authId); @Modifying void deleteByRefreshToken(String refreshToken); diff --git a/app/src/main/java/org/example/domain/auth/AuthService.java b/app/src/main/java/org/example/domain/auth/AuthService.java index a1b94cd..2f4af0e 100644 --- a/app/src/main/java/org/example/domain/auth/AuthService.java +++ b/app/src/main/java/org/example/domain/auth/AuthService.java @@ -29,15 +29,15 @@ public TokenResponse issueToken(final LoginRequest loginRequest) { // 이메일로 Member 조회 Member member = memberRepository.findByEmail(email).orElseThrow(() -> new RuntimeException("회원가입 되어 있지 않습니다.")); - Long member_id = member.getId(); + Long memberId = member.getId(); String username = member.getUsername(); - log.debug("member_id = {}",member_id); + log.debug("memberId = {}",memberId); log.debug("username = {}",username); // AccessToken, RefreshToken 생성 - String accessToken = jwtUtil.createAccessToken(member_id, username, "USER"); - String refreshToken = jwtUtil.createRefreshToken(member_id, username, "USER"); + String accessToken = jwtUtil.createAccessToken(memberId, username, "USER"); + String refreshToken = jwtUtil.createRefreshToken(memberId, username, "USER"); // refreshToken DB 저장 AuthEntity authEntity = AuthEntity.createWith(refreshToken); diff --git a/app/src/main/java/org/example/domain/auth/dto/request/RefreshRequest.java b/app/src/main/java/org/example/domain/auth/dto/request/RefreshRequest.java index ec2be49..5730bfa 100644 --- a/app/src/main/java/org/example/domain/auth/dto/request/RefreshRequest.java +++ b/app/src/main/java/org/example/domain/auth/dto/request/RefreshRequest.java @@ -1,7 +1,6 @@ package org.example.domain.auth.dto.request; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/app/src/main/java/org/example/domain/member/MemberController.java b/app/src/main/java/org/example/domain/member/MemberController.java index d8d0b81..e2280b4 100644 --- a/app/src/main/java/org/example/domain/member/MemberController.java +++ b/app/src/main/java/org/example/domain/member/MemberController.java @@ -15,6 +15,14 @@ public class MemberController { private final MemberService memberService; + @PostMapping("/add") + public ResponseEntity memberAdd(@Valid @RequestBody MemberJoinRequest memberJoinRequest) { + Member savedMember = memberService.addMember(memberJoinRequest); + MemberResponse memberResponse = MemberResponse.createMemberResponse(savedMember); + return ResponseEntity.status(201).body(memberResponse); + + } + // 사용자 프로필 조회, 사용자 기본 정보 제공 @GetMapping("/{user_id}") public ResponseEntity memberDetails(@PathVariable(value = "user_id") Long userId) {