Skip to content

Commit

Permalink
feat: 관리자용 Acutuator API 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
seokjin8678 committed Apr 22, 2024
1 parent 1d34181 commit 397dc21
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.festago.admin.application;

import com.festago.admin.infrastructure.ActuatorProxyClient;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AdminActuatorProxyService {

private final ActuatorProxyClient actuatorProxyClient;

public ResponseEntity<String> request(String path) {
return actuatorProxyClient.request(path);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.festago.admin.infrastructure;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

@Component
public class ActuatorProxyClient {

private final RestTemplate restTemplate;
private final int port;

public ActuatorProxyClient(
@Value("${management.server.port}") int port,
RestTemplateBuilder restTemplateBuilder
) {
this.restTemplate = restTemplateBuilder
.errorHandler(new AdminActuatorProxyErrorHandler())
.build();
this.port = port;
}

public ResponseEntity<String> request(String path) {
return restTemplate.getForEntity("http://localhost:" + port + "/actuator/" + path, String.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.festago.admin.infrastructure;

import com.festago.common.exception.ErrorCode;
import com.festago.common.exception.InternalServerException;
import com.festago.common.exception.NotFoundException;
import java.io.IOException;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.DefaultResponseErrorHandler;

public class AdminActuatorProxyErrorHandler extends DefaultResponseErrorHandler {

@Override
public void handleError(ClientHttpResponse response) throws IOException {
HttpStatusCode statusCode = response.getStatusCode();
if (statusCode.is4xxClientError()) {
throw new NotFoundException(ErrorCode.ACTUATOR_NOT_FOUND);
}
throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.festago.admin.presentation;

import com.festago.admin.application.AdminActuatorProxyService;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Hidden
@RestController
@RequestMapping("/admin/api/actuator")
@RequiredArgsConstructor
public class AdminActuatorController {

private final AdminActuatorProxyService adminActuatorProxyService;

@GetMapping("/{path}")
public ResponseEntity<String> getActuator(@PathVariable String path) {
return adminActuatorProxyService.request(path);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public enum ErrorCode {
SCHOOL_NOT_FOUND("존재하지 않는 학교입니다."),
ARTIST_NOT_FOUND("존재하지 않는 아티스트입니다."),
SOCIAL_MEDIA_NOT_FOUND("존재하지 않는 소셜미디어입니다."),
ACTUATOR_NOT_FOUND("존재하지 않는 Actuator 경로입니다."),

// 429
TOO_FREQUENT_REQUESTS("너무 잦은 요청입니다. 잠시 후 다시 시도해주세요."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.festago.admin.infrastructure;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.festago.common.exception.ErrorCode;
import com.festago.common.exception.InternalServerException;
import com.festago.common.exception.NotFoundException;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.response.MockRestResponseCreators;

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
@RestClientTest(ActuatorProxyClient.class)
class ActuatorProxyClientTest {

private static final String URI = "http://localhost:8090/actuator/health";

@Autowired
ActuatorProxyClient actuatorProxyClient;

@Autowired
MockRestServiceServer mockServer;

@Test
void 상태코드가_4xx이면_NotFound_예외() {
// given
mockServer.expect(requestTo(URI))
.andRespond(MockRestResponseCreators.withBadRequest()
.contentType(MediaType.APPLICATION_JSON));

// when & then
assertThatThrownBy(() -> actuatorProxyClient.request("health"))
.isInstanceOf(NotFoundException.class)
.hasMessage(ErrorCode.ACTUATOR_NOT_FOUND.getMessage());
}

@Test
void 상태코드가_5xx이면_InternalServer_예외() {
// given
mockServer.expect(requestTo(URI))
.andRespond(MockRestResponseCreators.withStatus(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(MediaType.APPLICATION_JSON));

// when & then
assertThatThrownBy(() -> actuatorProxyClient.request("health"))
.isInstanceOf(InternalServerException.class)
.hasMessage(ErrorCode.INTERNAL_SERVER_ERROR.getMessage());
}

@Test
void 성공() throws JsonProcessingException {
// given
mockServer.expect(requestTo(URI))
.andRespond(MockRestResponseCreators.withSuccess()
.body("data")
.contentType(MediaType.APPLICATION_JSON));

// when
var response = actuatorProxyClient.request("health");

// then
assertThat(response.getBody()).isEqualTo("data");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.festago.admin.presentation;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.festago.admin.application.AdminActuatorProxyService;
import com.festago.auth.domain.Role;
import com.festago.support.CustomWebMvcTest;
import com.festago.support.WithMockAuth;
import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

@CustomWebMvcTest
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
class AdminActuatorControllerTest {

private static final Cookie TOKEN_COOKIE = new Cookie("token", "token");

@Autowired
MockMvc mockMvc;

@Autowired
AdminActuatorProxyService adminActuatorProxyService;

@Nested
class 엑추에이터_조회 {

final String uri = "/admin/api/actuator/{path}";

@Nested
@DisplayName("GET " + uri)
class 올바른_주소로 {

@Test
@WithMockAuth(role = Role.ADMIN)
void 요청을_보내면_200_응답이_반환된다() throws Exception {
// when & then
mockMvc.perform(get(uri, "health")
.cookie(TOKEN_COOKIE)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}

@Test
void 토큰_없이_보내면_401_응답이_반환된다() throws Exception {
// when & then
mockMvc.perform(get(uri, "health"))
.andExpect(status().isUnauthorized());
}

@Test
@WithMockAuth(role = Role.MEMBER)
void 토큰의_권한이_Admin이_아니면_404_응답이_반환된다() throws Exception {
// when & then
mockMvc.perform(get(uri, "health")
.cookie(TOKEN_COOKIE))
.andExpect(status().isNotFound());
}
}
}
}
3 changes: 3 additions & 0 deletions backend/src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ spring:
open-in-view: false
flyway:
enabled: false
management:
server:
port: 8090

logging:
file:
Expand Down

0 comments on commit 397dc21

Please sign in to comment.