diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java index 48e7af4..cef35c9 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -18,7 +18,6 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import lombok.Builder; @@ -52,19 +51,12 @@ public class MemberProfile extends BaseTimeEntity { @JoinColumn(name = "affiliation_id") private Group group; - @Column(name = "default_monthly_limit") - private BigDecimal defaultMonthlyLimit; - - @Column(name = "default_daily_limit") - private BigDecimal defaultDailyLimit; - - @Builder public MemberProfile(Member member, String nickName, List addressHistory, MemberType type, Group group) { linkMember(member); this.nickName = nickName; - this.addressHistory = addressHistory; + this.addressHistory = (addressHistory == null) ? new java.util.ArrayList<>() : addressHistory; this.type = type; this.group = group; } @@ -134,8 +126,4 @@ public void changeGroup(Group newGroup) { this.group = newGroup; } - public void registerDefaultBudgets(BigDecimal defaultDailyLimit, BigDecimal defaultMonthlyLimit) { - this.defaultDailyLimit = defaultDailyLimit; - this.defaultMonthlyLimit = defaultMonthlyLimit; - } } diff --git a/src/main/java/com/stcom/smartmealtable/repository/MemberProfileRepository.java b/src/main/java/com/stcom/smartmealtable/repository/MemberProfileRepository.java index 1a22927..a97f188 100644 --- a/src/main/java/com/stcom/smartmealtable/repository/MemberProfileRepository.java +++ b/src/main/java/com/stcom/smartmealtable/repository/MemberProfileRepository.java @@ -15,6 +15,6 @@ public interface MemberProfileRepository extends JpaRepository findMemberProfileEntityGraphById(Long id); } diff --git a/src/main/java/com/stcom/smartmealtable/service/BudgetService.java b/src/main/java/com/stcom/smartmealtable/service/BudgetService.java index a488590..914a4da 100644 --- a/src/main/java/com/stcom/smartmealtable/service/BudgetService.java +++ b/src/main/java/com/stcom/smartmealtable/service/BudgetService.java @@ -10,6 +10,7 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.YearMonth; +import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -61,10 +62,11 @@ public void registerDefaultDailyBudgetBy(Long profileId, Long dailyLimit, LocalD MemberProfile profile = memberProfileRepository.findById(profileId) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 프로필로 접근")); // 일일 예산은 오늘 ~ 이번달 말일까지 디폴트 daily Limit로 여러 개 생성해준다. + List budgets = new ArrayList<>(); for (LocalDate date = startDate; date.getMonth() == startDate.getMonth(); date = date.plusDays(1)) { - DailyBudget dailyBudget = new DailyBudget(profile, BigDecimal.valueOf(dailyLimit), date); - budgetRepository.save(dailyBudget); + budgets.add(new DailyBudget(profile, BigDecimal.valueOf(dailyLimit), date)); } + budgetRepository.saveAll(budgets); } @Transactional diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java index 4c19ce3..448ab40 100644 --- a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java +++ b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java @@ -11,7 +11,6 @@ import com.stcom.smartmealtable.repository.GroupRepository; import com.stcom.smartmealtable.repository.MemberProfileRepository; import com.stcom.smartmealtable.repository.MemberRepository; -import java.math.BigDecimal; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -104,11 +103,4 @@ public void deleteAddress(Long profileId, Long addressEntityId) { .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원 주소 정보입니다.")); profile.removeAddress(addressEntity); } - - @Transactional - public void registerDefaultBudgets(Long profileId, Long dailyLimit, Long monthlyLimit) { - MemberProfile profile = memberProfileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 프로필입니다")); - profile.registerDefaultBudgets(BigDecimal.valueOf(dailyLimit), BigDecimal.valueOf(monthlyLimit)); - } } diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberService.java b/src/main/java/com/stcom/smartmealtable/service/MemberService.java index 22ee0e2..5d3580f 100644 --- a/src/main/java/com/stcom/smartmealtable/service/MemberService.java +++ b/src/main/java/com/stcom/smartmealtable/service/MemberService.java @@ -20,13 +20,7 @@ public class MemberService { private final MemberProfileRepository memberProfileRepository; private final SocialAccountRepository socialAccountRepository; private final AddressEntityRepository addressEntityRepository; - - /** - * 이메일 중복 검사를 수행합니다. 동일한 이메일을 가진 회원이 이미 존재하면 예외를 발생시킵니다. - * - * @param email 중복 검사할 이메일 - * @throws IllegalArgumentException 이메일이 이미 존재하는 경우 - */ + public void validateDuplicatedEmail(String email) { memberRepository.findByEmail(email).ifPresent(member -> { throw new IllegalArgumentException("이미 존재하는 이메일 입니다"); diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/AuthController.java b/src/main/java/com/stcom/smartmealtable/web/controller/AuthController.java new file mode 100644 index 0000000..4f5c6ef --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/controller/AuthController.java @@ -0,0 +1,103 @@ +package com.stcom.smartmealtable.web.controller; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.exception.PasswordPolicyException; +import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; +import com.stcom.smartmealtable.security.JwtTokenService; +import com.stcom.smartmealtable.service.MemberService; +import com.stcom.smartmealtable.service.TermService; +import com.stcom.smartmealtable.service.dto.MemberDto; +import com.stcom.smartmealtable.service.dto.TermAgreementRequestDto; +import com.stcom.smartmealtable.web.argumentresolver.UserContext; +import com.stcom.smartmealtable.web.dto.ApiResponse; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Email; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +/** + * 인증(로그인, 회원가입) 관련 API. + */ +@RestController +@Slf4j +@RequiredArgsConstructor +@RequestMapping("/api/v1/auth") +public class AuthController { + + private final MemberService memberService; + private final JwtTokenService jwtTokenService; + private final TermService termService; + + @GetMapping("/email/check") + public ResponseEntity> checkEmail(@Email @RequestParam String email) { + memberService.validateDuplicatedEmail(email); + return ResponseEntity.ok().body(ApiResponse.createSuccessWithNoContent()); + } + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping("/signup") + public ApiResponse signUp(@Valid @RequestBody SignUpRequest request) + throws PasswordPolicyException { + memberService.validateDuplicatedEmail(request.getEmail()); + memberService.checkPasswordDoubly(request.getPassword(), request.getConfirmPassword()); + + Member member = Member.builder() + .fullName(request.getFullName()) + .email(request.getEmail()) + .rawPassword(request.getPassword()) + .build(); + + memberService.saveMember(member); + JwtTokenResponseDto tokenDto = jwtTokenService.createTokenDto(member.getId(), null); + tokenDto.setNewUser(true); + return ApiResponse.createSuccess(tokenDto); + } + + @PostMapping("/signup/terms") + public ApiResponse agreeTerms(@UserContext MemberDto memberDto, + @RequestBody List agreements) { + termService.agreeTerms( + memberDto.getMemberId(), + agreements.stream() + .map(dto -> new TermAgreementRequestDto(dto.getTermId(), dto.getIsAgreed())) + .toList() + ); + return ApiResponse.createSuccessWithNoContent(); + } + + @DeleteMapping("/signup") + public ApiResponse cancelSignUp(@UserContext MemberDto memberDto) { + memberService.deleteByMemberId(memberDto.getMemberId()); + return ApiResponse.createSuccessWithNoContent(); + } + + @Data + @AllArgsConstructor + public static class SignUpRequest { + @Email + private String email; + private String password; + private String confirmPassword; + private String fullName; + } + + @Data + @AllArgsConstructor + public static class TermAgreementRequest { + private Long termId; + private Boolean isAgreed; + } +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/AuthTokenController.java b/src/main/java/com/stcom/smartmealtable/web/controller/AuthTokenController.java new file mode 100644 index 0000000..8518502 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/controller/AuthTokenController.java @@ -0,0 +1,112 @@ +package com.stcom.smartmealtable.web.controller; + +import com.stcom.smartmealtable.infrastructure.SocialAuthService; +import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; +import com.stcom.smartmealtable.infrastructure.dto.TokenDto; +import com.stcom.smartmealtable.security.JwtBlacklistService; +import com.stcom.smartmealtable.security.JwtTokenService; +import com.stcom.smartmealtable.service.LoginService; +import com.stcom.smartmealtable.service.dto.AuthResultDto; +import com.stcom.smartmealtable.service.dto.MemberDto; +import com.stcom.smartmealtable.web.argumentresolver.UserContext; +import com.stcom.smartmealtable.web.dto.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; +import java.util.Objects; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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; + +/** + * 인증 토큰 관련 API (로그인 / 로그아웃 / 토큰 리프레시 / 소셜 로그인). + */ +@RestController +@Slf4j +@RequiredArgsConstructor +@RequestMapping("/api/v1/auth") +public class AuthTokenController { + + private final LoginService loginService; + private final JwtTokenService jwtTokenService; + private final JwtBlacklistService jwtBlacklistService; + private final SocialAuthService socialAuthService; + + @PostMapping("/login") + public ApiResponse login(@RequestBody EmailLoginRequest request) { + AuthResultDto authResultDto = loginService.loginWithEmail(request.getEmail(), request.getPassword()); + JwtTokenResponseDto jwtDto = jwtTokenService.createTokenDto(authResultDto.getMemberId(), + authResultDto.getProfileId()); + if (authResultDto.isNewUser()) { + jwtDto.setNewUser(true); + } + return ApiResponse.createSuccess(jwtDto); + } + + @PostMapping("/logout") + public ApiResponse logout(HttpServletRequest request) { + String jwt = request.getHeader("Authorization"); + if (Objects.nonNull(jwt)) { + jwtBlacklistService.addToBlacklist(jwt); + } + return ApiResponse.createSuccessWithNoContent(); + } + + @PostMapping("/oauth2/code") + public ApiResponse socialLogin(@RequestBody SocialLoginRequest request) { + TokenDto token = socialAuthService.getTokenResponse(request.getProvider().toLowerCase(), + request.getAuthorizationCode()); + AuthResultDto authResultDto = loginService.socialLogin(token); + JwtTokenResponseDto jwtDto = jwtTokenService.createTokenDto(authResultDto.getMemberId(), + authResultDto.getProfileId()); + if (authResultDto.isNewUser()) { + jwtDto.setNewUser(true); + } + return ApiResponse.createSuccess(jwtDto); + } + + @PostMapping("/token/refresh") + public ApiResponse refreshAccessToken(@UserContext MemberDto memberDto, + @RequestBody RefreshTokenRequest request) { + String accessToken = jwtTokenService.createAccessToken(memberDto.getMemberId(), memberDto.getProfileId()); + return ApiResponse.createSuccess(new AccessTokenRefreshResponse(accessToken, 3600, "Bearer")); + } + + @Data + @AllArgsConstructor + public static class EmailLoginRequest { + @NotEmpty + @Email + private String email; + @NotEmpty + private String password; + } + + @Data + @AllArgsConstructor + public static class SocialLoginRequest { + @NotEmpty + private String provider; + @NotEmpty + private String authorizationCode; + } + + @Data + public static class RefreshTokenRequest { + @NotEmpty + private String refreshToken; + } + + @Data + @AllArgsConstructor + public static class AccessTokenRefreshResponse { + private String accessToken; + private int expiresIn; + private String tokenType; + } +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java b/src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java index 6912bf5..ebbc6ac 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java @@ -1,23 +1,14 @@ package com.stcom.smartmealtable.web.controller; import com.stcom.smartmealtable.domain.group.Group; -import com.stcom.smartmealtable.domain.group.SchoolType; -import com.stcom.smartmealtable.infrastructure.dto.AddressRequest; import com.stcom.smartmealtable.service.GroupService; import com.stcom.smartmealtable.web.dto.ApiResponse; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; +import com.stcom.smartmealtable.web.dto.group.GroupDto; import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Data; import lombok.RequiredArgsConstructor; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; -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.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -40,75 +31,9 @@ public ApiResponse> searchGroup(@RequestParam String keyword) { .toList()); } - @PostMapping("/schools") - public ApiResponse registerSchoolGroup(@RequestBody @Validated SchoolGroupCreateRequest request) { - groupService.createSchoolGroup(new AddressRequest(request.getRoadAddress(), request.getDetailAddress()), - request.getName(), request.getType()); - return ApiResponse.createSuccessWithNoContent(); - } - - @PatchMapping("/schools/{id}") - public ApiResponse editSchoolGroup(@PathVariable("id") Long id, - @RequestBody @Validated SchoolGroupUpdateRequest request) { - groupService.changeSchoolGroup(id, - new AddressRequest(request.getRoadAddress(), request.getDetailAddress()), - request.getName(), request.getType()); - return ApiResponse.createSuccessWithNoContent(); - } - @DeleteMapping("/{id}") public ApiResponse deleteGroup(@PathVariable("id") Long id) { groupService.deleteGroup(id); return ApiResponse.createSuccessWithNoContent(); } - - @Data - @AllArgsConstructor - static class GroupDto { - private Long id; - private String roadAddress; - private String name; - private String groupType; - - public GroupDto(Group group) { - this.id = group.getId(); - this.groupType = group.getTypeName(); - this.name = group.getName(); - this.roadAddress = group.getAddress().getRoadAddress(); - } - } - - @Data - @AllArgsConstructor - static class SchoolGroupCreateRequest { - - @NotEmpty - private String roadAddress; - - @NotEmpty - private String detailAddress; - - @NotEmpty - private String name; - - @NotNull - private SchoolType type; - } - - @Data - @AllArgsConstructor - static class SchoolGroupUpdateRequest { - - @NotEmpty - private String roadAddress; - - @NotEmpty - private String detailAddress; - - @NotEmpty - private String name; - - @NotNull - private SchoolType type; - } } diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/LoginController.java b/src/main/java/com/stcom/smartmealtable/web/controller/LoginController.java deleted file mode 100644 index 5a6103c..0000000 --- a/src/main/java/com/stcom/smartmealtable/web/controller/LoginController.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.stcom.smartmealtable.web.controller; - -import com.stcom.smartmealtable.exception.PasswordFailedExceededException; -import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; -import com.stcom.smartmealtable.security.JwtBlacklistService; -import com.stcom.smartmealtable.security.JwtTokenService; -import com.stcom.smartmealtable.service.LoginService; -import com.stcom.smartmealtable.service.dto.AuthResultDto; -import com.stcom.smartmealtable.web.dto.ApiResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotEmpty; -import lombok.Data; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.validation.annotation.Validated; -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 -@RequiredArgsConstructor -@Slf4j -@RequestMapping("/api/v1/auth") -public class LoginController { - - private final LoginService loginService; - private final JwtTokenService jwtTokenService; - private final JwtBlacklistService jwtBlacklistService; - - @PostMapping("/login") - public ApiResponse login(@Validated @RequestBody LoginRequest request) - throws PasswordFailedExceededException { - AuthResultDto authResultDto = loginService.loginWithEmail(request.getEmail(), request.getPassword()); - JwtTokenResponseDto jwtTokenResponseDto = - jwtTokenService.createTokenDto(authResultDto.getMemberId(), authResultDto.getProfileId()); - if (authResultDto.isNewUser()) { - jwtTokenResponseDto.setNewUser(true); - } - return ApiResponse.createSuccess(jwtTokenResponseDto); - } - - @PostMapping("/logout") - public ApiResponse logout(HttpServletRequest request) { - String jwt = request.getHeader("Authorization"); - jwtBlacklistService.addToBlacklist(jwt); - return ApiResponse.createSuccessWithNoContent(); - } - - @Data - static class LoginRequest { - - @NotEmpty - @Email - private String email; - - @NotEmpty - private String password; - } -} diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberAccountController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberAccountController.java new file mode 100644 index 0000000..ec9dc88 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberAccountController.java @@ -0,0 +1,52 @@ +package com.stcom.smartmealtable.web.controller; + +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +import com.stcom.smartmealtable.exception.PasswordPolicyException; +import com.stcom.smartmealtable.service.MemberService; +import com.stcom.smartmealtable.service.dto.MemberDto; +import com.stcom.smartmealtable.web.argumentresolver.UserContext; +import com.stcom.smartmealtable.web.dto.ApiResponse; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 회원 계정 관리(비밀번호 변경, 탈퇴) 전용 컨트롤러. + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/members") +public class MemberAccountController { + + private final MemberService memberService; + + @PatchMapping("/me/password") + public ApiResponse changePassword(@UserContext MemberDto memberDto, + @Valid @RequestBody PasswordChangeRequest request) + throws PasswordPolicyException, PasswordFailedExceededException { + memberService.checkPasswordDoubly(request.getNewPassword(), request.getConfirmPassword()); + memberService.changePassword(memberDto.getMemberId(), request.getOriginPassword(), request.getNewPassword()); + return ApiResponse.createSuccessWithNoContent(); + } + + @DeleteMapping("/me") + public ApiResponse deleteMember(@UserContext MemberDto memberDto) { + memberService.deleteByMemberId(memberDto.getMemberId()); + return ApiResponse.createSuccessWithNoContent(); + } + + + @Data + @AllArgsConstructor + public static class PasswordChangeRequest { + private String originPassword; + private String newPassword; + private String confirmPassword; + } +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberAddressController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberAddressController.java new file mode 100644 index 0000000..bbfae20 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberAddressController.java @@ -0,0 +1,70 @@ +package com.stcom.smartmealtable.web.controller; + +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.Address.AddressType; +import com.stcom.smartmealtable.infrastructure.AddressApiService; +import com.stcom.smartmealtable.infrastructure.dto.AddressRequest; +import com.stcom.smartmealtable.service.MemberProfileService; +import com.stcom.smartmealtable.service.dto.MemberDto; +import com.stcom.smartmealtable.web.argumentresolver.UserContext; +import com.stcom.smartmealtable.web.dto.ApiResponse; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/members/me/addresses") +public class MemberAddressController { + + private final MemberProfileService memberProfileService; + private final AddressApiService addressApiService; + + @PostMapping("/{id}/primary") + public ApiResponse changePrimaryAddress(@UserContext MemberDto memberDto, + @PathVariable("id") Long addressId) { + memberProfileService.changeAddressToPrimary(memberDto.getProfileId(), addressId); + return ApiResponse.createSuccessWithNoContent(); + } + + @PostMapping + public ApiResponse registerAddress(@UserContext MemberDto memberDto, + @Validated @RequestBody MemberAddressCURequest request) { + Address address = addressApiService.createAddressFromRequest(request.toAddressApiRequest()); + memberProfileService.saveNewAddress(memberDto.getProfileId(), address, request.getAlias(), + request.getAddressType()); + return ApiResponse.createSuccessWithNoContent(); + } + + @PatchMapping("/{id}") + public ApiResponse changeAddress(@UserContext MemberDto memberDto, + @PathVariable("id") Long addressId, + @Validated @RequestBody MemberAddressCURequest request) { + Address address = addressApiService.createAddressFromRequest(request.toAddressApiRequest()); + memberProfileService.changeAddress(memberDto.getProfileId(), addressId, address, request.getAlias(), + request.getAddressType()); + return ApiResponse.createSuccessWithNoContent(); + } + + @DeleteMapping("/{id}") + public ApiResponse deleteAddress(@UserContext MemberDto memberDto, + @PathVariable("id") Long addressId) { + memberProfileService.deleteAddress(memberDto.getProfileId(), addressId); + return ApiResponse.createSuccessWithNoContent(); + } + + @AllArgsConstructor + @Data + static class MemberAddressCURequest { + private String roadAddress; + private AddressType addressType; + private String alias; + private String detailAddress; + + public AddressRequest toAddressApiRequest() { + return new AddressRequest(roadAddress, detailAddress); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberBudgetController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberBudgetController.java new file mode 100644 index 0000000..41652b3 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberBudgetController.java @@ -0,0 +1,127 @@ +package com.stcom.smartmealtable.web.controller; + +import com.stcom.smartmealtable.domain.Budget.Budget; +import com.stcom.smartmealtable.domain.Budget.DailyBudget; +import com.stcom.smartmealtable.domain.Budget.MonthlyBudget; +import com.stcom.smartmealtable.service.BudgetService; +import com.stcom.smartmealtable.service.dto.MemberDto; +import com.stcom.smartmealtable.web.argumentresolver.UserContext; +import com.stcom.smartmealtable.web.dto.ApiResponse; +import com.stcom.smartmealtable.web.validation.YearMonthFormat; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/members/me/budgets") +public class MemberBudgetController { + + private final BudgetService budgetService; + + // 일별 예산 조회 + @GetMapping("/daily/{date}") + public ApiResponse dailyBudgetByDate(@UserContext MemberDto memberDto, + @PathVariable("date") @DateTimeFormat(iso = ISO.DATE) String date) { + DailyBudget dailyBudget = budgetService.getDailyBudgetBy(memberDto.getProfileId(), LocalDate.parse(date)); + return ApiResponse.createSuccess(DailyBudgetResponse.of(dailyBudget)); + } + + @PutMapping("/daily/{date}/default") + public ApiResponse registerDefaultDailyBudget(@UserContext MemberDto memberDto, + @PathVariable("date") @DateTimeFormat(iso = ISO.DATE) String date, + @RequestParam("limit") Long limit) { + budgetService.registerDefaultDailyBudgetBy(memberDto.getProfileId(), limit, LocalDate.parse(date)); + return ApiResponse.createSuccessWithNoContent(); + } + + @PatchMapping("/daily/{date}") + public ApiResponse editDailyBudget(@UserContext MemberDto memberDto, + @PathVariable("date") @DateTimeFormat(iso = ISO.DATE) String date, + @RequestParam("limit") Long limit) { + budgetService.editDailyBudgetCustom(memberDto.getProfileId(), LocalDate.parse(date), limit); + return ApiResponse.createSuccessWithNoContent(); + } + + // 해당 일자가 속한 일일 예산 주간 데이터 조회 + @GetMapping("/daily/{date}/week") + public ApiResponse> dailyBudgetWeekByDate(@UserContext MemberDto memberDto, + @PathVariable("date") @DateTimeFormat(iso = ISO.DATE) String date) { + List dailyBudgets = budgetService.getDailyBudgetsByWeek(memberDto.getProfileId(), + LocalDate.parse(date)); + + List responses = dailyBudgets.stream() + .map(DailyBudgetResponse::of) + .toList(); + + return ApiResponse.createSuccess(responses); + } + + @GetMapping("/monthly/{yearMonth}") + public ApiResponse monthlyBudgetByDate(@UserContext MemberDto memberDto, + @PathVariable("yearMonth") @YearMonthFormat YearMonth yearMonth) { + MonthlyBudget monthlyBudget = budgetService.getMonthlyBudgetBy(memberDto.getProfileId(), + yearMonth); + + return ApiResponse.createSuccess(MonthlyBudgetResponse.of(monthlyBudget)); + } + + @PutMapping("/monthly/{yearMonth}/default") + public ApiResponse registerDefaultMonthlyBudget(@UserContext MemberDto memberDto, + @PathVariable("yearMonth") @YearMonthFormat YearMonth yearMonth, + @RequestParam("limit") Long limit) { + budgetService.registerDefaultMonthlyBudgetBy(memberDto.getProfileId(), + limit, yearMonth); + + return ApiResponse.createSuccessWithNoContent(); + } + + @PatchMapping("/monthly/{yearMonth}") + public ApiResponse editMonthlyBudget(@UserContext MemberDto memberDto, + @PathVariable("yearMonth") @YearMonthFormat YearMonth yearMonth, + @RequestParam("limit") Long limit) { + budgetService.editMonthlyBudgetCustom(memberDto.getProfileId(), + yearMonth, limit); + + return ApiResponse.createSuccessWithNoContent(); + } + + @AllArgsConstructor + @Data + static class DailyBudgetResponse { + private Long dailySpentAmount; + private Long dailyLimitAmount; + private Long dailyAvailableAmount; + + public static DailyBudgetResponse of(Budget dailyBudget) { + return new DailyBudgetResponse( + dailyBudget.getSpendAmount().longValue(), + dailyBudget.getLimit().longValue(), + dailyBudget.getAvailableAmount().longValue() + ); + } + } + + @AllArgsConstructor + @Data + static class MonthlyBudgetResponse { + private Long monthlySpentAmount; + private Long monthlyLimitAmount; + private Long monthlyAvailableAmount; + + public static MonthlyBudgetResponse of(Budget monthlyBudget) { + return new MonthlyBudgetResponse( + monthlyBudget.getSpendAmount().longValue(), + monthlyBudget.getLimit().longValue(), + monthlyBudget.getAvailableAmount().longValue() + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java index dd3053e..6794941 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java @@ -1,108 +1,21 @@ package com.stcom.smartmealtable.web.controller; -import com.stcom.smartmealtable.domain.member.Member; -import com.stcom.smartmealtable.exception.PasswordFailedExceededException; -import com.stcom.smartmealtable.exception.PasswordPolicyException; -import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; -import com.stcom.smartmealtable.security.JwtTokenService; -import com.stcom.smartmealtable.service.MemberService; -import com.stcom.smartmealtable.service.TermService; -import com.stcom.smartmealtable.service.dto.MemberDto; -import com.stcom.smartmealtable.service.dto.TermAgreementRequestDto; -import com.stcom.smartmealtable.web.argumentresolver.UserContext; -import com.stcom.smartmealtable.web.dto.ApiResponse; -import jakarta.validation.Valid; import jakarta.validation.constraints.Email; -import java.util.List; import lombok.AllArgsConstructor; import lombok.Data; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -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.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; -@RestController -@Slf4j -@RequiredArgsConstructor -@RequestMapping("/api/v1/members") -public class MemberController { +/** + * 과거 테스트 코드 및 바이너리 호환성을 위한 DTO 컨테이너임. + */ +@Deprecated +public final class MemberController { - private final MemberService memberService; - private final JwtTokenService jwtTokenService; - private final TermService termService; - - @GetMapping("/email/check") - public ResponseEntity> checkEmail(@Email @RequestParam String email) { - memberService.validateDuplicatedEmail(email); - return ResponseEntity.ok().body(ApiResponse.createSuccessWithNoContent()); - } - - @ResponseStatus(HttpStatus.CREATED) - @PostMapping() - public ApiResponse createMember(@Valid @RequestBody CreateMemberRequest request, - BindingResult bindingResult) throws PasswordPolicyException { - memberService.validateDuplicatedEmail(request.getEmail()); - memberService.checkPasswordDoubly(request.getPassword(), request.getConfirmPassword()); - - Member member = Member.builder() - .fullName(request.getFullName()) - .email(request.getEmail()) - .rawPassword(request.getPassword()) - .build(); - - memberService.saveMember(member); - JwtTokenResponseDto tokenDto = jwtTokenService.createTokenDto(member.getId(), null); - tokenDto.setNewUser(true); - return ApiResponse.createSuccess(tokenDto); - } - - @PatchMapping("/me") - public ApiResponse editMember(@UserContext MemberDto memberDto, @Valid @RequestBody EditMemberRequest request, - BindingResult bindingResult) - throws PasswordPolicyException, PasswordFailedExceededException { - memberService.checkPasswordDoubly(request.getNewPassword(), request.getConfirmPassword()); - memberService.changePassword(memberDto.getMemberId(), request.getOriginPassword(), request.getNewPassword()); - return ApiResponse.createSuccessWithNoContent(); - } - - @DeleteMapping("/me") - public ApiResponse deleteMember(@UserContext MemberDto memberDto) { - memberService.deleteByMemberId(memberDto.getMemberId()); - return ApiResponse.createSuccessWithNoContent(); - } - - @PostMapping("/signup") - public ApiResponse signUpWithTermAgreement(@UserContext MemberDto memberDto, - @RequestBody List agreements) { - termService.agreeTerms( - memberDto.getMemberId(), - agreements.stream() - .map(dto -> new TermAgreementRequestDto(dto.getTermId(), dto.getIsAgreed())) - .toList() - ); - return ApiResponse.createSuccessWithNoContent(); - } - - @DeleteMapping("/signup") - public ApiResponse signUpCancel(@UserContext MemberDto memberDto) { - memberService.deleteByMemberId(memberDto.getMemberId()); - return ApiResponse.createSuccessWithNoContent(); + private MemberController() { } @Data @AllArgsConstructor - static class CreateMemberRequest { - + public static class CreateMemberRequest { @Email private String email; private String password; @@ -112,8 +25,7 @@ static class CreateMemberRequest { @Data @AllArgsConstructor - static class EditMemberRequest { - + public static class EditMemberRequest { private String originPassword; private String newPassword; private String confirmPassword; @@ -121,9 +33,8 @@ static class EditMemberRequest { @Data @AllArgsConstructor - static class TermAgreementDto { + public static class TermAgreementDto { private Long termId; private Boolean isAgreed; } - -} +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberPreferenceController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberPreferenceController.java new file mode 100644 index 0000000..b1441fa --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberPreferenceController.java @@ -0,0 +1,78 @@ +package com.stcom.smartmealtable.web.controller; + +import com.stcom.smartmealtable.domain.food.MemberCategoryPreference; +import com.stcom.smartmealtable.domain.food.PreferenceType; +import com.stcom.smartmealtable.service.MemberCategoryPreferenceService; +import com.stcom.smartmealtable.service.dto.MemberDto; +import com.stcom.smartmealtable.web.argumentresolver.UserContext; +import com.stcom.smartmealtable.web.dto.ApiResponse; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/members/me/preferences") +public class MemberPreferenceController { + + private final MemberCategoryPreferenceService memberCategoryPreferenceService; + + @GetMapping + public ApiResponse getCategoryPreferences(@UserContext MemberDto memberDto) { + List preferences = + memberCategoryPreferenceService.getPreferences(memberDto.getProfileId()); + + List liked = preferences.stream() + .filter(p -> p.getType() == PreferenceType.LIKE) + .map(p -> new CategoryPreferenceDto( + p.getCategory().getId(), + p.getCategory().getName(), + p.getPriority())) + .toList(); + + List disliked = preferences.stream() + .filter(p -> p.getType() == PreferenceType.DISLIKE) + .map(p -> new CategoryPreferenceDto( + p.getCategory().getId(), + p.getCategory().getName(), + p.getPriority())) + .toList(); + + return ApiResponse.createSuccess(new PreferencesResponse(liked, disliked)); + } + + @PostMapping + public ApiResponse saveCategoryPreferences(@UserContext MemberDto memberDto, + @RequestBody PreferencesRequest request) { + memberCategoryPreferenceService.savePreferences( + memberDto.getProfileId(), + request.getLiked(), + request.getDisliked()); + return ApiResponse.createSuccessWithNoContent(); + } + + @AllArgsConstructor + @Data + static class PreferencesRequest { + private List liked; + private List disliked; + } + + @AllArgsConstructor + @Data + static class PreferencesResponse { + private List liked; + private List disliked; + } + + @AllArgsConstructor + @Data + static class CategoryPreferenceDto { + private Long categoryId; + private String categoryName; + private Integer priority; + } +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java index cc1726f..c1c58e0 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java @@ -1,42 +1,16 @@ package com.stcom.smartmealtable.web.controller; -import com.stcom.smartmealtable.domain.Address.Address; -import com.stcom.smartmealtable.domain.Address.AddressType; -import com.stcom.smartmealtable.domain.Budget.Budget; -import com.stcom.smartmealtable.domain.Budget.DailyBudget; -import com.stcom.smartmealtable.domain.Budget.MonthlyBudget; -import com.stcom.smartmealtable.domain.food.MemberCategoryPreference; -import com.stcom.smartmealtable.domain.food.PreferenceType; import com.stcom.smartmealtable.domain.member.MemberProfile; import com.stcom.smartmealtable.domain.member.MemberType; -import com.stcom.smartmealtable.infrastructure.AddressApiService; -import com.stcom.smartmealtable.infrastructure.dto.AddressRequest; -import com.stcom.smartmealtable.service.BudgetService; -import com.stcom.smartmealtable.service.MemberCategoryPreferenceService; import com.stcom.smartmealtable.service.MemberProfileService; import com.stcom.smartmealtable.service.dto.MemberDto; import com.stcom.smartmealtable.web.argumentresolver.UserContext; import com.stcom.smartmealtable.web.dto.ApiResponse; -import com.stcom.smartmealtable.web.validation.YearMonthFormat; -import java.time.LocalDate; -import java.time.YearMonth; -import java.util.List; import lombok.AllArgsConstructor; import lombok.Data; import lombok.RequiredArgsConstructor; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @@ -44,9 +18,6 @@ public class MemberProfileController { private final MemberProfileService memberProfileService; - private final AddressApiService addressApiService; - private final MemberCategoryPreferenceService memberCategoryPreferenceService; - private final BudgetService budgetService; @GetMapping("/me") public ApiResponse getMemberProfilePageInfo(@UserContext MemberDto memberDto) { @@ -54,7 +25,7 @@ public ApiResponse getMemberProfilePageInfo(@UserCont return ApiResponse.createSuccess(new MemberProfilePageResponse(profile, memberDto)); } - @PostMapping() + @PostMapping public ApiResponse createMemberProfile(@UserContext MemberDto memberDto, @Validated @RequestBody MemberProfileRequest request) { memberProfileService.createProfile(request.getNickName(), memberDto.getMemberId(), request.getMemberType(), @@ -70,144 +41,6 @@ public ApiResponse changeMemberProfile(@UserContext MemberDto memberDto, return ApiResponse.createSuccessWithNoContent(); } - @PostMapping("/me/addresses/{id}/primary") - public ApiResponse changePrimaryAddress(@UserContext MemberDto memberDto, - @PathVariable("id") Long addressId) { - memberProfileService.changeAddressToPrimary(memberDto.getProfileId(), addressId); - return ApiResponse.createSuccessWithNoContent(); - } - - @PostMapping("/me/addresses") - public ApiResponse registerAddress(@UserContext MemberDto memberDto, MemberAddressCURequest request) { - Address address = addressApiService.createAddressFromRequest(request.toAddressApiRequest()); - memberProfileService.saveNewAddress(memberDto.getProfileId(), address, request.getAlias(), - request.getAddressType()); - return ApiResponse.createSuccessWithNoContent(); - } - - @PatchMapping("/me/addresses/{id}") - public ApiResponse changeAddress(@UserContext MemberDto memberDto, @PathVariable("id") Long addressId, - MemberAddressCURequest request) { - Address address = addressApiService.createAddressFromRequest(request.toAddressApiRequest()); - memberProfileService.changeAddress(memberDto.getProfileId(), addressId, address, request.getAlias(), - request.getAddressType()); - return ApiResponse.createSuccessWithNoContent(); - } - - @DeleteMapping("/me/addresses/{id}") - public ApiResponse deleteAddress(@UserContext MemberDto memberDto, @PathVariable("id") Long addressId) { - memberProfileService.deleteAddress(memberDto.getProfileId(), addressId); - return ApiResponse.createSuccessWithNoContent(); - } - - @GetMapping("/me/preferences") - public ApiResponse getCategoryPreferences(@UserContext MemberDto memberDto) { - List preferences = - memberCategoryPreferenceService.getPreferences(memberDto.getProfileId()); - - List liked = preferences.stream() - .filter(p -> p.getType() == PreferenceType.LIKE) - .map(p -> new CategoryPreferenceDto( - p.getCategory().getId(), - p.getCategory().getName(), - p.getPriority())) - .toList(); - - List disliked = preferences.stream() - .filter(p -> p.getType() == PreferenceType.DISLIKE) - .map(p -> new CategoryPreferenceDto( - p.getCategory().getId(), - p.getCategory().getName(), - p.getPriority())) - .toList(); - - return ApiResponse.createSuccess(new PreferencesResponse(liked, disliked)); - } - - @PostMapping("/me/preferences") - public ApiResponse saveCategoryPreferences(@UserContext MemberDto memberDto, - @RequestBody PreferencesRequest request) { - memberCategoryPreferenceService.savePreferences( - memberDto.getProfileId(), - request.getLiked(), - request.getDisliked()); - return ApiResponse.createSuccessWithNoContent(); - } - - /** - * 일별 예산 조회 - * - * @param memberDto - * @param date (ISO_LOCAL_DATE 형식, 예: 2025-06-01) - * @return - */ - @GetMapping("/me/budgets/daily/{date}") - public ApiResponse dailyBudgetByDate(@UserContext MemberDto memberDto, - @PathVariable("date") @DateTimeFormat(iso = ISO.DATE) String date) { - DailyBudget dailyBudget = budgetService.getDailyBudgetBy(memberDto.getProfileId(), LocalDate.parse(date)); - return ApiResponse.createSuccess(DailyBudgetResponse.of(dailyBudget)); - } - - @PutMapping("/me/budgets/daily/{date}/default") - public ApiResponse registerDefaultDailyBudget(@UserContext MemberDto memberDto, - @PathVariable("date") @DateTimeFormat(iso = ISO.DATE) String date, - @RequestParam("limit") Long limit) { - budgetService.registerDefaultDailyBudgetBy(memberDto.getProfileId(), limit, LocalDate.parse(date)); - return ApiResponse.createSuccessWithNoContent(); - } - - @PatchMapping("/me/budgets/daily/{date}") - public ApiResponse editDailyBudget(@UserContext MemberDto memberDto, - @PathVariable("date") @DateTimeFormat(iso = ISO.DATE) String date, - @RequestParam("limit") Long limit) { - budgetService.editDailyBudgetCustom(memberDto.getProfileId(), LocalDate.parse(date), limit); - return ApiResponse.createSuccessWithNoContent(); - } - - // 해당 일자가 속한 일일 예산 주간 데이터 조회 - @GetMapping("/me/budgets/daily/{date}/week") - public ApiResponse> dailyBudgetWeekByDate(@UserContext MemberDto memberDto, - @PathVariable("date") @DateTimeFormat(iso = ISO.DATE) String date) { - List dailyBudgets = budgetService.getDailyBudgetsByWeek(memberDto.getProfileId(), - LocalDate.parse(date)); - - List responses = dailyBudgets.stream() - .map(DailyBudgetResponse::of) - .toList(); - - return ApiResponse.createSuccess(responses); - } - - @GetMapping("/me/budgets/monthly/{yearMonth}") - public ApiResponse monthlyBudgetByDate(@UserContext MemberDto memberDto, - @PathVariable("yearMonth") @YearMonthFormat YearMonth yearMonth) { - MonthlyBudget monthlyBudget = budgetService.getMonthlyBudgetBy(memberDto.getProfileId(), - yearMonth); - - return ApiResponse.createSuccess(MonthlyBudgetResponse.of(monthlyBudget)); - } - - @PutMapping("/me/budgets/monthly/{yearMonth}/default") - public ApiResponse registerDefaultMonthlyBudget(@UserContext MemberDto memberDto, - @PathVariable("yearMonth") @YearMonthFormat YearMonth yearMonth, - @RequestParam("limit") Long limit) { - budgetService.registerDefaultMonthlyBudgetBy(memberDto.getProfileId(), - limit, yearMonth); - - return ApiResponse.createSuccessWithNoContent(); - } - - @PatchMapping("/me/budgets/monthly/{yearMonth}") - public ApiResponse editMonthlyBudget(@UserContext MemberDto memberDto, - @PathVariable("yearMonth") @YearMonthFormat YearMonth yearMonth, - @RequestParam("limit") Long limit) { - budgetService.editMonthlyBudgetCustom(memberDto.getProfileId(), - yearMonth, limit); - - return ApiResponse.createSuccessWithNoContent(); - } - - @AllArgsConstructor @Data static class MemberProfilePageResponse { @@ -220,8 +53,8 @@ static class MemberProfilePageResponse { public MemberProfilePageResponse(MemberProfile profile, MemberDto memberDto) { this.nickName = profile.getNickName(); this.email = memberDto.getEmail(); - Address address = profile.findPrimaryAddress().getAddress(); - this.primaryAddress = address.getRoadAddress() + address.getDetailAddress(); + this.primaryAddress = profile.findPrimaryAddress().getAddress().getRoadAddress() + + profile.findPrimaryAddress().getAddress().getDetailAddress(); this.memberType = profile.getType(); this.groupName = profile.getGroup().getName(); } @@ -234,72 +67,4 @@ static class MemberProfileRequest { private Long groupId; private MemberType memberType; } - - @AllArgsConstructor - @Data - static class MemberAddressCURequest { - private String roadAddress; - private AddressType addressType; - private String alias; - private String detailAddress; - - public AddressRequest toAddressApiRequest() { - return new AddressRequest(roadAddress, detailAddress); - } - } - - @AllArgsConstructor - @Data - static class PreferencesRequest { - private List liked; - private List disliked; - } - - @AllArgsConstructor - @Data - static class PreferencesResponse { - private List liked; - private List disliked; - } - - @AllArgsConstructor - @Data - static class CategoryPreferenceDto { - private Long categoryId; - private String categoryName; - private Integer priority; - } - - @AllArgsConstructor - @Data - static class DailyBudgetResponse { - private Long dailySpentAmount; - private Long dailyLimitAmount; - private Long dailyAvailableAmount; - - public static DailyBudgetResponse of(Budget dailyBudget) { - return new DailyBudgetResponse( - dailyBudget.getSpendAmount().longValue(), - dailyBudget.getLimit().longValue(), - dailyBudget.getAvailableAmount().longValue() - ); - } - } - - @AllArgsConstructor - @Data - static class MonthlyBudgetResponse { - private Long monthlySpentAmount; - private Long monthlyLimitAmount; - private Long monthlyAvailableAmount; - - public static MonthlyBudgetResponse of(Budget monthlyBudget) { - return new MonthlyBudgetResponse( - monthlyBudget.getSpendAmount().longValue(), - monthlyBudget.getLimit().longValue(), - monthlyBudget.getAvailableAmount().longValue() - ); - } - } - } diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/OAuth2Controller.java b/src/main/java/com/stcom/smartmealtable/web/controller/OAuth2Controller.java deleted file mode 100644 index d25461d..0000000 --- a/src/main/java/com/stcom/smartmealtable/web/controller/OAuth2Controller.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.stcom.smartmealtable.web.controller; - - -import com.stcom.smartmealtable.infrastructure.SocialAuthService; -import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; -import com.stcom.smartmealtable.infrastructure.dto.TokenDto; -import com.stcom.smartmealtable.security.JwtTokenService; -import com.stcom.smartmealtable.service.LoginService; -import com.stcom.smartmealtable.service.dto.AuthResultDto; -import com.stcom.smartmealtable.service.dto.MemberDto; -import com.stcom.smartmealtable.web.argumentresolver.UserContext; -import com.stcom.smartmealtable.web.dto.ApiResponse; -import jakarta.validation.constraints.NotEmpty; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -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 -@RequestMapping("/api/v1/auth") -@RequiredArgsConstructor -public class OAuth2Controller { - - private final JwtTokenService jwtTokenService; - private final SocialAuthService socialAuthService; - private final LoginService loginService; - - @PostMapping("/oauth2/code") - public ApiResponse getTokenFromSocial(@RequestBody JwtTokenRequest request) { - TokenDto token = socialAuthService.getTokenResponse( - request.getProvider().toLowerCase(), request.getAuthorizationCode()); - AuthResultDto authResultDto = loginService.socialLogin(token); - JwtTokenResponseDto jwtDto = - jwtTokenService.createTokenDto(authResultDto.getMemberId(), authResultDto.getProfileId()); - if (authResultDto.isNewUser()) { - jwtDto.setNewUser(true); - } - return ApiResponse.createSuccess(jwtDto); - } - - @PostMapping("/token/refresh") - public ApiResponse refreshAccessToken(@UserContext MemberDto memberDto, - @RequestBody JwtRefreshTokenRequest request) { - String accessToken = jwtTokenService.createAccessToken(memberDto.getMemberId(), memberDto.getProfileId()); - return ApiResponse.createSuccess( - new JwtRefreshedAccessTokenDto(accessToken, 3600, "Bearar") - ); - } - - - @Data - @AllArgsConstructor - static class JwtTokenRequest { - - @NotEmpty - private String provider; - - @NotEmpty - private String authorizationCode; - } - - @Data - @AllArgsConstructor - static class JwtRefreshedAccessTokenDto { - private String accessToken; - private int expiresIn; - private String tokenType; - } - - @Data - static class JwtRefreshTokenRequest { - - @NotEmpty - private String refreshToken; - } - -} diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/SchoolGroupController.java b/src/main/java/com/stcom/smartmealtable/web/controller/SchoolGroupController.java new file mode 100644 index 0000000..fc0feaf --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/controller/SchoolGroupController.java @@ -0,0 +1,49 @@ +package com.stcom.smartmealtable.web.controller; + +import com.stcom.smartmealtable.infrastructure.dto.AddressRequest; +import com.stcom.smartmealtable.service.GroupService; +import com.stcom.smartmealtable.web.dto.ApiResponse; +import com.stcom.smartmealtable.web.dto.group.SchoolGroupCreateRequest; +import com.stcom.smartmealtable.web.dto.group.SchoolGroupUpdateRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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; + +/** + * 학교 그룹 전용 API. + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/schools") +public class SchoolGroupController { + + private final GroupService groupService; + + @PostMapping() + public ApiResponse registerSchoolGroup(@RequestBody @Valid SchoolGroupCreateRequest request) { + groupService.createSchoolGroup(new AddressRequest(request.getRoadAddress(), request.getDetailAddress()), + request.getName(), request.getType()); + return ApiResponse.createSuccessWithNoContent(); + } + + @PatchMapping("/{id}") + public ApiResponse editSchoolGroup(@PathVariable("id") Long id, + @RequestBody @Valid SchoolGroupUpdateRequest request) { + groupService.changeSchoolGroup(id, + new AddressRequest(request.getRoadAddress(), request.getDetailAddress()), + request.getName(), request.getType()); + return ApiResponse.createSuccessWithNoContent(); + } + + @DeleteMapping("/{id}") + public ApiResponse deleteSchoolGroup(@PathVariable("id") Long id) { + groupService.deleteGroup(id); + return ApiResponse.createSuccessWithNoContent(); + } +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/web/dto/group/GroupDto.java b/src/main/java/com/stcom/smartmealtable/web/dto/group/GroupDto.java new file mode 100644 index 0000000..870c2a5 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/dto/group/GroupDto.java @@ -0,0 +1,24 @@ +package com.stcom.smartmealtable.web.dto.group; + +import com.stcom.smartmealtable.domain.group.Group; +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * 그룹 검색 응답용 DTO. + */ +@Data +@AllArgsConstructor +public class GroupDto { + private Long id; + private String roadAddress; + private String name; + private String groupType; + + public GroupDto(Group group) { + this.id = group.getId(); + this.groupType = group.getTypeName(); + this.name = group.getName(); + this.roadAddress = group.getAddress().getRoadAddress(); + } +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/web/dto/group/SchoolGroupCreateRequest.java b/src/main/java/com/stcom/smartmealtable/web/dto/group/SchoolGroupCreateRequest.java new file mode 100644 index 0000000..76f3511 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/dto/group/SchoolGroupCreateRequest.java @@ -0,0 +1,24 @@ +package com.stcom.smartmealtable.web.dto.group; + +import com.stcom.smartmealtable.domain.group.SchoolType; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class SchoolGroupCreateRequest { + + @NotEmpty + private String roadAddress; + + @NotEmpty + private String detailAddress; + + @NotEmpty + private String name; + + @NotNull + private SchoolType type; +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/web/dto/group/SchoolGroupUpdateRequest.java b/src/main/java/com/stcom/smartmealtable/web/dto/group/SchoolGroupUpdateRequest.java new file mode 100644 index 0000000..028e4b7 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/dto/group/SchoolGroupUpdateRequest.java @@ -0,0 +1,24 @@ +package com.stcom.smartmealtable.web.dto.group; + +import com.stcom.smartmealtable.domain.group.SchoolType; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class SchoolGroupUpdateRequest { + + @NotEmpty + private String roadAddress; + + @NotEmpty + private String detailAddress; + + @NotEmpty + private String name; + + @NotNull + private SchoolType type; +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetDomainIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetDomainIntegrationTest.java index 9706cd8..fd4b547 100644 --- a/src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetDomainIntegrationTest.java +++ b/src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetDomainIntegrationTest.java @@ -136,7 +136,7 @@ void budgetResetFunctionality() { BigDecimal usageRate = budget.getSpendAmount() .multiply(BigDecimal.valueOf(100)) .divide(budget.getLimit(), 2, RoundingMode.HALF_UP); - assertThat(usageRate).isEqualTo(BigDecimal.ZERO); + assertThat(usageRate.stripTrailingZeros()).isEqualByComparingTo(BigDecimal.ZERO); } @DisplayName("예산의 부동소수점 정밀도 테스트") diff --git a/src/test/java/com/stcom/smartmealtable/domain/group/CompanyGroupTest.java b/src/test/java/com/stcom/smartmealtable/domain/group/CompanyGroupTest.java new file mode 100644 index 0000000..982a525 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/domain/group/CompanyGroupTest.java @@ -0,0 +1,37 @@ +package com.stcom.smartmealtable.domain.group; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.Address.Address; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CompanyGroupTest { + + @Test + @DisplayName("회사 그룹의 타입명(산업군)과 주소 변경이 정상 동작해야 한다") + void changeFields() { + // given + Address address = Address.builder() + .roadAddress("서울특별시 강남구 테헤란로 1") + .detailAddress("10층") + .build(); + CompanyGroup group = new CompanyGroup(); + org.springframework.test.util.ReflectionTestUtils.setField(group, "address", address); + org.springframework.test.util.ReflectionTestUtils.setField(group, "name", "테스트IT"); + org.springframework.test.util.ReflectionTestUtils.setField(group, "industryType", IndustryType.IT); + + // when + Address newAddress = Address.builder() + .roadAddress("서울특별시 영등포구 국제금융로 2") + .detailAddress("6층") + .build(); + group.changeNameAndAddress("테스트금융", newAddress); + org.springframework.test.util.ReflectionTestUtils.setField(group, "industryType", IndustryType.FINANCE); + + // then + assertThat(group.getName()).isEqualTo("테스트금융"); + assertThat(group.getAddress().getRoadAddress()).isEqualTo("서울특별시 영등포구 국제금융로 2"); + assertThat(group.getTypeName()).isEqualTo("파이낸스"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/domain/group/SchoolGroupTest.java b/src/test/java/com/stcom/smartmealtable/domain/group/SchoolGroupTest.java new file mode 100644 index 0000000..3677e18 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/domain/group/SchoolGroupTest.java @@ -0,0 +1,37 @@ +package com.stcom.smartmealtable.domain.group; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.Address.Address; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * 순수 자바 단위 테스트: 엔티티의 비즈니스 로직 검증. + */ +class SchoolGroupTest { + + @Test + @DisplayName("학교 그룹 타입과 주소/이름 변경이 정상동작해야 한다") + void changeFields() { + // given + Address address = Address.builder() + .roadAddress("서울시 종로구 세종대로 1") + .detailAddress("본관 1층") + .build(); + SchoolGroup group = new SchoolGroup(address, "서울대학교", SchoolType.UNIVERSITY_FOUR_YEAR); + + // when + Address newAddress = Address.builder() + .roadAddress("서울시 관악구 관악로 1") + .detailAddress("행정관 2층") + .build(); + group.changeNameAndAddress("국민대학교", newAddress); + group.changeType(SchoolType.HIGH_SCHOOL); + + // then + assertThat(group.getName()).isEqualTo("국민대학교"); + assertThat(group.getAddress().getRoadAddress()).isEqualTo("서울시 관악구 관악로 1"); + assertThat(group.getTypeName()).isEqualTo(SchoolType.HIGH_SCHOOL.name()); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/repository/AddressEntityRepositoryTest.java b/src/test/java/com/stcom/smartmealtable/repository/AddressEntityRepositoryTest.java new file mode 100644 index 0000000..8791ccd --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/repository/AddressEntityRepositoryTest.java @@ -0,0 +1,50 @@ +package com.stcom.smartmealtable.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.Address.AddressEntity; +import com.stcom.smartmealtable.domain.Address.AddressType; +import java.util.Optional; +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.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; + +@DataJpaTest +@ActiveProfiles("test") +class AddressEntityRepositoryTest { + + @Autowired + private AddressEntityRepository repository; + + @Autowired + private TestEntityManager em; + + @Test + @DisplayName("주소 엔티티를 저장하고 조회할 수 있다") + void saveAndFind() { + // given + Address address = Address.builder() + .roadAddress("서울특별시 중구 세종대로 110") + .detailAddress("별관") + .build(); + AddressEntity entity = AddressEntity.builder() + .address(address) + .type(AddressType.HOME) + .alias("우리집") + .build(); + repository.save(entity); + em.flush(); + em.clear(); + + // when + Optional found = repository.findById(entity.getId()); + + // then + assertThat(found).isPresent(); + assertThat(found.get().getAlias()).isEqualTo("우리집"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/repository/FoodCategoryRepositoryTest.java b/src/test/java/com/stcom/smartmealtable/repository/FoodCategoryRepositoryTest.java new file mode 100644 index 0000000..6351f31 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/repository/FoodCategoryRepositoryTest.java @@ -0,0 +1,42 @@ +package com.stcom.smartmealtable.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.food.FoodCategory; +import java.util.Optional; +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.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.util.ReflectionTestUtils; + +@DataJpaTest +@ActiveProfiles("test") +class FoodCategoryRepositoryTest { + + @Autowired + private FoodCategoryRepository repository; + + @Autowired + private TestEntityManager em; + + @Test + @DisplayName("음식 카테고리를 저장하고 ID 로 조회할 수 있다") + void saveAndFind() { + // given + FoodCategory category = new FoodCategory(); + ReflectionTestUtils.setField(category, "name", "한식"); + repository.save(category); + em.flush(); + em.clear(); + + // when + Optional found = repository.findById(category.getId()); + + // then + assertThat(found).isPresent(); + assertThat(found.get().getName()).isEqualTo("한식"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/repository/GroupRepositoryTest.java b/src/test/java/com/stcom/smartmealtable/repository/GroupRepositoryTest.java new file mode 100644 index 0000000..41e1835 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/repository/GroupRepositoryTest.java @@ -0,0 +1,41 @@ +package com.stcom.smartmealtable.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.group.SchoolGroup; +import com.stcom.smartmealtable.domain.group.SchoolType; +import java.util.List; +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.orm.jpa.DataJpaTest; +import org.springframework.data.domain.Limit; +import org.springframework.test.context.ActiveProfiles; + +@DataJpaTest +@ActiveProfiles("test") +class GroupRepositoryTest { + + @Autowired + private GroupRepository groupRepository; + + @Test + @DisplayName("이름 키워드로 그룹을 조회할 수 있어야 한다") + void findByNameContaining() { + // given + Address address = Address.builder() + .roadAddress("서울특별시 강남구 테헤란로 123") + .detailAddress("7층") + .build(); + SchoolGroup saved = groupRepository.save(new SchoolGroup(address, "테스트고등학교", SchoolType.HIGH_SCHOOL)); + + // when + List result = + groupRepository.findByNameContaining("테스트", Limit.of(10)); + + // then + assertThat(result).isNotEmpty(); + assertThat(result.get(0).getId()).isEqualTo(saved.getId()); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/repository/MemberCategoryPreferenceRepositoryTest.java b/src/test/java/com/stcom/smartmealtable/repository/MemberCategoryPreferenceRepositoryTest.java new file mode 100644 index 0000000..44b5479 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/repository/MemberCategoryPreferenceRepositoryTest.java @@ -0,0 +1,84 @@ +package com.stcom.smartmealtable.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.food.FoodCategory; +import com.stcom.smartmealtable.domain.food.MemberCategoryPreference; +import com.stcom.smartmealtable.domain.food.PreferenceType; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.domain.member.MemberType; +import java.util.List; +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.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.util.ReflectionTestUtils; + +@DataJpaTest +@ActiveProfiles("test") +class MemberCategoryPreferenceRepositoryTest { + + @Autowired + private MemberCategoryPreferenceRepository repository; + + @Autowired + private MemberProfileRepository profileRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private FoodCategoryRepository categoryRepository; + + @Autowired + private TestEntityManager em; + + @Test + @DisplayName("프로필 ID 로 기본 정렬 선호/비선호 카테고리를 조회할 수 있다") + void findDefaultByMemberProfileId() throws Exception { + // given + Member member = new Member("pref@test.com"); + memberRepository.save(member); + + MemberProfile profile = MemberProfile.builder() + .member(member) + .nickName("닉") + .type(MemberType.WORKER) + .group(null) + .build(); + profileRepository.save(profile); + + FoodCategory korean = new FoodCategory(); + ReflectionTestUtils.setField(korean, "name", "한식"); + categoryRepository.save(korean); + FoodCategory sushi = new FoodCategory(); + ReflectionTestUtils.setField(sushi, "name", "일식"); + categoryRepository.save(sushi); + + MemberCategoryPreference like = MemberCategoryPreference.builder() + .memberProfile(profile) + .category(korean) + .type(PreferenceType.LIKE) + .priority(1) + .build(); + MemberCategoryPreference dislike = MemberCategoryPreference.builder() + .memberProfile(profile) + .category(sushi) + .type(PreferenceType.DISLIKE) + .priority(1) + .build(); + repository.saveAll(List.of(like, dislike)); + em.flush(); + em.clear(); + + // when + List prefs = repository.findDefaultByMemberProfileId(profile.getId()); + + // then + assertThat(prefs).hasSize(2); + assertThat(prefs.get(0).getType()).isEqualTo(PreferenceType.LIKE); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/repository/MemberProfileRepositoryTest.java b/src/test/java/com/stcom/smartmealtable/repository/MemberProfileRepositoryTest.java new file mode 100644 index 0000000..a53401f --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/repository/MemberProfileRepositoryTest.java @@ -0,0 +1,52 @@ +package com.stcom.smartmealtable.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.domain.member.MemberType; +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.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; + +@DataJpaTest +@ActiveProfiles("test") +class MemberProfileRepositoryTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MemberProfileRepository profileRepository; + + @Autowired + private TestEntityManager em; + + @Test + @DisplayName("MemberProfileEntityGraph 조회시 member 가 fetch 되어야 한다") + void findEntityGraph() { + // given + Member member = new Member("graph@test.com"); + memberRepository.save(member); + + MemberProfile profile = MemberProfile.builder() + .member(member) + .nickName("graph") + .type(MemberType.STUDENT) + .group(null) + .build(); + profileRepository.save(profile); + em.flush(); + em.clear(); + + // when + var found = profileRepository.findMemberProfileEntityGraphById(profile.getId()); + + // then + assertThat(found).isPresent(); + assertThat(found.get().getMember().getEmail()).isEqualTo("graph@test.com"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/repository/SocialAccountRepositoryTest.java b/src/test/java/com/stcom/smartmealtable/repository/SocialAccountRepositoryTest.java new file mode 100644 index 0000000..22ed37d --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/repository/SocialAccountRepositoryTest.java @@ -0,0 +1,56 @@ +package com.stcom.smartmealtable.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.social.SocialAccount; +import java.time.LocalDateTime; +import java.util.Optional; +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.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; + +@DataJpaTest +@ActiveProfiles("test") +class SocialAccountRepositoryTest { + + @Autowired + private SocialAccountRepository repository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TestEntityManager em; + + @Test + @DisplayName("소셜 계정을 저장하고 provider/userId 로 조회할 수 있어야 한다") + void saveAndFind() { + // given + Member member = new Member("social@example.com"); + memberRepository.save(member); + + SocialAccount account = SocialAccount.builder() + .member(member) + .provider("google") + .providerUserId("12345") + .tokenType("Bearer") + .accessToken("access") + .refreshToken("refresh") + .tokenExpiresAt(LocalDateTime.now().plusDays(1)) + .build(); + repository.save(account); + em.flush(); + em.clear(); + + // when + Optional found = repository.findByProviderAndProviderUserId("google", "12345"); + + // then + assertThat(found).isPresent(); + assertThat(found.get().getMember().getEmail()).isEqualTo("social@example.com"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/repository/TermAgreementRepositoryTest.java b/src/test/java/com/stcom/smartmealtable/repository/TermAgreementRepositoryTest.java new file mode 100644 index 0000000..f1fa3f7 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/repository/TermAgreementRepositoryTest.java @@ -0,0 +1,60 @@ +package com.stcom.smartmealtable.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.term.Term; +import com.stcom.smartmealtable.domain.term.TermAgreement; +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.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.util.ReflectionTestUtils; + +@DataJpaTest +@ActiveProfiles("test") +class TermAgreementRepositoryTest { + + @Autowired + private TermAgreementRepository repository; + + @Autowired + private TermRepository termRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TestEntityManager em; + + @Test + @DisplayName("약관 동의 엔티티를 저장하고 조회할 수 있다") + void saveAndFind() { + // given + Member member = new Member("agree@test.com"); + memberRepository.save(member); + + Term term = new Term(); + ReflectionTestUtils.setField(term, "title", "테스트 약관"); + termRepository.save(term); + + TermAgreement agreement = TermAgreement.builder() + .member(member) + .term(term) + .isAgreed(true) + .build(); + repository.save(agreement); + em.flush(); + em.clear(); + + // when + var found = repository.findById(agreement.getId()); + + // then + assertThat(found).isPresent(); + assertThat(found.get().getTerm().getTitle()).isEqualTo("테스트 약관"); + assertThat(found.get().getMember().getEmail()).isEqualTo("agree@test.com"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/repository/TermRepositoryTest.java b/src/test/java/com/stcom/smartmealtable/repository/TermRepositoryTest.java new file mode 100644 index 0000000..30db874 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/repository/TermRepositoryTest.java @@ -0,0 +1,57 @@ +package com.stcom.smartmealtable.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.term.Term; +import java.util.Optional; +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.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; + +@DataJpaTest +@ActiveProfiles("test") +class TermRepositoryTest { + + @Autowired + private TermRepository termRepository; + + @Autowired + private TestEntityManager em; + + @Test + @DisplayName("약관을 저장하고 조회할 수 있어야 한다") + void saveAndFind() { + // given + Term term = createTerm("테스트 약관", true); + termRepository.save(term); + em.flush(); + em.clear(); + + // when + Optional found = termRepository.findById(term.getId()); + + // then + assertThat(found).isPresent(); + assertThat(found.get().getTitle()).isEqualTo("테스트 약관"); + assertThat(found.get().getIsRequired()).isTrue(); + } + + private Term createTerm(String title, boolean required) { + Term term = new Term(); + try { + java.lang.reflect.Field titleField = Term.class.getDeclaredField("title"); + titleField.setAccessible(true); + titleField.set(term, title); + + java.lang.reflect.Field isRequiredField = Term.class.getDeclaredField("isRequired"); + isRequiredField.setAccessible(true); + isRequiredField.set(term, required); + } catch (Exception e) { + throw new RuntimeException(e); + } + return term; + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/BudgetServiceAdditionalIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/BudgetServiceAdditionalIntegrationTest.java new file mode 100644 index 0000000..4bfad9e --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/BudgetServiceAdditionalIntegrationTest.java @@ -0,0 +1,180 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.domain.Budget.DailyBudget; +import com.stcom.smartmealtable.domain.Budget.MonthlyBudget; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.repository.BudgetRepository; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import com.stcom.smartmealtable.repository.MemberRepository; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.List; +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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class BudgetServiceAdditionalIntegrationTest { + + @Autowired + private BudgetService budgetService; + + @Autowired + private BudgetRepository budgetRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MemberProfileRepository memberProfileRepository; + + private Member member; + private MemberProfile memberProfile; + + @BeforeEach + void setUp() { + member = Member.builder() + .email("budget_test@example.com") + .rawPassword("password123!") + .build(); + memberRepository.save(member); + + memberProfile = MemberProfile.builder() + .nickName("budgetUser") + .member(member) + .build(); + memberProfileRepository.save(memberProfile); + } + + @Test + @DisplayName("존재하지 않는 프로필로 월간 예산을 조회하면 예외가 발생한다") + void findRecentMonthlyBudgetWithInvalidProfileId() { + // given + Long invalidProfileId = 9999L; + + // when & then + assertThatThrownBy(() -> budgetService.findRecentMonthlyBudgetByMemberProfileId(invalidProfileId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + } + + @Test + @DisplayName("존재하지 않는 프로필로 일일 예산을 조회하면 예외가 발생한다") + void findRecentDailyBudgetWithInvalidProfileId() { + // given + Long invalidProfileId = 9999L; + + // when & then + assertThatThrownBy(() -> budgetService.findRecentDailyBudgetByMemberProfileId(invalidProfileId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + } + + @Test + @DisplayName("존재하지 않는 프로필로 월간 예산을 저장하면 예외가 발생한다") + void saveMonthlyBudgetWithInvalidProfileId() { + // given + Long invalidProfileId = 9999L; + + // when & then + assertThatThrownBy(() -> budgetService.saveMonthlyBudgetCustom(invalidProfileId, 100000L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + } + + @Test + @DisplayName("존재하지 않는 프로필로 일일 예산을 저장하면 예외가 발생한다") + void saveDailyBudgetWithInvalidProfileId() { + // given + Long invalidProfileId = 9999L; + + // when & then + assertThatThrownBy(() -> budgetService.saveDailyBudgetCustom(invalidProfileId, 10000L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + } + + @Test + @DisplayName("존재하지 않는 프로필로 기본 일일 예산을 등록하면 예외가 발생한다") + void registerDefaultDailyBudgetWithInvalidProfileId() { + // given + Long invalidProfileId = 9999L; + LocalDate startDate = LocalDate.of(2025, 6, 15); + + // when & then + assertThatThrownBy(() -> budgetService.registerDefaultDailyBudgetBy(invalidProfileId, 10000L, startDate)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + } + + @Test + @DisplayName("존재하지 않는 프로필로 기본 월간 예산을 등록하면 예외가 발생한다") + void registerDefaultMonthlyBudgetWithInvalidProfileId() { + // given + Long invalidProfileId = 9999L; + YearMonth yearMonth = YearMonth.of(2025, 6); + + // when & then + assertThatThrownBy(() -> budgetService.registerDefaultMonthlyBudgetBy(invalidProfileId, 500000L, yearMonth)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + } + + @Test + @DisplayName("일일 예산과 월간 예산이 둘 다 존재하는 경우 정상 조회된다") + void findBothDailyAndMonthlyBudgets() { + // given + Long dailyLimit = 10000L; + Long monthlyLimit = 300000L; + LocalDate today = LocalDate.of(2025, 6, 15); + YearMonth thisMonth = YearMonth.of(2025, 6); + + // 예산 생성 + DailyBudget dailyBudget = new DailyBudget(memberProfile, BigDecimal.valueOf(dailyLimit), today); + MonthlyBudget monthlyBudget = new MonthlyBudget(memberProfile, BigDecimal.valueOf(monthlyLimit), thisMonth); + budgetRepository.saveAll(List.of(dailyBudget, monthlyBudget)); + + // when + DailyBudget foundDaily = budgetService.getDailyBudgetBy(memberProfile.getId(), today); + MonthlyBudget foundMonthly = budgetService.getMonthlyBudgetBy(memberProfile.getId(), thisMonth); + + // then + assertThat(foundDaily.getLimit()).isEqualTo(BigDecimal.valueOf(dailyLimit)); + assertThat(foundMonthly.getLimit()).isEqualTo(BigDecimal.valueOf(monthlyLimit)); + } + + @Test + @DisplayName("존재하지 않는 날짜의 일일 예산을 수정하려고 하면 예외가 발생한다") + void editDailyBudgetWithNonExistentDate() { + // given + LocalDate nonExistentDate = LocalDate.of(2030, 12, 31); + + // when & then + assertThatThrownBy(() -> budgetService.editDailyBudgetCustom(memberProfile.getId(), nonExistentDate, 15000L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + } + + @Test + @DisplayName("존재하지 않는 년월의 월간 예산을 수정하려고 하면 예외가 발생한다") + void editMonthlyBudgetWithNonExistentYearMonth() { + // given + YearMonth nonExistentYearMonth = YearMonth.of(2030, 12); + + // when & then + assertThatThrownBy(() -> budgetService.editMonthlyBudgetCustom(memberProfile.getId(), nonExistentYearMonth, 500000L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/BudgetServiceAdditionalIntegrationTest2.java b/src/test/java/com/stcom/smartmealtable/service/BudgetServiceAdditionalIntegrationTest2.java new file mode 100644 index 0000000..4993f0f --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/BudgetServiceAdditionalIntegrationTest2.java @@ -0,0 +1,144 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.domain.Budget.DailyBudget; +import com.stcom.smartmealtable.domain.Budget.MonthlyBudget; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.repository.BudgetRepository; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import com.stcom.smartmealtable.repository.MemberRepository; +import java.math.BigDecimal; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.List; +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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class BudgetServiceAdditionalIntegrationTest2 { + + @Autowired + private BudgetService budgetService; + + @Autowired + private BudgetRepository budgetRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MemberProfileRepository memberProfileRepository; + + private Member member; + private MemberProfile memberProfile; + + @BeforeEach + void setUp() { + member = Member.builder() + .email("budget_test2@example.com") + .rawPassword("password123!") + .build(); + memberRepository.save(member); + + memberProfile = MemberProfile.builder() + .nickName("예산테스터2") + .member(member) + .build(); + memberProfileRepository.save(memberProfile); + } + + @Test + @DisplayName("주간 예산 조회 시 월요일부터 일요일까지의 예산이 모두 조회된다") + void getDailyBudgetsByWeek() { + // given + // 특정 주의 월요일 구하기 + LocalDate today = LocalDate.of(2025, 7, 16); // 수요일 + LocalDate monday = today.with(DayOfWeek.MONDAY); + + // 해당 주의 모든 일자에 예산 생성 (월~일) + for (int i = 0; i < 7; i++) { + LocalDate date = monday.plusDays(i); + // 요일별로 다른 금액 설정 + DailyBudget dailyBudget = new DailyBudget( + memberProfile, + BigDecimal.valueOf(10000 + i * 1000), + date); + budgetRepository.save(dailyBudget); + } + + // when - 해당 주의 중간 날짜(수요일)로 조회해도 월요일부터 일요일까지 모두 조회되어야 함 + List weekBudgets = budgetService.getDailyBudgetsByWeek(memberProfile.getId(), today); + + // then + assertThat(weekBudgets).hasSize(7); + assertThat(weekBudgets.get(0).getDate()).isEqualTo(monday); // 첫 번째는 월요일 + assertThat(weekBudgets.get(6).getDate()).isEqualTo(monday.plusDays(6)); // 마지막은 일요일 + + // 날짜순으로 정렬되어 있는지 확인 + for (int i = 0; i < weekBudgets.size(); i++) { + assertThat(weekBudgets.get(i).getDate()).isEqualTo(monday.plusDays(i)); + // 금액 확인 (10000 + i * 1000) + assertThat(weekBudgets.get(i).getLimit()).isEqualTo(BigDecimal.valueOf(10000 + i * 1000)); + } + } + + @Test + @DisplayName("빈 주간 예산 조회 시 빈 리스트가 반환된다") + void getDailyBudgetsByWeekWhenEmpty() { + // given + LocalDate date = LocalDate.of(2026, 1, 1); + + // when + List weekBudgets = budgetService.getDailyBudgetsByWeek(memberProfile.getId(), date); + + // then + assertThat(weekBudgets).isEmpty(); + } + + @Test + @DisplayName("일부 날짜만 예산이 있는 주간 조회 시 존재하는 날짜의 예산만 조회된다") + void getDailyBudgetsByWeekWithPartialData() { + // given + LocalDate monday = LocalDate.of(2025, 8, 4); + LocalDate wednesday = monday.plusDays(2); + LocalDate friday = monday.plusDays(4); + + // 수요일과 금요일에만 예산 설정 + DailyBudget wednesdayBudget = new DailyBudget(memberProfile, BigDecimal.valueOf(15000), wednesday); + DailyBudget fridayBudget = new DailyBudget(memberProfile, BigDecimal.valueOf(20000), friday); + budgetRepository.saveAll(List.of(wednesdayBudget, fridayBudget)); + + // when + List weekBudgets = budgetService.getDailyBudgetsByWeek(memberProfile.getId(), monday); + + // then + assertThat(weekBudgets).hasSize(2); // 수요일, 금요일 두 개만 있어야 함 + assertThat(weekBudgets.stream().map(DailyBudget::getDate)) + .containsExactlyInAnyOrder(wednesday, friday); + } + + @Test + @DisplayName("존재하지 않는 프로필 ID로 주간 예산 조회 시 빈 리스트가 반환된다") + void getDailyBudgetsByWeekWithInvalidProfileId() { + // given + Long invalidProfileId = 99999L; + LocalDate date = LocalDate.now(); + + // when + List weekBudgets = budgetService.getDailyBudgetsByWeek(invalidProfileId, date); + + // then + assertThat(weekBudgets).isEmpty(); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/BudgetServiceCompleteIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/BudgetServiceCompleteIntegrationTest.java new file mode 100644 index 0000000..567c046 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/BudgetServiceCompleteIntegrationTest.java @@ -0,0 +1,292 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.domain.Budget.DailyBudget; +import com.stcom.smartmealtable.domain.Budget.MonthlyBudget; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.repository.BudgetRepository; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import com.stcom.smartmealtable.repository.MemberRepository; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.List; +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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class BudgetServiceCompleteIntegrationTest { + + @Autowired + private BudgetService budgetService; + + @Autowired + private BudgetRepository budgetRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MemberProfileRepository memberProfileRepository; + + private Member member; + private MemberProfile memberProfile; + + @BeforeEach + void setUp() { + member = Member.builder() + .email("budget_complete_test@example.com") + .rawPassword("password123!") + .build(); + memberRepository.save(member); + + memberProfile = MemberProfile.builder() + .nickName("완전예산테스터") + .member(member) + .build(); + memberProfileRepository.save(memberProfile); + } + + @Test + @DisplayName("특정 일자의 일간 예산을 조회할 수 있다") + void getDailyBudgetBy() { + // given + LocalDate date = LocalDate.of(2025, 7, 15); + DailyBudget dailyBudget = new DailyBudget(memberProfile, BigDecimal.valueOf(25000), date); + budgetRepository.save(dailyBudget); + + // when + DailyBudget result = budgetService.getDailyBudgetBy(memberProfile.getId(), date); + + // then + assertThat(result.getDate()).isEqualTo(date); + assertThat(result.getLimit()).isEqualTo(BigDecimal.valueOf(25000)); + assertThat(result.getMemberProfile().getId()).isEqualTo(memberProfile.getId()); + } + + @Test + @DisplayName("존재하지 않는 일간 예산 조회 시 예외가 발생한다") + void getDailyBudgetByNotFound() { + // given + LocalDate date = LocalDate.of(2025, 7, 15); + + // when & then + assertThatThrownBy(() -> budgetService.getDailyBudgetBy(memberProfile.getId(), date)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("예산이 존재하지 않습니다."); + } + + @Test + @DisplayName("특정 월의 월간 예산을 조회할 수 있다") + void getMonthlyBudgetBy() { + // given + YearMonth yearMonth = YearMonth.of(2025, 7); + MonthlyBudget monthlyBudget = new MonthlyBudget(memberProfile, BigDecimal.valueOf(800000), yearMonth); + budgetRepository.save(monthlyBudget); + + // when + MonthlyBudget result = budgetService.getMonthlyBudgetBy(memberProfile.getId(), yearMonth); + + // then + assertThat(result.getYearMonth()).isEqualTo(yearMonth); + assertThat(result.getLimit()).isEqualTo(BigDecimal.valueOf(800000)); + assertThat(result.getMemberProfile().getId()).isEqualTo(memberProfile.getId()); + } + + @Test + @DisplayName("존재하지 않는 월간 예산 조회 시 예외가 발생한다") + void getMonthlyBudgetByNotFound() { + // given + YearMonth yearMonth = YearMonth.of(2025, 7); + + // when & then + assertThatThrownBy(() -> budgetService.getMonthlyBudgetBy(memberProfile.getId(), yearMonth)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + } + + @Test + @DisplayName("기본 일간 예산을 시작일부터 월말까지 자동 생성할 수 있다") + void registerDefaultDailyBudgetBy() { + // given + LocalDate startDate = LocalDate.of(2025, 7, 15); + Long dailyLimit = 20000L; + + // when + budgetService.registerDefaultDailyBudgetBy(memberProfile.getId(), dailyLimit, startDate); + + // then + List budgets = budgetRepository.findDailyBudgetsByMemberProfileIdAndDateBetween( + memberProfile.getId(), + startDate, + startDate.withDayOfMonth(startDate.lengthOfMonth()) + ); + + // 7월 15일부터 31일까지 17일간 + assertThat(budgets).hasSize(17); + assertThat(budgets.get(0).getDate()).isEqualTo(startDate); + assertThat(budgets.get(budgets.size() - 1).getDate()).isEqualTo(LocalDate.of(2025, 7, 31)); + assertThat(budgets.stream().allMatch(b -> b.getLimit().equals(BigDecimal.valueOf(dailyLimit)))).isTrue(); + } + + @Test + @DisplayName("기본 월간 예산을 생성할 수 있다") + void registerDefaultMonthlyBudgetBy() { + // given + YearMonth startYearMonth = YearMonth.of(2025, 8); + Long monthlyLimit = 900000L; + + // when + budgetService.registerDefaultMonthlyBudgetBy(memberProfile.getId(), monthlyLimit, startYearMonth); + + // then + MonthlyBudget budget = budgetRepository.findMonthlyBudgetByMemberProfileIdAndYearMonth( + memberProfile.getId(), startYearMonth + ).orElseThrow(); + + assertThat(budget.getYearMonth()).isEqualTo(startYearMonth); + assertThat(budget.getLimit()).isEqualTo(BigDecimal.valueOf(monthlyLimit)); + assertThat(budget.getMemberProfile().getId()).isEqualTo(memberProfile.getId()); + } + + @Test + @DisplayName("기본 예산 생성 시 존재하지 않는 프로필 ID로 시도하면 예외가 발생한다") + void registerDefaultBudgetWithInvalidProfileId() { + // given + Long invalidProfileId = 99999L; + LocalDate date = LocalDate.now(); + YearMonth yearMonth = YearMonth.now(); + + // when & then + assertThatThrownBy(() -> budgetService.registerDefaultDailyBudgetBy(invalidProfileId, 20000L, date)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + + assertThatThrownBy(() -> budgetService.registerDefaultMonthlyBudgetBy(invalidProfileId, 800000L, yearMonth)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + } + + @Test + @DisplayName("일간 예산 한도를 수정할 수 있다") + void editDailyBudgetCustom() { + // given + LocalDate date = LocalDate.of(2025, 8, 10); + DailyBudget originalBudget = new DailyBudget(memberProfile, BigDecimal.valueOf(20000), date); + budgetRepository.save(originalBudget); + + Long newLimit = 35000L; + + // when + budgetService.editDailyBudgetCustom(memberProfile.getId(), date, newLimit); + + // then + DailyBudget updatedBudget = budgetRepository.findDailyBudgetByMemberProfileIdAndDate( + memberProfile.getId(), date + ).orElseThrow(); + + assertThat(updatedBudget.getLimit()).isEqualTo(BigDecimal.valueOf(newLimit)); + } + + @Test + @DisplayName("월간 예산 한도를 수정할 수 있다") + void editMonthlyBudgetCustom() { + // given + YearMonth yearMonth = YearMonth.of(2025, 8); + MonthlyBudget originalBudget = new MonthlyBudget(memberProfile, BigDecimal.valueOf(800000), yearMonth); + budgetRepository.save(originalBudget); + + Long newLimit = 1200000L; + + // when + budgetService.editMonthlyBudgetCustom(memberProfile.getId(), yearMonth, newLimit); + + // then + MonthlyBudget updatedBudget = budgetRepository.findMonthlyBudgetByMemberProfileIdAndYearMonth( + memberProfile.getId(), yearMonth + ).orElseThrow(); + + assertThat(updatedBudget.getLimit()).isEqualTo(BigDecimal.valueOf(newLimit)); + } + + @Test + @DisplayName("존재하지 않는 예산 수정 시 예외가 발생한다") + void editBudgetCustomNotFound() { + // given + LocalDate date = LocalDate.of(2025, 8, 10); + YearMonth yearMonth = YearMonth.of(2025, 8); + + // when & then + assertThatThrownBy(() -> budgetService.editDailyBudgetCustom(memberProfile.getId(), date, 30000L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + + assertThatThrownBy(() -> budgetService.editMonthlyBudgetCustom(memberProfile.getId(), yearMonth, 1000000L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + } + + @Test + @DisplayName("사용자 정의 일간 예산을 저장할 수 있다") + void saveDailyBudgetCustom() { + // given + Long dailyLimit = 30000L; + + // when + budgetService.saveDailyBudgetCustom(memberProfile.getId(), dailyLimit); + + // then + // 오늘 날짜로 예산이 생성되는지 확인 + DailyBudget savedBudget = budgetRepository.findDailyBudgetByMemberProfileIdAndDate( + memberProfile.getId(), LocalDate.now() + ).orElseThrow(); + + assertThat(savedBudget.getLimit()).isEqualTo(BigDecimal.valueOf(dailyLimit)); + assertThat(savedBudget.getDate()).isEqualTo(LocalDate.now()); + } + + @Test + @DisplayName("사용자 정의 월간 예산을 저장할 수 있다") + void saveMonthlyBudgetCustom() { + // given + Long monthlyLimit = 900000L; + + // when + budgetService.saveMonthlyBudgetCustom(memberProfile.getId(), monthlyLimit); + + // then + // 이번 달로 예산이 생성되는지 확인 + MonthlyBudget savedBudget = budgetRepository.findMonthlyBudgetByMemberProfileIdAndYearMonth( + memberProfile.getId(), YearMonth.now() + ).orElseThrow(); + + assertThat(savedBudget.getLimit()).isEqualTo(BigDecimal.valueOf(monthlyLimit)); + assertThat(savedBudget.getYearMonth()).isEqualTo(YearMonth.now()); + } + + @Test + @DisplayName("사용자 정의 예산 저장 시 존재하지 않는 프로필 ID로 시도하면 예외가 발생한다") + void saveCustomBudgetWithInvalidProfileId() { + // given + Long invalidProfileId = 99999L; + + // when & then + assertThatThrownBy(() -> budgetService.saveDailyBudgetCustom(invalidProfileId, 25000L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + + assertThatThrownBy(() -> budgetService.saveMonthlyBudgetCustom(invalidProfileId, 800000L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/BudgetServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/BudgetServiceTest.java deleted file mode 100644 index ab6ee53..0000000 --- a/src/test/java/com/stcom/smartmealtable/service/BudgetServiceTest.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.stcom.smartmealtable.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.stcom.smartmealtable.domain.Budget.DailyBudget; -import com.stcom.smartmealtable.domain.Budget.MonthlyBudget; -import com.stcom.smartmealtable.domain.member.MemberProfile; -import com.stcom.smartmealtable.repository.BudgetRepository; -import com.stcom.smartmealtable.repository.MemberProfileRepository; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.YearMonth; -import java.util.Optional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class BudgetServiceTest { - - @InjectMocks - private BudgetService budgetService; - - @Mock - private BudgetRepository budgetRepository; - - @Mock - private MemberProfileRepository memberProfileRepository; - - @Test - @DisplayName("회원 프로필 ID로 최근 일일 예산을 조회할 수 있다") - void findRecentDailyBudgetByMemberProfileId() { - // given - Long memberProfileId = 1L; - MemberProfile memberProfile = new MemberProfile(); - DailyBudget dailyBudget = new DailyBudget(memberProfile, BigDecimal.valueOf(10000), LocalDate.now()); - - when(budgetRepository.findFirstDailyBudgetByMemberProfileId(memberProfileId)) - .thenReturn(Optional.of(dailyBudget)); - - // when - DailyBudget result = budgetService.findRecentDailyBudgetByMemberProfileId(memberProfileId); - - // then - assertThat(result).isEqualTo(dailyBudget); - assertThat(result.getLimit()).isEqualTo(BigDecimal.valueOf(10000)); - } - - @Test - @DisplayName("존재하지 않는 회원 프로필 ID로 일일 예산을 조회하면 예외가 발생한다") - void findRecentDailyBudgetByMemberProfileId_NotFound() { - // given - Long memberProfileId = 999L; - - when(budgetRepository.findFirstDailyBudgetByMemberProfileId(memberProfileId)) - .thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> budgetService.findRecentDailyBudgetByMemberProfileId(memberProfileId)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("존재하지 않는 프로필로 접근"); - } - - @Test - @DisplayName("회원 프로필 ID로 최근 월별 예산을 조회할 수 있다") - void findRecentMonthlyBudgetByMemberProfileId() { - // given - Long memberProfileId = 1L; - MemberProfile memberProfile = new MemberProfile(); - MonthlyBudget monthlyBudget = new MonthlyBudget(memberProfile, BigDecimal.valueOf(300000), YearMonth.now()); - - when(budgetRepository.findFirstMonthlyBudgetByMemberProfileId(memberProfileId)) - .thenReturn(Optional.of(monthlyBudget)); - - // when - MonthlyBudget result = budgetService.findRecentMonthlyBudgetByMemberProfileId(memberProfileId); - - // then - assertThat(result).isEqualTo(monthlyBudget); - assertThat(result.getLimit()).isEqualTo(BigDecimal.valueOf(300000)); - } - - @Test - @DisplayName("월별 예산을 저장할 수 있다") - void saveMonthlyBudgetCustom() { - // given - Long memberProfileId = 1L; - Long limit = 300000L; - MemberProfile memberProfile = new MemberProfile(); - - when(memberProfileRepository.findById(memberProfileId)) - .thenReturn(Optional.of(memberProfile)); - - // when - budgetService.saveMonthlyBudgetCustom(memberProfileId, limit); - - // then - verify(budgetRepository).save(any(MonthlyBudget.class)); - } - - @Test - @DisplayName("일일 예산을 저장할 수 있다") - void saveDailyBudgetCustom() { - // given - Long memberProfileId = 1L; - Long limit = 10000L; - MemberProfile memberProfile = new MemberProfile(); - - when(memberProfileRepository.findById(memberProfileId)) - .thenReturn(Optional.of(memberProfile)); - - // when - budgetService.saveDailyBudgetCustom(memberProfileId, limit); - - // then - verify(budgetRepository).save(any(DailyBudget.class)); - } - - @Test - @DisplayName("존재하지 않는 회원 프로필 ID로 예산을 저장하면 예외가 발생한다") - void saveBudget_NotFound() { - // given - Long memberProfileId = 999L; - Long limit = 10000L; - - when(memberProfileRepository.findById(memberProfileId)) - .thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> budgetService.saveDailyBudgetCustom(memberProfileId, limit)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("존재하지 않는 프로필로 접근"); - } - -} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/GroupServiceAdditionalIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/GroupServiceAdditionalIntegrationTest.java new file mode 100644 index 0000000..d850a47 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/GroupServiceAdditionalIntegrationTest.java @@ -0,0 +1,95 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.group.Group; +import com.stcom.smartmealtable.domain.group.SchoolType; +import com.stcom.smartmealtable.infrastructure.KakaoAddressApiService; +import com.stcom.smartmealtable.infrastructure.dto.AddressRequest; +import com.stcom.smartmealtable.repository.GroupRepository; +import java.util.List; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +@Import(GroupServiceAdditionalIntegrationTest.FakeKakaoConfig.class) +class GroupServiceAdditionalIntegrationTest { + + @TestConfiguration + static class FakeKakaoConfig { + @Bean + KakaoAddressApiService kakaoAddressApiService() { + return new KakaoAddressApiService() { + @Override + public Address createAddressFromRequest(AddressRequest requestDto) { + return Address.builder() + .roadAddress(requestDto.getRoadAddress()) + .detailAddress(requestDto.getDetailAddress()) + .build(); + } + }; + } + } + + @Autowired + private GroupService groupService; + @Autowired + private GroupRepository repository; + + @Test + @DisplayName("학교 그룹을 생성하고 삭제할 수 있다") + @Rollback + void createAndDeleteSchoolGroup() { + // when + groupService.createSchoolGroup(new AddressRequest("서울", "1"), "테스트고", SchoolType.HIGH_SCHOOL); + List saved = repository.findByNameContaining("테스트고", org.springframework.data.domain.Limit.of(10)); + Long id = saved.get(0).getId(); + + // then + assertThat(saved).hasSize(1); + + groupService.deleteGroup(id); + assertThat(repository.findById(id)).isEmpty(); + } + + @Test + @DisplayName("학교 그룹 수정 시 타입 변경") + void changeSchoolGroup() { + groupService.createSchoolGroup(new AddressRequest("부산", "detail"), "부산고", SchoolType.HIGH_SCHOOL); + Long id = repository.findByNameContaining("부산고", org.springframework.data.domain.Limit.of(10)).get(0).getId(); + + groupService.changeSchoolGroup(id, new AddressRequest("부산", "detail2"), "부산여고", SchoolType.MIDDLE_SCHOOL); + + Group changed = repository.findById(id).orElseThrow(); + assertThat(changed.getName()).isEqualTo("부산여고"); + assertThat(changed.getTypeName()).isEqualTo(SchoolType.MIDDLE_SCHOOL.name()); + } + + @Test + @DisplayName("학교 그룹 수정 시 학교 그룹이 아니면 예외") + void changeSchoolGroup_invalidType() { + // 직접 회사 그룹 저장 + Address addr = Address.builder().roadAddress("서울").build(); + Group company = new com.stcom.smartmealtable.domain.group.CompanyGroup(); + org.springframework.test.util.ReflectionTestUtils.setField(company, "address", addr); + org.springframework.test.util.ReflectionTestUtils.setField(company, "name", "컴퍼니"); + repository.save(company); + + assertThatThrownBy(() -> + groupService.changeSchoolGroup(company.getId(), new AddressRequest("a","b"), "x", SchoolType.HIGH_SCHOOL)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("학교 그룹이 아닙니다"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/GroupServiceCompleteIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/GroupServiceCompleteIntegrationTest.java new file mode 100644 index 0000000..1adaafd --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/GroupServiceCompleteIntegrationTest.java @@ -0,0 +1,249 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.group.Group; +import com.stcom.smartmealtable.domain.group.SchoolGroup; +import com.stcom.smartmealtable.domain.group.SchoolType; +import com.stcom.smartmealtable.infrastructure.dto.AddressRequest; +import com.stcom.smartmealtable.repository.GroupRepository; +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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.context.annotation.Bean; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Import; +import com.stcom.smartmealtable.infrastructure.KakaoAddressApiService; + +import java.util.List; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +@Import(GroupServiceCompleteIntegrationTest.TestConfig.class) +class GroupServiceCompleteIntegrationTest { + + @Autowired + private GroupService groupService; + + @Autowired + private GroupRepository groupRepository; + + private SchoolGroup schoolGroup1; + private SchoolGroup schoolGroup2; + private SchoolGroup schoolGroup3; + + @BeforeEach + void setUp() { + Address address1 = Address.builder() + .lotNumberAddress("서울시 강남구") + .roadAddress("테헤란로 123") + .detailAddress("1번 빌딩") + .build(); + + Address address2 = Address.builder() + .lotNumberAddress("서울시 서초구") + .roadAddress("강남대로 456") + .detailAddress("2번 빌딩") + .build(); + + Address address3 = Address.builder() + .lotNumberAddress("부산시 해운대구") + .roadAddress("센텀로 789") + .detailAddress("3번 빌딩") + .build(); + + schoolGroup1 = new SchoolGroup(address1, "서울대학교", SchoolType.UNIVERSITY_FOUR_YEAR); + schoolGroup2 = new SchoolGroup(address2, "연세대학교", SchoolType.UNIVERSITY_FOUR_YEAR); + schoolGroup3 = new SchoolGroup(address3, "카이스트", SchoolType.UNIVERSITY_FOUR_YEAR); + + groupRepository.save(schoolGroup1); + groupRepository.save(schoolGroup2); + groupRepository.save(schoolGroup3); + } + + @Test + @DisplayName("그룹 ID로 그룹을 조회할 수 있다") + void findGroupByGroupId() { + // when + Group foundGroup = groupService.findGroupByGroupId(schoolGroup1.getId()); + + // then + assertThat(foundGroup.getId()).isEqualTo(schoolGroup1.getId()); + assertThat(foundGroup.getName()).isEqualTo("서울대학교"); + } + + @Test + @DisplayName("존재하지 않는 그룹 ID로 조회하면 예외가 발생한다") + void findGroupByGroupIdWithInvalidId() { + // given + Long invalidGroupId = 99999L; + + // when & then + assertThatThrownBy(() -> groupService.findGroupByGroupId(invalidGroupId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원입니다"); + } + + @Test + @DisplayName("키워드로 그룹을 검색할 수 있다 - 완전 일치") + void findGroupsByKeywordExactMatch() { + // when + List groups = groupService.findGroupsByKeyword("서울대학교"); + + // then + assertThat(groups).hasSize(1); + assertThat(groups.get(0).getName()).isEqualTo("서울대학교"); + } + + @Test + @DisplayName("키워드로 그룹을 검색할 수 있다 - 부분 일치") + void findGroupsByKeywordPartialMatch() { + // when + List groups = groupService.findGroupsByKeyword("대학교"); + + // then + assertThat(groups).hasSize(2); + assertThat(groups).extracting(Group::getName) + .containsExactlyInAnyOrder("서울대학교", "연세대학교"); + } + + @Test + @DisplayName("키워드로 그룹을 검색할 수 있다 - 검색 결과 없음") + void findGroupsByKeywordNoResults() { + // when + List groups = groupService.findGroupsByKeyword("존재하지않는키워드"); + + // then + assertThat(groups).isEmpty(); + } + + @Test + @DisplayName("빈 키워드로 검색하면 모든 그룹을 반환한다") + void findGroupsByEmptyKeyword() { + // when + List groups = groupService.findGroupsByKeyword(""); + + // then + assertThat(groups).hasSize(3); + } + + @Test + @DisplayName("null 키워드로 검색하면 모든 그룹을 반환한다") + void findGroupsByNullKeyword() { + // when + List groups = groupService.findGroupsByKeyword(null); + + // then + assertThat(groups).isEmpty(); + } + + @Test + @DisplayName("새로운 학교 그룹을 생성할 수 있다") + void createSchoolGroup() { + // given + AddressRequest addressRequest = new AddressRequest("1234", "대전시 유성구 과학로 291"); + String name = "한국과학기술원"; + SchoolType schoolType = SchoolType.UNIVERSITY_FOUR_YEAR; + + // when + groupService.createSchoolGroup(addressRequest, name, schoolType); + + // then + List allGroups = groupRepository.findAll(); + assertThat(allGroups).hasSize(4); + Group createdGroup = allGroups.stream() + .filter(group -> group.getName().equals(name)) + .findFirst() + .orElseThrow(); + assertThat(createdGroup.getName()).isEqualTo(name); + assertThat(((SchoolGroup) createdGroup).getSchoolType()).isEqualTo(schoolType); + } + + @Test + @DisplayName("학교 그룹 정보를 변경할 수 있다") + void changeSchoolGroup() { + // given + AddressRequest newAddressRequest = new AddressRequest("5678", "서울시 동작구 상도로 369"); + String newName = "숭실대학교"; + SchoolType newType = SchoolType.UNIVERSITY_FOUR_YEAR; + + // when + groupService.changeSchoolGroup(schoolGroup1.getId(), newAddressRequest, newName, newType); + + // then + Group changedGroup = groupRepository.findById(schoolGroup1.getId()).orElseThrow(); + assertThat(changedGroup.getName()).isEqualTo(newName); + assertThat(((SchoolGroup) changedGroup).getSchoolType()).isEqualTo(newType); + } + + @Test + @DisplayName("존재하지 않는 그룹을 변경하려고 하면 예외가 발생한다") + void changeSchoolGroupWithInvalidId() { + // given + Long invalidGroupId = 99999L; + AddressRequest addressRequest = new AddressRequest("0000", "서울시 테스트로"); + + // when & then + assertThatThrownBy(() -> groupService.changeSchoolGroup(invalidGroupId, addressRequest, "테스트", SchoolType.UNIVERSITY_FOUR_YEAR)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원입니다"); + } + + @Test + @DisplayName("학교가 아닌 그룹을 학교 그룹으로 변경하려고 하면 예외가 발생한다") + void changeNonSchoolGroupToSchoolGroup() { + // 이 테스트는 현재 모든 그룹이 SchoolGroup이므로 생략하거나 + // CompanyGroup이 있을 때 다시 작성해야 함 + } + + @Test + @DisplayName("그룹을 삭제할 수 있다") + void deleteGroup() { + // given + Long groupIdToDelete = schoolGroup1.getId(); + + // when + groupService.deleteGroup(groupIdToDelete); + + // then + assertThat(groupRepository.findById(groupIdToDelete)).isEmpty(); + assertThat(groupRepository.findAll()).hasSize(2); + } + + @Test + @DisplayName("존재하지 않는 그룹을 삭제하려고 하면 예외가 발생한다") + void deleteGroupWithInvalidId() { + // given + Long invalidGroupId = 99999L; + + // when & then + assertThatThrownBy(() -> groupService.deleteGroup(invalidGroupId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원입니다"); + } + + @TestConfiguration + static class TestConfig { + @Bean + @org.springframework.context.annotation.Primary + public KakaoAddressApiService kakaoAddressApiService() { + return new KakaoAddressApiService() { + @Override + public Address createAddressFromRequest(AddressRequest requestDto) { + return Address.builder() + .lotNumberAddress(requestDto.getRoadAddress()) + .roadAddress(requestDto.getRoadAddress()) + .detailAddress(requestDto.getDetailAddress()) + .build(); + } + }; + } + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/GroupServiceIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/GroupServiceIntegrationTest.java new file mode 100644 index 0000000..43f2f4d --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/GroupServiceIntegrationTest.java @@ -0,0 +1,62 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.group.CompanyGroup; +import com.stcom.smartmealtable.domain.group.Group; +import com.stcom.smartmealtable.domain.group.IndustryType; +import com.stcom.smartmealtable.domain.group.SchoolGroup; +import com.stcom.smartmealtable.domain.group.SchoolType; +import com.stcom.smartmealtable.repository.GroupRepository; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +/** + * GroupService 통합 테스트. + */ +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class GroupServiceIntegrationTest { + + @Autowired + private GroupService groupService; + + @Autowired + private GroupRepository groupRepository; + + @Test + @DisplayName("키워드로 그룹 검색이 가능해야 한다") + void searchByKeyword() { + // given + Address address1 = Address.builder() + .roadAddress("서울특별시 종로구 세종대로 1") + .detailAddress("본관") + .build(); + Group group1 = new SchoolGroup(address1, "서울고등학교", SchoolType.HIGH_SCHOOL); + groupRepository.save(group1); + + Address address2 = Address.builder() + .roadAddress("서울특별시 서초구 강남대로 1") + .detailAddress("타워") + .build(); + Group group2 = new CompanyGroup(); + org.springframework.test.util.ReflectionTestUtils.setField(group2, "address", address2); + org.springframework.test.util.ReflectionTestUtils.setField(group2, "name", "테스트IT"); + org.springframework.test.util.ReflectionTestUtils.setField(group2, "industryType", IndustryType.IT); + groupRepository.save(group2); + + // when + List result = groupService.findGroupsByKeyword("서울"); + + // then + assertThat(result).hasSize(1); + assertThat(result.get(0).getName()).isEqualTo("서울고등학교"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/GroupServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/GroupServiceTest.java deleted file mode 100644 index 8be1f60..0000000 --- a/src/test/java/com/stcom/smartmealtable/service/GroupServiceTest.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.stcom.smartmealtable.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.stcom.smartmealtable.domain.Address.Address; -import com.stcom.smartmealtable.domain.group.CompanyGroup; -import com.stcom.smartmealtable.domain.group.Group; -import com.stcom.smartmealtable.domain.group.IndustryType; -import com.stcom.smartmealtable.domain.group.SchoolGroup; -import com.stcom.smartmealtable.domain.group.SchoolType; -import com.stcom.smartmealtable.repository.GroupRepository; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.Limit; - -@ExtendWith(MockitoExtension.class) -class GroupServiceTest { - - @Mock - private GroupRepository groupRepository; - - @InjectMocks - private GroupServiceImpl groupService; - - @Test - @DisplayName("ID로 그룹을 찾을 수 있어야 한다") - void findGroupByGroupId() { - // given - Long groupId = 1L; - - CompanyGroup companyGroup = new CompanyGroup(); - Address address = createAddress("서울시 강남구 테헤란로 123"); - - // 리플렉션으로 private 필드 설정 - org.springframework.test.util.ReflectionTestUtils.setField(companyGroup, "id", groupId); - org.springframework.test.util.ReflectionTestUtils.setField(companyGroup, "name", "IT 회사"); - org.springframework.test.util.ReflectionTestUtils.setField(companyGroup, "address", address); - org.springframework.test.util.ReflectionTestUtils.setField(companyGroup, "industryType", IndustryType.IT); - - when(groupRepository.findById(groupId)).thenReturn(Optional.of(companyGroup)); - - // when - Group foundGroup = groupService.findGroupByGroupId(groupId); - - // then - assertThat(foundGroup).isEqualTo(companyGroup); - assertThat(foundGroup.getName()).isEqualTo("IT 회사"); - assertThat(foundGroup.getTypeName()).isEqualTo("IT"); - assertThat(foundGroup.getAddress().getRoadAddress()).isEqualTo("서울시 강남구 테헤란로 123"); - verify(groupRepository, times(1)).findById(groupId); - } - - @Test - @DisplayName("존재하지 않는 그룹 ID로 조회 시 예외가 발생해야 한다") - void findGroupByGroupIdNotFound() { - // given - Long groupId = 999L; - when(groupRepository.findById(groupId)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> groupService.findGroupByGroupId(groupId)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("존재하지 않는 회원입니다"); - } - - @Test - @DisplayName("키워드로 그룹을 검색할 수 있어야 한다") - void findGroupsByKeyword() { - // given - String keyword = "학교"; - - SchoolGroup schoolGroup1 = createSchoolGroup(1L, "서울대학교", SchoolType.UNIVERSITY_FOUR_YEAR, - createAddress("서울시 관악구 관악로 1")); - - SchoolGroup schoolGroup2 = createSchoolGroup(2L, "부산대학교", SchoolType.UNIVERSITY_FOUR_YEAR, - createAddress("부산시 금정구 부산대학로 63번길 2")); - - List expectedGroups = Arrays.asList(schoolGroup1, schoolGroup2); - - when(groupRepository.findByNameContaining(keyword, Limit.of(10))).thenReturn(expectedGroups); - - // when - List foundGroups = groupService.findGroupsByKeyword(keyword); - - // then - assertThat(foundGroups).hasSize(2); - assertThat(foundGroups).containsExactly(schoolGroup1, schoolGroup2); - - assertThat(foundGroups.get(0).getName()).isEqualTo("서울대학교"); - assertThat(foundGroups.get(0).getTypeName()).isEqualTo("UNIVERSITY_FOUR_YEAR"); - - assertThat(foundGroups.get(1).getName()).isEqualTo("부산대학교"); - assertThat(foundGroups.get(1).getAddress().getRoadAddress()).isEqualTo("부산시 금정구 부산대학로 63번길 2"); - - verify(groupRepository, times(1)).findByNameContaining(keyword, Limit.of(10)); - } - - @Test - @DisplayName("키워드 검색 결과가 없을 경우 빈 리스트를 반환해야 한다") - void findGroupsByKeywordNoResult() { - // given - String keyword = "존재하지 않는 키워드"; - when(groupRepository.findByNameContaining(keyword, Limit.of(10))).thenReturn(List.of()); - - // when - List foundGroups = groupService.findGroupsByKeyword(keyword); - - // then - assertThat(foundGroups).isEmpty(); - verify(groupRepository, times(1)).findByNameContaining(keyword, Limit.of(10)); - } - - // 테스트용 주소 생성 헬퍼 메소드 - private Address createAddress(String roadAddress) { - Address address = new Address(); - org.springframework.test.util.ReflectionTestUtils.setField(address, "roadAddress", roadAddress); - return address; - } - - // 테스트용 학교 그룹 생성 헬퍼 메소드 - private SchoolGroup createSchoolGroup(Long id, String name, SchoolType schoolType, Address address) { - SchoolGroup group = new SchoolGroup(); - org.springframework.test.util.ReflectionTestUtils.setField(group, "id", id); - org.springframework.test.util.ReflectionTestUtils.setField(group, "name", name); - org.springframework.test.util.ReflectionTestUtils.setField(group, "address", address); - org.springframework.test.util.ReflectionTestUtils.setField(group, "schoolType", schoolType); - return group; - } -} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/LoginServiceAdditionalIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/LoginServiceAdditionalIntegrationTest.java new file mode 100644 index 0000000..f35e84c --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/LoginServiceAdditionalIntegrationTest.java @@ -0,0 +1,170 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.domain.member.MemberType; +import com.stcom.smartmealtable.domain.social.SocialAccount; +import com.stcom.smartmealtable.infrastructure.dto.TokenDto; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import com.stcom.smartmealtable.repository.MemberRepository; +import com.stcom.smartmealtable.repository.SocialAccountRepository; +import com.stcom.smartmealtable.service.dto.AuthResultDto; +import java.time.LocalDateTime; +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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class LoginServiceAdditionalIntegrationTest { + + @Autowired + private LoginService loginService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MemberProfileRepository memberProfileRepository; + + @Autowired + private SocialAccountRepository socialAccountRepository; + + private Member member; + + @BeforeEach + void setUp() { + member = Member.builder() + .email("login_additional@example.com") + .rawPassword("password123!") + .build(); + memberRepository.save(member); + } + + @Test + @DisplayName("존재하지 않는 이메일로 로그인 시도하면 예외가 발생한다") + void loginWithNonExistentEmail() { + // given + String nonExistentEmail = "nonexistent@example.com"; + String password = "password123!"; + + // when & then + assertThatThrownBy(() -> loginService.loginWithEmail(nonExistentEmail, password)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원입니다."); + } + + @Test + @DisplayName("잘못된 비밀번호로 로그인 시도하면 예외가 발생한다") + void loginWithIncorrectPassword() { + // given + String incorrectPassword = "wrongPassword123!"; + + // when & then + assertThatThrownBy(() -> loginService.loginWithEmail(member.getEmail(), incorrectPassword)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("비밀번호가 일치하지 않습니다"); + } + + @Test + @DisplayName("프로필이 등록된 회원이 로그인하면 newUser 플래그가 false이다") + void loginWithRegisteredProfile() throws Exception { + // given + MemberProfile profile = MemberProfile.builder() + .member(member) + .nickName("테스터") + .type(MemberType.STUDENT) + .build(); + memberProfileRepository.save(profile); + + // when + AuthResultDto result = loginService.loginWithEmail(member.getEmail(), "password123!"); + + // then + assertThat(result.isNewUser()).isFalse(); + assertThat(result.getProfileId()).isEqualTo(profile.getId()); + } + + @Test + @DisplayName("기존 소셜계정으로 다시 로그인하면 토큰이 갱신된다") + void socialLoginWithExistingSocialAccount() { + // given + // 기존 소셜 계정 생성 + SocialAccount existingAccount = SocialAccount.builder() + .member(member) + .provider("google") + .providerUserId("google123") + .accessToken("old-token") + .refreshToken("old-refresh") + .tokenType("Bearer") + .tokenExpiresAt(LocalDateTime.now().plusHours(1)) + .build(); + socialAccountRepository.save(existingAccount); + + // 프로필 생성 + MemberProfile profile = MemberProfile.builder() + .member(member) + .nickName("소셜유저") + .type(MemberType.STUDENT) + .build(); + memberProfileRepository.save(profile); + + // 새로운 토큰 정보 + TokenDto newToken = TokenDto.builder() + .accessToken("new-token") + .refreshToken("new-refresh") + .tokenType("Bearer") + .provider("google") + .providerUserId("google123") + .expiresIn(3600) + .email(member.getEmail()) + .build(); + + // when + AuthResultDto result = loginService.socialLogin(newToken); + + // then + assertThat(result.isNewUser()).isFalse(); + assertThat(result.getProfileId()).isEqualTo(profile.getId()); + + // 토큰 업데이트 확인 + SocialAccount updatedAccount = socialAccountRepository.findByProviderAndProviderUserId( + "google", "google123").orElseThrow(); + assertThat(updatedAccount.getAccessToken()).isEqualTo("new-token"); + assertThat(updatedAccount.getRefreshToken()).isEqualTo("new-refresh"); + } + + @Test + @DisplayName("새로운 소셜계정으로 로그인하면 계정이 생성된다") + void socialLoginWithNewAccount() { + // given + TokenDto newToken = TokenDto.builder() + .accessToken("brand-new-token") + .refreshToken("brand-new-refresh") + .tokenType("Bearer") + .provider("kakao") + .providerUserId("kakao123") + .expiresIn(3600) + .email("new_social@example.com") + .build(); + + // when + AuthResultDto result = loginService.socialLogin(newToken); + + // then + assertThat(result.isNewUser()).isTrue(); + + // 새 계정 확인 + SocialAccount newAccount = socialAccountRepository.findByProviderAndProviderUserId( + "kakao", "kakao123").orElseThrow(); + assertThat(newAccount.getAccessToken()).isEqualTo("brand-new-token"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/LoginServiceCompleteIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/LoginServiceCompleteIntegrationTest.java new file mode 100644 index 0000000..603a082 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/LoginServiceCompleteIntegrationTest.java @@ -0,0 +1,253 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.domain.member.MemberType; +import com.stcom.smartmealtable.domain.social.SocialAccount; +import com.stcom.smartmealtable.infrastructure.dto.TokenDto; +import com.stcom.smartmealtable.repository.MemberRepository; +import com.stcom.smartmealtable.repository.SocialAccountRepository; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import com.stcom.smartmealtable.service.dto.AuthResultDto; +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class LoginServiceCompleteIntegrationTest { + + @Autowired + private LoginService loginService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private SocialAccountRepository socialAccountRepository; + + @Autowired + private MemberProfileRepository memberProfileRepository; + + private Member testMember; + private Member memberWithProfile; + private SocialAccount existingSocialAccount; + + @BeforeEach + void setUp() { + // 기본 회원 생성 + testMember = Member.builder() + .email("test@example.com") + .rawPassword("TestPassword123!") + .build(); + memberRepository.save(testMember); + + // 프로필이 있는 회원 생성 + memberWithProfile = Member.builder() + .email("profile@example.com") + .rawPassword("TestPassword123!") + .build(); + memberRepository.save(memberWithProfile); + + // 프로필 생성 (linkMember가 memberProfile과 member를 연결함) + MemberProfile profile = MemberProfile.builder() + .nickName("TestUser") + .member(memberWithProfile) + .type(MemberType.STUDENT) + .build(); + + // 기존 소셜 계정 생성 + existingSocialAccount = SocialAccount.builder() + .member(testMember) + .provider("kakao") + .providerUserId("kakao123") + .tokenType("Bearer") + .accessToken("existing_access_token") + .refreshToken("existing_refresh_token") + .tokenExpiresAt(LocalDateTime.now().plusHours(1)) + .build(); + socialAccountRepository.save(existingSocialAccount); + + memberRepository.save(memberWithProfile); + memberProfileRepository.save(profile); + } + + @Test + @DisplayName("이메일과 비밀번호로 로그인할 수 있다") + void loginWithEmail() throws PasswordFailedExceededException { + // when + AuthResultDto result = loginService.loginWithEmail("test@example.com", "TestPassword123!"); + + // then + assertThat(result.getMemberId()).isEqualTo(testMember.getId()); + assertThat(result.getProfileId()).isNull(); + assertThat(result.isNewUser()).isTrue(); + } + + @Test + @DisplayName("프로필이 있는 회원으로 로그인하면 프로필 ID를 반환한다") + void loginWithEmailWithProfile() throws PasswordFailedExceededException { + // when + AuthResultDto result = loginService.loginWithEmail("profile@example.com", "TestPassword123!"); + + // then + assertThat(result.getMemberId()).isEqualTo(memberWithProfile.getId()); + assertThat(result.getProfileId()).isEqualTo(memberWithProfile.getMemberProfile().getId()); + assertThat(result.isNewUser()).isFalse(); + } + + @Test + @DisplayName("존재하지 않는 이메일로 로그인하면 예외가 발생한다") + void loginWithNonExistentEmail() { + // when & then + assertThatThrownBy(() -> loginService.loginWithEmail("nonexistent@example.com", "password")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원입니다."); + } + + @Test + @DisplayName("잘못된 비밀번호로 로그인하면 예외가 발생한다") + void loginWithWrongPassword() { + // when & then + assertThatThrownBy(() -> loginService.loginWithEmail("test@example.com", "WrongPassword")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("비밀번호가 일치하지 않습니다"); + } + + @Test + @DisplayName("기존 회원의 소셜 로그인을 할 수 있다") + void socialLoginExistingMember() { + // given + TokenDto tokenDto = TokenDto.builder() + .accessToken("new_access_token") + .refreshToken("new_refresh_token") + .expiresIn(3600) + .tokenType("Bearer") + .provider("kakao") + .providerUserId("kakao123") + .email("test@example.com") + .build(); + + // when + AuthResultDto result = loginService.socialLogin(tokenDto); + + // then + assertThat(result.getMemberId()).isEqualTo(testMember.getId()); + assertThat(result.getProfileId()).isNull(); + assertThat(result.isNewUser()).isTrue(); + + // 토큰이 업데이트되었는지 확인 + SocialAccount updatedAccount = socialAccountRepository.findByProviderAndProviderUserId( + "kakao", "kakao123").orElseThrow(); + assertThat(updatedAccount.getAccessToken()).isEqualTo("new_access_token"); + assertThat(updatedAccount.getRefreshToken()).isEqualTo("new_refresh_token"); + } + + @Test + @DisplayName("새로운 회원의 소셜 로그인을 할 수 있다") + void socialLoginNewMember() { + // given + TokenDto tokenDto = TokenDto.builder() + .accessToken("google_access_token") + .refreshToken("google_refresh_token") + .expiresIn(3600) + .tokenType("Bearer") + .provider("google") + .providerUserId("google123") + .email("newuser@example.com") + .build(); + + // when + AuthResultDto result = loginService.socialLogin(tokenDto); + + // then + assertThat(result.getMemberId()).isNotNull(); + assertThat(result.getProfileId()).isNull(); + assertThat(result.isNewUser()).isTrue(); + + // 새로운 회원이 생성되었는지 확인 + Member newMember = memberRepository.findByEmail("newuser@example.com").orElseThrow(); + assertThat(newMember.getEmail()).isEqualTo("newuser@example.com"); + + // 소셜 계정이 생성되었는지 확인 + SocialAccount socialAccount = socialAccountRepository.findByProviderAndProviderUserId( + "google", "google123").orElseThrow(); + assertThat(socialAccount.getMember().getId()).isEqualTo(newMember.getId()); + } + + @Test + @DisplayName("기존 소셜 계정이 없는 경우 새로 생성한다") + void socialLoginCreateNewSocialAccount() { + // given + TokenDto tokenDto = TokenDto.builder() + .accessToken("naver_access_token") + .refreshToken("naver_refresh_token") + .expiresIn(3600) + .tokenType("Bearer") + .provider("naver") + .providerUserId("naver456") + .email("test@example.com") + .build(); + + // when + AuthResultDto result = loginService.socialLogin(tokenDto); + + // then + assertThat(result.getMemberId()).isEqualTo(testMember.getId()); + assertThat(result.getProfileId()).isNull(); + assertThat(result.isNewUser()).isTrue(); + + // 새로운 소셜 계정이 생성되었는지 확인 + SocialAccount naverAccount = socialAccountRepository.findByProviderAndProviderUserId( + "naver", "naver456").orElseThrow(); + assertThat(naverAccount.getMember().getId()).isEqualTo(testMember.getId()); + assertThat(naverAccount.getProvider()).isEqualTo("naver"); + assertThat(naverAccount.getProviderUserId()).isEqualTo("naver456"); + } + + @Test + @DisplayName("프로필이 있는 회원의 소셜 로그인은 프로필 ID를 반환한다") + void socialLoginMemberWithProfile() { + // given + // memberWithProfile에 소셜 계정을 먼저 연결 + SocialAccount googleAccount = SocialAccount.builder() + .member(memberWithProfile) + .provider("google") + .providerUserId("google789") + .tokenType("Bearer") + .accessToken("google_access_token") + .refreshToken("google_refresh_token") + .tokenExpiresAt(LocalDateTime.now().plusHours(1)) + .build(); + socialAccountRepository.save(googleAccount); + + TokenDto tokenDto = TokenDto.builder() + .accessToken("updated_google_access_token") + .refreshToken("updated_google_refresh_token") + .expiresIn(3600) + .tokenType("Bearer") + .provider("google") + .providerUserId("google789") + .email("profile@example.com") + .build(); + + // when + AuthResultDto result = loginService.socialLogin(tokenDto); + + // then + assertThat(result.getMemberId()).isEqualTo(memberWithProfile.getId()); + assertThat(result.getProfileId()).isEqualTo(memberWithProfile.getMemberProfile().getId()); + assertThat(result.isNewUser()).isFalse(); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/LoginServiceIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/LoginServiceIntegrationTest.java new file mode 100644 index 0000000..841a9e1 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/LoginServiceIntegrationTest.java @@ -0,0 +1,70 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +import com.stcom.smartmealtable.infrastructure.dto.TokenDto; +import com.stcom.smartmealtable.service.dto.AuthResultDto; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +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.test.context.ActiveProfiles; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class LoginServiceIntegrationTest { + + @Autowired + private LoginService loginService; + + @Autowired + private MemberService memberService; + + @Test + @DisplayName("이메일/비밀번호 로그인 성공 후 AuthResultDto 가 반환되어야 한다") + @Rollback + void loginWithEmail() throws Exception { + // given + Member member = Member.builder() + .fullName("로그인유저") + .email("login@test.com") + .rawPassword("Password1!") + .build(); + memberService.saveMember(member); + + // when + AuthResultDto dto = loginService.loginWithEmail("login@test.com", "Password1!"); + + // then + assertThat(dto.getMemberId()).isEqualTo(member.getId()); + assertThat(dto.isNewUser()).isTrue(); + } + + @Test + @DisplayName("소셜 로그인 시 신규 회원이면 newUser 플래그가 true 여야 한다") + @Rollback + void socialLogin() { + // given + TokenDto token = TokenDto.builder() + .accessToken("token") + .refreshToken("refresh") + .tokenType("Bearer") + .provider("google") + .providerUserId("g123") + .expiresIn(3600) + .email("social@test.com") + .build(); + + // when + AuthResultDto dto = loginService.socialLogin(token); + + // then + assertThat(dto.isNewUser()).isTrue(); + assertThat(dto.getMemberId()).isNotNull(); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/LoginServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/LoginServiceTest.java deleted file mode 100644 index 86b3858..0000000 --- a/src/test/java/com/stcom/smartmealtable/service/LoginServiceTest.java +++ /dev/null @@ -1,238 +0,0 @@ -package com.stcom.smartmealtable.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.stcom.smartmealtable.domain.member.Member; -import com.stcom.smartmealtable.domain.member.MemberProfile; -import com.stcom.smartmealtable.domain.social.SocialAccount; -import com.stcom.smartmealtable.exception.PasswordPolicyException; -import com.stcom.smartmealtable.exception.PasswordFailedExceededException; -import com.stcom.smartmealtable.infrastructure.dto.TokenDto; -import com.stcom.smartmealtable.repository.MemberRepository; -import com.stcom.smartmealtable.repository.SocialAccountRepository; -import com.stcom.smartmealtable.service.dto.AuthResultDto; -import java.time.LocalDateTime; -import java.util.Optional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; - -@ExtendWith(MockitoExtension.class) -class LoginServiceTest { - - @Mock - private MemberRepository memberRepository; - - @Mock - private SocialAccountRepository socialAccountRepository; - - @InjectMocks - private LoginService loginService; - - @Captor - private ArgumentCaptor memberCaptor; - - @Captor - private ArgumentCaptor socialAccountCaptor; - - @Test - @DisplayName("이메일과 비밀번호로 로그인이 가능해야 한다") - void loginWithEmail() throws PasswordFailedExceededException, PasswordPolicyException { - // given - String email = "test@example.com"; - String password = "Password123!"; - - Member member = createMember(1L, email, password); - MemberProfile profile = mock(MemberProfile.class); - when(profile.getId()).thenReturn(10L); - - // 프로필이 등록되어 있지 않은 경우 - ReflectionTestUtils.setField(member, "memberProfile", profile); - - when(memberRepository.findByEmail(email)).thenReturn(Optional.of(member)); - - // when - AuthResultDto result = loginService.loginWithEmail(email, password); - - // then - assertThat(result.getMemberId()).isEqualTo(1L); - assertThat(result.getProfileId()).isEqualTo(10L); - assertThat(result.isNewUser()).isFalse(); // 프로필이 있으므로 새 사용자가 아님 - - verify(memberRepository, times(1)).findByEmail(email); - } - - @Test - @DisplayName("프로필이 없는 경우 신규 사용자로 처리해야 한다") - void loginWithEmailNewUser() throws PasswordFailedExceededException, PasswordPolicyException { - // given - String email = "new@example.com"; - String password = "Password123!"; - - Member member = createMember(2L, email, password); - // 프로필이 등록되어 있지 않음 - - when(memberRepository.findByEmail(email)).thenReturn(Optional.of(member)); - - // when - AuthResultDto result = loginService.loginWithEmail(email, password); - - // then - assertThat(result.getMemberId()).isEqualTo(2L); - assertThat(result.getProfileId()).isNull(); - assertThat(result.isNewUser()).isTrue(); // 프로필이 없으므로 신규 사용자 - } - - @Test - @DisplayName("존재하지 않는 이메일로 로그인 시도 시 예외가 발생해야 한다") - void loginWithNonExistingEmail() { - // given - String email = "nonexisting@example.com"; - String password = "Password123!"; - - when(memberRepository.findByEmail(email)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> loginService.loginWithEmail(email, password)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("존재하지 않는 회원입니다."); - } - - @Test - @DisplayName("잘못된 비밀번호로 로그인 시도 시 예외가 발생해야 한다") - void loginWithWrongPassword() throws PasswordPolicyException { - // given - String email = "test@example.com"; - String correctPassword = "Password123!"; - String wrongPassword = "WrongPassword123!"; - - Member member = createMember(1L, email, correctPassword); - - when(memberRepository.findByEmail(email)).thenReturn(Optional.of(member)); - - // when & then - assertThatThrownBy(() -> loginService.loginWithEmail(email, wrongPassword)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("비밀번호가 일치하지 않습니다"); - } - - @Test - @DisplayName("소셜 로그인 - 기존 회원인 경우") - void socialLoginExistingMember() throws PasswordPolicyException { - // given - String email = "test@example.com"; - String provider = "KAKAO"; - String providerUserId = "12345"; - - TokenDto tokenDto = createTokenDto(email, provider, providerUserId); - Member member = createMember(1L, email, null); - - SocialAccount existingSocialAccount = SocialAccount.builder() - .member(member) - .provider(provider) - .providerUserId(providerUserId) - .accessToken("old-token") - .refreshToken("old-refresh-token") - .tokenExpiresAt(LocalDateTime.now()) - .build(); - - when(memberRepository.findByEmail(email)).thenReturn(Optional.of(member)); - when(socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId)) - .thenReturn(Optional.of(existingSocialAccount)); - when(socialAccountRepository.findProfileIdByProviderAndProviderUserId(provider, providerUserId)) - .thenReturn(Optional.of(10L)); - - // when - AuthResultDto result = loginService.socialLogin(tokenDto); - - // then - assertThat(result.getMemberId()).isEqualTo(1L); - assertThat(result.getProfileId()).isEqualTo(10L); - assertThat(result.isNewUser()).isFalse(); - - assertThat(existingSocialAccount.getAccessToken()).isEqualTo("access-token-value"); - assertThat(existingSocialAccount.getRefreshToken()).isEqualTo("refresh-token-value"); - } - - @Test - @DisplayName("소셜 로그인 - 신규 회원인 경우") - void socialLoginNewMember() throws PasswordPolicyException { - // given - String email = "new@example.com"; - String provider = "GOOGLE"; - String providerUserId = "67890"; - - TokenDto tokenDto = createTokenDto(email, provider, providerUserId); - Member newMember = createMember(2L, email, null); - - when(memberRepository.findByEmail(email)).thenReturn(Optional.empty()); - when(memberRepository.save(any())).thenReturn(newMember); - when(socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId)) - .thenReturn(Optional.empty()); - when(socialAccountRepository.save(any())).thenAnswer(invocation -> { - SocialAccount account = invocation.getArgument(0); - ReflectionTestUtils.setField(account, "id", 1L); - return account; - }); - when(socialAccountRepository.findProfileIdByProviderAndProviderUserId(provider, providerUserId)) - .thenReturn(Optional.empty()); - - // when - AuthResultDto result = loginService.socialLogin(tokenDto); - - // then - verify(memberRepository, times(1)).save(memberCaptor.capture()); - verify(socialAccountRepository, times(1)).save(socialAccountCaptor.capture()); - - Member savedMember = memberCaptor.getValue(); - SocialAccount savedAccount = socialAccountCaptor.getValue(); - - assertThat(savedMember.getEmail()).isEqualTo(email); - assertThat(savedAccount.getProvider()).isEqualTo(provider); - assertThat(savedAccount.getProviderUserId()).isEqualTo(providerUserId); - - assertThat(result.getMemberId()).isEqualTo(2L); - assertThat(result.getProfileId()).isNull(); - assertThat(result.isNewUser()).isTrue(); - } - - private Member createMember(Long id, String email, String rawPassword) throws PasswordPolicyException { - Member member; - if (rawPassword != null) { - member = Member.builder() - .email(email) - .rawPassword(rawPassword) - .build(); - } else { - member = new Member(email); - } - ReflectionTestUtils.setField(member, "id", id); - return member; - } - - private TokenDto createTokenDto(String email, String provider, String providerUserId) { - TokenDto tokenDto = new TokenDto(); - - tokenDto.setEmail(email); - tokenDto.setProvider(provider); - tokenDto.setProviderUserId(providerUserId); - tokenDto.setTokenType("Bearer"); - tokenDto.setAccessToken("access-token-value"); - tokenDto.setRefreshToken("refresh-token-value"); - tokenDto.setExpiresIn(3600); - - return tokenDto; - } -} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceServiceAdditionalIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceServiceAdditionalIntegrationTest.java new file mode 100644 index 0000000..1ea2aac --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceServiceAdditionalIntegrationTest.java @@ -0,0 +1,208 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.domain.food.FoodCategory; +import com.stcom.smartmealtable.domain.food.MemberCategoryPreference; +import com.stcom.smartmealtable.domain.food.PreferenceType; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.domain.member.MemberType; +import com.stcom.smartmealtable.repository.FoodCategoryRepository; +import com.stcom.smartmealtable.repository.MemberCategoryPreferenceRepository; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import com.stcom.smartmealtable.repository.MemberRepository; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class MemberCategoryPreferenceServiceAdditionalIntegrationTest { + + @Autowired + private MemberCategoryPreferenceService preferenceService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MemberProfileRepository profileRepository; + + @Autowired + private FoodCategoryRepository categoryRepository; + + @Autowired + private MemberCategoryPreferenceRepository preferenceRepository; + + private Member member; + private MemberProfile profile; + private FoodCategory category1; + private FoodCategory category2; + private FoodCategory category3; + + @BeforeEach + void setUp() { + member = Member.builder() + .email("preference_test@example.com") + .rawPassword("password123!") + .build(); + memberRepository.save(member); + + profile = MemberProfile.builder() + .member(member) + .nickName("취향테스터") + .type(MemberType.STUDENT) + .build(); + profileRepository.save(profile); + + category1 = new FoodCategory(); + ReflectionTestUtils.setField(category1, "name", "한식"); + categoryRepository.save(category1); + + category2 = new FoodCategory(); + ReflectionTestUtils.setField(category2, "name", "중식"); + categoryRepository.save(category2); + + category3 = new FoodCategory(); + ReflectionTestUtils.setField(category3, "name", "일식"); + categoryRepository.save(category3); + } + + @Test + @DisplayName("선호와 비선호 카테고리를 동시에 저장하고 조회할 수 있다") + void saveBothLikedAndDislikedPreferences() { + // given + List liked = Arrays.asList(category1.getId(), category2.getId()); + List disliked = Collections.singletonList(category3.getId()); + + // when + preferenceService.savePreferences(profile.getId(), liked, disliked); + List preferences = preferenceService.getPreferences(profile.getId()); + + // then + assertThat(preferences).hasSize(3); + + // 좋아하는 카테고리 + List likedPrefs = preferences.stream() + .filter(p -> p.getType() == PreferenceType.LIKE) + .toList(); + assertThat(likedPrefs).hasSize(2); + assertThat(likedPrefs.get(0).getCategory().getName()).isEqualTo("한식"); + assertThat(likedPrefs.get(1).getCategory().getName()).isEqualTo("중식"); + + // 싫어하는 카테고리 + List dislikedPrefs = preferences.stream() + .filter(p -> p.getType() == PreferenceType.DISLIKE) + .toList(); + assertThat(dislikedPrefs).hasSize(1); + assertThat(dislikedPrefs.get(0).getCategory().getName()).isEqualTo("일식"); + } + + @Test + @DisplayName("존재하지 않는 프로필 ID로 선호도를 저장하면 예외가 발생한다") + void savePreferencesWithInvalidProfileId() { + // given + Long invalidProfileId = 9999L; + List liked = Collections.singletonList(category1.getId()); + List disliked = Collections.emptyList(); + + // when & then + assertThatThrownBy(() -> preferenceService.savePreferences(invalidProfileId, liked, disliked)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필입니다"); + } + + @Test + @DisplayName("존재하지 않는 카테고리 ID로 선호도를 저장하면 예외가 발생한다") + void savePreferencesWithInvalidCategoryId() { + // given + Long invalidCategoryId = 9999L; + List liked = Collections.singletonList(invalidCategoryId); + List disliked = Collections.emptyList(); + + // when & then + assertThatThrownBy(() -> preferenceService.savePreferences(profile.getId(), liked, disliked)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("존재하지 않는 카테고리입니다"); + } + + @Test + @DisplayName("선호도를 업데이트하면 기존 선호도가 모두 삭제되고 새로운 선호도가 저장된다") + void updatePreferences() { + // given + // 초기 선호도 설정 + preferenceService.savePreferences( + profile.getId(), + Collections.singletonList(category1.getId()), + Collections.singletonList(category2.getId()) + ); + + // 초기 상태 확인 + List initialPrefs = preferenceService.getPreferences(profile.getId()); + assertThat(initialPrefs).hasSize(2); + + // when + // 선호도 업데이트 - 완전히 반대로 변경 + preferenceService.savePreferences( + profile.getId(), + Collections.singletonList(category2.getId()), + Collections.singletonList(category1.getId()) + ); + + // then + List updatedPrefs = preferenceService.getPreferences(profile.getId()); + assertThat(updatedPrefs).hasSize(2); + + // 변경된 선호도 확인 + MemberCategoryPreference likedPref = updatedPrefs.stream() + .filter(p -> p.getType() == PreferenceType.LIKE) + .findFirst() + .orElseThrow(); + assertThat(likedPref.getCategory().getId()).isEqualTo(category2.getId()); + + MemberCategoryPreference dislikedPref = updatedPrefs.stream() + .filter(p -> p.getType() == PreferenceType.DISLIKE) + .findFirst() + .orElseThrow(); + assertThat(dislikedPref.getCategory().getId()).isEqualTo(category1.getId()); + } + + @Test + @DisplayName("선호도를 비우면 기존 선호도가 모두 삭제된다") + void clearPreferences() { + // given + // 초기 선호도 설정 + preferenceService.savePreferences( + profile.getId(), + Arrays.asList(category1.getId(), category2.getId()), + Collections.singletonList(category3.getId()) + ); + + // 초기 상태 확인 + List initialPrefs = preferenceService.getPreferences(profile.getId()); + assertThat(initialPrefs).hasSize(3); + + // when + // 선호도 비우기 + preferenceService.savePreferences( + profile.getId(), + Collections.emptyList(), + Collections.emptyList() + ); + + // then + List updatedPrefs = preferenceService.getPreferences(profile.getId()); + assertThat(updatedPrefs).isEmpty(); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceServiceIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceServiceIntegrationTest.java new file mode 100644 index 0000000..4521af9 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceServiceIntegrationTest.java @@ -0,0 +1,77 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.food.FoodCategory; +import com.stcom.smartmealtable.domain.food.MemberCategoryPreference; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.domain.member.MemberType; +import com.stcom.smartmealtable.repository.FoodCategoryRepository; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import com.stcom.smartmealtable.repository.MemberRepository; +import java.util.List; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +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.test.context.ActiveProfiles; +import org.springframework.test.util.ReflectionTestUtils; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class MemberCategoryPreferenceServiceIntegrationTest { + + @Autowired + private MemberCategoryPreferenceService preferenceService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MemberProfileRepository profileRepository; + + @Autowired + private FoodCategoryRepository categoryRepository; + + @Test + @DisplayName("선호/비선호 음식 카테고리를 저장하고 조회할 수 있다") + @Rollback + void saveAndGetPreferences() throws Exception { + // given 회원 & 프로필 & 카테고리 + Member member = Member.builder() + .fullName("음식왕") + .email("food@test.com") + .rawPassword("Password1!") + .build(); + memberRepository.save(member); + + MemberProfile profile = MemberProfile.builder() + .member(member) + .nickName("먹짱") + .type(MemberType.STUDENT) + .group(null) + .build(); + profileRepository.save(profile); + + FoodCategory catLike = new FoodCategory(); + ReflectionTestUtils.setField(catLike, "name", "한식"); + categoryRepository.save(catLike); + + FoodCategory catDislike = new FoodCategory(); + ReflectionTestUtils.setField(catDislike, "name", "멕시칸"); + categoryRepository.save(catDislike); + + // when + preferenceService.savePreferences(profile.getId(), List.of(catLike.getId()), List.of(catDislike.getId())); + List prefs = preferenceService.getPreferences(profile.getId()); + + // then + assertThat(prefs).hasSize(2); + assertThat(prefs.get(0).getType().name()).isEqualTo("LIKE"); + assertThat(prefs.get(1).getType().name()).isEqualTo("DISLIKE"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceServiceTest.java deleted file mode 100644 index 10f852a..0000000 --- a/src/test/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceServiceTest.java +++ /dev/null @@ -1,186 +0,0 @@ -package com.stcom.smartmealtable.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.stcom.smartmealtable.domain.food.FoodCategory; -import com.stcom.smartmealtable.domain.food.MemberCategoryPreference; -import com.stcom.smartmealtable.domain.food.PreferenceType; -import com.stcom.smartmealtable.domain.member.MemberProfile; -import com.stcom.smartmealtable.repository.FoodCategoryRepository; -import com.stcom.smartmealtable.repository.MemberCategoryPreferenceRepository; -import com.stcom.smartmealtable.repository.MemberProfileRepository; -import java.util.Arrays; -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.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; - -@ExtendWith(MockitoExtension.class) -class MemberCategoryPreferenceServiceTest { - - @Mock - private MemberCategoryPreferenceRepository preferenceRepository; - - @Mock - private FoodCategoryRepository categoryRepository; - - @Mock - private MemberProfileRepository profileRepository; - - @InjectMocks - private MemberCategoryPreferenceService preferenceService; - - @Captor - private ArgumentCaptor preferenceCaptor; - - private MemberProfile profile; - private FoodCategory koreanFood; - private FoodCategory westernFood; - private FoodCategory japaneseFood; - private FoodCategory chineseFood; - - @BeforeEach - void setUp() { - // 테스트 데이터 셋업 - profile = new MemberProfile(); - ReflectionTestUtils.setField(profile, "id", 1L); - - koreanFood = createFoodCategory(1L, "한식"); - westernFood = createFoodCategory(2L, "양식"); - japaneseFood = createFoodCategory(3L, "일식"); - chineseFood = createFoodCategory(4L, "중식"); - } - - @Test - @DisplayName("회원 음식 선호도를 저장할 수 있어야 한다") - void savePreferences() { - // given - Long profileId = 1L; - List liked = Arrays.asList(1L, 2L); // 한식, 양식 선호 - List disliked = Arrays.asList(3L); // 일식 비선호 - - when(profileRepository.findById(profileId)).thenReturn(Optional.of(profile)); - when(categoryRepository.findById(1L)).thenReturn(Optional.of(koreanFood)); - when(categoryRepository.findById(2L)).thenReturn(Optional.of(westernFood)); - when(categoryRepository.findById(3L)).thenReturn(Optional.of(japaneseFood)); - doNothing().when(preferenceRepository).deleteByMemberProfile_Id(profileId); - when(preferenceRepository.save(any(MemberCategoryPreference.class))).thenAnswer(invocation -> invocation.getArgument(0)); - - // when - preferenceService.savePreferences(profileId, liked, disliked); - - // then - verify(profileRepository, times(1)).findById(profileId); - verify(preferenceRepository, times(1)).deleteByMemberProfile_Id(profileId); - verify(categoryRepository, times(3)).findById(anyLong()); - verify(preferenceRepository, times(3)).save(preferenceCaptor.capture()); - - List savedPreferences = preferenceCaptor.getAllValues(); - assertThat(savedPreferences).hasSize(3); - - // 선호 음식 검증 - assertThat(savedPreferences.get(0).getType()).isEqualTo(PreferenceType.LIKE); - assertThat(savedPreferences.get(0).getCategory().getName()).isEqualTo("한식"); - assertThat(savedPreferences.get(0).getPriority()).isEqualTo(1); - - assertThat(savedPreferences.get(1).getType()).isEqualTo(PreferenceType.LIKE); - assertThat(savedPreferences.get(1).getCategory().getName()).isEqualTo("양식"); - assertThat(savedPreferences.get(1).getPriority()).isEqualTo(2); - - // 비선호 음식 검증 - assertThat(savedPreferences.get(2).getType()).isEqualTo(PreferenceType.DISLIKE); - assertThat(savedPreferences.get(2).getCategory().getName()).isEqualTo("일식"); - assertThat(savedPreferences.get(2).getPriority()).isEqualTo(1); - } - - @Test - @DisplayName("존재하지 않는 프로필로 음식 선호도 저장시 예외가 발생해야 한다") - void savePreferencesWithNonExistingProfile() { - // given - Long profileId = 999L; - List liked = Arrays.asList(1L); - List disliked = List.of(); - - when(profileRepository.findById(profileId)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> preferenceService.savePreferences(profileId, liked, disliked)) - .isInstanceOf(IllegalStateException.class) - .hasMessage("존재하지 않는 프로필입니다"); - } - - @Test - @DisplayName("존재하지 않는 카테고리로 음식 선호도 저장시 예외가 발생해야 한다") - void savePreferencesWithNonExistingCategory() { - // given - Long profileId = 1L; - List liked = Arrays.asList(999L); - List disliked = List.of(); - - when(profileRepository.findById(profileId)).thenReturn(Optional.of(profile)); - when(categoryRepository.findById(999L)).thenReturn(Optional.empty()); - doNothing().when(preferenceRepository).deleteByMemberProfile_Id(profileId); - - // when & then - assertThatThrownBy(() -> preferenceService.savePreferences(profileId, liked, disliked)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("존재하지 않는 카테고리입니다"); - } - - @Test - @DisplayName("프로필 ID로 음식 선호도를 조회할 수 있어야 한다") - void getPreferences() { - // given - Long profileId = 1L; - MemberCategoryPreference pref1 = createPreference(profile, koreanFood, PreferenceType.LIKE, 1); - MemberCategoryPreference pref2 = createPreference(profile, westernFood, PreferenceType.LIKE, 2); - MemberCategoryPreference pref3 = createPreference(profile, japaneseFood, PreferenceType.DISLIKE, 1); - - List expectedPreferences = Arrays.asList(pref1, pref2, pref3); - - when(preferenceRepository.findDefaultByMemberProfileId(profileId)).thenReturn(expectedPreferences); - - // when - List foundPreferences = preferenceService.getPreferences(profileId); - - // then - assertThat(foundPreferences).hasSize(3); - assertThat(foundPreferences).isEqualTo(expectedPreferences); - - verify(preferenceRepository, times(1)).findDefaultByMemberProfileId(profileId); - } - - private FoodCategory createFoodCategory(Long id, String name) { - FoodCategory foodCategory = new FoodCategory(); - ReflectionTestUtils.setField(foodCategory, "id", id); - ReflectionTestUtils.setField(foodCategory, "name", name); - return foodCategory; - } - - private MemberCategoryPreference createPreference(MemberProfile profile, FoodCategory category, - PreferenceType type, Integer priority) { - MemberCategoryPreference preference = MemberCategoryPreference.builder() - .memberProfile(profile) - .category(category) - .type(type) - .priority(priority) - .build(); - ReflectionTestUtils.setField(preference, "id", Long.valueOf(priority)); - return preference; - } -} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceAdditionalIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceAdditionalIntegrationTest.java new file mode 100644 index 0000000..3de3269 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceAdditionalIntegrationTest.java @@ -0,0 +1,79 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.Address.AddressType; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.domain.member.MemberType; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import com.stcom.smartmealtable.repository.MemberRepository; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class MemberProfileServiceAdditionalIntegrationTest { + + @Autowired + private MemberProfileService service; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MemberProfileRepository profileRepository; + + @Test + @DisplayName("프로필 닉네임/타입/그룹 변경이 동작해야 한다") + void changeProfile() { + Member member = new Member("cp@test.com"); + memberRepository.save(member); + MemberProfile profile = MemberProfile.builder() + .member(member) + .nickName("old") + .type(MemberType.STUDENT) + .group(null) + .build(); + profileRepository.save(profile); + + // when + service.changeProfile(profile.getId(), "newNick", MemberType.WORKER, null); + + // then + MemberProfile updated = profileRepository.findById(profile.getId()).orElseThrow(); + assertThat(updated.getNickName()).isEqualTo("newNick"); + assertThat(updated.getType()).isEqualTo(MemberType.WORKER); + } + + @Test + @DisplayName("주소 삭제가 동작해야 한다") + void deleteAddress() { + Member member = new Member("addr@test.com"); + memberRepository.save(member); + MemberProfile profile = MemberProfile.builder() + .member(member) + .nickName("a") + .type(MemberType.STUDENT) + .group(null) + .build(); + profileRepository.save(profile); + + Address address = Address.builder().roadAddress("road").detailAddress("d").build(); + service.saveNewAddress(profile.getId(), address, "집", AddressType.HOME); + Long addressId = profileRepository.findById(profile.getId()).orElseThrow().getAddressHistory().get(0).getId(); + + // when + service.deleteAddress(profile.getId(), addressId); + + // then + MemberProfile after = profileRepository.findById(profile.getId()).orElseThrow(); + assertThat(after.getAddressHistory()).isEmpty(); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceAdditionalIntegrationTest2.java b/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceAdditionalIntegrationTest2.java new file mode 100644 index 0000000..97c93b1 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceAdditionalIntegrationTest2.java @@ -0,0 +1,229 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.Address.AddressEntity; +import com.stcom.smartmealtable.domain.Address.AddressType; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.domain.member.MemberType; +import com.stcom.smartmealtable.repository.AddressEntityRepository; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import com.stcom.smartmealtable.repository.MemberRepository; +import jakarta.transaction.Transactional; +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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class MemberProfileServiceAdditionalIntegrationTest2 { + + @Autowired + private MemberProfileService service; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MemberProfileRepository profileRepository; + + @Autowired + private AddressEntityRepository addressEntityRepository; + + private Member member; + private MemberProfile profile; + private AddressEntity addressEntity; + + @BeforeEach + void setUp() { + // 테스트용 회원 생성 + member = Member.builder() + .email("profile_additional2@example.com") + .rawPassword("password123!") + .build(); + memberRepository.save(member); + + // 프로필 생성 + profile = MemberProfile.builder() + .member(member) + .nickName("프로필유저") + .type(MemberType.STUDENT) + .build(); + profileRepository.save(profile); + + // 주소 생성 및 추가 + Address address = Address.builder() + .roadAddress("서울시 강남구") + .detailAddress("123번지") + .build(); + + service.saveNewAddress(profile.getId(), address, "집", AddressType.HOME); + + // 저장된 주소 조회 + profile = profileRepository.findById(profile.getId()).orElseThrow(); + addressEntity = profile.getAddressHistory().get(0); + } + + @Test + @DisplayName("주소 정보를 수정할 수 있다") + void changeAddress() { + // given + Address newAddress = Address.builder() + .roadAddress("서울시 서초구") + .detailAddress("456번지") + .build(); + String newAlias = "새집"; + AddressType newType = AddressType.OFFICE; + + // when + service.changeAddress(profile.getId(), addressEntity.getId(), newAddress, newAlias, newType); + + // then + MemberProfile updatedProfile = profileRepository.findById(profile.getId()).orElseThrow(); + AddressEntity updatedAddressEntity = updatedProfile.getAddressHistory().get(0); + + assertThat(updatedAddressEntity.getAddress().getRoadAddress()).isEqualTo("서울시 서초구"); + assertThat(updatedAddressEntity.getAddress().getDetailAddress()).isEqualTo("456번지"); + assertThat(updatedAddressEntity.getAlias()).isEqualTo("새집"); + assertThat(updatedAddressEntity.getType()).isEqualTo(AddressType.OFFICE); + } + + @Test + @DisplayName("존재하지 않는 프로필 ID로 getProfileFetch 호출 시 예외가 발생한다") + void getProfileFetchWithInvalidId() { + // given + Long invalidProfileId = 99999L; + + // when & then + assertThatThrownBy(() -> service.getProfileFetch(invalidProfileId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필입니다"); + } + + @Test + @DisplayName("존재하지 않는 회원 ID로 프로필 생성 시 예외가 발생한다") + void createProfileWithInvalidMemberId() { + // given + Long invalidMemberId = 99999L; + + // when & then + assertThatThrownBy(() -> service.createProfile("테스트", invalidMemberId, MemberType.STUDENT, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원입니다"); + } + + @Test + @DisplayName("존재하지 않는 프로필 ID로 프로필 변경 시 예외가 발생한다") + void changeProfileWithInvalidId() { + // given + Long invalidProfileId = 99999L; + + // when & then + assertThatThrownBy(() -> service.changeProfile(invalidProfileId, "변경", MemberType.WORKER, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필입니다"); + } + + @Test + @DisplayName("존재하지 않는 프로필 ID로 기본 주소 변경 시 예외가 발생한다") + void changeAddressToPrimaryWithInvalidProfileId() { + // given + Long invalidProfileId = 99999L; + + // when & then + assertThatThrownBy(() -> service.changeAddressToPrimary(invalidProfileId, addressEntity.getId())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필입니다"); + } + + @Test + @DisplayName("존재하지 않는 주소 ID로 기본 주소 변경 시 예외가 발생한다") + void changeAddressToPrimaryWithInvalidAddressId() { + // given + Long invalidAddressId = 99999L; + + // when & then + assertThatThrownBy(() -> service.changeAddressToPrimary(profile.getId(), invalidAddressId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원 주소 정보입니다."); + } + + @Test + @DisplayName("존재하지 않는 프로필 ID로 주소 생성 시 예외가 발생한다") + void saveNewAddressWithInvalidProfileId() { + // given + Long invalidProfileId = 99999L; + Address address = Address.builder() + .roadAddress("서울시") + .detailAddress("123번지") + .build(); + + // when & then + assertThatThrownBy(() -> service.saveNewAddress(invalidProfileId, address, "집", AddressType.HOME)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필입니다"); + } + + @Test + @DisplayName("존재하지 않는 프로필 ID로 주소 변경 시 예외가 발생한다") + void changeAddressWithInvalidProfileId() { + // given + Long invalidProfileId = 99999L; + Address address = Address.builder() + .roadAddress("서울시") + .detailAddress("123번지") + .build(); + + // when & then + assertThatThrownBy(() -> service.changeAddress(invalidProfileId, addressEntity.getId(), address, "집", AddressType.HOME)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필입니다"); + } + + @Test + @DisplayName("존재하지 않는 주소 ID로 주소 변경 시 예외가 발생한다") + void changeAddressWithInvalidAddressId() { + // given + Long invalidAddressId = 99999L; + Address address = Address.builder() + .roadAddress("서울시") + .detailAddress("123번지") + .build(); + + // when & then + assertThatThrownBy(() -> service.changeAddress(profile.getId(), invalidAddressId, address, "집", AddressType.HOME)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원 주소 정보입니다."); + } + + @Test + @DisplayName("존재하지 않는 프로필 ID로 주소 삭제 시 예외가 발생한다") + void deleteAddressWithInvalidProfileId() { + // given + Long invalidProfileId = 99999L; + + // when & then + assertThatThrownBy(() -> service.deleteAddress(invalidProfileId, addressEntity.getId())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필입니다"); + } + + @Test + @DisplayName("존재하지 않는 주소 ID로 주소 삭제 시 예외가 발생한다") + void deleteAddressWithInvalidAddressId() { + // given + Long invalidAddressId = 99999L; + + // when & then + assertThatThrownBy(() -> service.deleteAddress(profile.getId(), invalidAddressId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원 주소 정보입니다."); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceCompleteIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceCompleteIntegrationTest.java new file mode 100644 index 0000000..a556d61 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceCompleteIntegrationTest.java @@ -0,0 +1,295 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.Address.AddressEntity; +import com.stcom.smartmealtable.domain.Address.AddressType; +import com.stcom.smartmealtable.domain.group.Group; +import com.stcom.smartmealtable.domain.group.SchoolGroup; +import com.stcom.smartmealtable.domain.group.SchoolType; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.domain.member.MemberType; +import com.stcom.smartmealtable.repository.AddressEntityRepository; +import com.stcom.smartmealtable.repository.GroupRepository; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import com.stcom.smartmealtable.repository.MemberRepository; +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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class MemberProfileServiceCompleteIntegrationTest { + + @Autowired + private MemberProfileService memberProfileService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MemberProfileRepository memberProfileRepository; + + @Autowired + private GroupRepository groupRepository; + + @Autowired + private AddressEntityRepository addressEntityRepository; + + private Member member; + private Group group; + + @BeforeEach + void setUp() { + member = Member.builder() + .email("profile_complete_test@example.com") + .rawPassword("password123!") + .build(); + memberRepository.save(member); + + Address groupAddress = Address.builder() + .lotNumberAddress("서울시 강남구") + .roadAddress("테헤란로 123") + .detailAddress("456번지") + .build(); + group = new SchoolGroup(groupAddress, "테스트학교", SchoolType.UNIVERSITY_FOUR_YEAR); + groupRepository.save(group); + } + + @Test + @DisplayName("새로운 프로필을 생성할 수 있다") + void createProfile() { + // given + String nickName = "새프로필"; + MemberType type = MemberType.STUDENT; + + // when + memberProfileService.createProfile(nickName, member.getId(), type, group.getId()); + + // then + MemberProfile created = memberProfileRepository.findAll().stream() + .filter(p -> p.getMember().getId().equals(member.getId())) + .findFirst() + .orElseThrow(); + + assertThat(created.getNickName()).isEqualTo(nickName); + assertThat(created.getType()).isEqualTo(type); + assertThat(created.getGroup().getId()).isEqualTo(group.getId()); + } + + @Test + @DisplayName("그룹 없이도 프로필을 생성할 수 있다") + void createProfileWithoutGroup() { + // given + String nickName = "그룹없는프로필"; + MemberType type = MemberType.WORKER; + + // when + memberProfileService.createProfile(nickName, member.getId(), type, null); + + // then + MemberProfile created = memberProfileRepository.findAll().stream() + .filter(p -> p.getMember().getId().equals(member.getId())) + .findFirst() + .orElseThrow(); + + assertThat(created.getNickName()).isEqualTo(nickName); + assertThat(created.getType()).isEqualTo(type); + assertThat(created.getGroup()).isNull(); + } + + @Test + @DisplayName("존재하지 않는 회원으로 프로필을 생성하면 예외가 발생한다") + void createProfileWithInvalidMemberId() { + // given + Long invalidMemberId = 99999L; + + // when & then + assertThatThrownBy(() -> memberProfileService.createProfile("닉네임", invalidMemberId, MemberType.STUDENT, group.getId())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원입니다"); + } + + @Test + @DisplayName("새로운 주소를 저장할 수 있다") + void saveNewAddress() { + // given + MemberProfile profile = createTestProfile(); + Address address = Address.builder() + .lotNumberAddress("부산시 해운대구") + .roadAddress("센텀로 100") + .detailAddress("101동 102호") + .build(); + String alias = "집"; + AddressType addressType = AddressType.HOME; + + // when + memberProfileService.saveNewAddress(profile.getId(), address, alias, addressType); + + // then + MemberProfile updatedProfile = memberProfileRepository.findById(profile.getId()).orElseThrow(); + assertThat(updatedProfile.getAddressHistory()).hasSize(1); + + AddressEntity savedAddress = updatedProfile.getAddressHistory().get(0); + assertThat(savedAddress.getAddress().getLotNumberAddress()).isEqualTo("부산시 해운대구"); + assertThat(savedAddress.getAlias()).isEqualTo(alias); + assertThat(savedAddress.getType()).isEqualTo(addressType); + } + + @Test + @DisplayName("주소를 기본 주소로 설정할 수 있다") + void changeAddressToPrimary() { + // given + MemberProfile profile = createTestProfile(); + AddressEntity addressEntity = createTestAddress(profile); + + // when + memberProfileService.changeAddressToPrimary(profile.getId(), addressEntity.getId()); + + // then + MemberProfile updatedProfile = memberProfileRepository.findById(profile.getId()).orElseThrow(); + assertThat(updatedProfile.findPrimaryAddress().getId()).isEqualTo(addressEntity.getId()); + } + + @Test + @DisplayName("존재하지 않는 프로필로 주소 관련 작업을 하면 예외가 발생한다") + void addressOperationsWithInvalidProfileId() { + // given + Long invalidProfileId = 99999L; + Address address = Address.builder() + .lotNumberAddress("테스트시") + .roadAddress("테스트로") + .detailAddress("테스트빌딩") + .build(); + + // when & then + assertThatThrownBy(() -> memberProfileService.saveNewAddress(invalidProfileId, address, "별칭", AddressType.HOME)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필입니다"); + + assertThatThrownBy(() -> memberProfileService.changeAddressToPrimary(invalidProfileId, 1L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필입니다"); + } + + @Test + @DisplayName("존재하지 않는 주소로 기본 주소 설정을 하면 예외가 발생한다") + void changeAddressToPrimaryWithInvalidAddressId() { + // given + MemberProfile profile = createTestProfile(); + Long invalidAddressId = 99999L; + + // when & then + assertThatThrownBy(() -> memberProfileService.changeAddressToPrimary(profile.getId(), invalidAddressId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원 주소 정보입니다."); + } + + @Test + @DisplayName("존재하지 않는 주소 엔티티로 주소 수정을 하면 예외가 발생한다") + void changeAddressWithInvalidAddressEntityId() { + // given + MemberProfile profile = createTestProfile(); + Long invalidAddressEntityId = 99999L; + Address newAddress = Address.builder() + .lotNumberAddress("새주소시") + .roadAddress("새주소로") + .detailAddress("새주소빌딩") + .build(); + + // when & then + assertThatThrownBy(() -> memberProfileService.changeAddress( + profile.getId(), invalidAddressEntityId, newAddress, "새별칭", AddressType.OFFICE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원 주소 정보입니다."); + } + + @Test + @DisplayName("존재하지 않는 주소 엔티티로 주소 삭제를 하면 예외가 발생한다") + void deleteAddressWithInvalidAddressEntityId() { + // given + MemberProfile profile = createTestProfile(); + Long invalidAddressEntityId = 99999L; + + // when & then + assertThatThrownBy(() -> memberProfileService.deleteAddress(profile.getId(), invalidAddressEntityId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원 주소 정보입니다."); + } + + @Test + @DisplayName("프로필 조회 시 존재하지 않는 프로필 ID로 조회하면 예외가 발생한다") + void getProfileFetchWithInvalidId() { + // given + Long invalidProfileId = 99999L; + + // when & then + assertThatThrownBy(() -> memberProfileService.getProfileFetch(invalidProfileId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필입니다"); + } + + @Test + @DisplayName("프로필을 정상적으로 조회할 수 있다") + void getProfileFetch() { + // given + MemberProfile profile = createTestProfile(); + + // when + MemberProfile retrieved = memberProfileService.getProfileFetch(profile.getId()); + + // then + assertThat(retrieved.getId()).isEqualTo(profile.getId()); + assertThat(retrieved.getNickName()).isEqualTo(profile.getNickName()); + assertThat(retrieved.getType()).isEqualTo(profile.getType()); + } + + @Test + @DisplayName("프로필 변경 시 존재하지 않는 프로필 ID로 시도하면 예외가 발생한다") + void changeProfileWithInvalidId() { + // given + Long invalidProfileId = 99999L; + + // when & then + assertThatThrownBy(() -> memberProfileService.changeProfile( + invalidProfileId, "새닉네임", MemberType.WORKER, group.getId())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 프로필입니다"); + } + + private MemberProfile createTestProfile() { + MemberProfile profile = MemberProfile.builder() + .nickName("테스트프로필") + .member(member) + .type(MemberType.STUDENT) + .group(group) + .build(); + return memberProfileRepository.save(profile); + } + + private AddressEntity createTestAddress(MemberProfile profile) { + Address address = Address.builder() + .lotNumberAddress("서울시") + .roadAddress("테스트로") + .detailAddress("테스트빌딩") + .build(); + AddressEntity addressEntity = AddressEntity.builder() + .address(address) + .alias("테스트주소") + .type(AddressType.HOME) + .build(); + + AddressEntity saved = addressEntityRepository.save(addressEntity); + profile.addAddress(saved); + memberProfileRepository.save(profile); + return saved; + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceIntegrationTest.java new file mode 100644 index 0000000..1661955 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceIntegrationTest.java @@ -0,0 +1,65 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.Address.AddressType; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.domain.member.MemberType; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import com.stcom.smartmealtable.repository.MemberRepository; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +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.test.context.ActiveProfiles; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class MemberProfileServiceIntegrationTest { + + @Autowired + private MemberProfileService memberProfileService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MemberProfileRepository memberProfileRepository; + + @Test + @DisplayName("프로필 생성 후 주소 추가 및 기본 주소 설정이 가능해야 한다") + @Rollback + void createAndUpdateProfile() throws Exception { + // given 회원 저장 + Member member = Member.builder() + .fullName("사용자A") + .email("usera@example.com") + .rawPassword("Password1!") + .build(); + memberRepository.save(member); + + // when 프로필 생성 + memberProfileService.createProfile("닉네임", member.getId(), MemberType.STUDENT, null); + MemberProfile profile = memberProfileRepository.findAll().get(0); + + // 주소 추가 + Address address = Address.builder() + .roadAddress("서울 특별시 강남구 역삼동") + .detailAddress("101호") + .build(); + memberProfileService.saveNewAddress(profile.getId(), address, "집", AddressType.HOME); + + // 기본 주소 설정 + Long addressEntityId = profile.getAddressHistory().get(0).getId(); + memberProfileService.changeAddressToPrimary(profile.getId(), addressEntityId); + + // then + MemberProfile updated = memberProfileService.getProfileFetch(profile.getId()); + assertThat(updated.findPrimaryAddress().getAlias()).isEqualTo("집"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceTest.java deleted file mode 100644 index 70995c3..0000000 --- a/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceTest.java +++ /dev/null @@ -1,394 +0,0 @@ -package com.stcom.smartmealtable.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.stcom.smartmealtable.domain.Address.Address; -import com.stcom.smartmealtable.domain.Address.AddressEntity; -import com.stcom.smartmealtable.domain.Address.AddressType; -import com.stcom.smartmealtable.domain.group.Group; -import com.stcom.smartmealtable.domain.group.SchoolGroup; -import com.stcom.smartmealtable.domain.group.SchoolType; -import com.stcom.smartmealtable.domain.member.Member; -import com.stcom.smartmealtable.domain.member.MemberProfile; -import com.stcom.smartmealtable.domain.member.MemberType; -import com.stcom.smartmealtable.repository.AddressEntityRepository; -import com.stcom.smartmealtable.repository.GroupRepository; -import com.stcom.smartmealtable.repository.MemberProfileRepository; -import com.stcom.smartmealtable.repository.MemberRepository; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; - -@ExtendWith(MockitoExtension.class) -class MemberProfileServiceTest { - - @Mock - private MemberProfileRepository memberProfileRepository; - - @Mock - private GroupRepository groupRepository; - - @Mock - private MemberRepository memberRepository; - - @Mock - private AddressEntityRepository addressEntityRepository; - - @InjectMocks - private MemberProfileService profileService; - - @Captor - private ArgumentCaptor profileCaptor; - - @Captor - private ArgumentCaptor addressEntityCaptor; - - private Member member; - private Group group; - private Address address; - private MemberProfile profile; - - @BeforeEach - void setUp() { - // 테스트 데이터 셋업 - member = new Member("test@example.com"); - ReflectionTestUtils.setField(member, "id", 1L); - - group = new SchoolGroup(); - ReflectionTestUtils.setField(group, "id", 1L); - ReflectionTestUtils.setField(group, "name", "서울대학교"); - ReflectionTestUtils.setField(group, "schoolType", SchoolType.UNIVERSITY_FOUR_YEAR); - - address = createAddress("서울특별시 관악구 관악로 1", "서울특별시 관악구", "101호", 37.459, 126.952); - } - - @Test - @DisplayName("프로필 ID로 프로필 정보를 조회할 수 있어야 한다") - void getProfileFetch() { - // given - Long profileId = 1L; - profile = createProfile(profileId, "닉네임", member, MemberType.STUDENT, group); - - when(memberProfileRepository.findMemberProfileEntityGraphById(profileId)) - .thenReturn(Optional.of(profile)); - - // when - MemberProfile fetchedProfile = profileService.getProfileFetch(profileId); - - // then - assertThat(fetchedProfile).isEqualTo(profile); - assertThat(fetchedProfile.getNickName()).isEqualTo("닉네임"); - assertThat(fetchedProfile.getMember()).isEqualTo(member); - assertThat(fetchedProfile.getType()).isEqualTo(MemberType.STUDENT); - assertThat(fetchedProfile.getGroup()).isEqualTo(group); - - verify(memberProfileRepository).findMemberProfileEntityGraphById(profileId); - } - - @Test - @DisplayName("프로필을 생성할 수 있어야 한다 - 그룹 있음") - void createProfileWithGroup() { - // given - String nickName = "새닉네임"; - Long memberId = 1L; - MemberType type = MemberType.STUDENT; - Long groupId = 1L; - - when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); - when(groupRepository.getReferenceById(groupId)).thenReturn(group); - when(memberProfileRepository.save(any(MemberProfile.class))).thenAnswer(invocation -> { - MemberProfile savedProfile = invocation.getArgument(0); - ReflectionTestUtils.setField(savedProfile, "id", 1L); - return savedProfile; - }); - - // when - profileService.createProfile(nickName, memberId, type, groupId); - - // then - verify(memberRepository).findById(memberId); - verify(groupRepository).getReferenceById(groupId); - verify(memberProfileRepository).save(profileCaptor.capture()); - - MemberProfile savedProfile = profileCaptor.getValue(); - assertThat(savedProfile.getNickName()).isEqualTo(nickName); - assertThat(savedProfile.getMember()).isEqualTo(member); - assertThat(savedProfile.getType()).isEqualTo(type); - assertThat(savedProfile.getGroup()).isEqualTo(group); - } - - @Test - @DisplayName("프로필을 생성할 수 있어야 한다 - 그룹 없음") - void createProfileWithoutGroup() { - // given - String nickName = "새닉네임"; - Long memberId = 1L; - MemberType type = MemberType.OTHER; - Long groupId = null; - - when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); - when(memberProfileRepository.save(any(MemberProfile.class))).thenAnswer(invocation -> { - MemberProfile savedProfile = invocation.getArgument(0); - ReflectionTestUtils.setField(savedProfile, "id", 1L); - return savedProfile; - }); - - // when - profileService.createProfile(nickName, memberId, type, groupId); - - // then - verify(memberRepository).findById(memberId); - verify(memberProfileRepository).save(profileCaptor.capture()); - - MemberProfile savedProfile = profileCaptor.getValue(); - assertThat(savedProfile.getNickName()).isEqualTo(nickName); - assertThat(savedProfile.getMember()).isEqualTo(member); - assertThat(savedProfile.getType()).isEqualTo(type); - assertThat(savedProfile.getGroup()).isNull(); - } - - @Test - @DisplayName("프로필 정보를 변경할 수 있어야 한다") - void changeProfile() { - // given - Long profileId = 1L; - String newNickName = "변경된닉네임"; - MemberType newType = MemberType.WORKER; - Long newGroupId = 2L; - - profile = createProfile(profileId, "원래닉네임", member, MemberType.STUDENT, group); - - Group newGroup = new SchoolGroup(); - ReflectionTestUtils.setField(newGroup, "id", 2L); - ReflectionTestUtils.setField(newGroup, "name", "회사"); - - when(memberProfileRepository.findById(profileId)).thenReturn(Optional.of(profile)); - when(groupRepository.getReferenceById(newGroupId)).thenReturn(newGroup); - - // when - profileService.changeProfile(profileId, newNickName, newType, newGroupId); - - // then - verify(memberProfileRepository).findById(profileId); - verify(groupRepository).getReferenceById(newGroupId); - - assertThat(profile.getNickName()).isEqualTo(newNickName); - assertThat(profile.getType()).isEqualTo(newType); - assertThat(profile.getGroup()).isEqualTo(newGroup); - } - - @Test - @DisplayName("새 주소를 추가할 수 있어야 한다") - void saveNewAddress() { - // given - Long profileId = 1L; - String alias = "집"; - AddressType addressType = AddressType.HOME; - - profile = createProfile(profileId, "닉네임", member, MemberType.STUDENT, group); - ReflectionTestUtils.setField(profile, "addressHistory", new ArrayList<>()); - - when(memberProfileRepository.findById(profileId)).thenReturn(Optional.of(profile)); - when(addressEntityRepository.save(any(AddressEntity.class))).thenAnswer(invocation -> { - AddressEntity savedAddress = invocation.getArgument(0); - ReflectionTestUtils.setField(savedAddress, "id", 1L); - return savedAddress; - }); - - // when - profileService.saveNewAddress(profileId, address, alias, addressType); - - // then - verify(memberProfileRepository).findById(profileId); - verify(addressEntityRepository).save(addressEntityCaptor.capture()); - - AddressEntity savedAddressEntity = addressEntityCaptor.getValue(); - assertThat(savedAddressEntity.getAddress()).isEqualTo(address); - assertThat(savedAddressEntity.getAlias()).isEqualTo(alias); - assertThat(savedAddressEntity.getType()).isEqualTo(addressType); - assertThat(profile.getAddressHistory()).hasSize(1); - } - - @Test - @DisplayName("주소 정보를 변경할 수 있어야 한다") - void changeAddress() { - // given - Long profileId = 1L; - Long addressEntityId = 1L; - String newAlias = "새집"; - AddressType newType = AddressType.HOME; - - profile = createProfile(profileId, "닉네임", member, MemberType.STUDENT, group); - - AddressEntity addressEntity = AddressEntity.builder() - .address(address) - .alias("구집") - .type(AddressType.ETC) - .build(); - ReflectionTestUtils.setField(addressEntity, "id", addressEntityId); - - List addresses = new ArrayList<>(); - addresses.add(addressEntity); - ReflectionTestUtils.setField(profile, "addressHistory", addresses); - - Address newAddress = createAddress("서울시 서초구 서초대로 123", "서초구", "202호", 37.5, 127.0); - - when(memberProfileRepository.findById(profileId)).thenReturn(Optional.of(profile)); - when(addressEntityRepository.findById(addressEntityId)).thenReturn(Optional.of(addressEntity)); - - // when - profileService.changeAddress(profileId, addressEntityId, newAddress, newAlias, newType); - - // then - verify(memberProfileRepository).findById(profileId); - verify(addressEntityRepository).findById(addressEntityId); - - // 주소 정보가 업데이트되었는지 확인 - assertThat(addressEntity.getAddress()).isEqualTo(newAddress); - assertThat(addressEntity.getAlias()).isEqualTo(newAlias); - assertThat(addressEntity.getType()).isEqualTo(newType); - } - - @Test - @DisplayName("주소를 삭제할 수 있어야 한다") - void deleteAddress() { - // given - Long profileId = 1L; - Long addressEntityId = 1L; - - profile = createProfile(profileId, "닉네임", member, MemberType.STUDENT, group); - - AddressEntity addressEntity = AddressEntity.builder() - .address(address) - .alias("집") - .type(AddressType.HOME) - .build(); - ReflectionTestUtils.setField(addressEntity, "id", addressEntityId); - - // 주소에 primary=true 설정 - ReflectionTestUtils.setField(addressEntity, "primary", true); - - List addresses = new ArrayList<>(); - addresses.add(addressEntity); - - // 두번째 주소 추가 - AddressEntity secondAddress = AddressEntity.builder() - .address(createAddress("서울시 강남구", "강남구", "301호", 37.4, 127.1)) - .alias("회사") - .type(AddressType.OFFICE) - .build(); - ReflectionTestUtils.setField(secondAddress, "id", 2L); - addresses.add(secondAddress); - - ReflectionTestUtils.setField(profile, "addressHistory", addresses); - - when(memberProfileRepository.findById(profileId)).thenReturn(Optional.of(profile)); - when(addressEntityRepository.findById(addressEntityId)).thenReturn(Optional.of(addressEntity)); - - // when - profileService.deleteAddress(profileId, addressEntityId); - - // then - verify(memberProfileRepository).findById(profileId); - verify(addressEntityRepository).findById(addressEntityId); - - // 주소가 삭제되었는지 확인 - assertThat(profile.getAddressHistory()).hasSize(1); - assertThat(profile.getAddressHistory().get(0)).isEqualTo(secondAddress); - } - - @Test - @DisplayName("기본 예산을 설정할 수 있어야 한다") - void registerDefaultBudgets() { - // given - Long profileId = 1L; - Long dailyLimit = 10000L; - Long monthlyLimit = 300000L; - - profile = createProfile(profileId, "닉네임", member, MemberType.STUDENT, group); - - when(memberProfileRepository.findById(profileId)).thenReturn(Optional.of(profile)); - - // when - profileService.registerDefaultBudgets(profileId, dailyLimit, monthlyLimit); - - // then - verify(memberProfileRepository).findById(profileId); - - // 기본 예산 설정 로직은 실제로는 MemberProfile 내부 메서드에 있으므로 - // 이 테스트에서는 메서드 호출 여부만 확인 - } - - @Test - @DisplayName("존재하지 않는 프로필 ID로 조회 시 예외가 발생해야 한다") - void getProfileFetchNotFound() { - // given - Long nonExistingProfileId = 999L; - when(memberProfileRepository.findMemberProfileEntityGraphById(nonExistingProfileId)) - .thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> profileService.getProfileFetch(nonExistingProfileId)) - .isInstanceOf(IllegalStateException.class) - .hasMessage("존재하지 않는 프로필입니다"); - } - - @Test - @DisplayName("존재하지 않는 회원 ID로 프로필 생성 시 예외가 발생해야 한다") - void createProfileWithNonExistingMember() { - // given - Long nonExistingMemberId = 999L; - when(memberRepository.findById(nonExistingMemberId)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> profileService.createProfile("닉네임", nonExistingMemberId, MemberType.STUDENT, 1L)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("존재하지 않는 회원입니다"); - } - - private MemberProfile createProfile(Long id, String nickName, Member member, MemberType type, Group group) { - MemberProfile profile = MemberProfile.builder() - .nickName(nickName) - .member(member) - .type(type) - .group(group) - .build(); - ReflectionTestUtils.setField(profile, "id", id); - return profile; - } - - private Address createAddress(String roadAddress, String detailAddress, String alias, - double latitude, double longitude) { - Address address = Address.builder() - .roadAddress(roadAddress) - .detailAddress(detailAddress) - .latitude(latitude) - .longitude(longitude) - .build(); - return address; - } -} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/MemberServiceAdditionalIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/MemberServiceAdditionalIntegrationTest.java new file mode 100644 index 0000000..49df970 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/MemberServiceAdditionalIntegrationTest.java @@ -0,0 +1,121 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.repository.MemberRepository; +import jakarta.transaction.Transactional; +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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class MemberServiceAdditionalIntegrationTest { + + @Autowired + private MemberService memberService; + + @Autowired + private MemberRepository memberRepository; + + private Member savedMember; + + @BeforeEach + void setUp() { + // 테스트에 사용할 회원 데이터 저장 + Member member = Member.builder() + .fullName("테스트회원") + .email("additional_test@example.com") + .rawPassword("Password123!") + .build(); + savedMember = memberRepository.save(member); + } + + @Test + @DisplayName("ID로 회원을 찾을 수 있다") + void findMemberByMemberId() { + // when + Member foundMember = memberService.findMemberByMemberId(savedMember.getId()); + + // then + assertThat(foundMember).isNotNull(); + assertThat(foundMember.getId()).isEqualTo(savedMember.getId()); + assertThat(foundMember.getEmail()).isEqualTo("additional_test@example.com"); + } + + @Test + @DisplayName("존재하지 않는 ID로 회원 조회 시 예외가 발생한다") + void findMemberByMemberIdThrowsExceptionWhenIdNotExists() { + // given + Long nonExistentId = 99999L; + + // when & then + assertThatThrownBy(() -> memberService.findMemberByMemberId(nonExistentId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원입니다"); + } + + @Test + @DisplayName("비밀번호 확인이 일치하면 정상 처리된다") + void checkPasswordDoublySuccess() { + // when & then + // 예외가 발생하지 않으면 테스트 성공 + memberService.checkPasswordDoubly("Password123!", "Password123!"); + } + + @Test + @DisplayName("비밀번호 확인이 일치하지 않으면 예외가 발생한다") + void checkPasswordDoublyThrowsExceptionWhenPasswordsDoNotMatch() { + // given + String password = "Password123!"; + String confirmPassword = "DifferentPassword123!"; + + // when & then + assertThatThrownBy(() -> memberService.checkPasswordDoubly(password, confirmPassword)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("검증 비밀번호가 일치하지 않습니다"); + } + + @Test + @DisplayName("중복되지 않은 이메일은 유효성 검증을 통과한다") + void validateDuplicatedEmailSuccess() { + // given + String uniqueEmail = "unique_email@example.com"; + + // when & then + // 예외가 발생하지 않으면 테스트 성공 + memberService.validateDuplicatedEmail(uniqueEmail); + } + + @Test + @DisplayName("비밀번호 변경 시 원래 비밀번호가 일치하지 않으면 예외가 발생한다") + void changePasswordThrowsExceptionWhenOriginalPasswordDoesNotMatch() { + // given + String wrongOriginalPassword = "WrongPassword123!"; + String newPassword = "NewPassword123!"; + + // when & then + assertThatThrownBy(() -> memberService.changePassword(savedMember.getId(), wrongOriginalPassword, newPassword)) + .isInstanceOf(PasswordFailedExceededException.class) + .hasMessage("기존 비밀번호가 일치하지 않습니다"); + } + + @Test + @DisplayName("존재하지 않는 회원 ID로 회원 삭제 시 예외가 발생한다") + void deleteByMemberIdThrowsExceptionWhenIdNotExists() { + // given + Long nonExistentId = 99999L; + + // when & then + assertThatThrownBy(() -> memberService.deleteByMemberId(nonExistentId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("회원이 존재하지 않습니다"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/MemberServiceFailureIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/MemberServiceFailureIntegrationTest.java new file mode 100644 index 0000000..fec5960 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/MemberServiceFailureIntegrationTest.java @@ -0,0 +1,64 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.exception.PasswordPolicyException; +import com.stcom.smartmealtable.repository.MemberRepository; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +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.test.context.ActiveProfiles; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class MemberServiceFailureIntegrationTest { + + @Autowired + private MemberService memberService; + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("존재하지 않는 회원 ID 로 비밀번호 변경 시 예외가 발생해야 한다") + @Rollback + void changePassword_memberNotFound() { + assertThatThrownBy(() -> memberService.changePassword(999L, "old", "new")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("회원이 존재하지 않습니다"); + } + + @Test + @DisplayName("패스워드 중복 확인이 일치하지 않으면 예외가 발생해야 한다") + void checkPasswordDoublyMismatch() { + assertThatThrownBy(() -> memberService.checkPasswordDoubly("a", "b")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("회원 삭제시 존재하지 않는 ID 일 경우 예외가 발생한다") + void deleteMember_notFound() { + assertThatThrownBy(() -> memberService.deleteByMemberId(999L)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("중복 이메일 검증에서 이미 존재하는 이메일이면 예외 발생") + @Rollback + void duplicateEmail() throws PasswordPolicyException { + Member member = Member.builder() + .fullName("dup") + .email("dup@test.com") + .rawPassword("Password1!") + .build(); + memberRepository.save(member); + + assertThatThrownBy(() -> memberService.validateDuplicatedEmail("dup@test.com")) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/MemberServiceIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/MemberServiceIntegrationTest.java new file mode 100644 index 0000000..42c781d --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/MemberServiceIntegrationTest.java @@ -0,0 +1,88 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +import com.stcom.smartmealtable.exception.PasswordPolicyException; +import com.stcom.smartmealtable.repository.MemberRepository; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +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.test.context.ActiveProfiles; + +/** + * MemberService 통합 테스트 – Mock 사용 없이 실제 DB 상호작용 검증. + */ +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class MemberServiceIntegrationTest { + + @Autowired + private MemberService memberService; + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("회원 생성 후 중복 이메일 검증 시 예외가 발생해야 한다") + @Rollback + void validateDuplicatedEmail() throws Exception { + // given + Member member = Member.builder() + .fullName("홍길동") + .email("duplicate@example.com") + .rawPassword("Password1!") + .build(); + memberService.saveMember(member); + + // when & then + assertThatThrownBy(() -> memberService.validateDuplicatedEmail("duplicate@example.com")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미 존재하는 이메일 입니다"); + } + + @Test + @DisplayName("비밀번호 변경이 정상적으로 수행되어야 한다") + @Rollback + void changePassword() throws Exception { + // given + Member member = Member.builder() + .fullName("김철수") + .email("changepw@example.com") + .rawPassword("Origin123!") + .build(); + memberRepository.save(member); + + // when + memberService.changePassword(member.getId(), "Origin123!", "NewPassword1!"); + + // then + Member changed = memberRepository.findById(member.getId()).orElseThrow(); + assertThat(changed.isMatchedPassword("NewPassword1!")).isTrue(); + } + + @Test + @DisplayName("회원 삭제가 모든 연관 데이터와 함께 정상적으로 동작해야 한다") + @Rollback + void deleteMember() throws PasswordPolicyException, PasswordFailedExceededException { + // given + Member member = Member.builder() + .fullName("이영희") + .email("deleteme@example.com") + .rawPassword("Password1!") + .build(); + memberRepository.save(member); + + // when + memberService.deleteByMemberId(member.getId()); + + // then + assertThat(memberRepository.findById(member.getId())).isEmpty(); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/MemberServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/MemberServiceTest.java deleted file mode 100644 index ece2673..0000000 --- a/src/test/java/com/stcom/smartmealtable/service/MemberServiceTest.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.stcom.smartmealtable.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.stcom.smartmealtable.domain.member.Member; -import com.stcom.smartmealtable.exception.PasswordFailedExceededException; -import com.stcom.smartmealtable.exception.PasswordPolicyException; -import com.stcom.smartmealtable.repository.AddressEntityRepository; -import com.stcom.smartmealtable.repository.MemberProfileRepository; -import com.stcom.smartmealtable.repository.MemberRepository; -import com.stcom.smartmealtable.repository.SocialAccountRepository; -import java.util.Optional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class MemberServiceTest { - - @Mock - private MemberRepository memberRepository; - - @Mock - private MemberProfileRepository memberProfileRepository; - - @Mock - private SocialAccountRepository socialAccountRepository; - - @Mock - private AddressEntityRepository addressEntityRepository; - - @InjectMocks - private MemberService memberService; - - @Test - @DisplayName("회원 정보를 정상적으로 저장할 수 있어야 한다") - void saveMember() throws PasswordPolicyException { - // given - Member member = Member.builder() - .email("test@example.com") - .fullName("홍길동") - .rawPassword("Password123!") - .build(); - - when(memberRepository.save(any(Member.class))).thenReturn(member); - - // when - memberService.saveMember(member); - - // then - verify(memberRepository, times(1)).save(any(Member.class)); - } - - @Test - @DisplayName("ID로 회원 조회 시 회원이 존재하면 회원 정보를 반환해야 한다") - void findMemberByMemberId() throws PasswordPolicyException { - // given - Long memberId = 1L; - Member member = Member.builder() - .email("test@example.com") - .fullName("홍길동") - .rawPassword("Password123!") - .build(); - - when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); - - // when - Member foundMember = memberService.findMemberByMemberId(memberId); - - // then - assertThat(foundMember).isEqualTo(member); - verify(memberRepository, times(1)).findById(memberId); - } - - @Test - @DisplayName("ID로 회원 조회 시 회원이 존재하지 않으면 예외가 발생해야 한다") - void findMemberByMemberIdNotFound() { - // given - Long memberId = 999L; - when(memberRepository.findById(memberId)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> memberService.findMemberByMemberId(memberId)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("존재하지 않는 회원입니다"); - } - - @Test - @DisplayName("비밀번호 확인이 일치하지 않으면 예외가 발생해야 한다") - void checkPasswordDoublyFail() { - // given - String password = "Password123!"; - String confirmPassword = "DifferentPassword123!"; - - // when & then - assertThatThrownBy(() -> memberService.checkPasswordDoubly(password, confirmPassword)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("비밀번호가 일치하지 않습니다"); - } - - @Test - @DisplayName("비밀번호 변경이 정상적으로 동작해야 한다") - void changePassword() throws PasswordPolicyException, PasswordFailedExceededException { - // given - Long memberId = 1L; - String originPassword = "OriginPassword123!"; - String newPassword = "NewPassword123!"; - - Member member = Member.builder() - .email("test@example.com") - .fullName("홍길동") - .rawPassword(originPassword) - .build(); - - when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); - - // when - memberService.changePassword(memberId, originPassword, newPassword); - - // then - assertThat(member.isMatchedPassword(newPassword)).isTrue(); - } - - @Test - @DisplayName("회원 삭제가 정상적으로 동작해야 한다") - void deleteByMemberId() throws PasswordPolicyException { - // given - Long memberId = 1L; - Member member = Member.builder() - .email("test@example.com") - .fullName("홍길동") - .rawPassword("Password123!") - .build(); - - when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); - doNothing().when(memberProfileRepository).deleteMemberProfileByMember(any(Member.class)); - doNothing().when(socialAccountRepository).deleteSocialAccountByMember(any(Member.class)); - doNothing().when(memberRepository).delete(any(Member.class)); - - // when - memberService.deleteByMemberId(memberId); - - // then - verify(memberProfileRepository, times(1)).deleteMemberProfileByMember(member); - verify(socialAccountRepository, times(1)).deleteSocialAccountByMember(member); - verify(memberRepository, times(1)).delete(member); - } -} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceAdditionalIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceAdditionalIntegrationTest.java new file mode 100644 index 0000000..d9bb9c5 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceAdditionalIntegrationTest.java @@ -0,0 +1,80 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.social.SocialAccount; +import com.stcom.smartmealtable.repository.MemberRepository; +import com.stcom.smartmealtable.repository.SocialAccountRepository; +import java.time.LocalDateTime; +import java.util.List; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +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.test.context.ActiveProfiles; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class SocialAccountServiceAdditionalIntegrationTest { + + @Autowired + private SocialAccountService socialAccountService; + + @Autowired + private SocialAccountRepository repository; + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("소셜 계정 토큰을 업데이트 할 수 있다") + @Rollback + void updateToken() { + // given + Member member = new Member("update@test.com"); + memberRepository.save(member); + SocialAccount account = SocialAccount.builder() + .member(member) + .provider("kakao") + .providerUserId("u1") + .tokenType("Bearer") + .accessToken("old") + .refreshToken("old_r") + .tokenExpiresAt(LocalDateTime.now()) + .build(); + repository.save(account); + + // when + socialAccountService.updateToken(account.getId(), "new", "new_r", LocalDateTime.now().plusDays(1)); + + // then + SocialAccount updated = repository.findById(account.getId()).orElseThrow(); + assertThat(updated.getAccessToken()).isEqualTo("new"); + } + + @Test + @DisplayName("회원이 연동한 모든 provider 목록을 조회할 수 있다") + void findAllProviders() { + // given + Member member = new Member("providers@test.com"); + memberRepository.save(member); + repository.save(SocialAccount.builder() + .member(member).provider("google").providerUserId("g1") + .tokenType("Bearer").accessToken("a").refreshToken("r") + .tokenExpiresAt(LocalDateTime.now()).build()); + repository.save(SocialAccount.builder() + .member(member).provider("kakao").providerUserId("k1") + .tokenType("Bearer").accessToken("a").refreshToken("r") + .tokenExpiresAt(LocalDateTime.now()).build()); + + // when + List providers = socialAccountService.findAllProviders(member.getId()); + + // then + assertThat(providers).containsExactlyInAnyOrder("google", "kakao"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceCompleteIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceCompleteIntegrationTest.java new file mode 100644 index 0000000..15851e6 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceCompleteIntegrationTest.java @@ -0,0 +1,255 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.social.SocialAccount; +import com.stcom.smartmealtable.infrastructure.dto.TokenDto; +import com.stcom.smartmealtable.repository.MemberRepository; +import com.stcom.smartmealtable.repository.SocialAccountRepository; +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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class SocialAccountServiceCompleteIntegrationTest { + + @Autowired + private SocialAccountService socialAccountService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private SocialAccountRepository socialAccountRepository; + + private Member testMember; + private SocialAccount kakaoAccount; + private SocialAccount naverAccount; + + @BeforeEach + void setUp() { + testMember = new Member("test@example.com"); + memberRepository.save(testMember); + + kakaoAccount = SocialAccount.builder() + .member(testMember) + .provider("kakao") + .providerUserId("kakao123") + .tokenType("Bearer") + .accessToken("kakao_access_token") + .refreshToken("kakao_refresh_token") + .tokenExpiresAt(LocalDateTime.now().plusHours(1)) + .build(); + + naverAccount = SocialAccount.builder() + .member(testMember) + .provider("naver") + .providerUserId("naver456") + .tokenType("Bearer") + .accessToken("naver_access_token") + .refreshToken("naver_refresh_token") + .tokenExpiresAt(LocalDateTime.now().plusHours(1)) + .build(); + + socialAccountRepository.save(kakaoAccount); + socialAccountRepository.save(naverAccount); + } + + @Test + @DisplayName("새로운 회원과 소셜 계정을 생성할 수 있다") + void createNewMemberAndLinkSocialAccount() { + // given + TokenDto tokenDto = TokenDto.builder() + .accessToken("google_access_token") + .refreshToken("google_refresh_token") + .expiresIn(3600) + .tokenType("Bearer") + .provider("google") + .providerUserId("google123") + .email("newuser@example.com") + .build(); + + // when + socialAccountService.createNewMemberAndLinkSocialAccount(tokenDto); + + // then + Member newMember = memberRepository.findByEmail("newuser@example.com").orElseThrow(); + assertThat(newMember.getEmail()).isEqualTo("newuser@example.com"); + + SocialAccount socialAccount = socialAccountRepository.findByProviderAndProviderUserId( + "google", "google123").orElseThrow(); + assertThat(socialAccount.getProvider()).isEqualTo("google"); + assertThat(socialAccount.getProviderUserId()).isEqualTo("google123"); + assertThat(socialAccount.getMember().getId()).isEqualTo(newMember.getId()); + } + + @Test + @DisplayName("기존 회원에게 소셜 계정을 연결할 수 있다") + void linkSocialAccount() { + // given + TokenDto tokenDto = TokenDto.builder() + .accessToken("google_access_token_2") + .refreshToken("google_refresh_token_2") + .expiresIn(3600) + .tokenType("Bearer") + .provider("google") + .providerUserId("google789") + .email(testMember.getEmail()) + .build(); + + // when + socialAccountService.linkSocialAccount(tokenDto); + + // then + SocialAccount linkedAccount = socialAccountRepository.findByProviderAndProviderUserId( + "google", "google789").orElseThrow(); + assertThat(linkedAccount.getProvider()).isEqualTo("google"); + assertThat(linkedAccount.getProviderUserId()).isEqualTo("google789"); + assertThat(linkedAccount.getMember().getId()).isEqualTo(testMember.getId()); + } + + @Test + @DisplayName("존재하지 않는 회원으로 소셜 계정을 연결하려고 하면 예외가 발생한다") + void linkSocialAccountWithNonExistentMember() { + // given + TokenDto tokenDto = TokenDto.builder() + .accessToken("google_access_token_3") + .refreshToken("google_refresh_token_3") + .expiresIn(3600) + .tokenType("Bearer") + .provider("google") + .providerUserId("google999") + .email("nonexistent@example.com") + .build(); + + // when & then + assertThatThrownBy(() -> socialAccountService.linkSocialAccount(tokenDto)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("회원 엔티티가 존재하지 않은 상태로 소셜 계정 연결을 시도했습니다."); + } + + @Test + @DisplayName("소셜 계정을 조회할 수 있다") + void findSocialAccount() { + // when + SocialAccount foundAccount = socialAccountService.findSocialAccount("kakao", "kakao123"); + + // then + assertThat(foundAccount).isNotNull(); + assertThat(foundAccount.getProvider()).isEqualTo("kakao"); + assertThat(foundAccount.getProviderUserId()).isEqualTo("kakao123"); + assertThat(foundAccount.getMember().getId()).isEqualTo(testMember.getId()); + } + + @Test + @DisplayName("존재하지 않는 소셜 계정을 조회하면 null을 반환한다") + void findSocialAccountNotFound() { + // when + SocialAccount foundAccount = socialAccountService.findSocialAccount("twitter", "twitter123"); + + // then + assertThat(foundAccount).isNull(); + } + + @Test + @DisplayName("신규 사용자인지 확인할 수 있다") + void isNewUser() { + // when + boolean isNew = socialAccountService.isNewUser("twitter", "twitter123"); + + // then + assertThat(isNew).isTrue(); + } + + @Test + @DisplayName("기존 사용자인지 확인할 수 있다") + void isExistingUser() { + // when + boolean isNew = socialAccountService.isNewUser("kakao", "kakao123"); + + // then + assertThat(isNew).isFalse(); + } + + @Test + @DisplayName("토큰을 업데이트할 수 있다") + void updateToken() { + // given + String newAccessToken = "new_kakao_access_token"; + String newRefreshToken = "new_kakao_refresh_token"; + LocalDateTime newExpiresAt = LocalDateTime.now().plusHours(2); + + // when + socialAccountService.updateToken(kakaoAccount.getId(), newAccessToken, newRefreshToken, newExpiresAt); + + // then + SocialAccount updatedAccount = socialAccountRepository.findById(kakaoAccount.getId()).orElseThrow(); + assertThat(updatedAccount.getAccessToken()).isEqualTo(newAccessToken); + assertThat(updatedAccount.getRefreshToken()).isEqualTo(newRefreshToken); + assertThat(updatedAccount.getTokenExpiresAt()).isEqualTo(newExpiresAt); + } + + @Test + @DisplayName("존재하지 않는 소셜 계정의 토큰을 업데이트하려고 하면 예외가 발생한다") + void updateTokenWithInvalidAccountId() { + // given + Long invalidAccountId = 99999L; + String accessToken = "test_token"; + String refreshToken = "test_refresh"; + LocalDateTime expiresAt = LocalDateTime.now(); + + // when & then + assertThatThrownBy(() -> socialAccountService.updateToken(invalidAccountId, accessToken, refreshToken, expiresAt)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("확인되지 않은 계정입니다"); + } + + @Test + @DisplayName("회원의 모든 소셜 제공자를 조회할 수 있다") + void findAllProviders() { + // when + List providers = socialAccountService.findAllProviders(testMember.getId()); + + // then + assertThat(providers).hasSize(2); + assertThat(providers).containsExactlyInAnyOrder("kakao", "naver"); + } + + @Test + @DisplayName("소셜 계정이 없는 회원의 제공자 목록은 빈 리스트를 반환한다") + void findAllProvidersForMemberWithNoSocialAccounts() { + // given + Member memberWithoutSocial = new Member("nosocial@example.com"); + memberRepository.save(memberWithoutSocial); + + // when + List providers = socialAccountService.findAllProviders(memberWithoutSocial.getId()); + + // then + assertThat(providers).isEmpty(); + } + + @Test + @DisplayName("존재하지 않는 회원의 제공자를 조회하면 빈 리스트를 반환한다") + void findAllProvidersForNonExistentMember() { + // given + Long nonExistentMemberId = 99999L; + + // when + List providers = socialAccountService.findAllProviders(nonExistentMemberId); + + // then + assertThat(providers).isEmpty(); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceIntegrationTest.java new file mode 100644 index 0000000..dd17cec --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceIntegrationTest.java @@ -0,0 +1,49 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.infrastructure.dto.TokenDto; +import com.stcom.smartmealtable.repository.SocialAccountRepository; +import java.time.LocalDateTime; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class SocialAccountServiceIntegrationTest { + + @Autowired + private SocialAccountService socialAccountService; + + @Autowired + private SocialAccountRepository repository; + + @Test + @DisplayName("새로운 사용자를 소셜 계정으로 생성하고 조회할 수 있다") + void createNewMemberAndLink() { + // given + TokenDto token = TokenDto.builder() + .accessToken("access") + .refreshToken("refresh") + .expiresIn(3600) + .tokenType("Bearer") + .provider("kakao") + .providerUserId("k123") + .email("socialnew@example.com") + .build(); + + // when + socialAccountService.createNewMemberAndLinkSocialAccount(token); + + // then + var saved = repository.findByProviderAndProviderUserId("kakao", "k123"); + assertThat(saved).isPresent(); + assertThat(saved.get().getMember().getEmail()).isEqualTo("socialnew@example.com"); + assertThat(saved.get().getTokenExpiresAt()).isAfter(LocalDateTime.now()); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceTest.java deleted file mode 100644 index c8bb193..0000000 --- a/src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceTest.java +++ /dev/null @@ -1,302 +0,0 @@ -package com.stcom.smartmealtable.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.stcom.smartmealtable.domain.member.Member; -import com.stcom.smartmealtable.domain.social.SocialAccount; -import com.stcom.smartmealtable.infrastructure.dto.TokenDto; -import com.stcom.smartmealtable.repository.MemberRepository; -import com.stcom.smartmealtable.repository.SocialAccountRepository; -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; - -@ExtendWith(MockitoExtension.class) -class SocialAccountServiceTest { - - @Mock - private SocialAccountRepository socialAccountRepository; - - @Mock - private MemberRepository memberRepository; - - @InjectMocks - private SocialAccountService socialAccountService; - - @Captor - private ArgumentCaptor memberCaptor; - - @Captor - private ArgumentCaptor socialAccountCaptor; - - @Test - @DisplayName("새 회원 생성 및 소셜 계정 연결이 가능해야 한다") - void createNewMemberAndLinkSocialAccount() { - // given - TokenDto tokenDto = createTokenDto("test@example.com", "KAKAO", "12345"); - - when(memberRepository.save(any(Member.class))).thenAnswer(invocation -> { - Member member = invocation.getArgument(0); - ReflectionTestUtils.setField(member, "id", 1L); - return member; - }); - - when(socialAccountRepository.save(any(SocialAccount.class))).thenAnswer(invocation -> { - SocialAccount account = invocation.getArgument(0); - ReflectionTestUtils.setField(account, "id", 1L); - return account; - }); - - // when - socialAccountService.createNewMemberAndLinkSocialAccount(tokenDto); - - // then - verify(memberRepository, times(1)).save(memberCaptor.capture()); - verify(socialAccountRepository, times(1)).save(socialAccountCaptor.capture()); - - Member savedMember = memberCaptor.getValue(); - SocialAccount savedAccount = socialAccountCaptor.getValue(); - - assertThat(savedMember.getEmail()).isEqualTo("test@example.com"); - assertThat(savedAccount.getMember()).isEqualTo(savedMember); - assertThat(savedAccount.getProvider()).isEqualTo("KAKAO"); - assertThat(savedAccount.getProviderUserId()).isEqualTo("12345"); - assertThat(savedAccount.getAccessToken()).isEqualTo("access-token-value"); - assertThat(savedAccount.getRefreshToken()).isEqualTo("refresh-token-value"); - } - - @Test - @DisplayName("기존 회원에 소셜 계정 연결이 가능해야 한다") - void linkSocialAccount() { - // given - String email = "test@example.com"; - TokenDto tokenDto = createTokenDto(email, "GOOGLE", "67890"); - - Member existingMember = new Member(email); - ReflectionTestUtils.setField(existingMember, "id", 1L); - - when(memberRepository.findByEmail(email)).thenReturn(Optional.of(existingMember)); - when(socialAccountRepository.save(any(SocialAccount.class))).thenAnswer(invocation -> { - SocialAccount account = invocation.getArgument(0); - ReflectionTestUtils.setField(account, "id", 2L); - return account; - }); - - // when - socialAccountService.linkSocialAccount(tokenDto); - - // then - verify(memberRepository, times(1)).findByEmail(email); - verify(socialAccountRepository, times(1)).save(socialAccountCaptor.capture()); - - SocialAccount savedAccount = socialAccountCaptor.getValue(); - - assertThat(savedAccount.getMember()).isEqualTo(existingMember); - assertThat(savedAccount.getProvider()).isEqualTo("GOOGLE"); - assertThat(savedAccount.getProviderUserId()).isEqualTo("67890"); - assertThat(savedAccount.getAccessToken()).isEqualTo("access-token-value"); - } - - @Test - @DisplayName("존재하지 않는 이메일로 소셜 계정 연결 시 예외가 발생해야 한다") - void linkSocialAccountWithNonExistingEmail() { - // given - String email = "nonexisting@example.com"; - TokenDto tokenDto = createTokenDto(email, "KAKAO", "12345"); - - when(memberRepository.findByEmail(email)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> socialAccountService.linkSocialAccount(tokenDto)) - .isInstanceOf(IllegalStateException.class) - .hasMessage("회원이 null일 수는 없습니다"); - } - - @Test - @DisplayName("Provider와 ProviderUserId로 소셜 계정을 찾을 수 있어야 한다") - void findSocialAccount() { - // given - String provider = "KAKAO"; - String providerUserId = "12345"; - - Member member = new Member("test@example.com"); - ReflectionTestUtils.setField(member, "id", 1L); - - SocialAccount socialAccount = SocialAccount.builder() - .member(member) - .provider(provider) - .providerUserId(providerUserId) - .build(); - ReflectionTestUtils.setField(socialAccount, "id", 1L); - - when(socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId)) - .thenReturn(Optional.of(socialAccount)); - - // when - SocialAccount foundAccount = socialAccountService.findSocialAccount(provider, providerUserId); - - // then - assertThat(foundAccount).isNotNull(); - assertThat(foundAccount.getProvider()).isEqualTo(provider); - assertThat(foundAccount.getProviderUserId()).isEqualTo(providerUserId); - assertThat(foundAccount.getMember()).isEqualTo(member); - - verify(socialAccountRepository, times(1)).findByProviderAndProviderUserId(provider, providerUserId); - } - - @Test - @DisplayName("존재하지 않는 소셜 계정 찾기 시 null을 반환해야 한다") - void findNonExistingSocialAccount() { - // given - String provider = "KAKAO"; - String providerUserId = "nonexisting"; - - when(socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId)) - .thenReturn(Optional.empty()); - - // when - SocialAccount foundAccount = socialAccountService.findSocialAccount(provider, providerUserId); - - // then - assertThat(foundAccount).isNull(); - } - - @Test - @DisplayName("신규 사용자 여부를 확인할 수 있어야 한다") - void isNewUser() { - // given - String existingProvider = "KAKAO"; - String existingProviderId = "12345"; - - String newProvider = "GOOGLE"; - String newProviderId = "67890"; - - when(socialAccountRepository.findByProviderAndProviderUserId(existingProvider, existingProviderId)) - .thenReturn(Optional.of(new SocialAccount())); - - when(socialAccountRepository.findByProviderAndProviderUserId(newProvider, newProviderId)) - .thenReturn(Optional.empty()); - - // when - boolean existingUserResult = socialAccountService.isNewUser(existingProvider, existingProviderId); - boolean newUserResult = socialAccountService.isNewUser(newProvider, newProviderId); - - // then - assertThat(existingUserResult).isFalse(); - assertThat(newUserResult).isTrue(); - } - - @Test - @DisplayName("토큰 정보를 업데이트할 수 있어야 한다") - void updateToken() { - // given - Long socialAccountId = 1L; - - Member member = new Member("test@example.com"); - SocialAccount socialAccount = SocialAccount.builder() - .member(member) - .provider("KAKAO") - .providerUserId("12345") - .tokenType("Bearer") - .accessToken("old-access-token") - .refreshToken("old-refresh-token") - .tokenExpiresAt(LocalDateTime.now()) - .build(); - ReflectionTestUtils.setField(socialAccount, "id", socialAccountId); - - String newAccessToken = "new-access-token"; - String newRefreshToken = "new-refresh-token"; - LocalDateTime newExpiresAt = LocalDateTime.now().plusHours(1); - - when(socialAccountRepository.findById(socialAccountId)).thenReturn(Optional.of(socialAccount)); - - // when - socialAccountService.updateToken(socialAccountId, newAccessToken, newRefreshToken, newExpiresAt); - - // then - verify(socialAccountRepository, times(1)).findById(socialAccountId); - - assertThat(socialAccount.getAccessToken()).isEqualTo(newAccessToken); - assertThat(socialAccount.getRefreshToken()).isEqualTo(newRefreshToken); - assertThat(socialAccount.getTokenExpiresAt()).isEqualTo(newExpiresAt); - } - - @Test - @DisplayName("존재하지 않는 소셜 계정 ID로 토큰 업데이트 시 예외가 발생해야 한다") - void updateTokenWithNonExistingId() { - // given - Long nonExistingId = 999L; - - when(socialAccountRepository.findById(nonExistingId)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> socialAccountService.updateToken( - nonExistingId, "new-token", "new-refresh-token", LocalDateTime.now())) - .isInstanceOf(IllegalStateException.class) - .hasMessage("확인되지 않은 계정입니다"); - } - - @Test - @DisplayName("회원 ID로 연결된 모든 소셜 Provider 목록을 가져올 수 있어야 한다") - void findAllProviders() { - // given - Long memberId = 1L; - - Member member = new Member("test@example.com"); - - SocialAccount kakaoAccount = SocialAccount.builder() - .member(member) - .provider("KAKAO") - .providerUserId("kakao-12345") - .build(); - - SocialAccount googleAccount = SocialAccount.builder() - .member(member) - .provider("GOOGLE") - .providerUserId("google-67890") - .build(); - - List socialAccounts = Arrays.asList(kakaoAccount, googleAccount); - - when(socialAccountRepository.findAllByMemberId(memberId)).thenReturn(socialAccounts); - - // when - List providers = socialAccountService.findAllProviders(memberId); - - // then - assertThat(providers).hasSize(2); - assertThat(providers).containsExactly("KAKAO", "GOOGLE"); - - verify(socialAccountRepository, times(1)).findAllByMemberId(memberId); - } - - private TokenDto createTokenDto(String email, String provider, String providerUserId) { - TokenDto tokenDto = new TokenDto(); - - tokenDto.setEmail(email); - tokenDto.setProvider(provider); - tokenDto.setProviderUserId(providerUserId); - tokenDto.setTokenType("Bearer"); - tokenDto.setAccessToken("access-token-value"); - tokenDto.setRefreshToken("refresh-token-value"); - tokenDto.setExpiresIn(3600); - - return tokenDto; - } -} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/TermServiceAdditionalIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/TermServiceAdditionalIntegrationTest.java new file mode 100644 index 0000000..b9bd8e3 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/TermServiceAdditionalIntegrationTest.java @@ -0,0 +1,205 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.term.Term; +import com.stcom.smartmealtable.domain.term.TermAgreement; +import com.stcom.smartmealtable.repository.MemberRepository; +import com.stcom.smartmealtable.repository.TermAgreementRepository; +import com.stcom.smartmealtable.repository.TermRepository; +import com.stcom.smartmealtable.service.dto.TermAgreementRequestDto; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class TermServiceAdditionalIntegrationTest { + + @Autowired + private TermService termService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TermRepository termRepository; + + @Autowired + private TermAgreementRepository termAgreementRepository; + + private Member member; + private Term requiredTerm; + private Term optionalTerm; + + @BeforeEach + void setUp() { + member = Member.builder() + .email("term_test@example.com") + .rawPassword("password123!") + .build(); + memberRepository.save(member); + + requiredTerm = createTerm("필수 약관", true); + optionalTerm = createTerm("선택 약관", false); + termRepository.saveAll(Arrays.asList(requiredTerm, optionalTerm)); + } + + @Test + @DisplayName("필수 약관에 동의하지 않으면 예외가 발생한다") + void agreeTermsWithoutRequiredTerms() { + // given + List requests = Collections.singletonList( + new TermAgreementRequestDto(requiredTerm.getId(), false) + ); + + // when & then + assertThatThrownBy(() -> termService.agreeTerms(member.getId(), requests)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("필수 약관에 동의해야 합니다"); + } + + @Test + @DisplayName("존재하지 않는 회원 ID로 약관 동의를 시도하면 예외가 발생한다") + void agreeTermsWithInvalidMemberId() { + // given + Long invalidMemberId = 9999L; + List requests = Collections.singletonList( + new TermAgreementRequestDto(requiredTerm.getId(), true) + ); + + // when & then + assertThatThrownBy(() -> termService.agreeTerms(invalidMemberId, requests)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원입니다"); + } + + @Test + @DisplayName("존재하지 않는 약관 ID로 약관 동의를 시도하면 예외가 발생한다") + void agreeTermsWithInvalidTermId() { + // given + Long invalidTermId = 9999L; + + // 모든 필수 약관에 동의하는 리스트를 만든 다음 존재하지 않는 약관 ID를 추가 + List requests = new java.util.ArrayList<>(); + // 필수 약관에 먼저 동의 (필수 약관 검증을 통과하기 위해) + requests.add(new TermAgreementRequestDto(requiredTerm.getId(), true)); + // 존재하지 않는 약관 ID 추가 + requests.add(new TermAgreementRequestDto(invalidTermId, true)); + + // when & then + assertThatThrownBy(() -> termService.agreeTerms(member.getId(), requests)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("존재하지 않는 약관입니다"); + } + + @Test + @DisplayName("필수 약관을 포함하지 않고 약관 동의를 시도하면 예외가 발생한다") + void agreeTermsWithMissingRequiredTerm() { + // given + // 선택 약관만 동의 + List requests = Collections.singletonList( + new TermAgreementRequestDto(optionalTerm.getId(), true) + ); + + // when & then + assertThatThrownBy(() -> termService.agreeTerms(member.getId(), requests)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("필수 약관에 동의해야 합니다"); + } + + @Test + @DisplayName("모든 약관에 동의하면 정상적으로 저장된다") + void agreeAllTerms() { + // given + List requests = Arrays.asList( + new TermAgreementRequestDto(requiredTerm.getId(), true), + new TermAgreementRequestDto(optionalTerm.getId(), true) + ); + + // when + termService.agreeTerms(member.getId(), requests); + + // then + List agreements = termAgreementRepository.findAll().stream() + .filter(a -> a.getMember().getId().equals(member.getId())) + .collect(Collectors.toList()); + assertThat(agreements).hasSize(2); + assertThat(agreements.stream().allMatch(TermAgreement::getIsAgreed)).isTrue(); + } + + @Test + @DisplayName("필수 약관에만 동의하고 선택 약관에는 동의하지 않아도 정상적으로 저장된다") + void agreeRequiredTermsOnly() { + // given + List requests = Arrays.asList( + new TermAgreementRequestDto(requiredTerm.getId(), true), + new TermAgreementRequestDto(optionalTerm.getId(), false) + ); + + // when + termService.agreeTerms(member.getId(), requests); + + // then + List agreements = termAgreementRepository.findAll().stream() + .filter(a -> a.getMember().getId().equals(member.getId())) + .collect(Collectors.toList()); + assertThat(agreements).hasSize(2); + + TermAgreement requiredAgreement = agreements.stream() + .filter(a -> a.getTerm().getId().equals(requiredTerm.getId())) + .findFirst() + .orElseThrow(); + assertThat(requiredAgreement.getIsAgreed()).isTrue(); + + TermAgreement optionalAgreement = agreements.stream() + .filter(a -> a.getTerm().getId().equals(optionalTerm.getId())) + .findFirst() + .orElseThrow(); + assertThat(optionalAgreement.getIsAgreed()).isFalse(); + } + + @Test + @DisplayName("모든 약관 목록을 조회할 수 있다") + void findAllTerms() { + // given + // 추가 약관 생성 + Term additionalTerm = createTerm("추가 약관", true); + termRepository.save(additionalTerm); + + // when + List terms = termService.findAll(); + + // then + assertThat(terms).hasSize(3); + assertThat(terms.stream().map(Term::getTitle).toList()) + .contains("필수 약관", "선택 약관", "추가 약관"); + } + + private Term createTerm(String title, boolean required) { + Term term = new Term(); + try { + java.lang.reflect.Field titleField = Term.class.getDeclaredField("title"); + titleField.setAccessible(true); + titleField.set(term, title); + + java.lang.reflect.Field isRequiredField = Term.class.getDeclaredField("isRequired"); + isRequiredField.setAccessible(true); + isRequiredField.set(term, required); + } catch (Exception e) { + throw new RuntimeException(e); + } + return term; + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/TermServiceIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/service/TermServiceIntegrationTest.java new file mode 100644 index 0000000..4ac44f5 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/TermServiceIntegrationTest.java @@ -0,0 +1,81 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.term.Term; +import com.stcom.smartmealtable.repository.MemberRepository; +import com.stcom.smartmealtable.repository.TermRepository; +import com.stcom.smartmealtable.service.dto.TermAgreementRequestDto; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +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.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +/** + * 통합 테스트: 스프링 컨텍스트와 실제 JPA 구현체로 Service 계층 검증. + */ +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class TermServiceIntegrationTest { + + @Autowired + private TermService termService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TermRepository termRepository; + + @Test + @DisplayName("필수 약관에 동의하면 정상적으로 저장되어야 한다") + @Rollback + void agreeRequiredTerms() throws Exception { + // given + Member member = Member.builder() + .fullName("홍길동") + .email("test@example.com") + .rawPassword("Password1!") + .build(); + memberRepository.save(member); + + Term required1 = createTerm("이용약관", true); + Term required2 = createTerm("개인정보 처리방침", true); + termRepository.saveAll(List.of(required1, required2)); + + List requests = List.of( + new TermAgreementRequestDto(required1.getId(), true), + new TermAgreementRequestDto(required2.getId(), true) + ); + + // when + termService.agreeTerms(member.getId(), requests); + + // then + // TermAgreementRepository 를 통해 조회해도 되지만, 간단히 member 의 약관 동의 수를 확인 + long count = termRepository.count(); + assertThat(count).isEqualTo(2); + } + + private Term createTerm(String title, boolean required) { + Term term = new Term(); + try { + java.lang.reflect.Field titleField = Term.class.getDeclaredField("title"); + titleField.setAccessible(true); + titleField.set(term, title); + + java.lang.reflect.Field isRequiredField = Term.class.getDeclaredField("isRequired"); + isRequiredField.setAccessible(true); + isRequiredField.set(term, required); + } catch (Exception e) { + throw new RuntimeException(e); + } + return term; + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/TermServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/TermServiceTest.java deleted file mode 100644 index b28eee9..0000000 --- a/src/test/java/com/stcom/smartmealtable/service/TermServiceTest.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.stcom.smartmealtable.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.stcom.smartmealtable.domain.member.Member; -import com.stcom.smartmealtable.domain.term.Term; -import com.stcom.smartmealtable.domain.term.TermAgreement; -import com.stcom.smartmealtable.repository.MemberRepository; -import com.stcom.smartmealtable.repository.TermAgreementRepository; -import com.stcom.smartmealtable.repository.TermRepository; -import com.stcom.smartmealtable.service.dto.TermAgreementRequestDto; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class TermServiceTest { - - @InjectMocks - private TermService termService; - - @Mock - private TermRepository termRepository; - - @Mock - private MemberRepository memberRepository; - - @Mock - private TermAgreementRepository termAgreementRepository; - - @Test - @DisplayName("모든 약관을 조회할 수 있다") - void findAll() { - // given - Term term1 = createTerm(1L, "이용약관", true); - Term term2 = createTerm(2L, "개인정보 처리방침", true); - Term term3 = createTerm(3L, "마케팅 정보 수신 동의", false); - - when(termRepository.findAll()).thenReturn(Arrays.asList(term1, term2, term3)); - - // when - List terms = termService.findAll(); - - // then - assertThat(terms).hasSize(3); - assertThat(terms.get(0).getTitle()).isEqualTo("이용약관"); - assertThat(terms.get(1).getTitle()).isEqualTo("개인정보 처리방침"); - assertThat(terms.get(2).getTitle()).isEqualTo("마케팅 정보 수신 동의"); - } - - @Test - @DisplayName("회원이 약관에 동의할 수 있다") - void agreeTerms() { - // given - Long memberId = 1L; - Member member = createMember(memberId); - - Term term1 = createTerm(1L, "이용약관", true); - Term term2 = createTerm(2L, "개인정보 처리방침", true); - Term term3 = createTerm(3L, "마케팅 정보 수신 동의", false); - - TermAgreementRequestDto dto1 = new TermAgreementRequestDto(1L, true); - TermAgreementRequestDto dto2 = new TermAgreementRequestDto(2L, true); - TermAgreementRequestDto dto3 = new TermAgreementRequestDto(3L, false); - - when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); - when(termRepository.findAll()).thenReturn(Arrays.asList(term1, term2, term3)); - when(termRepository.findById(1L)).thenReturn(Optional.of(term1)); - when(termRepository.findById(2L)).thenReturn(Optional.of(term2)); - when(termRepository.findById(3L)).thenReturn(Optional.of(term3)); - - // when - termService.agreeTerms(memberId, Arrays.asList(dto1, dto2, dto3)); - - // then - verify(termAgreementRepository, times(3)).save(any(TermAgreement.class)); - } - - @Test - @DisplayName("필수 약관에 동의하지 않으면 예외가 발생한다") - void agreeTerms_RequiredTermNotAgreed() { - // given - Long memberId = 1L; - Member member = createMember(memberId); - - Term term1 = createTerm(1L, "이용약관", true); - Term term2 = createTerm(2L, "개인정보 처리방침", true); - - TermAgreementRequestDto dto1 = new TermAgreementRequestDto(1L, true); - TermAgreementRequestDto dto2 = new TermAgreementRequestDto(2L, false); // 필수 약관이지만 동의하지 않음 - - when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); - when(termRepository.findAll()).thenReturn(Arrays.asList(term1, term2)); - - // when & then - assertThatThrownBy(() -> termService.agreeTerms(memberId, Arrays.asList(dto1, dto2))) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("필수 약관에 동의해야 합니다"); - } - - @Test - @DisplayName("존재하지 않는 회원 ID로 약관 동의를 시도하면 예외가 발생한다") - void agreeTerms_MemberNotFound() { - // given - Long nonExistentMemberId = 999L; - TermAgreementRequestDto dto = new TermAgreementRequestDto(1L, true); - - when(memberRepository.findById(nonExistentMemberId)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> termService.agreeTerms(nonExistentMemberId, List.of(dto))) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("존재하지 않는 회원입니다"); - } - - @Test - @DisplayName("존재하지 않는 약관 ID로 약관 동의를 시도하면 예외가 발생한다") - void agreeTerms_TermNotFound() { - // given - Long memberId = 1L; - Long nonExistentTermId = 999L; - Member member = createMember(memberId); - TermAgreementRequestDto dto = new TermAgreementRequestDto(nonExistentTermId, true); - - when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); - when(termRepository.findAll()).thenReturn(List.of()); - when(termRepository.findById(nonExistentTermId)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> termService.agreeTerms(memberId, List.of(dto))) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("존재하지 않는 약관입니다"); - } - - // 테스트용 회원 생성 헬퍼 메소드 - private Member createMember(Long id) { - Member member = new Member(); - return member; - } - - // 테스트용 약관 생성 헬퍼 메소드 - private Term createTerm(Long id, String title, Boolean isRequired) { - Term term = new Term(); - // 리플렉션을 통해 private 필드에 값 설정 - try { - java.lang.reflect.Field idField = Term.class.getDeclaredField("id"); - idField.setAccessible(true); - idField.set(term, id); - - java.lang.reflect.Field titleField = Term.class.getDeclaredField("title"); - titleField.setAccessible(true); - titleField.set(term, title); - - java.lang.reflect.Field isRequiredField = Term.class.getDeclaredField("isRequired"); - isRequiredField.setAccessible(true); - isRequiredField.set(term, isRequired); - } catch (Exception e) { - throw new RuntimeException(e); - } - return term; - } -} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/exception/ResourceNotFoundException.java b/src/test/java/com/stcom/smartmealtable/service/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/exception/ResourceNotFoundException.java @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/web/controller/MemberControllerTest.java b/src/test/java/com/stcom/smartmealtable/web/controller/MemberControllerTest.java deleted file mode 100644 index d6f2365..0000000 --- a/src/test/java/com/stcom/smartmealtable/web/controller/MemberControllerTest.java +++ /dev/null @@ -1,212 +0,0 @@ -package com.stcom.smartmealtable.web.controller; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.stcom.smartmealtable.domain.member.Member; -import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; -import com.stcom.smartmealtable.security.JwtTokenService; -import com.stcom.smartmealtable.service.MemberService; -import com.stcom.smartmealtable.service.TermService; -import com.stcom.smartmealtable.service.dto.MemberDto; -import com.stcom.smartmealtable.web.argumentresolver.UserContext; -import com.stcom.smartmealtable.web.argumentresolver.UserContextArgumentResolver; -import com.stcom.smartmealtable.web.controller.MemberController.CreateMemberRequest; -import com.stcom.smartmealtable.web.controller.MemberController.EditMemberRequest; -import com.stcom.smartmealtable.web.controller.MemberController.TermAgreementDto; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.MediaType; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.filter.CharacterEncodingFilter; - -import io.jsonwebtoken.Claims; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) -@AutoConfigureMockMvc -@Transactional -@TestPropertySource(properties = { - "spring.main.allow-bean-definition-overriding=true" -}) -class MemberControllerTest { - - @Autowired - private MockMvc mockMvc; - - @Autowired - private ObjectMapper objectMapper; - - @MockBean - private MemberService memberService; - - @MockBean - private JwtTokenService jwtTokenService; - - @MockBean - private TermService termService; - - @Autowired - private WebApplicationContext context; - - @BeforeEach - void setup() { - // 테스트용 MockMvc 설정 - mockMvc = MockMvcBuilders - .webAppContextSetup(context) - .addFilter(new CharacterEncodingFilter("UTF-8", true)) - .build(); - - // UserContext 어노테이션 처리를 위한 설정 - // Claims 모킹 설정 - Claims claims = Mockito.mock(Claims.class); - when(claims.get("memberId", String.class)).thenReturn("1"); - when(jwtTokenService.extractClaims(anyString())).thenReturn(claims); - } - - @Test - @DisplayName("이메일 중복 확인 API가 정상적으로 동작해야 한다") - void checkEmail() throws Exception { - // given - String email = "test@example.com"; - doNothing().when(memberService).validateDuplicatedEmail(email); - - // when & then - mockMvc.perform(get("/api/v1/members/email/check") - .param("email", email)) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value("SUCCESS")); - } - - @Test - @DisplayName("회원 생성 API가 정상적으로 동작해야 한다") - void createMember() throws Exception { - // given - CreateMemberRequest request = new CreateMemberRequest( - "test@example.com", "Password123!", "Password123!", "홍길동"); - - // Member 모킹 - Member mockMember = mock(Member.class); - when(mockMember.getId()).thenReturn(1L); - - // Member Builder 모킹 - Member.MemberBuilder mockBuilder = mock(Member.MemberBuilder.class); - when(mockBuilder.fullName(anyString())).thenReturn(mockBuilder); - when(mockBuilder.email(anyString())).thenReturn(mockBuilder); - when(mockBuilder.rawPassword(anyString())).thenReturn(mockBuilder); - when(mockBuilder.build()).thenReturn(mockMember); - - // Member 정적 메소드 모킹 - try (var mockStatic = Mockito.mockStatic(Member.class)) { - mockStatic.when(Member::builder).thenReturn(mockBuilder); - - JwtTokenResponseDto tokenDto = new JwtTokenResponseDto( - "test-access-token", - "test-refresh-token", - 3600, - "Bearer" - ); - tokenDto.setNewUser(true); - - // MemberService 모킹 - doNothing().when(memberService).validateDuplicatedEmail(anyString()); - doNothing().when(memberService).checkPasswordDoubly(anyString(), anyString()); - doNothing().when(memberService).saveMember(any(Member.class)); - - // JwtTokenService 모킹 - when(jwtTokenService.createTokenDto(anyLong(), any())).thenReturn(tokenDto); - - // when & then - mockMvc.perform(post("/api/v1/members") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andDo(print()) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.status").value("SUCCESS")) - .andExpect(jsonPath("$.data.accessToken").value("test-access-token")) - .andExpect(jsonPath("$.data.refreshToken").value("test-refresh-token")) - .andExpect(jsonPath("$.data.newUser").value(true)); - } - } - - @Test - @DisplayName("회원 정보 수정 API가 정상적으로 동작해야 한다") - void editMember() throws Exception { - // given - EditMemberRequest request = new EditMemberRequest( - "OldPassword123!", "NewPassword123!", "NewPassword123!"); - - doNothing().when(memberService).checkPasswordDoubly(anyString(), anyString()); - doNothing().when(memberService).changePassword(anyLong(), anyString(), anyString()); - - // when & then - mockMvc.perform(patch("/api/v1/members/me") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - .header("Authorization", "Bearer test-token")) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value("SUCCESS")); - } - - @Test - @DisplayName("회원 탈퇴 API가 정상적으로 동작해야 한다") - void deleteMember() throws Exception { - // given - doNothing().when(memberService).deleteByMemberId(anyLong()); - - // when & then - mockMvc.perform(delete("/api/v1/members/me") - .header("Authorization", "Bearer test-token")) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value("SUCCESS")); - } - - @Test - @DisplayName("약관 동의 API가 정상적으로 동작해야 한다") - void signUpWithTermAgreement() throws Exception { - // given - List agreements = Arrays.asList( - new TermAgreementDto(1L, true), - new TermAgreementDto(2L, false) - ); - - doNothing().when(termService).agreeTerms(anyLong(), any()); - - // when & then - mockMvc.perform(post("/api/v1/members/signup") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(agreements)) - .header("Authorization", "Bearer test-token")) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value("SUCCESS")); - } -} \ No newline at end of file