diff --git a/backend/src/docs/asciidoc/admin.adoc b/backend/src/docs/asciidoc/admin.adoc index 10cd1f758..a249e659b 100644 --- a/backend/src/docs/asciidoc/admin.adoc +++ b/backend/src/docs/asciidoc/admin.adoc @@ -1,7 +1,47 @@ == 어드민 API +=== 어드민 로그인 + +==== curl + +include::{snippets}/admin/auth/login/curl-request.adoc[] + +==== request + +include::{snippets}/admin/auth/login/http-request.adoc[] + +request fields + +include::{snippets}/admin/auth/login/request-fields.adoc[] + +==== response + +include::{snippets}/admin/auth/login/http-response.adoc[] + +''' + +=== 어드민 로그아웃 + +CAUTION: 해당 API는 '어드민 로그인'으로 세션을 발급 받은 후에 이용해야 합니다 + +==== curl + +include::{snippets}/admin/auth/logout/curl-request.adoc[] + +==== request + +include::{snippets}/admin/auth/logout/http-request.adoc[] + +==== response + +include::{snippets}/admin/auth/logout/http-response.adoc[] + +''' + === 밸런스 게임 콘텐츠들 조회 +CAUTION: 해당 API는 '어드민 로그인'으로 세션을 발급 받은 후에 이용해야 합니다 + ==== curl include::{snippets}/admin/balance/get/curl-request.adoc[] @@ -26,6 +66,8 @@ include::{snippets}/admin/balance/get/response-fields.adoc[] === 밸런스 게임 콘텐츠 생성 +CAUTION: 해당 API는 '어드민 로그인'으로 세션을 발급 받은 후에 이용해야 합니다 + ==== curl include::{snippets}/admin/balance/create/curl-request.adoc[] @@ -50,6 +92,8 @@ include::{snippets}/admin/balance/create/response-fields.adoc[] === 밸런스 게임 콘텐츠 수정 +CAUTION: 해당 API는 '어드민 로그인'으로 세션을 발급 받은 후에 이용해야 합니다 + ==== curl include::{snippets}/admin/balance/patch-content/curl-request.adoc[] @@ -74,6 +118,8 @@ include::{snippets}/admin/balance/patch-content/response-fields.adoc[] === 밸런스 게임 선택지 수정 +CAUTION: 해당 API는 '어드민 로그인'으로 세션을 발급 받은 후에 이용해야 합니다 + ==== curl include::{snippets}/admin/balance/patch-option/curl-request.adoc[] @@ -98,6 +144,8 @@ include::{snippets}/admin/balance/patch-option/response-fields.adoc[] === 밸런스 게임 선택지 삭제 +CAUTION: 해당 API는 '어드민 로그인'으로 세션을 발급 받은 후에 이용해야 합니다 + ==== curl include::{snippets}/admin/balance/delete/curl-request.adoc[] diff --git a/backend/src/main/java/ddangkong/config/AdminAuthConfig.java b/backend/src/main/java/ddangkong/config/AdminAuthConfig.java new file mode 100644 index 000000000..37c9b0fe9 --- /dev/null +++ b/backend/src/main/java/ddangkong/config/AdminAuthConfig.java @@ -0,0 +1,19 @@ +package ddangkong.config; + +import ddangkong.controller.admin.AdminAuthorizationInterceptor; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class AdminAuthConfig implements WebMvcConfigurer { + + private final AdminAuthorizationInterceptor adminAuthorizationInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(adminAuthorizationInterceptor); + } +} diff --git a/backend/src/main/java/ddangkong/controller/admin/Admin.java b/backend/src/main/java/ddangkong/controller/admin/Admin.java new file mode 100644 index 000000000..fe9739a3f --- /dev/null +++ b/backend/src/main/java/ddangkong/controller/admin/Admin.java @@ -0,0 +1,14 @@ +package ddangkong.controller.admin; + +import java.io.Serializable; +import lombok.Getter; + +@Getter +public class Admin implements Serializable { + + private final String nickname; + + public Admin(String nickname) { + this.nickname = nickname; + } +} diff --git a/backend/src/main/java/ddangkong/controller/admin/AdminAuth.java b/backend/src/main/java/ddangkong/controller/admin/AdminAuth.java new file mode 100644 index 000000000..d43912686 --- /dev/null +++ b/backend/src/main/java/ddangkong/controller/admin/AdminAuth.java @@ -0,0 +1,12 @@ +package ddangkong.controller.admin; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface AdminAuth { + +} diff --git a/backend/src/main/java/ddangkong/controller/admin/AdminAuthorizationInterceptor.java b/backend/src/main/java/ddangkong/controller/admin/AdminAuthorizationInterceptor.java new file mode 100644 index 000000000..ec3d3ade5 --- /dev/null +++ b/backend/src/main/java/ddangkong/controller/admin/AdminAuthorizationInterceptor.java @@ -0,0 +1,46 @@ +package ddangkong.controller.admin; + +import ddangkong.exception.admin.NotExistAdminSessionException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; + +@Component +public class AdminAuthorizationInterceptor implements HandlerInterceptor { + + private final String sessionKey; + + public AdminAuthorizationInterceptor(@Value("${admin.session-key}") String sessionKey) { + this.sessionKey = sessionKey; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if (hasAdminAuthAnnotation(handler)) { + authorizeAdmin(request); + } + return true; + } + + private boolean hasAdminAuthAnnotation(Object handler) { + if (handler instanceof ResourceHttpRequestHandler) { + return false; + } + + HandlerMethod handlerMethod = (HandlerMethod) handler; + return handlerMethod.getMethodAnnotation(AdminAuth.class) != null || + handlerMethod.getBeanType().getAnnotation(AdminAuth.class) != null; + } + + private void authorizeAdmin(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session == null || session.getAttribute(sessionKey) == null) { + throw new NotExistAdminSessionException(); + } + } +} diff --git a/backend/src/main/java/ddangkong/controller/admin/AdminController.java b/backend/src/main/java/ddangkong/controller/admin/AdminController.java new file mode 100644 index 000000000..dfeb5a140 --- /dev/null +++ b/backend/src/main/java/ddangkong/controller/admin/AdminController.java @@ -0,0 +1,48 @@ +package ddangkong.controller.admin; + +import ddangkong.facade.admin.AdminService; +import ddangkong.facade.admin.dto.AdminLoginRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +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; + +@Slf4j +@RestController +@RequestMapping("/api") +public class AdminController { + + private final AdminService adminService; + + private final String sessionKey; + + public AdminController(AdminService adminService, @Value("${admin.session-key}") String sessionKey) { + this.adminService = adminService; + this.sessionKey = sessionKey; + } + + @PostMapping("/admin/login") + public void login(@RequestBody AdminLoginRequest loginRequest, HttpServletRequest httpRequest) { + adminService.validatePassword(loginRequest); + + Admin admin = new Admin(loginRequest.nickname()); + HttpSession session = httpRequest.getSession(); + session.setAttribute(sessionKey, admin); + log.info("어드민이 로그인 했습니다. nickname = {}, session = {}", loginRequest.nickname(), session.getId()); + } + + @AdminAuth + @PostMapping("/admin/logout") + public void logout(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session != null) { + Admin admin = (Admin) session.getAttribute(sessionKey); + session.invalidate(); + log.info("어드민이 로그아웃 했습니다. nickname = {}, session = {}", admin.getNickname(), session.getId()); + } + } +} diff --git a/backend/src/main/java/ddangkong/controller/balance/AdminBalanceController.java b/backend/src/main/java/ddangkong/controller/balance/AdminBalanceController.java index 568f4a13b..9473aba8d 100644 --- a/backend/src/main/java/ddangkong/controller/balance/AdminBalanceController.java +++ b/backend/src/main/java/ddangkong/controller/balance/AdminBalanceController.java @@ -1,9 +1,10 @@ package ddangkong.controller.balance; +import ddangkong.controller.admin.AdminAuth; import ddangkong.domain.balance.content.Category; import ddangkong.facade.balance.AdminBalanceContentFacade; -import ddangkong.facade.balance.dto.BalanceContentCreateResponse; import ddangkong.facade.balance.dto.BalanceContentCreateRequest; +import ddangkong.facade.balance.dto.BalanceContentCreateResponse; import ddangkong.facade.balance.dto.BalanceContentPatchRequest; import ddangkong.facade.balance.dto.BalanceContentPatchResponse; import ddangkong.facade.balance.dto.BalanceContentsAdminResponse; @@ -29,27 +30,32 @@ public class AdminBalanceController { private final AdminBalanceContentFacade adminBalanceContentFacade; + @AdminAuth @GetMapping("/admin/balances/contents") public BalanceContentsAdminResponse getContents(@RequestParam Category category) { return adminBalanceContentFacade.getContents(category); } + @AdminAuth @ResponseStatus(HttpStatus.CREATED) @PostMapping("/admin/balances/contents") public BalanceContentCreateResponse createContent(@RequestBody BalanceContentCreateRequest request) { return adminBalanceContentFacade.createContent(request); } + @AdminAuth @PatchMapping("/admin/balances/contents") public BalanceContentPatchResponse updateContent(@RequestBody BalanceContentPatchRequest request) { return adminBalanceContentFacade.updateContent(request); } + @AdminAuth @PatchMapping("/admin/balances/options") public BalanceOptionPatchResponse updateOption(@RequestBody BalanceOptionPatchRequest request) { return adminBalanceContentFacade.updateOption(request); } + @AdminAuth @ResponseStatus(HttpStatus.NO_CONTENT) @DeleteMapping("/admin/balances/contents/{contentId}") public void deleteContent(@PathVariable long contentId) { diff --git a/backend/src/main/java/ddangkong/controller/exception/ErrorResponse.java b/backend/src/main/java/ddangkong/controller/exception/ErrorResponse.java index 5b553869a..865fa5194 100644 --- a/backend/src/main/java/ddangkong/controller/exception/ErrorResponse.java +++ b/backend/src/main/java/ddangkong/controller/exception/ErrorResponse.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import ddangkong.exception.BadRequestException; import ddangkong.exception.ClientErrorCode; +import ddangkong.exception.UnauthorizedException; import jakarta.validation.ConstraintViolation; import jakarta.validation.Path; import java.util.List; @@ -24,6 +25,10 @@ public ErrorResponse(T e) { this(e.getErrorCode(), e.getMessage(), null, null); } + public ErrorResponse(T e) { + this(e.getErrorCode(), e.getMessage(), null, null); + } + public ErrorResponse(ClientErrorCode errorCode) { this(errorCode, null, null); } diff --git a/backend/src/main/java/ddangkong/controller/exception/GlobalExceptionHandler.java b/backend/src/main/java/ddangkong/controller/exception/GlobalExceptionHandler.java index 04fe40b57..1e7434957 100644 --- a/backend/src/main/java/ddangkong/controller/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/ddangkong/controller/exception/GlobalExceptionHandler.java @@ -3,6 +3,7 @@ import ddangkong.exception.BadRequestException; import ddangkong.exception.ClientErrorCode; import ddangkong.exception.InternalServerException; +import ddangkong.exception.UnauthorizedException; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.connector.ClientAbortException; @@ -77,6 +78,15 @@ public ErrorResponse handleNoResourceFoundException(NoResourceFoundException e) return new ErrorResponse(ClientErrorCode.NO_RESOURCE_FOUND); } + @ExceptionHandler + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public ErrorResponse handleUnauthorizedException(UnauthorizedException e) { + log.warn(e.getMessage()); + + return new ErrorResponse(e); + } + + @ExceptionHandler @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) public ErrorResponse handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { diff --git a/backend/src/main/java/ddangkong/exception/ClientErrorCode.java b/backend/src/main/java/ddangkong/exception/ClientErrorCode.java index da4cfa736..54bc19e56 100644 --- a/backend/src/main/java/ddangkong/exception/ClientErrorCode.java +++ b/backend/src/main/java/ddangkong/exception/ClientErrorCode.java @@ -54,6 +54,8 @@ public enum ClientErrorCode { INVALID_COOKIE("유효하지 않는 쿠키입니다."), // ADMIN TODO Enum 분리, Docs 분리 + NOT_MATCH_ADMIN_PASSWORD("어드민 비밀번호가 일치하지 않습니다."), + NOT_EXIST_ADMIN_SESSION("어드민으로 로그인하지 않았습니다."), BLANK_BALANCE_CONTENT_NAME("컨텐츠 이름이 비어 있습니다."), LONG_BALANCE_CONTENT_NAME("컨텐츠 이름은 최대 %d자 입니다."), BLANK_BALANCE_OPTION_NAME("컨텐츠 옵션이 비어 있습니다."), diff --git a/backend/src/main/java/ddangkong/exception/UnauthorizedException.java b/backend/src/main/java/ddangkong/exception/UnauthorizedException.java new file mode 100644 index 000000000..f8ca5ccc1 --- /dev/null +++ b/backend/src/main/java/ddangkong/exception/UnauthorizedException.java @@ -0,0 +1,10 @@ +package ddangkong.exception; + +public abstract class UnauthorizedException extends RuntimeException { + + public UnauthorizedException(String message) { + super(message); + } + + public abstract String getErrorCode(); +} diff --git a/backend/src/main/java/ddangkong/exception/admin/NotExistAdminSessionException.java b/backend/src/main/java/ddangkong/exception/admin/NotExistAdminSessionException.java new file mode 100644 index 000000000..862e6b04c --- /dev/null +++ b/backend/src/main/java/ddangkong/exception/admin/NotExistAdminSessionException.java @@ -0,0 +1,17 @@ +package ddangkong.exception.admin; + +import static ddangkong.exception.ClientErrorCode.NOT_EXIST_ADMIN_SESSION; + +import ddangkong.exception.UnauthorizedException; + +public class NotExistAdminSessionException extends UnauthorizedException { + + public NotExistAdminSessionException() { + super(NOT_EXIST_ADMIN_SESSION.getMessage()); + } + + @Override + public String getErrorCode() { + return NOT_EXIST_ADMIN_SESSION.name(); + } +} diff --git a/backend/src/main/java/ddangkong/exception/admin/NotMatchAdminPasswordException.java b/backend/src/main/java/ddangkong/exception/admin/NotMatchAdminPasswordException.java new file mode 100644 index 000000000..2c985229b --- /dev/null +++ b/backend/src/main/java/ddangkong/exception/admin/NotMatchAdminPasswordException.java @@ -0,0 +1,17 @@ +package ddangkong.exception.admin; + +import static ddangkong.exception.ClientErrorCode.NOT_MATCH_ADMIN_PASSWORD; + +import ddangkong.exception.BadRequestException; + +public class NotMatchAdminPasswordException extends BadRequestException { + + public NotMatchAdminPasswordException() { + super(NOT_MATCH_ADMIN_PASSWORD.getMessage()); + } + + @Override + public String getErrorCode() { + return NOT_MATCH_ADMIN_PASSWORD.name(); + } +} diff --git a/backend/src/main/java/ddangkong/facade/admin/AdminService.java b/backend/src/main/java/ddangkong/facade/admin/AdminService.java new file mode 100644 index 000000000..34af65f8d --- /dev/null +++ b/backend/src/main/java/ddangkong/facade/admin/AdminService.java @@ -0,0 +1,22 @@ +package ddangkong.facade.admin; + +import ddangkong.exception.admin.NotMatchAdminPasswordException; +import ddangkong.facade.admin.dto.AdminLoginRequest; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class AdminService { + + private final String adminPassword; + + public AdminService(@Value("${admin.password}") String adminPassword) { + this.adminPassword = adminPassword; + } + + public void validatePassword(AdminLoginRequest request) { + if (!adminPassword.equals(request.password())) { + throw new NotMatchAdminPasswordException(); + } + } +} diff --git a/backend/src/main/java/ddangkong/facade/admin/dto/AdminLoginRequest.java b/backend/src/main/java/ddangkong/facade/admin/dto/AdminLoginRequest.java new file mode 100644 index 000000000..eb5f2cc02 --- /dev/null +++ b/backend/src/main/java/ddangkong/facade/admin/dto/AdminLoginRequest.java @@ -0,0 +1,4 @@ +package ddangkong.facade.admin.dto; + +public record AdminLoginRequest(String nickname, String password) { +} diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index fb150d2e2..e7853e0f6 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -28,6 +28,10 @@ encrypt: secret-key: ${secret.encrypt.secret-key} algorithm: ${secret.encrypt.algorithm} +admin: + session-key : admin + password : ${secret.admin.password} + logging: config: classpath:logback-dev.xml location : ${secret.application.log.location} diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index cb8e17aa6..d7369aa59 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -30,6 +30,10 @@ encrypt: secret-key: ${secret.encrypt.secret-key} algorithm: ${secret.encrypt.algorithm} +admin: + session-key : admin + password : ${secret.admin.password} + logging: config: classpath:logback-prod.xml discord: diff --git a/backend/src/test/java/ddangkong/controller/admin/AdminControllerTest.java b/backend/src/test/java/ddangkong/controller/admin/AdminControllerTest.java new file mode 100644 index 000000000..1dbe5e78f --- /dev/null +++ b/backend/src/test/java/ddangkong/controller/admin/AdminControllerTest.java @@ -0,0 +1,53 @@ +package ddangkong.controller.admin; + +import ddangkong.facade.admin.dto.AdminLoginRequest; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class AdminControllerTest extends BaseAdminControllerTest { + + @Nested + class 어드민_로그인 { + + @Test + void 비밀번호가_일치하면_로그인_할_수_있다() { + // given + AdminLoginRequest body = new AdminLoginRequest("이든", adminPassword); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(body) + .when().post("/api/admin/login") + .then().log().all() + .statusCode(200) + .cookie("JSESSIONID"); + } + } + + @Nested + class 어드민_로그아웃 { + + @Test + void 이미_로그인_한_유저는_로그아웃_할_수_있다() { + // when & then + RestAssured.given().log().all() + .sessionId(sessionId) + .when().post("/api/admin/logout") + .then().log().all() + .statusCode(200); + } + + @Test + void 로그인_하지_않은_유저는_로그아웃_시도시_에러를_발생한다() { + // when & then + RestAssured.given().log().all() + .cookie("JSESSIONID", "NO-SESSION") + .when().post("/api/admin/logout") + .then().log().all() + .statusCode(401); + } + } +} diff --git a/backend/src/test/java/ddangkong/controller/admin/BaseAdminControllerTest.java b/backend/src/test/java/ddangkong/controller/admin/BaseAdminControllerTest.java new file mode 100644 index 000000000..71fa53019 --- /dev/null +++ b/backend/src/test/java/ddangkong/controller/admin/BaseAdminControllerTest.java @@ -0,0 +1,29 @@ +package ddangkong.controller.admin; + +import ddangkong.controller.BaseControllerTest; +import ddangkong.facade.admin.dto.AdminLoginRequest; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Value; + +public abstract class BaseAdminControllerTest extends BaseControllerTest { + + @Value("${admin.password}") + protected String adminPassword; + + protected String sessionId; + + @BeforeEach + void 세션_발급() { + AdminLoginRequest body = new AdminLoginRequest("이든", adminPassword); + sessionId = RestAssured.given() + .contentType(ContentType.JSON) + .body(body) + .when().post("/api/admin/login") + .then() + .statusCode(200) + .extract() + .cookie("JSESSIONID"); + } +} diff --git a/backend/src/test/java/ddangkong/controller/balance/AdminBalanceControllerTest.java b/backend/src/test/java/ddangkong/controller/balance/AdminBalanceControllerTest.java index bf02492c5..0f640e141 100644 --- a/backend/src/test/java/ddangkong/controller/balance/AdminBalanceControllerTest.java +++ b/backend/src/test/java/ddangkong/controller/balance/AdminBalanceControllerTest.java @@ -3,7 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; -import ddangkong.controller.BaseControllerTest; +import ddangkong.controller.admin.BaseAdminControllerTest; import ddangkong.domain.balance.content.BalanceContent; import ddangkong.domain.balance.content.Category; import ddangkong.domain.balance.option.BalanceOption; @@ -19,7 +19,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -class AdminBalanceControllerTest extends BaseControllerTest { +class AdminBalanceControllerTest extends BaseAdminControllerTest { @Nested class 밸런스_게임_컨텐츠_조회 { @@ -41,6 +41,7 @@ class 밸런스_게임_컨텐츠_조회 { // when BalanceContentsAdminResponse actual = RestAssured.given() .queryParam("category", content1.getCategory()) + .sessionId(sessionId) .get("/api/admin/balances/contents") .then().contentType(ContentType.JSON).log().all() .statusCode(200) @@ -62,6 +63,7 @@ class 밸런스_게임_질문지_추가 { // when BalanceContentCreateResponse actual = RestAssured.given() + .sessionId(sessionId) .contentType(ContentType.JSON) .body(request) .post("/api/admin/balances/contents") @@ -75,8 +77,8 @@ class 밸런스_게임_질문지_추가 { () -> assertThat(actual.category()).isEqualTo(request.category()), () -> assertThat(actual.firstOption().name()).isEqualTo(request.firstOption()), () -> assertThat(actual.secondOption().name()).isEqualTo(request.secondOption()), - () -> assertThat(actual.firstOption().count()).isEqualTo(0), - () -> assertThat(actual.firstOption().percent()).isEqualTo(0) + () -> assertThat(actual.firstOption().count()).isZero(), + () -> assertThat(actual.firstOption().percent()).isZero() ); } } @@ -92,6 +94,7 @@ class 밸런스_게임_질문지_변경 { // when BalanceContentPatchResponse actual = RestAssured.given() + .sessionId(sessionId) .contentType(ContentType.JSON) .body(request) .patch("/api/admin/balances/contents") @@ -119,6 +122,7 @@ class 밸런스_게임_선택지_변경 { // when BalanceOptionPatchResponse actual = RestAssured.given() + .sessionId(sessionId) .contentType(ContentType.JSON) .body(request) .patch("/api/admin/balances/options") @@ -149,6 +153,7 @@ class 밸런스_게임_컨텐츠_삭제 { // when & then RestAssured.given() .pathParam("contentId", content.getId()) + .sessionId(sessionId) .delete("/api/admin/balances/contents/{contentId}") .then().log().all() .statusCode(204); diff --git a/backend/src/test/java/ddangkong/documentation/admin/AdminDocumentationTest.java b/backend/src/test/java/ddangkong/documentation/admin/AdminDocumentationTest.java new file mode 100644 index 000000000..2c45d81df --- /dev/null +++ b/backend/src/test/java/ddangkong/documentation/admin/AdminDocumentationTest.java @@ -0,0 +1,67 @@ +package ddangkong.documentation.admin; + +import static org.mockito.Mockito.doNothing; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import ddangkong.controller.admin.AdminController; +import ddangkong.facade.admin.dto.AdminLoginRequest; +import ddangkong.facade.admin.AdminService; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; + +@WebMvcTest(value = AdminController.class) +public class AdminDocumentationTest extends BaseAdminDocumentationTest { + + @MockBean + private AdminService adminService; + + @Nested + class 어드민_로그인 { + + private static final String ENDPOINT = "/api/admin/login"; + + @Test + void 로그인_할_수_있다() throws Exception { + // given + AdminLoginRequest request = new AdminLoginRequest("이든", "password"); + String content = objectMapper.writeValueAsString(request); + doNothing().when(adminService).validatePassword(request); + + // when & then + mockMvc.perform(post(ENDPOINT) + .content(content) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andDo(document("admin/auth/login", + requestFields( + fieldWithPath("nickname").type(STRING).description("닉네임"), + fieldWithPath("password").type(STRING).description("어드민 비밀번호") + ) + )); + } + } + + @Nested + class 어드민_로그아웃 { + + private static final String ENDPOINT = "/api/admin/logout"; + + @Test + void 로그아웃_할_수_있다() throws Exception { + // when & then + mockMvc.perform(post(ENDPOINT) + .session(session)) + .andExpect(status().isOk()) + .andDo(document("admin/auth/logout")); + } + } +} diff --git a/backend/src/test/java/ddangkong/documentation/admin/BaseAdminDocumentationTest.java b/backend/src/test/java/ddangkong/documentation/admin/BaseAdminDocumentationTest.java new file mode 100644 index 000000000..b2f517aa9 --- /dev/null +++ b/backend/src/test/java/ddangkong/documentation/admin/BaseAdminDocumentationTest.java @@ -0,0 +1,21 @@ +package ddangkong.documentation.admin; + +import ddangkong.controller.admin.Admin; +import ddangkong.documentation.BaseDocumentationTest; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mock.web.MockHttpSession; + +public abstract class BaseAdminDocumentationTest extends BaseDocumentationTest { + + @Value("${admin.session-key}") + private String sessionKey; + + protected MockHttpSession session; + + @BeforeEach + void setSession() { + session = new MockHttpSession(); + session.setAttribute(sessionKey, new Admin("admin")); + } +} diff --git a/backend/src/test/java/ddangkong/documentation/balance/AdminBalanceDocumentationTest.java b/backend/src/test/java/ddangkong/documentation/balance/AdminBalanceDocumentationTest.java index 704b35232..e8ce928a5 100644 --- a/backend/src/test/java/ddangkong/documentation/balance/AdminBalanceDocumentationTest.java +++ b/backend/src/test/java/ddangkong/documentation/balance/AdminBalanceDocumentationTest.java @@ -19,7 +19,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import ddangkong.controller.balance.AdminBalanceController; -import ddangkong.documentation.BaseDocumentationTest; +import ddangkong.documentation.admin.BaseAdminDocumentationTest; import ddangkong.domain.balance.content.Category; import ddangkong.facade.balance.AdminBalanceContentFacade; import ddangkong.facade.balance.dto.BalanceContentAdminResponse; @@ -39,7 +39,7 @@ import org.springframework.http.MediaType; @WebMvcTest(value = AdminBalanceController.class) -public class AdminBalanceDocumentationTest extends BaseDocumentationTest { +public class AdminBalanceDocumentationTest extends BaseAdminDocumentationTest { @MockBean private AdminBalanceContentFacade adminBalanceContentFacade; @@ -64,6 +64,7 @@ class 밸런스_게임_컨텐츠_조회 { // when & then mockMvc.perform(get(ENDPOINT) .queryParam("category", category.name()) + .session(session) ) .andExpect(status().isOk()) .andDo(document("admin/balance/get", @@ -112,6 +113,7 @@ class 밸런스_게임_질문지_추가 { // when & then mockMvc.perform(post(ENDPOINT) + .session(session) .content(content) .contentType(MediaType.APPLICATION_JSON) ) @@ -154,6 +156,7 @@ class 밸런스_게임_질문지_변경 { // when & then mockMvc.perform(patch(ENDPOINT) + .session(session) .content(content) .contentType(MediaType.APPLICATION_JSON) ) @@ -185,6 +188,7 @@ class 밸런스_게임_선택지_변경 { // when & then mockMvc.perform(patch(ENDPOINT) + .session(session) .content(content) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) @@ -211,7 +215,7 @@ class 밸런스_게임_컨텐츠_삭제 { doNothing().when(adminBalanceContentFacade).deleteContent(contentId); // when & then - mockMvc.perform(delete(ENDPOINT, contentId)) + mockMvc.perform(delete(ENDPOINT, contentId).session(session)) .andExpect(status().isNoContent()) .andDo(document("admin/balance/delete", pathParameters( diff --git a/backend/src/test/java/ddangkong/facade/admin/AdminServiceTest.java b/backend/src/test/java/ddangkong/facade/admin/AdminServiceTest.java new file mode 100644 index 000000000..a6153a8a5 --- /dev/null +++ b/backend/src/test/java/ddangkong/facade/admin/AdminServiceTest.java @@ -0,0 +1,45 @@ +package ddangkong.facade.admin; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import ddangkong.exception.admin.NotMatchAdminPasswordException; +import ddangkong.facade.BaseServiceTest; +import ddangkong.facade.admin.dto.AdminLoginRequest; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +class AdminServiceTest extends BaseServiceTest { + + @Value("${admin.password}") + private String adminPassword; + + @Autowired + private AdminService adminService; + + @Nested + class 어드민_비밀번호_검증 { + + @Test + void 어드민_비밀번호가_일치하면_아무_일도_일어나지_않는다() { + // given + AdminLoginRequest request = new AdminLoginRequest("admin", adminPassword); + + // when & then + assertThatCode(() -> adminService.validatePassword(request)) + .doesNotThrowAnyException(); + } + + @Test + void 어드민_비밀번호가_다르면_예외가_발생한다() { + // given + AdminLoginRequest request = new AdminLoginRequest("admin", "no-password"); + + // when & then + assertThatThrownBy(() -> adminService.validatePassword(request)) + .isInstanceOf(NotMatchAdminPasswordException.class); + } + } +} diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index 05895ee4c..a14bc8cee 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -20,6 +20,10 @@ encrypt: secret-key: 1234567890123456 algorithm: AES +admin: + session-key : admin + password : 1234 + logging: level: org: