Skip to content
Merged

Dev #279

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 24 additions & 18 deletions src/main/java/com/pooli/permission/controller/RoleController.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,31 +88,37 @@ public class RoleController {
@PatchMapping("/representative")
public ResponseEntity<RepresentativeRoleTransferResDto> transferRepresentativeRole(
@AuthenticationPrincipal AuthUserDetails userDetails,
@Parameter(description = "현재 대표 회선 ID (ADMIN 필수, OWNER 무시)", example = "1001") @RequestParam(required = false) Long currentLineId,
@Parameter(description = "변경 대상 회선 ID", example = "1002") @RequestParam Long changeLineId,
HttpServletRequest request,
HttpServletResponse response) {
RepresentativeRoleTransferResDto result =
roleService.transferRepresentativeRole(userDetails.getLineId(), changeLineId);
roleService.transferRepresentativeRole(currentLineId, changeLineId, userDetails);

// 역할 양도 후 현재 사용자 세션의 ROLE_FAMILY_OWNER → ROLE_FAMILY_MEMBER 교체
List<GrantedAuthority> updatedAuthorities = userDetails.getAuthorities().stream()
.map(auth -> "ROLE_FAMILY_OWNER".equals(auth.getAuthority())
? new SimpleGrantedAuthority("ROLE_FAMILY_MEMBER")
: auth)
.collect(Collectors.toList());
// ADMIN이 대리 실행한 경우 세션 권한 변경 불필요
boolean isAdmin = userDetails.getAuthorities().stream()
.anyMatch(a -> "ROLE_ADMIN".equals(a.getAuthority()));
if (!isAdmin) {
// 역할 양도 후 현재 사용자 세션의 ROLE_FAMILY_OWNER → ROLE_FAMILY_MEMBER 교체
List<GrantedAuthority> updatedAuthorities = userDetails.getAuthorities().stream()
.map(auth -> "ROLE_FAMILY_OWNER".equals(auth.getAuthority())
? new SimpleGrantedAuthority("ROLE_FAMILY_MEMBER")
: auth)
.collect(Collectors.toList());

AuthUserDetails updatedUserDetails = AuthUserDetails.builder()
.userId(userDetails.getUserId())
.email(userDetails.getEmail())
.lineId(userDetails.getLineId())
.authorities(updatedAuthorities)
.build();
AuthUserDetails updatedUserDetails = AuthUserDetails.builder()
.userId(userDetails.getUserId())
.email(userDetails.getEmail())
.lineId(userDetails.getLineId())
.authorities(updatedAuthorities)
.build();

SecurityContext newContext = SecurityContextHolder.createEmptyContext();
newContext.setAuthentication(
new UsernamePasswordAuthenticationToken(updatedUserDetails, null, updatedAuthorities));
SecurityContextHolder.setContext(newContext);
securityContextRepository.saveContext(newContext, request, response);
SecurityContext newContext = SecurityContextHolder.createEmptyContext();
newContext.setAuthentication(
new UsernamePasswordAuthenticationToken(updatedUserDetails, null, updatedAuthorities));
SecurityContextHolder.setContext(newContext);
securityContextRepository.saveContext(newContext, request, response);
}

return ResponseEntity.ok(result);
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/pooli/permission/service/RoleService.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package com.pooli.permission.service;

import com.pooli.auth.service.AuthUserDetails;
import com.pooli.permission.domain.dto.response.RepresentativeRoleTransferResDto;

public interface RoleService {

// RY1-292: 대표자 권한 양도
RepresentativeRoleTransferResDto transferRepresentativeRole(Long currentLineId, Long changeLineId);
RepresentativeRoleTransferResDto transferRepresentativeRole(Long currentLineId, Long changeLineId, AuthUserDetails userDetails);
}
31 changes: 26 additions & 5 deletions src/main/java/com/pooli/permission/service/RoleServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.pooli.auth.service.AuthUserDetails;
import com.pooli.common.exception.ApplicationException;
import com.pooli.common.exception.CommonErrorCode;
import com.pooli.family.domain.entity.FamilyLine;
import com.pooli.family.domain.enums.FamilyRole;
import com.pooli.notification.domain.enums.AlarmCode;
Expand All @@ -14,7 +16,9 @@
import com.pooli.permission.mapper.FamilyLineMapper;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class RoleServiceImpl implements RoleService {
Expand All @@ -24,13 +28,14 @@ public class RoleServiceImpl implements RoleService {

@Override
@Transactional
public RepresentativeRoleTransferResDto transferRepresentativeRole(Long currentLineId, Long changeLineId) {
public RepresentativeRoleTransferResDto transferRepresentativeRole(Long currentLineId, Long changeLineId, AuthUserDetails userDetails) {
Long resolvedLineId = resolveCurrentLineId(currentLineId, userDetails);

if (currentLineId.equals(changeLineId)) {
if (resolvedLineId.equals(changeLineId)) {
throw new ApplicationException(PermissionErrorCode.ROLE_TRANSFER_SELF);
}

FamilyLine currentFamilyLine = familyLineMapper.findByLineId(currentLineId)
FamilyLine currentFamilyLine = familyLineMapper.findByLineId(resolvedLineId)
.orElseThrow(() -> new ApplicationException(PermissionErrorCode.ROLE_TRANSFER_SOURCE_NOT_FOUND));

FamilyLine targetFamilyLine = familyLineMapper.findByLineId(changeLineId)
Expand All @@ -56,13 +61,29 @@ public RepresentativeRoleTransferResDto transferRepresentativeRole(Long currentL
.role(FamilyRole.OWNER)
.build());

familyLineMapper.findAllFamilyIdByLineId(currentLineId)
familyLineMapper.findAllFamilyIdByLineId(resolvedLineId)
.forEach(lineId ->
alarmHistoryService.createAlarm(lineId, AlarmCode.PERMISSION, AlarmType.ROLE_TRANSFERRED));

return RepresentativeRoleTransferResDto.builder()
.currentOwnerLineId(currentLineId)
.currentOwnerLineId(resolvedLineId)
.changeOwnerLineId(changeLineId)
.build();
}

// ADMIN이면 currentLineId 필수, OWNER이면 세션 lineId 강제 사용
private Long resolveCurrentLineId(Long currentLineId, AuthUserDetails userDetails) {
boolean isAdmin = userDetails.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
if (isAdmin) {
if (currentLineId == null) {
throw new ApplicationException(CommonErrorCode.MISSING_REQUEST_PARAM);
}
return currentLineId;
}
if (currentLineId != null) {
log.warn("OWNER supplied currentLineId={} ignored, using session lineId={}", currentLineId, userDetails.getLineId());
}
return userDetails.getLineId();
}
}
93 changes: 81 additions & 12 deletions src/test/java/com/pooli/permission/service/RoleServiceImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

import com.pooli.auth.service.AuthUserDetails;
import com.pooli.common.exception.ApplicationException;
import com.pooli.common.exception.CommonErrorCode;
import com.pooli.family.domain.entity.FamilyLine;
import com.pooli.family.domain.enums.FamilyRole;
import com.pooli.notification.domain.enums.AlarmCode;
Expand Down Expand Up @@ -42,26 +44,42 @@ class RoleServiceImplTest {
@InjectMocks
private RoleServiceImpl roleService;

// OWNER 사용자 (lineId = 1001L)
private final AuthUserDetails ownerUser = AuthUserDetails.builder()
.userId(1L)
.email("user@example.com")
.lineId(1001L)
.authorities(AuthUserDetails.toAuthorities(List.of("USER", "FAMILY_OWNER")))
.build();

// ADMIN 사용자 (lineId = null)
private final AuthUserDetails adminUser = AuthUserDetails.builder()
.userId(100L)
.email("admin@example.com")
.lineId(null)
.authorities(AuthUserDetails.toAuthorities(List.of("ADMIN")))
.build();

@Test
@DisplayName("자기 자신에게 대표자 양도를 요청하면 PERMISSION-4004를 반환한다")
@DisplayName("OWNER: 자기 자신에게 대표자 양도를 요청하면 PERMISSION-4004를 반환한다")
void transferRepresentativeRole_selfTransfer_throwsRoleTransferSelf() {
ApplicationException ex = assertThrows(
ApplicationException.class,
() -> roleService.transferRepresentativeRole(1001L, 1001L)
() -> roleService.transferRepresentativeRole(null, 1001L, ownerUser)
);

assertThat(ex.getErrorCode()).isEqualTo(PermissionErrorCode.ROLE_TRANSFER_SELF);
verifyNoInteractions(familyLineMapper, alarmHistoryService);
}

@Test
@DisplayName("현재 대표 회선이 없으면 PERMISSION-4403을 반환한다")
@DisplayName("OWNER: 현재 대표 회선이 없으면 PERMISSION-4403을 반환한다")
void transferRepresentativeRole_sourceMissing_throwsSourceNotFound() {
when(familyLineMapper.findByLineId(1001L)).thenReturn(Optional.empty());

ApplicationException ex = assertThrows(
ApplicationException.class,
() -> roleService.transferRepresentativeRole(1001L, 1002L)
() -> roleService.transferRepresentativeRole(null, 1002L, ownerUser)
);

assertThat(ex.getErrorCode()).isEqualTo(PermissionErrorCode.ROLE_TRANSFER_SOURCE_NOT_FOUND);
Expand All @@ -71,15 +89,15 @@ void transferRepresentativeRole_sourceMissing_throwsSourceNotFound() {
}

@Test
@DisplayName("양도 대상 회선이 없으면 PERMISSION-4402를 반환한다")
@DisplayName("OWNER: 양도 대상 회선이 없으면 PERMISSION-4402를 반환한다")
void transferRepresentativeRole_targetMissing_throwsTargetNotFound() {
when(familyLineMapper.findByLineId(1001L))
.thenReturn(Optional.of(familyLine(1L, 1001L, FamilyRole.OWNER)));
when(familyLineMapper.findByLineId(1002L)).thenReturn(Optional.empty());

ApplicationException ex = assertThrows(
ApplicationException.class,
() -> roleService.transferRepresentativeRole(1001L, 1002L)
() -> roleService.transferRepresentativeRole(null, 1002L, ownerUser)
);

assertThat(ex.getErrorCode()).isEqualTo(PermissionErrorCode.ROLE_TRANSFER_TARGET_NOT_FOUND);
Expand All @@ -88,7 +106,7 @@ void transferRepresentativeRole_targetMissing_throwsTargetNotFound() {
}

@Test
@DisplayName("다른 가족 간 양도 요청이면 PERMISSION-4901을 반환한다")
@DisplayName("OWNER: 다른 가족 간 양도 요청이면 PERMISSION-4901을 반환한다")
void transferRepresentativeRole_differentFamily_throwsDifferentFamily() {
when(familyLineMapper.findByLineId(1001L))
.thenReturn(Optional.of(familyLine(1L, 1001L, FamilyRole.OWNER)));
Expand All @@ -97,7 +115,7 @@ void transferRepresentativeRole_differentFamily_throwsDifferentFamily() {

ApplicationException ex = assertThrows(
ApplicationException.class,
() -> roleService.transferRepresentativeRole(1001L, 1002L)
() -> roleService.transferRepresentativeRole(null, 1002L, ownerUser)
);

assertThat(ex.getErrorCode()).isEqualTo(PermissionErrorCode.ROLE_TRANSFER_DIFFERENT_FAMILY);
Expand All @@ -106,7 +124,7 @@ void transferRepresentativeRole_differentFamily_throwsDifferentFamily() {
}

@Test
@DisplayName("대상이 이미 OWNER면 PERMISSION-4902를 반환한다")
@DisplayName("OWNER: 대상이 이미 OWNER면 PERMISSION-4902를 반환한다")
void transferRepresentativeRole_targetAlreadyOwner_throwsAlreadyRepresentative() {
when(familyLineMapper.findByLineId(1001L))
.thenReturn(Optional.of(familyLine(1L, 1001L, FamilyRole.OWNER)));
Expand All @@ -115,7 +133,7 @@ void transferRepresentativeRole_targetAlreadyOwner_throwsAlreadyRepresentative()

ApplicationException ex = assertThrows(
ApplicationException.class,
() -> roleService.transferRepresentativeRole(1001L, 1002L)
() -> roleService.transferRepresentativeRole(null, 1002L, ownerUser)
);

assertThat(ex.getErrorCode()).isEqualTo(PermissionErrorCode.ROLE_TRANSFER_TARGET_ALREADY_REPRESENTATIVE);
Expand All @@ -124,7 +142,7 @@ void transferRepresentativeRole_targetAlreadyOwner_throwsAlreadyRepresentative()
}

@Test
@DisplayName("대표자 양도 성공 시 OWNER/MEMBER 역할을 교체하고 가족 전체에 ROLE_TRANSFERRED 알림을 보낸다")
@DisplayName("OWNER: 대표자 양도 성공 시 OWNER/MEMBER 역할을 교체하고 가족 전체에 ROLE_TRANSFERRED 알림을 보낸다")
void transferRepresentativeRole_success_updatesRoleAndSendsAlarmToFamilyMembers() {
when(familyLineMapper.findByLineId(1001L))
.thenReturn(Optional.of(familyLine(1L, 1001L, FamilyRole.OWNER)));
Expand All @@ -133,7 +151,7 @@ void transferRepresentativeRole_success_updatesRoleAndSendsAlarmToFamilyMembers(
when(familyLineMapper.findAllFamilyIdByLineId(1001L))
.thenReturn(List.of(1001L, 1002L, 1003L));

RepresentativeRoleTransferResDto result = roleService.transferRepresentativeRole(1001L, 1002L);
RepresentativeRoleTransferResDto result = roleService.transferRepresentativeRole(null, 1002L, ownerUser);

assertThat(result.getCurrentOwnerLineId()).isEqualTo(1001L);
assertThat(result.getChangeOwnerLineId()).isEqualTo(1002L);
Expand All @@ -156,6 +174,57 @@ void transferRepresentativeRole_success_updatesRoleAndSendsAlarmToFamilyMembers(
assertThat(alarmTargetCaptor.getAllValues()).containsExactly(1001L, 1002L, 1003L);
}

// --- ADMIN 케이스 ---

@Test
@DisplayName("ADMIN: currentLineId 없이 호출하면 COMMON:4004 필수 파라미터 누락을 반환한다")
void transferRepresentativeRole_admin_missingCurrentLineId_throwsMissingParam() {
ApplicationException ex = assertThrows(
ApplicationException.class,
() -> roleService.transferRepresentativeRole(null, 1002L, adminUser)
);

assertThat(ex.getErrorCode()).isEqualTo(CommonErrorCode.MISSING_REQUEST_PARAM);
verifyNoInteractions(familyLineMapper, alarmHistoryService);
}

@Test
@DisplayName("ADMIN: currentLineId를 지정하면 해당 회선 기준으로 양도가 정상 처리된다")
void transferRepresentativeRole_admin_withCurrentLineId_success() {
when(familyLineMapper.findByLineId(1001L))
.thenReturn(Optional.of(familyLine(1L, 1001L, FamilyRole.OWNER)));
when(familyLineMapper.findByLineId(1002L))
.thenReturn(Optional.of(familyLine(1L, 1002L, FamilyRole.MEMBER)));
when(familyLineMapper.findAllFamilyIdByLineId(1001L))
.thenReturn(List.of(1001L, 1002L));

RepresentativeRoleTransferResDto result = roleService.transferRepresentativeRole(1001L, 1002L, adminUser);

assertThat(result.getCurrentOwnerLineId()).isEqualTo(1001L);
assertThat(result.getChangeOwnerLineId()).isEqualTo(1002L);

ArgumentCaptor<FamilyLine> updateCaptor = ArgumentCaptor.forClass(FamilyLine.class);
verify(familyLineMapper, times(2)).updateRole(updateCaptor.capture());
assertThat(updateCaptor.getAllValues())
.extracting(FamilyLine::getLineId, FamilyLine::getRole)
.containsExactlyInAnyOrder(
tuple(1001L, FamilyRole.MEMBER),
tuple(1002L, FamilyRole.OWNER)
);
}

@Test
@DisplayName("ADMIN: 자기 자신에게 양도 시도 시 PERMISSION-4004를 반환한다")
void transferRepresentativeRole_admin_selfTransfer_throwsRoleTransferSelf() {
ApplicationException ex = assertThrows(
ApplicationException.class,
() -> roleService.transferRepresentativeRole(1001L, 1001L, adminUser)
);

assertThat(ex.getErrorCode()).isEqualTo(PermissionErrorCode.ROLE_TRANSFER_SELF);
verifyNoInteractions(familyLineMapper, alarmHistoryService);
}

private FamilyLine familyLine(Long familyId, Long lineId, FamilyRole role) {
return FamilyLine.builder()
.familyId(familyId)
Expand Down
Loading