From 7d4a31984b5c24bd2fef866331fa9c67da33376b Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Thu, 3 Sep 2026 16:49:30 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20Grafana=20Loki=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=EC=9A=A9=20=EB=A1=9C=EA=B7=B8=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EB=B0=8F=20=EB=B3=BC=EB=A5=A8=20=EB=A7=88?= =?UTF-8?q?=EC=9A=B4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 4 ++++ src/main/resources/logback-spring.xml | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index cfb20bc5..fcc40881 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -210,7 +210,9 @@ jobs: PUBLISHED_CONTAINERS=$(sudo docker ps -q --filter publish=9000) if [ -n "$PUBLISHED_CONTAINERS" ]; then sudo docker rm -f $PUBLISHED_CONTAINERS; fi sudo docker pull ${{ secrets.DOCKER_REPO }}/eatssu-prod + sudo mkdir -p /var/log/eatssu sudo docker run -d --name eatssu-prod --restart unless-stopped -p 9000:9000 -p 127.0.0.1:9001:9001 \ + -v /var/log/eatssu:/var/log/eatssu \ --log-driver=awslogs \ --log-opt awslogs-region=ap-northeast-2 \ --log-opt awslogs-group=prod-ec2-group \ @@ -265,7 +267,9 @@ jobs: PUBLISHED_CONTAINERS=$(sudo docker ps -q --filter publish=9000) if [ -n "$PUBLISHED_CONTAINERS" ]; then sudo docker rm -f $PUBLISHED_CONTAINERS; fi sudo docker pull ${{ secrets.DOCKER_REPO }}/eatssu-dev + sudo mkdir -p /var/log/eatssu sudo docker run -d --name eatssu-dev --restart unless-stopped -p 9000:9000 -p 127.0.0.1:9001:9001 \ + -v /var/log/eatssu:/var/log/eatssu \ --log-driver=awslogs \ --log-opt awslogs-region=ap-northeast-2 \ --log-opt awslogs-group=dev-ec2-group \ diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 28f81a9d..373010c1 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -8,6 +8,9 @@ Thread : 동시성 문제 관련 사용, Request : 요청 단위 --> + + @@ -22,8 +25,28 @@ + + + /var/log/eatssu/app.log + + ${FILE_LOG_PATTERN} + + + /var/log/eatssu/app.%d{yyyy-MM-dd}.%i.log + 100MB + 3 + 1GB + + + + + true + + + + From 7fe0c95686b7bcce70b2d81d0de2c89d7d140925 Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Thu, 3 Sep 2026 16:53:32 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20Grafana=20Alloy=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(Prometheus+Loki)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- monitoring/alloy-config.alloy | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 monitoring/alloy-config.alloy diff --git a/monitoring/alloy-config.alloy b/monitoring/alloy-config.alloy new file mode 100644 index 00000000..6dab469f --- /dev/null +++ b/monitoring/alloy-config.alloy @@ -0,0 +1,40 @@ +logging { + level = "warn" +} + +prometheus.scrape "spring_app" { + targets = [{ + __address__ = "localhost:9001", + __metrics_path__ = "/actuator/prometheus", + }] + forward_to = [prometheus.remote_write.metrics_hosted_prometheus.receiver] +} + +prometheus.remote_write "metrics_hosted_prometheus" { + endpoint { + name = "hosted-prometheus" + url = "https://prometheus-prod-49-prod-ap-northeast-0.grafana.net/api/prom/push" + basic_auth { + username = "3340646" + password = "__GRAFANA_TOKEN__" + } + } +} + +loki.write "grafanacloud" { + endpoint { + url = "https://logs-prod-030.grafana.net/loki/api/v1/push" + basic_auth { + username = "1665998" + password = "__GRAFANA_TOKEN__" + } + } +} + +loki.source.file "app_log" { + targets = [{ + __path__ = "/var/log/eatssu/app.log", + application = "__APP_LABEL__", + }] + forward_to = [loki.write.grafanacloud.receiver] +} From f6b3cc80afc026b1d261d76e0e30071214acacf1 Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Thu, 3 Sep 2026 16:54:20 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feat:=20cd.yml=EC=97=90=20Grafana=20Alloy?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EB=B0=B0=ED=8F=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9E=AC=EC=8B=9C=EC=9E=91=20=EC=8A=A4=ED=85=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index fcc40881..20c49fef 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -171,6 +171,30 @@ jobs: target: /home/${{ secrets.USERNAME }}/eatssu-deploy/dev rm: true + - name: prod Grafana Alloy 설정 전송 + uses: appleboy/scp-action@master + if: github.ref_name == 'main' + with: + host: ${{ secrets.HOST_PROD }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.PROD_PRIVATE_KEY }} + port: 22 + source: "monitoring/alloy-config.alloy" + target: /home/${{ secrets.USERNAME }}/eatssu-deploy/prod + rm: true + + - name: dev Grafana Alloy 설정 전송 + uses: appleboy/scp-action@master + if: github.ref_name == 'develop' + with: + host: ${{ secrets.HOST_DEV }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.DEV_PRIVATE_KEY }} + port: 22 + source: "monitoring/alloy-config.alloy" + target: /home/${{ secrets.USERNAME }}/eatssu-deploy/dev + rm: true + - name: prod에 배포 uses: appleboy/ssh-action@master id: deploy-prod @@ -228,6 +252,11 @@ jobs: -e EATSSU_DEEPL_API_KEY="${{ secrets.EATSSU_DEEPL_API_KEY }}" \ ${{ secrets.DOCKER_REPO }}/eatssu-prod sudo docker image prune -f + ALLOY_CONFIG_SRC="$HOME/eatssu-deploy/prod/monitoring/alloy-config.alloy" + sed -e 's|__GRAFANA_TOKEN__|${{ secrets.EATSSU_GRAFANA_TOKEN }}|g' \ + -e 's|__APP_LABEL__|eatssu-prod|g' \ + "$ALLOY_CONFIG_SRC" | sudo tee /etc/alloy/config.alloy > /dev/null + sudo systemctl restart alloy - name: dev 서버에 배포 uses: appleboy/ssh-action@master @@ -285,3 +314,8 @@ jobs: -e EATSSU_DEEPL_API_KEY="${{ secrets.EATSSU_DEEPL_API_KEY }}" \ ${{ secrets.DOCKER_REPO }}/eatssu-dev sudo docker image prune -f + ALLOY_CONFIG_SRC="$HOME/eatssu-deploy/dev/monitoring/alloy-config.alloy" + sed -e 's|__GRAFANA_TOKEN__|${{ secrets.EATSSU_GRAFANA_TOKEN }}|g' \ + -e 's|__APP_LABEL__|eatssu-dev|g' \ + "$ALLOY_CONFIG_SRC" | sudo tee /etc/alloy/config.alloy > /dev/null + sudo systemctl restart alloy From dee3c9301c8283a1b8549bad01fe78580f792a40 Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Thu, 3 Sep 2026 17:00:20 +0900 Subject: [PATCH 04/13] =?UTF-8?q?fix:=20logback=20FILE=20=EC=96=B4?= =?UTF-8?q?=ED=8E=9C=EB=8D=94=EB=A5=BC=20prod/dev=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=EC=97=90=EC=84=9C=EB=A7=8C=20=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/logback-spring.xml | 41 +++++++++++++++------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 373010c1..0c65a545 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -25,28 +25,33 @@ - - - /var/log/eatssu/app.log - - ${FILE_LOG_PATTERN} - - - /var/log/eatssu/app.%d{yyyy-MM-dd}.%i.log - 100MB - 3 - 1GB - - + + + + /var/log/eatssu/app.log + + ${FILE_LOG_PATTERN} + + + /var/log/eatssu/app.%d{yyyy-MM-dd}.%i.log + 100MB + 3 + 1GB + + - - true - - + + true + + + - + + + From e74f320bfc3f99ba2fb467b9da31ac2920f4f5b5 Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Thu, 3 Sep 2026 17:21:46 +0900 Subject: [PATCH 05/13] =?UTF-8?q?hotfix:=20Grafana=20Alloy=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=A0=84=EC=86=A1=20=EC=8B=9C=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EC=82=AD=EC=A0=9C=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 20c49fef..a9f8c8fc 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -181,7 +181,6 @@ jobs: port: 22 source: "monitoring/alloy-config.alloy" target: /home/${{ secrets.USERNAME }}/eatssu-deploy/prod - rm: true - name: dev Grafana Alloy 설정 전송 uses: appleboy/scp-action@master @@ -193,7 +192,6 @@ jobs: port: 22 source: "monitoring/alloy-config.alloy" target: /home/${{ secrets.USERNAME }}/eatssu-deploy/dev - rm: true - name: prod에 배포 uses: appleboy/ssh-action@master From dee8f1b6d3649f95622568d830ecfeff4b049bea Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Thu, 3 Sep 2026 17:40:04 +0900 Subject: [PATCH 06/13] =?UTF-8?q?feat:=20Slack=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=95=8C=EB=A6=BC=EC=97=90=20Grafana=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EB=94=A5=EB=A7=81=ED=81=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 2 + .../slack/entity/SlackMessageFormat.java | 66 ++++++++++++++++++- .../slack/entity/SlackMessageFormatTest.java | 33 ++++++++++ 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index a9f8c8fc..6cfff769 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -248,6 +248,8 @@ jobs: -e EATSSU_SLACK_TOKEN="${{ secrets.EATSSU_SLACK_TOKEN }}" \ -e EATSSU_PROD_DOMAIN="${{ secrets.EATSSU_PROD_DOMAIN }}" \ -e EATSSU_DEEPL_API_KEY="${{ secrets.EATSSU_DEEPL_API_KEY }}" \ + -e GRAFANA_BASE_URL="${{ secrets.EATSSU_GRAFANA_BASE_URL }}" \ + -e GRAFANA_LOKI_DATASOURCE_UID="${{ secrets.EATSSU_GRAFANA_LOKI_DATASOURCE_UID }}" \ ${{ secrets.DOCKER_REPO }}/eatssu-prod sudo docker image prune -f ALLOY_CONFIG_SRC="$HOME/eatssu-deploy/prod/monitoring/alloy-config.alloy" diff --git a/src/main/java/ssu/eatssu/domain/slack/entity/SlackMessageFormat.java b/src/main/java/ssu/eatssu/domain/slack/entity/SlackMessageFormat.java index bc8d0749..81abc5b1 100644 --- a/src/main/java/ssu/eatssu/domain/slack/entity/SlackMessageFormat.java +++ b/src/main/java/ssu/eatssu/domain/slack/entity/SlackMessageFormat.java @@ -1,5 +1,7 @@ package ssu.eatssu.domain.slack.entity; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.MDC; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import ssu.eatssu.domain.inquiry.entity.Inquiry; @@ -8,15 +10,31 @@ import ssu.eatssu.domain.user.entity.User; import ssu.eatssu.global.handler.response.BaseException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.text.MessageFormat; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; @Component public class SlackMessageFormat { + private static final long LOG_LINK_WINDOW_MILLIS = 5 * 60 * 1000; + private static String serverEnv; + private static String grafanaBaseUrl; + private static String lokiDatasourceUid; + private static ObjectMapper objectMapper; - private SlackMessageFormat(@Value("${server.env:unknown}") String serverEnvValue) { + private SlackMessageFormat(@Value("${server.env:unknown}") String serverEnvValue, + @Value("${grafana.base-url:}") String grafanaBaseUrlValue, + @Value("${grafana.loki-datasource-uid:}") String lokiDatasourceUidValue, + ObjectMapper objectMapperValue) { SlackMessageFormat.serverEnv = serverEnvValue; + SlackMessageFormat.grafanaBaseUrl = grafanaBaseUrlValue; + SlackMessageFormat.lokiDatasourceUid = lokiDatasourceUidValue; + SlackMessageFormat.objectMapper = objectMapperValue; } public static String sendReport(Report report) { @@ -113,6 +131,50 @@ public static String sendServerError(Throwable ex, String method, String uri, St userId, args != null && args.length() > 500 ? args.substring(0, 500) + "...(truncated)" : args }; - return messageFormat.format(formatArgs); + String message = messageFormat.format(formatArgs); + + String logLink = buildGrafanaLogLink(); + if (logLink != null) { + message += "\n" + logLink; + } + return message; + } + + private static String buildGrafanaLogLink() { + String requestId = MDC.get("requestId"); + if (grafanaBaseUrl == null || grafanaBaseUrl.isBlank() + || lokiDatasourceUid == null || lokiDatasourceUid.isBlank() + || requestId == null || requestId.isBlank()) { + return null; + } + + String application = "eatssu-" + serverEnv; + String logQl = "{application=\"" + application + "\"} |= \"reqId=" + requestId + "\""; + long now = System.currentTimeMillis(); + + Map query = new LinkedHashMap<>(); + query.put("refId", "A"); + query.put("expr", logQl); + query.put("datasource", Map.of("type", "loki", "uid", lokiDatasourceUid)); + query.put("editorMode", "code"); + + Map range = new LinkedHashMap<>(); + range.put("from", String.valueOf(now - LOG_LINK_WINDOW_MILLIS)); + range.put("to", String.valueOf(now + LOG_LINK_WINDOW_MILLIS)); + + Map pane = new LinkedHashMap<>(); + pane.put("datasource", lokiDatasourceUid); + pane.put("queries", List.of(query)); + pane.put("range", range); + pane.put("compact", false); + + try { + String panesJson = objectMapper.writeValueAsString(Map.of("wes", pane)); + String encodedPanes = URLEncoder.encode(panesJson, StandardCharsets.UTF_8); + String url = grafanaBaseUrl + "/explore?schemaVersion=1&panes=" + encodedPanes; + return "<" + url + "|Grafana에서 로그 보기>"; + } catch (Exception e) { + return null; + } } } diff --git a/src/test/java/ssu/eatssu/domain/slack/entity/SlackMessageFormatTest.java b/src/test/java/ssu/eatssu/domain/slack/entity/SlackMessageFormatTest.java index fc9fda04..4a14f9fb 100644 --- a/src/test/java/ssu/eatssu/domain/slack/entity/SlackMessageFormatTest.java +++ b/src/test/java/ssu/eatssu/domain/slack/entity/SlackMessageFormatTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.slf4j.MDC; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.util.ReflectionTestUtils; @@ -54,11 +55,17 @@ class SlackMessageFormatTest { @BeforeEach void setUp() { ReflectionTestUtils.setField(SlackMessageFormat.class, "serverEnv", "test"); + ReflectionTestUtils.setField(SlackMessageFormat.class, "grafanaBaseUrl", ""); + ReflectionTestUtils.setField(SlackMessageFormat.class, "lokiDatasourceUid", ""); + MDC.remove("requestId"); cleanUp(); } @AfterEach void tearDown() { + ReflectionTestUtils.setField(SlackMessageFormat.class, "grafanaBaseUrl", ""); + ReflectionTestUtils.setField(SlackMessageFormat.class, "lokiDatasourceUid", ""); + MDC.remove("requestId"); cleanUp(); } @@ -87,6 +94,32 @@ void sendServerErrorFormatsRegularExceptionAndTruncatesLongArguments() { assertThat(message).contains("예외 타입", "IllegalStateException", "메시지 없음", "...(truncated)"); } + @Test + void sendServerErrorOmitsGrafanaLinkWhenNotConfigured() { + // given: grafana.base-url is left unconfigured (default test setup) + + // when + String message = SlackMessageFormat.sendServerError(new IllegalStateException(), "GET", "/menus", "1", null); + + // then + assertThat(message).doesNotContain("Grafana에서 로그 보기"); + } + + @Test + void sendServerErrorIncludesGrafanaLogLinkWhenConfiguredAndRequestIdPresent() { + // given + ReflectionTestUtils.setField(SlackMessageFormat.class, "grafanaBaseUrl", "https://example.grafana.net"); + ReflectionTestUtils.setField(SlackMessageFormat.class, "lokiDatasourceUid", "grafanacloud-logs"); + MDC.put("requestId", "test-request-id"); + + // when + String message = SlackMessageFormat.sendServerError(new IllegalStateException(), "GET", "/menus", "1", null); + + // then + assertThat(message).contains("https://example.grafana.net/explore?schemaVersion=1&panes=", + "Grafana에서 로그 보기"); + } + @Test void slackChannelsExposeConfiguredAddress() { assertThat(SlackChannel.SERVER_ERROR.getKrName()).isEqualTo("C092J4J6F0U"); From cfeeeaf771c58beb15241601aeaca19dfe029118 Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Thu, 3 Sep 2026 17:46:45 +0900 Subject: [PATCH 07/13] =?UTF-8?q?fix:=20/error-test=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=EB=A5=BC=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=99=94?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=EB=A6=AC=EC=8A=A4=ED=8A=B8=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ssu/eatssu/domain/auth/infrastructure/SecurityConfig.java | 2 +- .../eatssu/domain/auth/security/JwtAuthenticationFilter.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ssu/eatssu/domain/auth/infrastructure/SecurityConfig.java b/src/main/java/ssu/eatssu/domain/auth/infrastructure/SecurityConfig.java index c6c21ca9..b3aece14 100644 --- a/src/main/java/ssu/eatssu/domain/auth/infrastructure/SecurityConfig.java +++ b/src/main/java/ssu/eatssu/domain/auth/infrastructure/SecurityConfig.java @@ -29,7 +29,7 @@ public class SecurityConfig { private static final String[] AUTH_WHITELIST = { "/", "/oauths/kakao", "/oauths/apple", "/menus/**", "/meals/**", "/admin/login", "/oauths/v2/kakao","/oauths/v2/apple", "/reviews", "/reviews/menus/**", "/reviews/meals/**", "/v2/reviews/statistics/**", - "/v2/reviews/menus/**", "/v2/reviews/meals/**", "/actuator/health", "/actuator/prometheus", "/error-test/**", + "/v2/reviews/menus/**", "/v2/reviews/meals/**", "/actuator/health", "/actuator/prometheus", "/good-price-stores", "/good-price-stores/**" }; diff --git a/src/main/java/ssu/eatssu/domain/auth/security/JwtAuthenticationFilter.java b/src/main/java/ssu/eatssu/domain/auth/security/JwtAuthenticationFilter.java index e845aba9..097ced16 100644 --- a/src/main/java/ssu/eatssu/domain/auth/security/JwtAuthenticationFilter.java +++ b/src/main/java/ssu/eatssu/domain/auth/security/JwtAuthenticationFilter.java @@ -28,7 +28,7 @@ public class JwtAuthenticationFilter extends GenericFilterBean { private static final List AUTH_WHITELIST = List.of( "/", "/oauths/kakao", "/oauths/apple", "/menus/**", "/meals/**", "/admin/login", "/reviews", "/reviews/menus/**", "/reviews/meals/**", "/v2/reviews/statistics/**", - "/v2/reviews/menus/**", "/v2/reviews/meals/**", "/actuator/**", "/error-test/**", + "/v2/reviews/menus/**", "/v2/reviews/meals/**", "/actuator/**", "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/oauths/valid/token", "/admin/img/**", "/css/**", "/js/**", "/favicon.ico", "/error/**", "/webjars/**", "/h2-console/**", From 491b290968f491d384dae62a7874bea5311113b1 Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Fri, 4 Sep 2026 13:48:35 +0900 Subject: [PATCH 08/13] =?UTF-8?q?fix:=20Slack=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=95=8C=EB=A6=BC=EC=97=90=EC=84=9C=204xx?= =?UTF-8?q?=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/log/ControllerLogAspect.java | 12 ++++++++++- .../global/log/ControllerLogAspectTest.java | 21 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/main/java/ssu/eatssu/global/log/ControllerLogAspect.java b/src/main/java/ssu/eatssu/global/log/ControllerLogAspect.java index 21af77f4..9b6f7824 100644 --- a/src/main/java/ssu/eatssu/global/log/ControllerLogAspect.java +++ b/src/main/java/ssu/eatssu/global/log/ControllerLogAspect.java @@ -17,6 +17,7 @@ import ssu.eatssu.domain.auth.security.CustomUserDetails; import ssu.eatssu.domain.slack.service.SlackErrorNotifier; import ssu.eatssu.global.handler.response.BaseException; +import ssu.eatssu.global.handler.response.BaseResponseStatus; import ssu.eatssu.global.log.annotation.LogMask; import java.lang.reflect.Field; @@ -99,7 +100,9 @@ public Object logApi(ProceedingJoinPoint joinPoint) throws Throwable { String exceptionType = e.getClass().getSimpleName(); log.error("EXCEPTION {} {} ({} ms) type={} cause={}", method, uri, time, exceptionType, causeMessage, e); - slackErrorNotifier.notify(e, method, uri, userId, argsJson); + if (shouldNotifySlack(e)) { + slackErrorNotifier.notify(e, method, uri, userId, argsJson); + } throw e; } } @@ -124,6 +127,13 @@ private boolean isAuthApi(String uri) { return uri != null && uri.startsWith("/oauths/"); } + private boolean shouldNotifySlack(Throwable e) { + if (e instanceof BaseException baseException) { + return BaseResponseStatus.sendSlackNotification(baseException.getStatus()); + } + return true; + } + private String getCauseMessage(Throwable e) { if (e instanceof BaseException baseException) { return baseException.getStatus().getMessage(); diff --git a/src/test/java/ssu/eatssu/global/log/ControllerLogAspectTest.java b/src/test/java/ssu/eatssu/global/log/ControllerLogAspectTest.java index 5614a86a..be0606ff 100644 --- a/src/test/java/ssu/eatssu/global/log/ControllerLogAspectTest.java +++ b/src/test/java/ssu/eatssu/global/log/ControllerLogAspectTest.java @@ -111,7 +111,7 @@ void shouldMaskAnnotatedRequestFields() throws Exception { } @Test - void 컨트롤러_예외를_슬랙에_알리고_다시_던진다() throws Throwable { + void 컨트롤러_5xx_예외를_슬랙에_알리고_다시_던진다() throws Throwable { SlackErrorNotifier notifier = mock(SlackErrorNotifier.class); ControllerLogAspect aspect = new ControllerLogAspect(new ObjectMapper(), notifier); ProceedingJoinPoint joinPoint = mock(ProceedingJoinPoint.class); @@ -121,12 +121,29 @@ void shouldMaskAnnotatedRequestFields() throws Exception { when(joinPoint.getSignature()).thenReturn(signature); when(signature.getParameterNames()).thenReturn(new String[0]); when(joinPoint.getArgs()).thenReturn(new Object[0]); - when(joinPoint.proceed()).thenThrow(new BaseException(BaseResponseStatus.NOT_FOUND_MENU)); + when(joinPoint.proceed()).thenThrow(new BaseException(BaseResponseStatus.INTERNAL_SERVER_ERROR)); assertThatThrownBy(() -> aspect.logApi(joinPoint)).isInstanceOf(BaseException.class); verify(notifier).notify(any(), anyString(), anyString(), anyString(), anyString()); } + @Test + void doesNotNotifySlackFor4xxBusinessException() throws Throwable { + SlackErrorNotifier notifier = mock(SlackErrorNotifier.class); + ControllerLogAspect aspect = new ControllerLogAspect(new ObjectMapper(), notifier); + ProceedingJoinPoint joinPoint = mock(ProceedingJoinPoint.class); + MethodSignature signature = mock(MethodSignature.class); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/menus"); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + when(joinPoint.getSignature()).thenReturn(signature); + when(signature.getParameterNames()).thenReturn(new String[0]); + when(joinPoint.getArgs()).thenReturn(new Object[0]); + when(joinPoint.proceed()).thenThrow(new BaseException(BaseResponseStatus.NOT_FOUND_MENU)); + + assertThatThrownBy(() -> aspect.logApi(joinPoint)).isInstanceOf(BaseException.class); + verify(notifier, org.mockito.Mockito.never()).notify(any(), anyString(), anyString(), anyString(), anyString()); + } + @Test void 제외_인자와_null_인자와_긴_인자를_안전하게_로그한다() throws Throwable { ProceedingJoinPoint joinPoint = mock(ProceedingJoinPoint.class); From 3b846376e19d717789a70bab895390bc81dc9985 Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Fri, 4 Sep 2026 14:04:35 +0900 Subject: [PATCH 09/13] =?UTF-8?q?fix:=20Swagger=20UI/API=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=EC=97=90=20Basic=20Auth=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 4 ++++ src/main/resources/application-dev.yml | 2 ++ src/main/resources/application-prod.yml | 2 ++ 3 files changed, 8 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6cfff769..3c5646bb 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -250,6 +250,8 @@ jobs: -e EATSSU_DEEPL_API_KEY="${{ secrets.EATSSU_DEEPL_API_KEY }}" \ -e GRAFANA_BASE_URL="${{ secrets.EATSSU_GRAFANA_BASE_URL }}" \ -e GRAFANA_LOKI_DATASOURCE_UID="${{ secrets.EATSSU_GRAFANA_LOKI_DATASOURCE_UID }}" \ + -e SWAGGER_USER="${{ secrets.SWAGGER_PROD_USER }}" \ + -e SWAGGER_PASSWORD="${{ secrets.SWAGGER_PROD_PASSWORD }}" \ ${{ secrets.DOCKER_REPO }}/eatssu-prod sudo docker image prune -f ALLOY_CONFIG_SRC="$HOME/eatssu-deploy/prod/monitoring/alloy-config.alloy" @@ -312,6 +314,8 @@ jobs: -e EATSSU_SLACK_TOKEN="${{ secrets.EATSSU_SLACK_TOKEN }}" \ -e EATSSU_DEV_DOMAIN="${{ secrets.EATSSU_DEV_DOMAIN }}" \ -e EATSSU_DEEPL_API_KEY="${{ secrets.EATSSU_DEEPL_API_KEY }}" \ + -e SWAGGER_USER="${{ secrets.SWAGGER_DEV_USER }}" \ + -e SWAGGER_PASSWORD="${{ secrets.SWAGGER_DEV_PASSWORD }}" \ ${{ secrets.DOCKER_REPO }}/eatssu-dev sudo docker image prune -f ALLOY_CONFIG_SRC="$HOME/eatssu-deploy/dev/monitoring/alloy-config.alloy" diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index e97dde3e..5286c067 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -77,6 +77,8 @@ deepl: swagger: url: ${EATSSU_DEV_DOMAIN} description: Test Server Swagger API + username: ${SWAGGER_USER} + password: ${SWAGGER_PASSWORD} springdoc: swagger-ui: diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 6b38fd4d..e3bacb43 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -78,6 +78,8 @@ deepl: swagger: url: ${EATSSU_PROD_DOMAIN} description: Prod-Server url + username: ${SWAGGER_USER} + password: ${SWAGGER_PASSWORD} springdoc: swagger-ui: From 7a31cfbd3e4d2d4233e52aa7d51a5c499a02e560 Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Fri, 4 Sep 2026 14:05:01 +0900 Subject: [PATCH 10/13] =?UTF-8?q?fix:=20SecurityConfig=EC=97=90=EC=84=9C?= =?UTF-8?q?=20dev,=20prod=20=ED=94=84=EB=A1=9C=ED=95=84=EC=97=90=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/infrastructure/SecurityConfig.java | 39 +++++++++++++++++++ .../infrastructure/SecurityConfigTest.java | 30 ++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityConfigTest.java diff --git a/src/main/java/ssu/eatssu/domain/auth/infrastructure/SecurityConfig.java b/src/main/java/ssu/eatssu/domain/auth/infrastructure/SecurityConfig.java index b3aece14..eac42a4c 100644 --- a/src/main/java/ssu/eatssu/domain/auth/infrastructure/SecurityConfig.java +++ b/src/main/java/ssu/eatssu/domain/auth/infrastructure/SecurityConfig.java @@ -1,12 +1,20 @@ package ssu.eatssu.domain.auth.infrastructure; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; @@ -26,6 +34,10 @@ public class SecurityConfig { "/favicon.ico", "/error/**", "/webjars/**", "/h2-console/**" }; + private static final String[] SWAGGER_PATHS = { + "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**" + }; + private static final String[] AUTH_WHITELIST = { "/", "/oauths/kakao", "/oauths/apple", "/menus/**", "/meals/**", "/admin/login", "/oauths/v2/kakao","/oauths/v2/apple", "/reviews", "/reviews/menus/**", "/reviews/meals/**", "/v2/reviews/statistics/**", @@ -47,6 +59,33 @@ public PasswordEncoder passwordEncoder() { } @Bean + @Order(1) + @Profile({"dev", "prod"}) + public SecurityFilterChain swaggerFilterChain(HttpSecurity http, + UserDetailsService swaggerUserDetailsService) throws Exception { + http + .securityMatcher(SWAGGER_PATHS) + .csrf(csrf -> csrf.disable()) + .userDetailsService(swaggerUserDetailsService) + .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()) + .httpBasic(Customizer.withDefaults()); + return http.build(); + } + + @Bean + @Profile({"dev", "prod"}) + public UserDetailsService swaggerUserDetailsService(@Value("${swagger.username}") String username, + @Value("${swagger.password}") String password, + PasswordEncoder passwordEncoder) { + UserDetails swaggerUser = User.withUsername(username) + .password(passwordEncoder.encode(password)) + .roles("SWAGGER") + .build(); + return new InMemoryUserDetailsManager(swaggerUser); + } + + @Bean + @Order(2) protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() diff --git a/src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityConfigTest.java b/src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityConfigTest.java new file mode 100644 index 00000000..8d2f2201 --- /dev/null +++ b/src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityConfigTest.java @@ -0,0 +1,30 @@ +package ssu.eatssu.domain.auth.infrastructure; + +import org.junit.jupiter.api.Test; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import ssu.eatssu.domain.auth.security.JwtTokenProvider; +import ssu.eatssu.global.handler.JwtAccessDeniedHandler; +import ssu.eatssu.global.handler.JwtAuthenticationEntryPoint; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class SecurityConfigTest { + + private final SecurityConfig securityConfig = new SecurityConfig( + mock(JwtTokenProvider.class), mock(JwtAccessDeniedHandler.class), mock(JwtAuthenticationEntryPoint.class)); + + @Test + void swaggerUserDetailsServiceEncodesPasswordAndExposesUsername() { + PasswordEncoder passwordEncoder = securityConfig.passwordEncoder(); + + UserDetailsService userDetailsService = + securityConfig.swaggerUserDetailsService("eatssu-dev", "eatssu2026!", passwordEncoder); + UserDetails user = userDetailsService.loadUserByUsername("eatssu-dev"); + + assertThat(user.getUsername()).isEqualTo("eatssu-dev"); + assertThat(passwordEncoder.matches("eatssu2026!", user.getPassword())).isTrue(); + } +} From 55079791d58550b2707d737c63cc74842bd713ce Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Fri, 4 Sep 2026 14:17:26 +0900 Subject: [PATCH 11/13] =?UTF-8?q?test:=20SecurityConfig=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EC=B2=B4=EC=9D=B8=20=EC=9D=B8=EA=B0=80=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20MockMvc=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SecurityFilterChainTest.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityFilterChainTest.java diff --git a/src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityFilterChainTest.java b/src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityFilterChainTest.java new file mode 100644 index 00000000..cbd9db8a --- /dev/null +++ b/src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityFilterChainTest.java @@ -0,0 +1,52 @@ +package ssu.eatssu.domain.auth.infrastructure; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class SecurityFilterChainTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void 화이트리스트_경로는_인증_없이_시큐리티를_통과한다() throws Exception { + mockMvc.perform(get("/menus")) + .andExpect(status().isBadRequest()); + } + + @Test + void 화이트리스트에_없는_경로는_인증하지_않으면_401을_반환한다() throws Exception { + mockMvc.perform(get("/protected-test-path")) + .andExpect(status().isUnauthorized()); + } + + @Test + void 관리자_경로는_인증하지_않으면_401을_반환한다() throws Exception { + mockMvc.perform(get("/admin/anything")) + .andExpect(status().isUnauthorized()); + } + + @Test + void 관리자_경로는_ADMIN_권한이_없으면_403을_반환한다() throws Exception { + mockMvc.perform(get("/admin/anything") + .with(user("user").authorities(new SimpleGrantedAuthority("ROLE_USER")))) + .andExpect(status().isForbidden()); + } + + @Test + void 관리자_경로는_ADMIN_권한이_있으면_시큐리티를_통과한다() throws Exception { + mockMvc.perform(get("/admin/anything") + .with(user("admin").authorities(new SimpleGrantedAuthority("ROLE_ADMIN")))) + .andExpect(status().isNotFound()); + } +} From deb1ed46ebdd88f7723319d9c22bb50ad3a4f23a Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Fri, 4 Sep 2026 14:23:42 +0900 Subject: [PATCH 12/13] =?UTF-8?q?test:=20SecurityConfig=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EC=B2=B4=EC=9D=B8=20=EC=9D=B8=EA=B0=80=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20MockMvc=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SecurityFilterChainTest.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityFilterChainTest.java b/src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityFilterChainTest.java index cbd9db8a..52e98e24 100644 --- a/src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityFilterChainTest.java +++ b/src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityFilterChainTest.java @@ -4,10 +4,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.test.web.servlet.MockMvc; +import ssu.eatssu.domain.auth.security.CustomUserDetails; +import ssu.eatssu.domain.auth.security.JwtTokenProvider; +import ssu.eatssu.domain.user.entity.DeviceType; +import ssu.eatssu.domain.user.entity.Role; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -18,6 +22,16 @@ class SecurityFilterChainTest { @Autowired private MockMvc mockMvc; + @Autowired + private JwtTokenProvider jwtTokenProvider; + + private String accessTokenWithRole(Role role) { + CustomUserDetails principal = new CustomUserDetails(1L, "test@eatssu.com", "", role, DeviceType.IOS); + Authentication authentication = + new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities()); + return jwtTokenProvider.generateTokens(authentication).accessToken(); + } + @Test void 화이트리스트_경로는_인증_없이_시큐리티를_통과한다() throws Exception { mockMvc.perform(get("/menus")) @@ -38,15 +52,17 @@ class SecurityFilterChainTest { @Test void 관리자_경로는_ADMIN_권한이_없으면_403을_반환한다() throws Exception { - mockMvc.perform(get("/admin/anything") - .with(user("user").authorities(new SimpleGrantedAuthority("ROLE_USER")))) + String token = accessTokenWithRole(Role.USER); + + mockMvc.perform(get("/admin/anything").header("Authorization", "Bearer " + token)) .andExpect(status().isForbidden()); } @Test void 관리자_경로는_ADMIN_권한이_있으면_시큐리티를_통과한다() throws Exception { - mockMvc.perform(get("/admin/anything") - .with(user("admin").authorities(new SimpleGrantedAuthority("ROLE_ADMIN")))) + String token = accessTokenWithRole(Role.ADMIN); + + mockMvc.perform(get("/admin/anything").header("Authorization", "Bearer " + token)) .andExpect(status().isNotFound()); } } From d21a0bf12120ee2b3d9c97270d3a690c9aa4f035 Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Fri, 4 Sep 2026 14:46:42 +0900 Subject: [PATCH 13/13] =?UTF-8?q?test:=20Swagger=20Basic=20Auth=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SecurityFilterChainTest.java | 22 ++++++++++++++++++- src/test/resources/logback-security-test.xml | 11 ++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/logback-security-test.xml diff --git a/src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityFilterChainTest.java b/src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityFilterChainTest.java index 52e98e24..1a353db3 100644 --- a/src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityFilterChainTest.java +++ b/src/test/java/ssu/eatssu/domain/auth/infrastructure/SecurityFilterChainTest.java @@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.test.web.servlet.MockMvc; @@ -13,9 +14,16 @@ import ssu.eatssu.domain.user.entity.Role; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@SpringBootTest +@SpringBootTest(properties = { + "logging.config=classpath:logback-security-test.xml", + "spring.flyway.enabled=false", + "swagger.username=test-swagger", + "swagger.password=test-password" +}) +@ActiveProfiles({"dev", "test"}) @AutoConfigureMockMvc class SecurityFilterChainTest { @@ -65,4 +73,16 @@ private String accessTokenWithRole(Role role) { mockMvc.perform(get("/admin/anything").header("Authorization", "Bearer " + token)) .andExpect(status().isNotFound()); } + + @Test + void Swagger_경로는_Basic_Auth_없이는_401을_반환한다() throws Exception { + mockMvc.perform(get("/swagger-ui/not-found.css")) + .andExpect(status().isUnauthorized()); + } + + @Test + void Swagger_경로는_유효한_Basic_Auth로_접근할_수_있다() throws Exception { + mockMvc.perform(get("/swagger-ui/not-found.css").with(httpBasic("test-swagger", "test-password"))) + .andExpect(status().isNotFound()); + } } diff --git a/src/test/resources/logback-security-test.xml b/src/test/resources/logback-security-test.xml new file mode 100644 index 00000000..49fc35c7 --- /dev/null +++ b/src/test/resources/logback-security-test.xml @@ -0,0 +1,11 @@ + + + + + %msg%n + + + + + +