Skip to content

Commit

Permalink
Merge pull request #122 from cvs-go/feature#121
Browse files Browse the repository at this point in the history
회원 탈퇴 API 수정
  • Loading branch information
feel-coding authored Apr 28, 2024
2 parents 0a88e7c + 276a63c commit a4a6305
Show file tree
Hide file tree
Showing 13 changed files with 128 additions and 20 deletions.
3 changes: 2 additions & 1 deletion sql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,12 @@ create table user (
nickname varchar(16) not null unique,
password varchar(255) not null,
role varchar(20) not null,
user_id varchar(50) not null unique,
user_id varchar(50) unique,
profile_image_url varchar(255),
created_at datetime,
modified_at datetime,
is_deleted tinyint(1),
deleted_date date,
primary key (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Expand Down
10 changes: 10 additions & 0 deletions src/docs/asciidoc/api-doc.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,15 @@ include::{snippets}/auth-controller-test/respond_200_when_login_succeed/http-req
include::{snippets}/auth-controller-test/respond_200_when_login_succeed/response-fields.adoc[]
==== Sample Response
include::{snippets}/auth-controller-test/respond_200_when_login_succeed/http-response.adoc[]
==== Error Response
|===
| HTTP Status | Error Code | Detail

| `401 UNAUTHORIZED` | `INVALID_PASSWORD` | 비밀번호가 일치하지 않는 경우
| `403 FORBIDDEN` | `FORBIDDEN_USER` | 탈퇴한 회원인 경우
| `404 NOT FOUND` | `NOT_FOUND_USER` | 해당하는 아이디를 가진 사용자가 없는 경우
|===

=== 2-2. 로그아웃
==== Request Headers
include::{snippets}/auth-controller-test/respond_200_when_succeed_to_logout/request-headers.adoc[]
Expand All @@ -202,6 +211,7 @@ include::{snippets}/auth-controller-test/respond_200_when_succeed_to_logout/requ
include::{snippets}/auth-controller-test/respond_200_when_succeed_to_logout/http-request.adoc[]
==== Sample Response
include::{snippets}/auth-controller-test/respond_200_when_succeed_to_logout/http-response.adoc[]

=== 2-3. 토큰 재발급
==== Request Headers
include::{snippets}/auth-controller-test/respond_200_when_succeed_to_reissue_tokens/request-headers.adoc[]
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/cvsgo/config/SchedulerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.cvsgo.config;

import com.cvsgo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

@RequiredArgsConstructor
@EnableScheduling
@Configuration
public class SchedulerConfig {

private final UserService userService;

@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
public void deleteUserId() {
userService.deleteUserId();
}

}
15 changes: 12 additions & 3 deletions src/main/java/com/cvsgo/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import lombok.AccessLevel;
Expand All @@ -22,7 +23,7 @@
import org.springframework.security.crypto.password.PasswordEncoder;

@Getter
@SQLDelete(sql = "UPDATE user SET is_deleted = true WHERE id = ?")
@SQLDelete(sql = "UPDATE user SET is_deleted = true, deleted_date = NOW() WHERE id = ?")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class User extends BaseTimeEntity {
Expand All @@ -31,7 +32,6 @@ public class User extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotNull
@Column(unique = true)
private String userId;

Expand All @@ -50,17 +50,21 @@ public class User extends BaseTimeEntity {

private Boolean isDeleted = Boolean.FALSE;

private LocalDate deletedDate;

@OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST)
private List<UserTag> userTags = new ArrayList<>();

@Builder
public User(Long id, String userId, String password, String nickname, Role role, Boolean isDeleted) {
public User(Long id, String userId, String password, String nickname, Role role,
Boolean isDeleted, LocalDate deletedDate) {
this.id = id;
this.userId = userId;
this.password = password;
this.nickname = nickname;
this.role = role;
this.isDeleted = isDeleted;
this.deletedDate = deletedDate;
}

public static User create(String userId, String password, String nickname, List<Tag> tags) {
Expand All @@ -69,6 +73,7 @@ public static User create(String userId, String password, String nickname, List<
.password(password)
.nickname(nickname)
.role(Role.ASSOCIATE)
.isDeleted(false)
.build();
for (Tag tag : tags) {
user.addTag(tag);
Expand Down Expand Up @@ -111,4 +116,8 @@ public void updateProfileImageUrl(String profileImageUrl) {
this.profileImageUrl = profileImageUrl;
}

public void deleteUserId() {
this.userId = null;
}

}
1 change: 1 addition & 0 deletions src/main/java/com/cvsgo/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public enum ErrorCode {
DUPLICATE_REVIEW_LIKE( "하나의 리뷰에 좋아요는 한 번만 할 수 있습니다."),

/* 403 FORBIDDEN */
FORBIDDEN_USER("탈퇴한 회원입니다."),
FORBIDDEN_REVIEW("해당 리뷰에 대한 권한이 없습니다."),

/* 404 NOT_FOUND */
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/cvsgo/exception/ExceptionConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public interface ExceptionConstants {
UnauthorizedException UNAUTHORIZED_USER = new UnauthorizedException(ErrorCode.UNAUTHORIZED_USER);

BadRequestException INVALID_FILE_SIZE = new BadRequestException(ErrorCode.INVALID_FILE_SIZE);
ForbiddenException FORBIDDEN_USER = new ForbiddenException(ErrorCode.FORBIDDEN_USER);

ForbiddenException FORBIDDEN_REVIEW = new ForbiddenException(ErrorCode.FORBIDDEN_REVIEW);

Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/cvsgo/repository/UserRepository.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.cvsgo.repository;

import com.cvsgo.entity.User;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;
Expand All @@ -10,4 +12,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUserId(String userId);

Optional<User> findByNickname(String nickname);

List<User> findByDeletedDate(LocalDate privacyDate);
}
7 changes: 5 additions & 2 deletions src/main/java/com/cvsgo/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.cvsgo.service;

import static com.cvsgo.exception.ExceptionConstants.FORBIDDEN_USER;
import static com.cvsgo.exception.ExceptionConstants.NOT_FOUND_USER;
import static com.cvsgo.exception.ExceptionConstants.UNAUTHORIZED_USER;
import static com.cvsgo.util.AuthConstants.ACCESS_TOKEN_TTL_MILLISECOND;
Expand All @@ -11,6 +12,7 @@
import com.cvsgo.dto.auth.TokenDto;
import com.cvsgo.entity.RefreshToken;
import com.cvsgo.entity.User;
import com.cvsgo.exception.ForbiddenException;
import com.cvsgo.exception.NotFoundException;
import com.cvsgo.exception.UnauthorizedException;
import com.cvsgo.repository.RefreshTokenRepository;
Expand Down Expand Up @@ -54,14 +56,15 @@ public AuthService(@Value("${jwt.secret-key}") final String secretKey,
*
* @param request 로그인 요청 정보
* @return 토큰 정보
* @throws NotFoundException 해당하는 아이디를 가진 사용자가 없는 경우
* @throws UnauthorizedException 비밀번호가 일치하지 않는 경우
* @throws ForbiddenException 탈퇴한 회원인 경우
* @throws NotFoundException 해당하는 아이디를 가진 사용자가 없는 경우
*/
@Transactional
public LoginResponseDto login(LoginRequestDto request) {
User user = userRepository.findByUserId(request.getEmail())
.orElseThrow(() -> NOT_FOUND_USER);
if (Boolean.TRUE.equals(user.getIsDeleted())) throw NOT_FOUND_USER;
if (Boolean.TRUE.equals(user.getIsDeleted())) throw FORBIDDEN_USER;
user.validatePassword(request.getPassword(), passwordEncoder);

String accessToken = createAccessToken(user, key, ACCESS_TOKEN_TTL_MILLISECOND);
Expand Down
12 changes: 11 additions & 1 deletion src/main/java/com/cvsgo/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.cvsgo.repository.UserRepository;
import com.cvsgo.repository.UserTagRepository;
import jakarta.persistence.EntityManager;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -145,14 +146,23 @@ public void updateUser(User user, UpdateUserRequestDto request) {
/**
* 사용자를 논리 삭제한다.
*
* @param user 로그인한 사용자
* @param user 로그인한 사용자
*/
@Transactional
public void deleteUser(User user) {
refreshTokenRepository.deleteAllByUser(user);
userRepository.delete(user);
}

/**
* 탈퇴 후 30일이 지나면 userId가 초기화된다.
*/
@Transactional
public void deleteUserId() {
userRepository.findByDeletedDate(LocalDate.now().minusDays(30))
.forEach(User::deleteUserId);
}

/**
* 회원 팔로우를 생성한다.
*
Expand Down
8 changes: 4 additions & 4 deletions src/test/java/com/cvsgo/controller/AuthControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -134,19 +134,19 @@ void respond_400_when_login_but_user_does_not_exist() throws Exception {
}

@Test
@DisplayName("탈퇴한 계정이면 로그인 API 호출시 HTTP 400를 응답한다")
void respond_400_when_login_but_user_is_deleted() throws Exception {
@DisplayName("탈퇴한 계정이면 로그인 API 호출시 HTTP 403를 응답한다")
void respond_403_when_login_but_user_is_deleted() throws Exception {
LoginRequestDto loginRequestDto = LoginRequestDto.builder()
.email("[email protected]")
.password("password1!")
.build();

given(authService.login(any())).willThrow(ExceptionConstants.NOT_FOUND_USER);
given(authService.login(any())).willThrow(ExceptionConstants.FORBIDDEN_USER);

mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginRequestDto)))
.andExpect(status().isNotFound())
.andExpect(status().isForbidden())
.andDo(print());
}

Expand Down
36 changes: 30 additions & 6 deletions src/test/java/com/cvsgo/repository/UserRepositoryTest.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
package com.cvsgo.repository;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.cvsgo.config.TestConfig;
import com.cvsgo.entity.Role;
import com.cvsgo.entity.User;
import jakarta.persistence.EntityManager;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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 org.springframework.context.annotation.Import;

import static org.assertj.core.api.Assertions.as;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;

@Import(TestConfig.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
Expand Down Expand Up @@ -71,6 +71,30 @@ void succeed_to_find_user_by_email() {
assertThat(foundUser).isPresent();
}

@Test
@DisplayName("탈퇴 후 30일이 경과한 사용자를 찾는다")
void succeed_to_find_privacy_date_equal() {
User invalidUser = User.builder()
.userId("invalidUser")
.password("111111111a!")
.nickname("탈퇴 30일")
.role(Role.ASSOCIATE)
.deletedDate(LocalDate.now().minusDays(30))
.build();
User validUser = User.builder()
.userId("validUser")
.password("111111111a!")
.nickname("탈퇴 29일")
.role(Role.ASSOCIATE)
.deletedDate(LocalDate.now().minusDays(29))
.build();
userRepository.saveAll(List.of(invalidUser, validUser));

List<User> foundUser = userRepository.findByDeletedDate(LocalDate.now().minusDays(30));

assertThat(foundUser).hasSize(1);
}

@Test
@DisplayName("사용자를 soft delete 한다")
void succeed_to_soft_delete_user() {
Expand Down
5 changes: 3 additions & 2 deletions src/test/java/com/cvsgo/service/AuthServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.cvsgo.dto.auth.TokenDto;
import com.cvsgo.entity.RefreshToken;
import com.cvsgo.entity.User;
import com.cvsgo.exception.ForbiddenException;
import com.cvsgo.exception.NotFoundException;
import com.cvsgo.exception.UnauthorizedException;
import com.cvsgo.repository.RefreshTokenRepository;
Expand Down Expand Up @@ -95,7 +96,7 @@ void should_throw_NotFoundException_when_user_tries_to_login_but_user_does_not_e
}

@Test
@DisplayName("탈퇴한 사용자이면 NotFoundException이 발생한다")
@DisplayName("탈퇴한 사용자이면 ForbiddenException이 발생한다")
void should_throw_NotFoundException_when_user_tries_to_login_but_user_is_deleted() {
LoginRequestDto loginRequestDto = LoginRequestDto.builder()
.email("[email protected]")
Expand All @@ -110,7 +111,7 @@ void should_throw_NotFoundException_when_user_tries_to_login_but_user_is_deleted
given(userRepository.findByUserId(loginRequestDto.getEmail()))
.willReturn(Optional.of(user));

assertThrows(NotFoundException.class, () -> authService.login(loginRequestDto));
assertThrows(ForbiddenException.class, () -> authService.login(loginRequestDto));

then(userRepository).should(times(1)).findByUserId(any());
}
Expand Down
25 changes: 24 additions & 1 deletion src/test/java/com/cvsgo/service/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import com.cvsgo.dto.user.SignUpRequestDto;
import com.cvsgo.dto.user.UpdateUserRequestDto;
Expand All @@ -29,6 +30,7 @@
import com.cvsgo.repository.UserRepository;
import com.cvsgo.repository.UserTagRepository;
import jakarta.persistence.EntityManager;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -240,6 +242,27 @@ void succeed_to_soft_delete_user() {
then(userRepository).should(times(1)).delete(any());
}

@Test
@DisplayName("탈퇴 후 30일이 경과하면 userId를 초기화한다")
void succeed_to_delete_user_id() {
User invalidUser = User.builder()
.userId("testUserBeforeMidnight")
.deletedDate(LocalDate.now().minusDays(30))
.build();
User validUser = User.builder()
.userId("testUserAfterMidnight")
.deletedDate(LocalDate.now().minusDays(29))
.build();

given(userRepository.findByDeletedDate(LocalDate.now().minusDays(30))).willReturn(List.of(invalidUser));

userService.deleteUserId();

then(userRepository).should(times(1)).findByDeletedDate(any());
assertNull(invalidUser.getUserId());
assertNotNull(validUser.getUserId());
}

@Test
@DisplayName("회원 팔로우를 정상적으로 생성한다")
void succeed_to_create_user_follow() {
Expand Down

0 comments on commit a4a6305

Please sign in to comment.