diff --git a/docs/operations/policies/ERD_DECISIONS_AND_LIFECYCLE.md b/docs/operations/policies/ERD_DECISIONS_AND_LIFECYCLE.md index 72a77b14..726d4db2 100644 --- a/docs/operations/policies/ERD_DECISIONS_AND_LIFECYCLE.md +++ b/docs/operations/policies/ERD_DECISIONS_AND_LIFECYCLE.md @@ -31,7 +31,7 @@ | `BlockUser` | hard delete | 차단 관계는 하드 삭제한다. | | `Report` | preserve | 신고 데이터는 보존한다. | | `ConsentItem` | preserve | 동의 항목 정의 데이터는 보존한다. | -| `UserConsent` | pending | 사용자별 terms consent 상태로 유지하며, retention/anonymization/deletion policy는 pending이다. | +| `UserConsent` | pending | 사용자별 동의 상태로 유지하며, retention/anonymization/deletion policy는 pending이다. | ## DailyJogak Creation Rules - routine midnight batch는 오늘자 `DailyJogak` row를 생성한다. diff --git a/src/main/java/com/mogak/spring/domain/consent/UserConsent.java b/src/main/java/com/mogak/spring/domain/consent/UserConsent.java index 305c11cf..cc274d1f 100644 --- a/src/main/java/com/mogak/spring/domain/consent/UserConsent.java +++ b/src/main/java/com/mogak/spring/domain/consent/UserConsent.java @@ -63,6 +63,7 @@ public void update(boolean agreed, LocalDateTime changedAt) { this.withdrawnAt = null; return; } + this.agreedAt = null; this.withdrawnAt = changedAt; } } diff --git a/src/main/java/com/mogak/spring/repository/ConsentItemRepository.java b/src/main/java/com/mogak/spring/repository/ConsentItemRepository.java index 3b10e5f2..16a4d54c 100644 --- a/src/main/java/com/mogak/spring/repository/ConsentItemRepository.java +++ b/src/main/java/com/mogak/spring/repository/ConsentItemRepository.java @@ -1,9 +1,12 @@ package com.mogak.spring.repository; import com.mogak.spring.domain.consent.ConsentItem; +import java.util.Collection; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface ConsentItemRepository extends JpaRepository { List findAllByActiveTrueOrderByIdAsc(); + + List findAllByCodeInAndActiveTrue(Collection codes); } diff --git a/src/main/java/com/mogak/spring/repository/UserConsentRepository.java b/src/main/java/com/mogak/spring/repository/UserConsentRepository.java index 9d5a9e11..7d55fae3 100644 --- a/src/main/java/com/mogak/spring/repository/UserConsentRepository.java +++ b/src/main/java/com/mogak/spring/repository/UserConsentRepository.java @@ -1,6 +1,7 @@ package com.mogak.spring.repository; import com.mogak.spring.domain.consent.UserConsent; +import java.util.Collection; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -9,4 +10,6 @@ public interface UserConsentRepository extends JpaRepository Optional findByUserIdAndConsentItemId(Long userId, Long consentItemId); List findAllByUserId(Long userId); + + List findAllByUserIdAndConsentItemCodeIn(Long userId, Collection codes); } diff --git a/src/main/java/com/mogak/spring/service/ConsentService.java b/src/main/java/com/mogak/spring/service/ConsentService.java index 5212980e..54d4b773 100644 --- a/src/main/java/com/mogak/spring/service/ConsentService.java +++ b/src/main/java/com/mogak/spring/service/ConsentService.java @@ -1,8 +1,10 @@ package com.mogak.spring.service; import com.mogak.spring.domain.user.User; +import com.mogak.spring.service.command.MarketingConsentCommand; import com.mogak.spring.service.command.UserConsentCommand; import com.mogak.spring.service.result.ConsentItemResult; +import com.mogak.spring.service.result.MarketingConsentResult; import java.util.List; public interface ConsentService { @@ -11,4 +13,8 @@ public interface ConsentService { void saveUserConsents(User user, List consents); void updateUserConsents(Long userId, List consents); + + MarketingConsentResult getMarketingConsent(Long userId); + + MarketingConsentResult updateMarketingConsent(Long userId, MarketingConsentCommand command); } diff --git a/src/main/java/com/mogak/spring/service/ConsentServiceImpl.java b/src/main/java/com/mogak/spring/service/ConsentServiceImpl.java index e43ab55c..314e0c52 100644 --- a/src/main/java/com/mogak/spring/service/ConsentServiceImpl.java +++ b/src/main/java/com/mogak/spring/service/ConsentServiceImpl.java @@ -8,8 +8,12 @@ import com.mogak.spring.repository.ConsentItemRepository; import com.mogak.spring.repository.UserConsentRepository; import com.mogak.spring.repository.UserRepository; +import com.mogak.spring.service.command.MarketingConsentCommand; import com.mogak.spring.service.command.UserConsentCommand; import com.mogak.spring.service.result.ConsentItemResult; +import com.mogak.spring.service.result.MarketingConsentResult; +import java.util.Collection; +import java.util.HashMap; import java.time.LocalDateTime; import java.util.HashSet; import java.util.List; @@ -24,6 +28,13 @@ @RequiredArgsConstructor @Service public class ConsentServiceImpl implements ConsentService { + private static final String MARKETING_CODE = "MARKETING"; + private static final String ADVERTISEMENT_CODE = "ADVERTISEMENT"; + private static final List MARKETING_CONSENT_CODES = List.of( + MARKETING_CODE, + ADVERTISEMENT_CODE + ); + private final ConsentItemRepository consentItemRepository; private final UserConsentRepository userConsentRepository; private final UserRepository userRepository; @@ -42,6 +53,13 @@ public List getActiveConsentItems() { .toList(); } + @Transactional(readOnly = true) + @Override + public MarketingConsentResult getMarketingConsent(Long userId) { + validateActiveUser(userId); + return getCurrentMarketingConsent(userId); + } + @Transactional @Override public void saveUserConsents(User user, List consents) { @@ -54,14 +72,36 @@ public void saveUserConsents(User user, List consents) { @Transactional @Override public void updateUserConsents(Long userId, List consents) { - User user = userRepository.findActiveById(userId) - .orElseThrow(() -> new BaseException(ErrorCode.NOT_EXIST_USER)); + User user = validateActiveUser(userId); if (consents == null || consents.isEmpty()) { return; } upsertUserConsents(user, consents); } + @Transactional + @Override + public MarketingConsentResult updateMarketingConsent(Long userId, MarketingConsentCommand command) { + if (command == null || command.isEmpty()) { + throw new BaseException(ErrorCode.INVALID_PARAMETER_ERROR); + } + + User user = validateActiveUser(userId); + List requestedCodes = requestedCodes(command); + Map consentItems = findActiveConsentItemsByCode(requestedCodes); + Map userConsents = findUserConsentsByCode(userId, requestedCodes); + LocalDateTime now = LocalDateTime.now(); + + if (command.marketingAgreed() != null) { + updateMarketingConsent(user, command.marketingAgreed(), MARKETING_CODE, consentItems, userConsents, now); + } + if (command.advertisementAgreed() != null) { + updateMarketingConsent(user, command.advertisementAgreed(), ADVERTISEMENT_CODE, consentItems, userConsents, now); + } + + return getCurrentMarketingConsent(userId); + } + private void upsertUserConsents(User user, List consents) { validateConsentCommands(consents); Map consentItems = findConsentItems(consents); @@ -110,4 +150,71 @@ private Map findConsentItems(List consent return consentItemRepository.findAllById(ids).stream() .collect(Collectors.toMap(ConsentItem::getId, Function.identity())); } + + private User validateActiveUser(Long userId) { + return userRepository.findActiveById(userId) + .orElseThrow(() -> new BaseException(ErrorCode.NOT_EXIST_USER)); + } + + private MarketingConsentResult getCurrentMarketingConsent(Long userId) { + Map userConsents = findUserConsentsByCode(userId, MARKETING_CONSENT_CODES); + return new MarketingConsentResult( + agreed(userConsents, MARKETING_CODE), + agreed(userConsents, ADVERTISEMENT_CODE) + ); + } + + private boolean agreed(Map userConsents, String code) { + UserConsent userConsent = userConsents.get(code); + return userConsent != null && userConsent.isAgreed(); + } + + private List requestedCodes(MarketingConsentCommand command) { + return MARKETING_CONSENT_CODES.stream() + .filter(code -> { + if (MARKETING_CODE.equals(code)) { + return command.marketingAgreed() != null; + } + if (ADVERTISEMENT_CODE.equals(code)) { + return command.advertisementAgreed() != null; + } + return false; + }) + .toList(); + } + + private Map findActiveConsentItemsByCode(Collection codes) { + Map consentItems = consentItemRepository.findAllByCodeInAndActiveTrue(codes).stream() + .collect(Collectors.toMap(ConsentItem::getCode, Function.identity())); + if (consentItems.size() != codes.size()) { + throw new BaseException(ErrorCode.NOT_EXIST_CONSENT_ITEM); + } + return consentItems; + } + + private Map findUserConsentsByCode(Long userId, Collection codes) { + Map userConsents = new HashMap<>(); + userConsentRepository.findAllByUserIdAndConsentItemCodeIn(userId, codes) + .forEach(userConsent -> userConsents.put(userConsent.getConsentItem().getCode(), userConsent)); + return userConsents; + } + + private void updateMarketingConsent( + User user, + Boolean agreed, + String code, + Map consentItems, + Map userConsents, + LocalDateTime now + ) { + UserConsent userConsent = userConsents.get(code); + if (userConsent == null) { + userConsent = UserConsent.builder() + .user(user) + .consentItem(consentItems.get(code)) + .build(); + userConsentRepository.save(userConsent); + } + userConsent.update(agreed, now); + } } diff --git a/src/main/java/com/mogak/spring/service/command/MarketingConsentCommand.java b/src/main/java/com/mogak/spring/service/command/MarketingConsentCommand.java new file mode 100644 index 00000000..7dddc5db --- /dev/null +++ b/src/main/java/com/mogak/spring/service/command/MarketingConsentCommand.java @@ -0,0 +1,10 @@ +package com.mogak.spring.service.command; + +public record MarketingConsentCommand( + Boolean marketingAgreed, + Boolean advertisementAgreed +) { + public boolean isEmpty() { + return marketingAgreed == null && advertisementAgreed == null; + } +} diff --git a/src/main/java/com/mogak/spring/service/result/MarketingConsentResult.java b/src/main/java/com/mogak/spring/service/result/MarketingConsentResult.java new file mode 100644 index 00000000..56fe306f --- /dev/null +++ b/src/main/java/com/mogak/spring/service/result/MarketingConsentResult.java @@ -0,0 +1,7 @@ +package com.mogak.spring.service.result; + +public record MarketingConsentResult( + boolean marketingAgreed, + boolean advertisementAgreed +) { +} diff --git a/src/main/java/com/mogak/spring/web/controller/ConsentController.java b/src/main/java/com/mogak/spring/web/controller/ConsentController.java index dbf0d14c..b281f3c7 100644 --- a/src/main/java/com/mogak/spring/web/controller/ConsentController.java +++ b/src/main/java/com/mogak/spring/web/controller/ConsentController.java @@ -4,7 +4,10 @@ import com.mogak.spring.global.ErrorCode; import com.mogak.spring.jwt.AuthenticatedUser; import com.mogak.spring.service.ConsentService; +import com.mogak.spring.service.result.MarketingConsentResult; import com.mogak.spring.web.dto.consentdto.ConsentItemResponse; +import com.mogak.spring.web.dto.consentdto.MarketingConsentPatchRequest; +import com.mogak.spring.web.dto.consentdto.MarketingConsentResponse; import com.mogak.spring.web.dto.consentdto.UserConsentUpdateRequest; import jakarta.validation.Valid; import java.util.List; @@ -12,6 +15,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -30,6 +34,14 @@ public ResponseEntity>> getConsents() { return ResponseEntity.ok(new BaseResponse<>(responses)); } + @GetMapping("/api/users/marketing-consent") + public ResponseEntity> getMarketingConsent( + @AuthenticationPrincipal AuthenticatedUser authenticatedUser + ) { + MarketingConsentResult result = consentService.getMarketingConsent(authenticatedUser.getUserId()); + return ResponseEntity.ok(new BaseResponse<>(MarketingConsentResponse.from(result))); + } + @PutMapping("/api/users/consents") public ResponseEntity> updateUserConsents( @AuthenticationPrincipal AuthenticatedUser authenticatedUser, @@ -38,4 +50,16 @@ public ResponseEntity> updateUserConsents( consentService.updateUserConsents(authenticatedUser.getUserId(), request.toCommands()); return ResponseEntity.ok(new BaseResponse<>(ErrorCode.SUCCESS)); } + + @PatchMapping("/api/users/marketing-consent") + public ResponseEntity> updateMarketingConsent( + @AuthenticationPrincipal AuthenticatedUser authenticatedUser, + @Valid @RequestBody MarketingConsentPatchRequest request + ) { + MarketingConsentResult result = consentService.updateMarketingConsent( + authenticatedUser.getUserId(), + request.toCommand() + ); + return ResponseEntity.ok(new BaseResponse<>(MarketingConsentResponse.from(result))); + } } diff --git a/src/main/java/com/mogak/spring/web/dto/consentdto/MarketingConsentPatchRequest.java b/src/main/java/com/mogak/spring/web/dto/consentdto/MarketingConsentPatchRequest.java new file mode 100644 index 00000000..6b9a043e --- /dev/null +++ b/src/main/java/com/mogak/spring/web/dto/consentdto/MarketingConsentPatchRequest.java @@ -0,0 +1,18 @@ +package com.mogak.spring.web.dto.consentdto; + +import com.mogak.spring.service.command.MarketingConsentCommand; +import jakarta.validation.constraints.AssertTrue; + +public record MarketingConsentPatchRequest( + Boolean marketingAgreed, + Boolean advertisementAgreed +) { + @AssertTrue(message = "변경할 동의 값을 입력해주세요.") + public boolean hasAnyAgreement() { + return marketingAgreed != null || advertisementAgreed != null; + } + + public MarketingConsentCommand toCommand() { + return new MarketingConsentCommand(marketingAgreed, advertisementAgreed); + } +} diff --git a/src/main/java/com/mogak/spring/web/dto/consentdto/MarketingConsentResponse.java b/src/main/java/com/mogak/spring/web/dto/consentdto/MarketingConsentResponse.java new file mode 100644 index 00000000..f388ea67 --- /dev/null +++ b/src/main/java/com/mogak/spring/web/dto/consentdto/MarketingConsentResponse.java @@ -0,0 +1,12 @@ +package com.mogak.spring.web.dto.consentdto; + +import com.mogak.spring.service.result.MarketingConsentResult; + +public record MarketingConsentResponse( + boolean marketingAgreed, + boolean advertisementAgreed +) { + public static MarketingConsentResponse from(MarketingConsentResult result) { + return new MarketingConsentResponse(result.marketingAgreed(), result.advertisementAgreed()); + } +} diff --git a/src/test/java/com/mogak/spring/config/SecurityConfigTest.java b/src/test/java/com/mogak/spring/config/SecurityConfigTest.java index 488c6573..5f73cf96 100644 --- a/src/test/java/com/mogak/spring/config/SecurityConfigTest.java +++ b/src/test/java/com/mogak/spring/config/SecurityConfigTest.java @@ -52,6 +52,7 @@ import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +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.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -204,6 +205,24 @@ void consentItemsEndpointIsPublic() throws Exception { .andExpect(jsonPath("$.result[0].code").value("MARKETING")); } + @Test + @DisplayName("광고/마케팅 동의 조회 API는 토큰 없이 호출하면 401을 반환한다") + void marketingConsentGetRejectsMissingToken() throws Exception { + mockMvc.perform(get("/api/users/marketing-consent")) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value("T003")); + } + + @Test + @DisplayName("광고/마케팅 동의 변경 API는 토큰 없이 호출하면 401을 반환한다") + void marketingConsentPatchRejectsMissingToken() throws Exception { + mockMvc.perform(patch("/api/users/marketing-consent") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"marketingAgreed\":true}")) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value("T003")); + } + private org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder joinRequest(String role) throws Exception { MockMultipartFile request = new MockMultipartFile( diff --git a/src/test/java/com/mogak/spring/service/ConsentServiceImplTest.java b/src/test/java/com/mogak/spring/service/ConsentServiceImplTest.java index fb8c6932..4280bd9c 100644 --- a/src/test/java/com/mogak/spring/service/ConsentServiceImplTest.java +++ b/src/test/java/com/mogak/spring/service/ConsentServiceImplTest.java @@ -7,8 +7,10 @@ import com.mogak.spring.repository.ConsentItemRepository; import com.mogak.spring.repository.UserConsentRepository; import com.mogak.spring.repository.UserRepository; +import com.mogak.spring.service.command.MarketingConsentCommand; import com.mogak.spring.service.command.UserConsentCommand; import com.mogak.spring.service.result.ConsentItemResult; +import com.mogak.spring.service.result.MarketingConsentResult; import com.mogak.spring.support.ErrorCodeAssertions; import com.mogak.spring.support.TestFixtureFactory; import java.util.ArrayList; @@ -24,7 +26,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -58,6 +59,44 @@ void getActiveConsentItemsReturnsResults() { )); } + @Test + @DisplayName("저장된 광고와 마케팅 동의 상태를 반환한다") + void getMarketingConsentReturnsSavedState() { + User user = TestFixtureFactory.user(10L, "user@test.com", "tester", null, null); + ConsentItem marketing = consentItem(1L, "MARKETING", true, false); + ConsentItem advertisement = consentItem(2L, "ADVERTISEMENT", true, false); + UserConsent marketingConsent = userConsent(user, marketing, true); + UserConsent advertisementConsent = userConsent(user, advertisement, false); + + when(userRepository.findActiveById(10L)).thenReturn(Optional.of(user)); + when(userConsentRepository.findAllByUserIdAndConsentItemCodeIn( + 10L, + List.of("MARKETING", "ADVERTISEMENT") + )).thenReturn(List.of(marketingConsent, advertisementConsent)); + + MarketingConsentResult result = consentService.getMarketingConsent(10L); + + assertThat(result.marketingAgreed()).isTrue(); + assertThat(result.advertisementAgreed()).isFalse(); + } + + @Test + @DisplayName("저장된 동의가 없으면 광고와 마케팅 동의 상태를 false로 반환한다") + void getMarketingConsentDefaultsToFalse() { + User user = TestFixtureFactory.user(10L, "user@test.com", "tester", null, null); + + when(userRepository.findActiveById(10L)).thenReturn(Optional.of(user)); + when(userConsentRepository.findAllByUserIdAndConsentItemCodeIn( + 10L, + List.of("MARKETING", "ADVERTISEMENT") + )).thenReturn(List.of()); + + MarketingConsentResult result = consentService.getMarketingConsent(10L); + + assertThat(result.marketingAgreed()).isFalse(); + assertThat(result.advertisementAgreed()).isFalse(); + } + @Test @DisplayName("신규 사용자 동의 상태를 저장한다") void saveUserConsentsCreatesUserConsent() { @@ -78,6 +117,37 @@ void saveUserConsentsCreatesUserConsent() { assertThat(saved.getWithdrawnAt()).isNull(); } + @Test + @DisplayName("마케팅 동의만 들어오면 마케팅 동의만 생성하고 광고 동의는 기존 상태를 유지한다") + void updateMarketingConsentUpdatesOnlyRequestedField() { + User user = TestFixtureFactory.user(10L, "user@test.com", "tester", null, null); + ConsentItem marketing = consentItem(1L, "MARKETING", true, false); + + when(userRepository.findActiveById(10L)).thenReturn(Optional.of(user)); + when(consentItemRepository.findAllByCodeInAndActiveTrue(List.of("MARKETING"))) + .thenReturn(List.of(marketing)); + when(userConsentRepository.findAllByUserIdAndConsentItemCodeIn( + 10L, + List.of("MARKETING") + )).thenReturn(List.of()); + when(userConsentRepository.findAllByUserIdAndConsentItemCodeIn( + 10L, + List.of("MARKETING", "ADVERTISEMENT") + )).thenReturn(List.of(userConsent(user, marketing, true))); + + MarketingConsentResult result = consentService.updateMarketingConsent( + 10L, + new MarketingConsentCommand(true, null) + ); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserConsent.class); + verify(userConsentRepository).save(captor.capture()); + assertThat(captor.getValue().getConsentItem()).isEqualTo(marketing); + assertThat(captor.getValue().isAgreed()).isTrue(); + assertThat(result.marketingAgreed()).isTrue(); + assertThat(result.advertisementAgreed()).isFalse(); + } + @Test @DisplayName("기존 사용자 동의 상태를 철회 상태로 갱신한다") void updateUserConsentsUpdatesExistingUserConsent() { @@ -94,10 +164,22 @@ void updateUserConsentsUpdatesExistingUserConsent() { consentService.updateUserConsents(10L, List.of(new UserConsentCommand(1L, false))); assertThat(existing.isAgreed()).isFalse(); + assertThat(existing.getAgreedAt()).isNull(); assertThat(existing.getWithdrawnAt()).isNotNull(); verify(userConsentRepository, never()).save(existing); } + @Test + @DisplayName("변경할 광고/마케팅 동의 값이 없으면 실패한다") + void updateMarketingConsentRejectsEmptyCommand() { + Throwable throwable = catchThrowable(() -> consentService.updateMarketingConsent( + 10L, + new MarketingConsentCommand(null, null) + )); + + ErrorCodeAssertions.assertErrorCode(throwable, ErrorCode.INVALID_PARAMETER_ERROR); + } + @Test @DisplayName("중복된 동의 항목 요청은 실패한다") void duplicateConsentItemThrows() { @@ -161,4 +243,13 @@ private ConsentItem consentItem(Long id, String code, boolean active, boolean re .required(required) .build(); } + + private UserConsent userConsent(User user, ConsentItem consentItem, boolean agreed) { + UserConsent consent = UserConsent.builder() + .user(user) + .consentItem(consentItem) + .build(); + consent.update(agreed, java.time.LocalDateTime.now()); + return consent; + } } diff --git a/src/test/java/com/mogak/spring/web/controller/ConsentControllerTest.java b/src/test/java/com/mogak/spring/web/controller/ConsentControllerTest.java index 07739d63..ad9a5a69 100644 --- a/src/test/java/com/mogak/spring/web/controller/ConsentControllerTest.java +++ b/src/test/java/com/mogak/spring/web/controller/ConsentControllerTest.java @@ -4,8 +4,10 @@ import com.mogak.spring.jwt.JwtTokenProvider; import com.mogak.spring.security.SecurityAuthority; import com.mogak.spring.service.ConsentService; +import com.mogak.spring.service.command.MarketingConsentCommand; import com.mogak.spring.service.command.UserConsentCommand; import com.mogak.spring.service.result.ConsentItemResult; +import com.mogak.spring.service.result.MarketingConsentResult; import com.mogak.spring.support.SecurityContextTestHelper; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -24,12 +26,14 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; 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.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -81,6 +85,24 @@ void getConsentsReturnsBaseResponseContract() throws Exception { .andExpect(jsonPath("$.result[1].required").value(false)); } + @Test + @DisplayName("현재 사용자의 광고와 마케팅 동의 상태를 조회한다") + void getMarketingConsentContract() throws Exception { + SecurityContextTestHelper.setAuthentication(10L, "user@test.com", SecurityAuthority.USER.getAuthority()); + when(consentService.getMarketingConsent(10L)) + .thenReturn(new MarketingConsentResult(true, false)); + + mockMvc.perform(get("/api/users/marketing-consent")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.time").exists()) + .andExpect(jsonPath("$.status").value("OK")) + .andExpect(jsonPath("$.code").value("success")) + .andExpect(jsonPath("$.message").value("요청에 성공했습니다.")) + .andExpect(jsonPath("$.result.marketingAgreed").value(true)) + .andExpect(jsonPath("$.result.advertisementAgreed").value(false)); + } + @Test @DisplayName("사용자 동의 변경 요청은 인증 유저와 동의 목록을 서비스로 전달한다") void updateUserConsentsForwardsAuthenticatedUserAndAgreements() throws Exception { @@ -104,6 +126,27 @@ void updateUserConsentsForwardsAuthenticatedUserAndAgreements() throws Exception ); } + @Test + @DisplayName("현재 사용자의 광고와 마케팅 동의 상태 중 요청된 값만 변경한다") + void patchMarketingConsentContract() throws Exception { + SecurityContextTestHelper.setAuthentication(10L, "user@test.com", SecurityAuthority.USER.getAuthority()); + when(consentService.updateMarketingConsent(anyLong(), any(MarketingConsentCommand.class))) + .thenReturn(new MarketingConsentResult(true, false)); + + mockMvc.perform(patch("/api/users/marketing-consent") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"marketingAgreed\":true}")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.result.marketingAgreed").value(true)) + .andExpect(jsonPath("$.result.advertisementAgreed").value(false)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(MarketingConsentCommand.class); + verify(consentService).updateMarketingConsent(org.mockito.Mockito.eq(10L), captor.capture()); + assertThat(captor.getValue().marketingAgreed()).isTrue(); + assertThat(captor.getValue().advertisementAgreed()).isNull(); + } + @Test @DisplayName("사용자 동의 변경 요청에 null 항목이 있으면 400을 반환한다") void updateUserConsentsRejectsNullAgreementItem() throws Exception { @@ -119,4 +162,18 @@ void updateUserConsentsRejectsNullAgreementItem() throws Exception { verify(consentService, never()).updateUserConsents(anyLong(), anyList()); } + + @Test + @DisplayName("변경할 동의 값이 없는 요청은 400을 반환한다") + void patchMarketingConsentRejectsEmptyRequest() throws Exception { + SecurityContextTestHelper.setAuthentication(10L, "user@test.com", SecurityAuthority.USER.getAuthority()); + + mockMvc.perform(patch("/api/users/marketing-consent") + .contentType(MediaType.APPLICATION_JSON) + .content("{}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("Z005")); + + verify(consentService, never()).updateMarketingConsent(anyLong(), any()); + } }