Skip to content

Commit

Permalink
✨ [STMT-266] 관리자 위임 API 구현 (#150)
Browse files Browse the repository at this point in the history
* ✨ [STMT-266] 관리자 위임 API 구현

* ✅ [STMT-266] 관리자 위임 API 테스트 케이스 작성

* 📝 [STMT-266] 관리자 위임 API 명세서 작성
  • Loading branch information
05AM authored Sep 10, 2024
1 parent bc78785 commit 35f5dce
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 0 deletions.
28 changes: 28 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,34 @@ include::{snippets}/study-member-join/fail/not-exist-study/response-fields.adoc[
include::{snippets}/study-member-join/fail/not-exist-member/response-body.adoc[]
include::{snippets}/study-member-join/fail/not-exist-member/response-fields.adoc[]

=== 관리자 위임

다른 멤버에게 관리자를 위임하는 API입니다.

==== PATCH /v1/api/studies/{studyId}/members/{memberId}/admin/delegate

===== 요청
include::{snippets}/delegate-admin/success/http-request.adoc[]

====== 헤더
include::{snippets}/delegate-admin/success/request-headers.adoc[]

====== 경로 변수
include::{snippets}/delegate-admin/success/path-parameters.adoc[]

===== 응답 성공 (200)
include::{snippets}/delegate-admin/success/response-body.adoc[]
include::{snippets}/delegate-admin/success/response-fields.adoc[]

===== 응답 실패 (403)
.요청자가 해당 스터디 관리자가 아닌 경우
include::{snippets}/delegate-admin/fail/not-admin/response-body.adoc[]
include::{snippets}/delegate-admin/fail/not-admin/response-fields.adoc[]

.위임 대상자가 스터디 멤버가 아닌 경우
include::{snippets}/delegate-admin/fail/member-not-joined/response-body.adoc[]
include::{snippets}/delegate-admin/fail/member-not-joined/response-fields.adoc[]

== 파일 관리

=== Presigned URL 발급
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.stumeet.server.studymember.adapter.in.web;

import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import com.stumeet.server.common.annotation.WebAdapter;
import com.stumeet.server.common.auth.model.LoginMember;
import com.stumeet.server.common.model.ApiResponse;
import com.stumeet.server.common.response.SuccessCode;
import com.stumeet.server.studymember.application.port.in.AdminDelegationUseCase;

import lombok.RequiredArgsConstructor;

@WebAdapter
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class AdminDelegationApi {

private final AdminDelegationUseCase adminDelegationUseCase;

@PatchMapping("/studies/{studyId}/members/{memberId}/admin/delegate")
public ResponseEntity<ApiResponse<Void>> delegateAdmin(
@AuthenticationPrincipal LoginMember member,
@PathVariable Long studyId,
@PathVariable Long memberId
) {
adminDelegationUseCase.delegateAdmin(studyId, member.getId(), memberId);

return ResponseEntity.ok(ApiResponse.success(SuccessCode.UPDATE_SUCCESS));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.stumeet.server.studymember.adapter.out.persistence;

import com.stumeet.server.common.annotation.PersistenceAdapter;
import com.stumeet.server.studymember.application.port.out.AdminManagementPort;

import lombok.RequiredArgsConstructor;

@PersistenceAdapter
@RequiredArgsConstructor
public class AdminManagementAdapter implements AdminManagementPort {

private final JpaStudyMemberRepository jpaStudyMemberRepository;

@Override
public void delegateAdmin(Long studyId, Long adminId, Long memberId) {
jpaStudyMemberRepository.removeAdmin(studyId, adminId);
jpaStudyMemberRepository.appointAdmin(studyId, memberId);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
package com.stumeet.server.studymember.adapter.out.persistence;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

public interface JpaStudyMemberRepository extends JpaRepository<StudyMemberJpaEntity, Long>, JpaStudyMemberRepositoryCustom {
long countByStudyId(Long studyId);

void deleteByStudyIdAndMemberId(Long studyId, Long memberId);

void deleteAllByStudyId(Long studyId);

@Modifying
@Query("UPDATE StudyMemberJpaEntity sm "
+ "SET sm.isAdmin = false "
+ "WHERE sm.study.id = :studyId "
+ "AND sm.member.id = :adminId")
void removeAdmin(Long studyId, Long adminId);

@Modifying
@Query("UPDATE StudyMemberJpaEntity sm "
+ "SET sm.isAdmin = true "
+ "WHERE sm.study.id = :studyId "
+ "AND sm.member.id = :memberId")
void appointAdmin(Long studyId, Long memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.stumeet.server.studymember.application.port.in;

public interface AdminDelegationUseCase {

void delegateAdmin(Long studyId, Long adminId, Long memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.stumeet.server.studymember.application.port.out;

public interface AdminManagementPort {

void delegateAdmin(Long studyId, Long adminId, Long memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.stumeet.server.studymember.application.service;

import org.springframework.transaction.annotation.Transactional;

import com.stumeet.server.common.annotation.UseCase;
import com.stumeet.server.studymember.application.port.in.AdminDelegationUseCase;
import com.stumeet.server.studymember.application.port.in.StudyMemberValidationUseCase;
import com.stumeet.server.studymember.application.port.out.AdminManagementPort;

import lombok.RequiredArgsConstructor;

@Transactional
@UseCase
@RequiredArgsConstructor
public class AdminDelegationService implements AdminDelegationUseCase {

private final StudyMemberValidationUseCase studyMemberValidationUseCase;
private final AdminManagementPort adminManagementPort;

@Override
public void delegateAdmin(Long studyId, Long adminId, Long memberId) {
studyMemberValidationUseCase.checkAdmin(studyId, adminId);
studyMemberValidationUseCase.checkStudyJoinMember(studyId, memberId);

adminManagementPort.delegateAdmin(studyId, adminId, memberId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.stumeet.server.studymember.adapter.in.web;

import static org.springframework.restdocs.headers.HeaderDocumentation.*;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
import static org.springframework.restdocs.request.RequestDocumentation.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import com.stumeet.server.common.auth.model.AuthenticationHeader;
import com.stumeet.server.helper.WithMockMember;
import com.stumeet.server.stub.MemberStub;
import com.stumeet.server.stub.StudyStub;
import com.stumeet.server.stub.TokenStub;
import com.stumeet.server.template.ApiTest;

class AdminDelegationApiTest extends ApiTest {

@Nested
@DisplayName("관리자 위임")
class DelegateAdmin {
private static final String PATH = "/api/v1/studies/{studyId}/members/{memberId}/admin/delegate";

@DisplayName("[성공] 관리자 위임에 성공한다.")
@Test
@WithMockMember
void success() throws Exception {
Long studyId = StudyStub.getStudyId();
Long memberId = 2L;

mockMvc.perform(patch(PATH, studyId, memberId)
.header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken()))
.andExpect(status().isOk())
.andDo(document("delegate-admin/success",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
requestHeaders(
headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰")
),
pathParameters(
parameterWithName("studyId").description("스터디 ID"),
parameterWithName("memberId").description("관리자 위임할 멤버 ID")
),
responseFields(
fieldWithPath("code").description("응답 코드"),
fieldWithPath("message").description("응답 메시지")
)));
}

@DisplayName("[실패] 요청자가 해당 스터디의 관리자가 아닌 경우 관리자 위임에 실패한다.")
@Test
@WithMockMember(id = 2L)
void fail_when_not_admin() throws Exception {
Long studyId = StudyStub.getStudyId();
Long memberId = MemberStub.getMemberId();

mockMvc.perform(patch(PATH, studyId, memberId)
.header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken()))
.andExpect(status().isForbidden())
.andDo(document("delegate-admin/fail/not-admin",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
pathParameters(
parameterWithName("studyId").description("스터디 ID"),
parameterWithName("memberId").description("관리자 위임할 멤버 ID")
),
responseFields(
fieldWithPath("code").description("응답 코드"),
fieldWithPath("message").description("응답 메시지")
)));
}

@DisplayName("[실패] 위임 대상자가 스터디 멤버가 아닌 경우 관리자 위임에 실패한다.")
@Test
@WithMockMember
void fail_when_not_study_member() throws Exception {
Long studyId = StudyStub.getStudyId();
Long memberId = 3L;

mockMvc.perform(patch(PATH, studyId, memberId)
.header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken()))
.andExpect(status().isForbidden())
.andDo(document("delegate-admin/fail/member-not-joined",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
pathParameters(
parameterWithName("studyId").description("스터디 ID"),
parameterWithName("memberId").description("관리자 위임할 멤버 ID")
),
responseFields(
fieldWithPath("code").description("응답 코드"),
fieldWithPath("message").description("응답 메시지")
)));
}
}
}

0 comments on commit 35f5dce

Please sign in to comment.