diff --git a/ontime-back/src/main/java/devkor/ontime_back/config/SwaggerConfig.java b/ontime-back/src/main/java/devkor/ontime_back/config/SwaggerConfig.java index 816e2da..fe10976 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/config/SwaggerConfig.java +++ b/ontime-back/src/main/java/devkor/ontime_back/config/SwaggerConfig.java @@ -1,44 +1,147 @@ package devkor.ontime_back.config; import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.servers.Server; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springdoc.core.customizers.OpenApiCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.util.Collections; +import java.util.List; +import java.util.Set; + @Configuration @OpenAPIDefinition( servers = { @Server(url = "https://3.38.172.54.nip.io", description = "New Production Server"), - @Server(url = "http://localhost:8080", description = "Local Serever") + @Server(url = "http://localhost:8080", description = "Local Server") } ) public class SwaggerConfig { + private static final String ACCESS_TOKEN_SCHEME = "accessToken"; + private static final String REFRESH_TOKEN_SCHEME = "refreshToken"; + + private static final Set PUBLIC_PATHS = Set.of( + "/", + "/health", + "/account-deletion", + "/privacy-policy", + "/sign-up", + "/login", + "/oauth2/google/login", + "/oauth2/kakao/login", + "/oauth2/apple/login", + "/swagger-ui.html", + "/error" + ); + + private static final List PUBLIC_PATH_PREFIXES = List.of( + "/actuator/health", + "/v3/api-docs", + "/swagger-ui", + "/swagger-resources", + "/webjars", + "/css", + "/images", + "/js", + "/favicon.ico", + "/h2-console" + ); + @Bean public OpenAPI openAPI() { return new OpenAPI() .components(new Components() - .addSecuritySchemes("accessToken", new SecurityScheme() - .name("Authorization") // 헤더 이름 + .addSecuritySchemes(ACCESS_TOKEN_SCHEME, new SecurityScheme() + .name("Authorization") .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT") + .description("Send as `Authorization: Bearer `.") + ) + .addSecuritySchemes(REFRESH_TOKEN_SCHEME, new SecurityScheme() + .name("Authorization-refresh") + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .description("Send as `Authorization-refresh: Bearer ` to reissue an access token.") ) ) - .addSecurityItem(new SecurityRequirement().addList("accessToken")) // 요청에 SecurityScheme 적용 + .addSecurityItem(new SecurityRequirement().addList(ACCESS_TOKEN_SCHEME)) .info(apiInfo()); } + @Bean + public OpenApiCustomizer coherentOperationCustomizer() { + return openApi -> { + if (openApi.getPaths() == null) { + return; + } + + openApi.getPaths().forEach((path, pathItem) -> { + if (pathItem == null) { + return; + } + + pathItem.readOperationsMap().forEach((httpMethod, operation) -> { + if (httpMethod == PathItem.HttpMethod.GET) { + operation.setRequestBody(null); + } + if (isPublicPath(path)) { + operation.setSecurity(Collections.emptyList()); + } + removeStaleResponseExamples(operation); + }); + }); + }; + } + private Info apiInfo() { return new Info() .title("Ontime") - .description("Ontime API 명세서\n\n\n\n [JWT 인증 과정]\n\n/sign-up, /login, /{userId}/additional-info\n\n위 세 url을 제외하고는 헤더에 엑세스 토큰을 담아 요청을 보내야 함.\n\n(형식: \"Authorization [엑세스 토큰]\")\n\n\n토큰이 유효하면 요청이 처리될 것이고, 토큰이 유효하지 않으면 실패메세지가 반환될 것임.\n\n\n 엑세스토큰 인증이 실패하면 동일한 url(사실 아무 url이나 상관 없음. 실제로 해당 url로 요청 보내기전에 필터가 가로채서 처리함)로 헤더에 리프레시토큰을 담아 요청을 보내면 리프레시토큰의 유효성에 따라 엑세스토큰이 ResponseBody 재발급 될 것임.\n\n(형식: \"Authorization-refresh [리프레시토큰]\")") + .description(""" + Ontime API 명세서 + + [JWT 인증 과정] + 공개 엔드포인트(`/sign-up`, `/login`, `/oauth2/google/login`, `/oauth2/kakao/login`, `/oauth2/apple/login`, `/health`, `/account-deletion`, `/privacy-policy`)를 제외한 API는 access token이 필요합니다. + + Access token 요청 형식: `Authorization: Bearer ` + + Refresh token으로 access token을 재발급할 때는 보호 API 호출 전에 `Authorization-refresh: Bearer ` 헤더를 보냅니다. 재발급 성공 시 새 access token은 응답 헤더 `Authorization`으로 반환됩니다. + + 일반 로그인과 소셜 로그인, 회원가입 성공 시 access token은 `Authorization` 헤더로, refresh token은 `Authorization-refresh` 헤더로 반환됩니다. + """) .version("1.0.0"); } + + private boolean isPublicPath(String path) { + return PUBLIC_PATHS.contains(path) + || PUBLIC_PATH_PREFIXES.stream().anyMatch(path::startsWith); + } + + private void removeStaleResponseExamples(Operation operation) { + if (operation.getResponses() == null) { + return; + } + + operation.getResponses().values().forEach(apiResponse -> { + if (apiResponse.getContent() == null) { + return; + } + + apiResponse.getContent().values().forEach(mediaType -> { + mediaType.setExample(null); + mediaType.setExamples(null); + if (mediaType.getSchema() != null) { + mediaType.getSchema().setExample(null); + } + }); + }); + } } diff --git a/ontime-back/src/main/java/devkor/ontime_back/controller/SocialAuthController.java b/ontime-back/src/main/java/devkor/ontime_back/controller/SocialAuthController.java index 97244a1..8e53076 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/controller/SocialAuthController.java +++ b/ontime-back/src/main/java/devkor/ontime_back/controller/SocialAuthController.java @@ -2,13 +2,14 @@ import devkor.ontime_back.dto.FeedbackAddDto; import devkor.ontime_back.dto.OAuthAppleRequestDto; -import devkor.ontime_back.dto.OAuthGoogleUserDto; +import devkor.ontime_back.dto.OAuthGoogleRequestDto; import devkor.ontime_back.dto.OAuthKakaoUserDto; import devkor.ontime_back.global.oauth.apple.AppleLoginService; import devkor.ontime_back.global.oauth.google.GoogleLoginService; import devkor.ontime_back.response.ApiResponseForm; import devkor.ontime_back.service.UserAuthService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -34,27 +35,30 @@ public class SocialAuthController { @Operation( summary = "구글 소셜 로그인/회원가입", requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "구글 회원정보 데이터", + description = "구글 identity token 데이터", required = true, content = @Content( schema = @Schema( - type = "object", - example = "{\n \"idToken\": \"eyJhbGxxxxxxx\" ,\n \"refreshToken\": \"\"}}" + implementation = OAuthGoogleRequestDto.class, + example = "{\n \"idToken\": \"eyJhbGxxxxxxx\",\n \"refreshToken\": \"google-refresh-token\"\n}" ) ) ) ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "구글 로그인/회원가입 성공 (로그인시 data : login, 회원가입시 data : register", content = @Content( + @ApiResponse(responseCode = "200", description = "구글 로그인/회원가입 성공. 토큰은 응답 헤더에 반환됨", headers = { + @Header(name = "Authorization", description = "Bearer access token"), + @Header(name = "Authorization-refresh", description = "Bearer refresh token") + }, content = @Content( mediaType = "application/json", schema = @Schema( - example = "{\n \"message\": \"유저의 ROLE이 GUEST이므로 온보딩API를 호출해 온보딩을 진행해야합니다.\",\n \"role\": \"GUEST\"}" + example = "{\n \"status\": \"success\",\n \"code\": \"200\",\n \"message\": \"로그인에 성공하였습니다.\",\n \"data\": {\n \"userId\": 1,\n \"email\": \"user@example.com\",\n \"name\": \"junbeom\",\n \"spareTime\": 10,\n \"note\": null,\n \"punctualityScore\": 100.0,\n \"role\": \"USER\"\n }\n}" ) )), @ApiResponse(responseCode = "4XX", description = "실패", content = @Content(mediaType = "application/json", schema = @Schema(example = "실패 메세지(이메일이 이미 존재할 경우, 이름이 이미 존재할 경우 다르게 출력)"))) }) @PostMapping("/google/login") - public String googleRegisterOrLogin(@Valid @RequestBody OAuthGoogleUserDto oAuthGoogleUserDto, HttpServletResponse response) { + public String googleRegisterOrLogin(@Valid @RequestBody OAuthGoogleRequestDto oAuthGoogleRequestDto, HttpServletResponse response) { return "구글 로그인/회원가입 성공"; // 로그인 처리는 필터에서 적용 } @@ -65,14 +69,17 @@ public String googleRegisterOrLogin(@Valid @RequestBody OAuthGoogleUserDto oAuth required = true, content = @Content( schema = @Schema( - type = "object", - example = "{\n \"id\": \"4803687123\", \n \"profile\": {\n \"nickname\": \"김철수\", \n \"thumbnail_image_url\": \"http://dfsklafj;ewoai.jpg\", \n \"profile_image_url\": \"http://dfsklafj;ewoai.jpg\", \n\"is_default_image\": false, \n \"is_default_nickname\": false\n }\n}" + implementation = OAuthKakaoUserDto.class, + example = "{\n \"id\": \"4803687123\",\n \"profile\": {\n \"nickname\": \"김철수\",\n \"thumbnailImageUrl\": \"https://example.com/thumb.jpg\",\n \"profile_image_url\": \"https://example.com/profile.jpg\",\n \"defaultImage\": false,\n \"defaultNickname\": false\n }\n}" ) ) ) ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "카카오 로그인/회원가입 성공 (로그인시 data : login, 회원가입시 data : register", content = @Content( + @ApiResponse(responseCode = "200", description = "카카오 로그인/회원가입 성공. 토큰은 응답 헤더에 반환됨", headers = { + @Header(name = "Authorization", description = "Bearer access token"), + @Header(name = "Authorization-refresh", description = "Bearer refresh token") + }, content = @Content( mediaType = "application/json", schema = @Schema( example = "{\n \"message\": \"유저의 ROLE이 GUEST이므로 온보딩API를 호출해 온보딩을 진행해야합니다.\",\n \"role\": \"GUEST\"}" @@ -92,17 +99,19 @@ public String kakaoRegisterOrLogin(@Valid @RequestBody OAuthKakaoUserDto oAuthKa required = true, content = @Content( schema = @Schema( - type = "object", + implementation = OAuthAppleRequestDto.class, example = "{\n \"idToken\": \".\",\n \"authCode\": \".\",\n \"fullName\": \"허진서\" }" ) ) ) ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "카카오 로그인/회원가입 성공 (로그인시 data : login, 회원가입시 data : register", content = @Content( + @ApiResponse(responseCode = "200", description = "애플 로그인/회원가입 성공. 토큰은 응답 헤더에 반환됨", headers = { + @Header(name = "Authorization", description = "Bearer access token"), + @Header(name = "Authorization-refresh", description = "Bearer refresh token") + }, content = @Content( mediaType = "application/json", schema = @Schema( - type = "object", - example = "{\n \"status\": \"success\",\n \"code\": \"200\",\n \"message\": \"%s\",\n \"data\": { \"userId\": %d,\n \"email\": \"%s\",\n \"name\": \"%s\",\n \"spareTime\": \"%s\",\n \"note\": \"%s\",\n \"punctualityScore\": %f,\n \"role\": \"%s\" }\n }" + example = "{\n \"status\": \"success\",\n \"code\": \"200\",\n \"message\": \"로그인에 성공하였습니다.\",\n \"data\": {\n \"userId\": 1,\n \"email\": \"user@example.com\",\n \"name\": \"junbeom\",\n \"spareTime\": 10,\n \"note\": null,\n \"punctualityScore\": 100.0,\n \"role\": \"USER\"\n }\n}" ) )), @ApiResponse(responseCode = "4XX", description = "실패", content = @Content(mediaType = "application/json", schema = @Schema(example = "실패 메세지(이메일이 이미 존재할 경우, 이름이 이미 존재할 경우 다르게 출력)"))) diff --git a/ontime-back/src/main/java/devkor/ontime_back/controller/UserAuthController.java b/ontime-back/src/main/java/devkor/ontime_back/controller/UserAuthController.java index c45375d..2b79cac 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/controller/UserAuthController.java +++ b/ontime-back/src/main/java/devkor/ontime_back/controller/UserAuthController.java @@ -5,11 +5,10 @@ import devkor.ontime_back.dto.LoginRequestDto; import devkor.ontime_back.dto.UserInfoResponse; import devkor.ontime_back.dto.UserSignUpDto; -import devkor.ontime_back.entity.User; -import devkor.ontime_back.repository.UserRepository; import devkor.ontime_back.response.ApiResponseForm; import devkor.ontime_back.service.UserAuthService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -34,15 +33,18 @@ public class UserAuthController { description = "회원가입 요청 JSON 데이터", required = true, content = @Content( - schema = @Schema(type = "object", example = "{\"email\": \"user@example.com\", \"password\": \"password123\", \"name\": \"junbeom\"}") + schema = @Schema(implementation = UserSignUpDto.class, example = "{\"email\": \"user@example.com\", \"password\": \"password123!\", \"name\": \"junbeom\"}") ) ) ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "회원가입 성공", content = @Content( + @ApiResponse(responseCode = "200", description = "회원가입 성공", headers = { + @Header(name = "Authorization", description = "Bearer access token"), + @Header(name = "Authorization-refresh", description = "Bearer refresh token") + }, content = @Content( mediaType = "application/json", schema = @Schema( - example = "{\n \"status\": \"success\",\n \"code\": \"200\",\n \"message\": \"회원가입이 성공적으로 완료되었습니다. 온보딩을 진행해주세요( /user/onboarding )\",\n \"data\": {\n \"userId\": 1\n }}" + example = "{\n \"status\": \"success\",\n \"code\": 200,\n \"message\": \"회원가입이 성공적으로 완료되었습니다. 온보딩을 진행해주세요( /user/onboarding )\",\n \"data\": {\n \"userId\": 1,\n \"email\": \"user@example.com\",\n \"name\": \"junbeom\",\n \"spareTime\": null,\n \"note\": null,\n \"punctualityScore\": null,\n \"role\": \"GUEST\",\n \"socialType\": null\n }\n}" ) )), @ApiResponse(responseCode = "4XX", description = "회원가입 실패", content = @Content(mediaType = "application/json", schema = @Schema(example = "실패 메세지(이메일이 이미 존재할 경우, 이름이 이미 존재할 경우 다르게 출력)"))) @@ -57,7 +59,10 @@ public ResponseEntity> signUp(HttpServletReque @Operation(summary = "일반 로그인 (로그인 요청을 통해 JWT 토큰을 발급받음)") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "일반 로그인 성공(반환 문자열 없음. 헤더에 토큰 반환)", content = @Content(mediaType = "application/json", schema = @Schema(example = "{\n \"message\": \"유저의 ROLE이 GUEST이므로 온보딩API를 호출해 온보딩을 진행해야합니다.\", \"role\": \"GUEST\"}"))), + @ApiResponse(responseCode = "200", description = "일반 로그인 성공. 토큰은 응답 헤더에 반환됨", headers = { + @Header(name = "Authorization", description = "Bearer access token"), + @Header(name = "Authorization-refresh", description = "Bearer refresh token") + }, content = @Content(mediaType = "application/json", schema = @Schema(example = "{\n \"status\": \"success\",\n \"code\": \"200\",\n \"message\": \"로그인에 성공하였습니다.\",\n \"data\": {\n \"userId\": 1,\n \"email\": \"user@example.com\",\n \"name\": \"junbeom\",\n \"spareTime\": 10,\n \"note\": null,\n \"punctualityScore\": 100.0,\n \"role\": \"USER\"\n }\n}"))), @ApiResponse(responseCode = "4XX", description = "일반 로그인 실패", content = @Content(mediaType = "application/json", schema = @Schema(example = "실패 메세지(정확히 어떤 메세지인지는 모름)"))) }) @PostMapping("/login") @@ -66,7 +71,7 @@ public String login( description = "로그인 요청 JSON 데이터", required = true, content = @Content( - schema = @Schema(type = "object", example = "{\"email\": \"user@example.com\", \"password\": \"password123\"}") + schema = @Schema(implementation = LoginRequestDto.class, example = "{\"email\": \"user@example.com\", \"password\": \"password123!\"}") ) ) @Valid @RequestBody LoginRequestDto loginRequest) { @@ -81,7 +86,7 @@ public String login( required = true, content = @Content( schema = @Schema( - type = "object", + implementation = ChangePasswordDto.class, example = "{\"currentPassword\": \"password123\", \"newPassword\": \"1q2w3e4r!\"}" ) ) @@ -111,7 +116,7 @@ public ResponseEntity> changePassword(HttpServletRequest description = "계정 삭제 요청 JSON 데이터는 선택사항. 탈퇴 피드백을 남기려면 feedbackId, message를 전달", content = @Content( schema = @Schema( - type = "object", + implementation = FeedbackAddDto.class, example = "{\"feedbackId\": \"d784cde3-9ff9-4054-872a-500bbcc2198a\", \"message\": \"탈퇴 피드백입니다.\"}" ) ) diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/kakao/KakaoLoginFilter.java b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/kakao/KakaoLoginFilter.java index 6c6e105..43c7847 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/kakao/KakaoLoginFilter.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/kakao/KakaoLoginFilter.java @@ -151,7 +151,6 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR log.info("카카오 로그인 성공"); SecurityContextHolder.getContext().setAuthentication(authResult); response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().write("{\"status\":\"success\", \"data\":\"login/register\"}"); } // 인증 실패 처리 diff --git a/ontime-back/src/main/java/devkor/ontime_back/response/ApiResponseForm.java b/ontime-back/src/main/java/devkor/ontime_back/response/ApiResponseForm.java index f6f3f81..d1e5137 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/response/ApiResponseForm.java +++ b/ontime-back/src/main/java/devkor/ontime_back/response/ApiResponseForm.java @@ -1,15 +1,28 @@ package devkor.ontime_back.response; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; @Getter +@Schema(description = "공통 API 응답 래퍼") // @AllArgsConstructor(access = AccessLevel.PRIVATE) -> super 사용시 이용불가 // @EqualsAndHashCode(callSuper = true) // equals()와 hashCode() 메서드를 자동으로 생성하도록 public class ApiResponseForm { // 제네릭 api 응답 객체 + @Schema( + description = "응답 상태. 성공 응답은 success, 일반 오류는 error, JWT 필터 오류는 토큰 상태별 값을 반환합니다.", + example = "success", + allowableValues = {"success", "fail", "error", "accessTokenEmpty", "accessTokenInvalid", "refreshTokenInvalid"} + ) private String status; + + @Schema(description = "애플리케이션 응답 코드", example = "200") private Object code; + + @Schema(description = "응답 메시지", example = "OK") private String message; + + @Schema(description = "응답 데이터. 오류 응답에서는 null일 수 있습니다.", nullable = true) private final T data; public ApiResponseForm(String status, Object code, String message, T data) { this.status = status; // HttpResponse의 생성자 호출 (부모 클래스의 생성자 또는 메서드를 호출, 자식 클래스는 부모 클래스의 private 필드에 직접 접근 X) diff --git a/ontime-back/src/main/java/devkor/ontime_back/response/ValidationErrorResponse.java b/ontime-back/src/main/java/devkor/ontime_back/response/ValidationErrorResponse.java index 9540e7f..24c9030 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/response/ValidationErrorResponse.java +++ b/ontime-back/src/main/java/devkor/ontime_back/response/ValidationErrorResponse.java @@ -1,9 +1,21 @@ package devkor.ontime_back.response; +import io.swagger.v3.oas.annotations.media.Schema; + import java.util.List; -public record ValidationErrorResponse(List errors) { +@Schema(description = "요청 검증 실패 상세") +public record ValidationErrorResponse( + @Schema(description = "필드별 검증 오류 목록") + List errors +) { - public record FieldError(String field, String message) { + @Schema(description = "필드 검증 오류") + public record FieldError( + @Schema(description = "오류가 발생한 필드명", example = "email") + String field, + @Schema(description = "검증 실패 메시지", example = "이메일 형식이 올바르지 않습니다.") + String message + ) { } } diff --git a/ontime-back/src/test/java/devkor/ontime_back/config/SwaggerConfigTest.java b/ontime-back/src/test/java/devkor/ontime_back/config/SwaggerConfigTest.java new file mode 100644 index 0000000..06a374f --- /dev/null +++ b/ontime-back/src/test/java/devkor/ontime_back/config/SwaggerConfigTest.java @@ -0,0 +1,70 @@ +package devkor.ontime_back.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.parameters.RequestBody; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class SwaggerConfigTest { + + private final SwaggerConfig swaggerConfig = new SwaggerConfig(); + + @Test + void openApiDocumentsRealTokenHeaders() { + OpenAPI openAPI = swaggerConfig.openAPI(); + + assertThat(openAPI.getComponents().getSecuritySchemes()) + .containsKeys("accessToken", "refreshToken"); + assertThat(openAPI.getSecurity()) + .containsExactly(new SecurityRequirement().addList("accessToken")); + assertThat(openAPI.getInfo().getDescription()) + .contains("Authorization: Bearer ") + .contains("Authorization-refresh: Bearer ") + .contains("응답 헤더 `Authorization`"); + } + + @Test + void customizerRemovesGetBodiesAndClearsSecurityForPublicEndpoints() { + MediaType staleResponseContent = new MediaType() + .schema(new StringSchema().example("stale")) + .example("stale"); + Operation privateGet = new Operation() + .requestBody(new RequestBody()) + .security(List.of(new SecurityRequirement().addList("accessToken"))) + .responses(new ApiResponses() + .addApiResponse("200", new ApiResponse() + .content(new Content().addMediaType("application/json", staleResponseContent)))); + Operation publicPost = new Operation() + .security(List.of(new SecurityRequirement().addList("accessToken"))); + Operation privatePost = new Operation() + .security(List.of(new SecurityRequirement().addList("accessToken"))); + + OpenAPI openAPI = new OpenAPI().paths(new Paths() + .addPathItem("/schedules", new PathItem() + .get(privateGet) + .post(privatePost)) + .addPathItem("/login", new PathItem() + .post(publicPost))); + + swaggerConfig.coherentOperationCustomizer().customise(openAPI); + + assertThat(privateGet.getRequestBody()).isNull(); + assertThat(privateGet.getSecurity()).containsExactly(new SecurityRequirement().addList("accessToken")); + assertThat(staleResponseContent.getExample()).isNull(); + assertThat(staleResponseContent.getSchema().getExample()).isNull(); + assertThat(privatePost.getSecurity()).containsExactly(new SecurityRequirement().addList("accessToken")); + assertThat(publicPost.getSecurity()).isEmpty(); + } +}