diff --git a/backend/build.gradle b/backend/build.gradle index a1841323b..0d8478273 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -1,38 +1,62 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.1.1' - id 'io.spring.dependency-management' version '1.1.0' + id 'java' + id 'org.springframework.boot' version '3.1.1' + id 'io.spring.dependency-management' version '1.1.0' + id "org.asciidoctor.jvm.convert" version "3.3.2" } group = 'hanglog' version = '0.0.1-SNAPSHOT' java { - sourceCompatibility = '17' + sourceCompatibility = '17' } configurations { - compileOnly { - extendsFrom annotationProcessor - } + asciidoctorExt + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0' - compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.h2database:h2' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' + + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.h2database:h2' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() +} + +asciidoctor { + dependsOn test + configurations 'asciidoctorExt' +} + +tasks.register('copyApiDocument', Copy) { + dependsOn asciidoctor + doFirst { + delete file("src/main/resources/static/docs") + } + from asciidoctor.outputDir + into file("src/main/resources/static/docs") +} + +build { + dependsOn copyApiDocument } + diff --git a/backend/src/docs/asciidoc/docs.adoc b/backend/src/docs/asciidoc/docs.adoc new file mode 100644 index 000000000..90702a44a --- /dev/null +++ b/backend/src/docs/asciidoc/docs.adoc @@ -0,0 +1,48 @@ += HangLog +:toc: left +:source-highlighter: highlightjs +:sectlinks: + +[[overview-http-status-codes]] +=== HTTP status codes + +|=== +| 상태 코드 | 설명 + +| `200 OK` +| 성공 + +| `201 Created` +| 리소스 생성 + +| `204 NO_CONTENT` +| 성공 후 반환 값 없음 + +| `400 Bad Request` +| 잘못된 요청 + +| `401 Unauthorized` +| 비인증 상태 + +| `403 Forbidden` +| 권한 거부 + +| `404 Not Found` +| 존재하지 않는 요청 리소스 + +| `500 Internal Server Error` +| 서버 에러 +|=== + + +== 여행 API + +=== 단일 여행 생성 + +==== 요청 +include::{snippets}/trip-controller-test/create-trip/http-request.adoc[] +include::{snippets}/trip-controller-test/create-trip/request-fields.adoc[] + +==== 응답 +include::{snippets}/trip-controller-test/create-trip/http-response.adoc[] +include::{snippets}/trip-controller-test/create-trip/response-headers.adoc[] diff --git a/backend/src/test/java/hanglog/trip/presentation/TripControllerTest.java b/backend/src/test/java/hanglog/trip/presentation/TripControllerTest.java index 6e7aeb448..4a228e15e 100644 --- a/backend/src/test/java/hanglog/trip/presentation/TripControllerTest.java +++ b/backend/src/test/java/hanglog/trip/presentation/TripControllerTest.java @@ -1,9 +1,14 @@ package hanglog.trip.presentation; +import static hanglog.trip.restdocs.RestDocsConfiguration.field; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.springframework.http.HttpHeaders.LOCATION; import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -11,25 +16,27 @@ import com.fasterxml.jackson.databind.ObjectMapper; import hanglog.trip.presentation.dto.request.TripRequest; +import hanglog.trip.restdocs.RestDocsConfiguration; +import hanglog.trip.restdocs.RestDocsTest; import hanglog.trip.service.TripService; import java.time.LocalDate; import java.util.Collections; import java.util.List; -import java.util.regex.Matcher; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; -import org.springframework.test.web.servlet.MockMvc; +import org.springframework.restdocs.payload.JsonFieldType; @WebMvcTest(TripController.class) @MockBean(JpaMetamodelMappingContext.class) -class TripControllerTest { - - @Autowired - private MockMvc mockMvc; +@AutoConfigureRestDocs +@Import(RestDocsConfiguration.class) +class TripControllerTest extends RestDocsTest { @Autowired private ObjectMapper objectMapper; @@ -53,7 +60,28 @@ void createTrip() throws Exception { .contentType(APPLICATION_JSON) .content(objectMapper.writeValueAsString(tripRequest))) .andExpect(status().isCreated()) - .andExpect(header().string(LOCATION, "/trips/1")); + .andExpect(header().string(LOCATION, "/trips/1")) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("startDate") + .type(JsonFieldType.STRING) + .description("여행 시작 날짜") + .attributes(field("constraint", "yyyy-MM-dd")), + fieldWithPath("endDate") + .type(JsonFieldType.STRING) + .description("여행 종료 날짜") + .attributes(field("constraint", "yyyy-MM-dd")), + fieldWithPath("cityIds") + .type(JsonFieldType.ARRAY) + .description("도시 ID 목록") + .attributes(field("constraint", "1개 이상의 양의 정수")) + ), + responseHeaders( + headerWithName(LOCATION).description("생성된 여행 URL") + ) + ) + ); } @DisplayName("여행 시작 날짜를 입력하지 않으면 예외가 발생한다.") diff --git a/backend/src/test/java/hanglog/trip/restdocs/RestDocsConfiguration.java b/backend/src/test/java/hanglog/trip/restdocs/RestDocsConfiguration.java new file mode 100644 index 000000000..4568c7aab --- /dev/null +++ b/backend/src/test/java/hanglog/trip/restdocs/RestDocsConfiguration.java @@ -0,0 +1,32 @@ +package hanglog.trip.restdocs; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.operation.preprocess.Preprocessors; +import org.springframework.restdocs.snippet.Attributes.Attribute; + + +@Configuration +public class RestDocsConfiguration { + + public static Attribute field(final String key, final String value) { + return new Attribute(key, value); + } + + @Bean + public RestDocumentationResultHandler write() { + return MockMvcRestDocumentation.document( + "{class-name}/{method-name}", + Preprocessors.preprocessRequest( + Preprocessors.removeHeaders("Content-Length", "Host"), + Preprocessors.prettyPrint() + ), + Preprocessors.preprocessResponse( + Preprocessors.removeHeaders("Transfer-Encoding", "Date", "Keep-Alive", "Connection"), + Preprocessors.prettyPrint() + ) + ); + } +} diff --git a/backend/src/test/java/hanglog/trip/restdocs/RestDocsTest.java b/backend/src/test/java/hanglog/trip/restdocs/RestDocsTest.java new file mode 100644 index 000000000..021bad66f --- /dev/null +++ b/backend/src/test/java/hanglog/trip/restdocs/RestDocsTest.java @@ -0,0 +1,37 @@ +package hanglog.trip.restdocs; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; + +@Import(RestDocsConfiguration.class) +@ExtendWith(RestDocumentationExtension.class) +public abstract class RestDocsTest { + + @Autowired + protected RestDocumentationResultHandler restDocs; + + @Autowired + protected MockMvc mockMvc; + + @BeforeEach + void setUp( + final WebApplicationContext context, + final RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(documentationConfiguration(restDocumentation)) + .alwaysDo(restDocs) + .addFilters(new CharacterEncodingFilter("UTF-8", true)) + .build(); + } +}