Skip to content

Commit

Permalink
[BE] OpenAPI 3.0 스펙 문서 생성 자동화 (Swagger UI 통해 API 문서 조회) (#262)
Browse files Browse the repository at this point in the history
* chore: Spring REST Docs 저장 경로 수정

* feat: restdocs-api-spec 플러그인 및 의존성 추가

* feat: restdocs-api-spec 설정 작성

참고: https://github.com/ePages-de/restdocs-api-spec#openapi-301-1

* feat: 컨트롤러 테스트에서 MockMvcRestDocumentationWrapper 사용하도록 수정

- 참고: https://github.com/ePages-de/restdocs-api-spec#mockmvc-based-tests

* feat: Swagger UI dist 폴더의 파일을 static/docs/swagger 경로로 이동

1.Swagger UI Release 파일 다운로드
 https://github.com/swagger-api/swagger-ui/releases/tag/v5.9.0

2. 다운 받은 폴더의 dist 경로에 있는 파일을 resources/static/docs/swagger 경로로 이동

* feat: Swagger UI index.html 파일에 Open API 3.0 스펙 파일 경로 추가

* feat: Swagger UI에서 API 테스트 위해 allowed-origins 추가

- http://localhost:63342 (로컬 Swagger UI)
- https://api.bootme.co.kr
- https://staging.api.bootme.co.kr

* feat: README 파일의 API 문서 링크 수정

* feat: 오타 수정
  • Loading branch information
Jinwook94 committed Oct 17, 2023
1 parent 00aaf9a commit 6f2e7b0
Show file tree
Hide file tree
Showing 31 changed files with 236 additions and 31 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ classDef cloudwatch fill:#f2d5e0, stroke:#FFF

## API 문서

- https://api.bootme.co.kr/docs/index.html
- https://api.bootme.co.kr/docs/swagger/index.html
- https://api.bootme.co.kr/docs/rest/index.html

<br>
43 changes: 38 additions & 5 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id 'org.springframework.boot' version '3.1.4'
id 'io.spring.dependency-management' version '1.1.3'
id "org.asciidoctor.jvm.convert" version '3.3.2'
id 'com.epages.restdocs-api-spec' version '0.19.0'
id 'java'
id 'jacoco'
}
Expand Down Expand Up @@ -63,6 +64,7 @@ dependencies {
// rest docs
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.19.0'
}

ext {
Expand Down Expand Up @@ -120,7 +122,7 @@ jacocoTestCoverageVerification {
}

asciidoctor.doFirst {
delete file('src/main/resources/static/docs')
delete file('src/main/resources/static/docs/rest')
}

asciidoctor {
Expand All @@ -133,20 +135,51 @@ asciidoctor {
task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
into file("src/main/resources/static/docs/rest")
}

task buildDocument(type: Copy) {
dependsOn copyDocument
from file("src/main/resources/static/docs")
into file("build/resources/main/static/docs")
from file("src/main/resources/static/docs/rest")
into file("build/resources/main/static/docs/rest")
}

openapi3 {
servers = [
{
url = "https://api.bootme.co.kr"
description = "운영 서버"
},
{
url = "https://staging.api.bootme.co.kr"
description = "스테이징 서버"
},
{
url = "http://localhost:8080"
description = "로컬"
}
]
title = 'BootMe API'
description 'BootMe API 문서'
version = '0.0.1'
format = 'json'
contact = {
name = 'Jin-wook Kim'
email = 'kimjinwook94@gmail.com'
}
separatePublicApi = false
outputDirectory = "src/main/resources/static/docs/swagger/open-api-spec"
outputFileNamePrefix = 'open-api-3.0.1'
}

bootJar {
def dateTime = new Date().format('yyyy-MMdd-HHmm')
archivesBaseName = 'bootme-' + dateTime
archiveFileName = archivesBaseName + '.jar'

dependsOn buildDocument
dependsOn(
buildDocument,
':openapi3'
)
enabled = true
}
2 changes: 1 addition & 1 deletion backend/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ jasypt:
encryptor:
password: encryptKey

allowed-origins: http://localhost:3000,http://localhost:3001,https://bootme.co.kr,https://www.bootme.co.kr,https://staging.bootme.co.kr
allowed-origins: http://localhost:3000,http://localhost:3001,http://localhost:63342,https://bootme.co.kr,https://www.bootme.co.kr,https://staging.bootme.co.kr,https://api.bootme.co.kr,https://staging.api.bootme.co.kr
allowed-ips: ENC(nbcm5j30lelbJkCZkAUxhyeIKaSfynmWWz5QZ5j4qap3jkHlZHFwurEsapQKsKTZeqrF+W40Qh0=)

spring:
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions backend/src/main/resources/static/docs/swagger/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}

*,
*:before,
*:after {
box-sizing: inherit;
}

body {
margin: 0;
background: #fafafa;
}
37 changes: 37 additions & 0 deletions backend/src/main/resources/static/docs/swagger/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="swagger-ui.css" />
<link rel="stylesheet" type="text/css" href="index.css" />
<link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" />
</head>

<body>
<div id="swagger-ui"></div>
<script src="swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script src="swagger-initializer.js" charset="UTF-8"> </script>
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
url: "./open-api-spec/open-api-3.0.1.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
window.ui = ui
}
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;

