diff --git a/build.gradle b/build.gradle index fc2a1d7..673ca9d 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' + + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9' } tasks.named('test') { diff --git a/src/main/java/com/matzip/common/config/OpenApiConfig.java b/src/main/java/com/matzip/common/config/OpenApiConfig.java new file mode 100644 index 0000000..7598802 --- /dev/null +++ b/src/main/java/com/matzip/common/config/OpenApiConfig.java @@ -0,0 +1,30 @@ +package com.matzip.common.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + + private static final String SECURITY_SCHEME_NAME = "bearerAuth"; + + @Bean + public OpenAPI matzipOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("KNU-Matzip API") + .description("공주대 맛집 서비스 API 문서") + .version("v1")) + .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)) + .components(new Components().addSecuritySchemes(SECURITY_SCHEME_NAME, + new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); + } +} diff --git a/src/main/java/com/matzip/common/security/jwt/filter/JwtAuthenticationFilter.java b/src/main/java/com/matzip/common/security/jwt/filter/JwtAuthenticationFilter.java index 7038c4b..bec7983 100644 --- a/src/main/java/com/matzip/common/security/jwt/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/matzip/common/security/jwt/filter/JwtAuthenticationFilter.java @@ -36,7 +36,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { "/api/v1/categories", "/api/v1/events", "/admin/api", - "/actuator/prometheus" + "/actuator/prometheus", + "/swagger-ui", + "/v3/api-docs" ); private final JwtProvider jwtProvider; diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index e81e27c..06355ec 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -45,4 +45,11 @@ management: include: prometheus metrics: tags: - application: ${spring.application.name} \ No newline at end of file + application: ${spring.application.name} + +# 운영 환경에서는 API 문서/Swagger UI 비활성화 +springdoc: + api-docs: + enabled: false + swagger-ui: + enabled: false \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a2f71a6..2e0136f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -21,4 +21,13 @@ server: app: discord: - webhook-url: ${DISCORD_WEBHOOK_URL:} \ No newline at end of file + webhook-url: ${DISCORD_WEBHOOK_URL:} + +springdoc: + swagger-ui: + path: /swagger-ui.html + operations-sorter: method + tags-sorter: alpha + persist-authorization: true + # 로컬 테스트용 토큰 발급 API는 문서에서 제외 + paths-to-exclude: /api/v1/auth/dev/** \ No newline at end of file diff --git a/src/test/java/com/matzip/common/config/OpenApiSmokeTest.java b/src/test/java/com/matzip/common/config/OpenApiSmokeTest.java new file mode 100644 index 0000000..b7866b2 --- /dev/null +++ b/src/test/java/com/matzip/common/config/OpenApiSmokeTest.java @@ -0,0 +1,36 @@ +package com.matzip.common.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = "springdoc.paths-to-exclude=/api/v1/auth/dev/**") +class OpenApiSmokeTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void apiDocs_토큰없이_200_반환하고_경로포함() { + ResponseEntity response = restTemplate.getForEntity("/v3/api-docs", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).contains("/api/v1/places"); + assertThat(response.getBody()).contains("bearerAuth"); + assertThat(response.getBody()).doesNotContain("/api/v1/auth/dev/token"); + } + + @Test + void swaggerUi_토큰없이_401아님() { + ResponseEntity response = restTemplate.getForEntity("/swagger-ui/index.html", String.class); + + assertThat(response.getStatusCode()).isNotEqualTo(HttpStatus.UNAUTHORIZED); + } +}