if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1).replace('?', '&');
} else {
qp = location.search.substring(1);
}

arr = qp.split("&");
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};

isValid = qp.state === sentState;

if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
});
}

if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}

oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}

if (document.readyState !== 'loading') {
run();
} else {
document.addEventListener('DOMContentLoaded', function () {
run();
});
}
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
window.onload = function() {
//<editor-fold desc="Changeable Configuration Block">

// the following lines will be replaced by docker/configurator, when it runs in a docker-container
window.ui = SwaggerUIBundle({
url: "https://petstore.swagger.io/v2/swagger.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});

//</editor-fold>
};

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions backend/src/main/resources/static/docs/swagger/swagger-ui.css

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions backend/src/main/resources/static/docs/swagger/swagger-ui.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willDoNothing;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import static com.bootme.util.fixture.CourseFixture.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.*;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import static com.bootme.util.fixture.CommentFixture.getCommentResponse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.*;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import static com.bootme.util.fixture.CourseFixture.getCompanyResponse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.*;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import static com.bootme.util.fixture.CourseFixture.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.*;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
Expand All @@ -28,7 +28,7 @@ void upload() throws Exception {

//when
ResultActions perform = mockMvc.perform(
MockMvcRequestBuilders.multipart("/images")
RestDocumentationRequestBuilders.multipart("/images")
.file(imageFile)
.param("imageType", "PROFILE")
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import org.springframework.test.web.servlet.ResultActions;

import static com.bootme.util.fixture.MemberFixture.*;
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.*;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.test.web.servlet.ResultActions;

import java.util.List;

import static com.bootme.util.fixture.NotificationFixture.getNotificationResponse;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

Expand All @@ -33,8 +33,9 @@ void findNotificationsByMemberId() throws Exception {
given(notificationService.findNotificationsByMemberId(anyLong())).willReturn(List.of(notificationResponse1, notificationResponse2, notificationResponse3));

//when
ResultActions perform = mockMvc.perform(get("/notifications/{memberId}", 1)
.accept(MediaType.APPLICATION_JSON));
ResultActions perform = mockMvc.perform(
RestDocumentationRequestBuilders.get("/notifications/{memberId}", 1)
.accept(MediaType.APPLICATION_JSON));

//then
perform.andExpect(status().isOk());
Expand All @@ -56,8 +57,9 @@ void markAllNotificationsAsCheckedForMember() throws Exception {
given(notificationService.findNotificationsByMemberId(anyLong())).willReturn(List.of(notificationResponse1, notificationResponse2, notificationResponse3));

//when
ResultActions perform = mockMvc.perform(put("/notifications/{memberId}/checked", 1)
.accept(MediaType.APPLICATION_JSON));
ResultActions perform = mockMvc.perform(
RestDocumentationRequestBuilders.put("/notifications/{memberId}/checked", 1)
.accept(MediaType.APPLICATION_JSON));

//then
perform.andExpect(status().isOk());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import static com.bootme.util.fixture.PostFixture.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.*;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
Expand Down
Loading

0 comments on commit 6f2e7b0

Please sign in to comment.