From ed7b895f9d34d152fc4c55d5448b9d5779988e66 Mon Sep 17 00:00:00 2001 From: Matt Warman Date: Fri, 14 Dec 2018 06:26:15 -0500 Subject: [PATCH 1/4] See #48. Spring REST Docs test-driven documentation. --- build.gradle | 16 +++- etc/pmd/ruleset.xml | 6 ++ .../_includes/http-response-error.adoc | 16 ++++ .../_includes/response-fields-error.adoc | 32 +++++++ src/docs/asciidoc/create-greeting.adoc | 40 ++++++++ src/docs/asciidoc/delete-greeting.adoc | 32 +++++++ src/docs/asciidoc/get-greeting.adoc | 40 ++++++++ src/docs/asciidoc/get-greetings.adoc | 36 +++++++ src/docs/asciidoc/index.adoc | 27 ++++++ src/docs/asciidoc/send-greeting.adoc | 44 +++++++++ src/docs/asciidoc/update-greeting.adoc | 44 +++++++++ .../ws/web/api/ExceptionDetail.java | 18 ++-- .../resources/config/application.properties | 5 +- .../com/leanstacks/ws/AbstractDocTest.java | 75 +++++++++++++++ .../ws/web/api/CreateGreetingDocTest.java | 92 ++++++++++++++++++ .../ws/web/api/DeleteGreetingDocTest.java | 58 ++++++++++++ .../ws/web/api/GetGreetingDocTest.java | 87 +++++++++++++++++ .../ws/web/api/GetGreetingsDocTest.java | 75 +++++++++++++++ .../ws/web/api/SendGreetingDocTest.java | 90 ++++++++++++++++++ .../ws/web/api/UpdateGreetingDocTest.java | 94 +++++++++++++++++++ 20 files changed, 916 insertions(+), 11 deletions(-) create mode 100644 src/docs/asciidoc/_includes/http-response-error.adoc create mode 100644 src/docs/asciidoc/_includes/response-fields-error.adoc create mode 100644 src/docs/asciidoc/create-greeting.adoc create mode 100644 src/docs/asciidoc/delete-greeting.adoc create mode 100644 src/docs/asciidoc/get-greeting.adoc create mode 100644 src/docs/asciidoc/get-greetings.adoc create mode 100644 src/docs/asciidoc/index.adoc create mode 100644 src/docs/asciidoc/send-greeting.adoc create mode 100644 src/docs/asciidoc/update-greeting.adoc create mode 100644 src/test/java/com/leanstacks/ws/AbstractDocTest.java create mode 100644 src/test/java/com/leanstacks/ws/web/api/CreateGreetingDocTest.java create mode 100644 src/test/java/com/leanstacks/ws/web/api/DeleteGreetingDocTest.java create mode 100644 src/test/java/com/leanstacks/ws/web/api/GetGreetingDocTest.java create mode 100644 src/test/java/com/leanstacks/ws/web/api/GetGreetingsDocTest.java create mode 100644 src/test/java/com/leanstacks/ws/web/api/SendGreetingDocTest.java create mode 100644 src/test/java/com/leanstacks/ws/web/api/UpdateGreetingDocTest.java diff --git a/build.gradle b/build.gradle index 65df97e..322faec 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,6 @@ plugins { id 'org.springframework.boot' version '2.1.1.RELEASE' + id 'org.asciidoctor.convert' version '1.5.3' } apply plugin: 'io.spring.dependency-management' @@ -12,6 +13,8 @@ apply plugin: 'project-report' apply plugin: 'build-dashboard' ext { + snippetsDir = file('build/generated-snippets') + jacocoVersion = '0.8.2' checkstyleVersion = '8.14' pmdVersion = '6.9.0' @@ -45,6 +48,9 @@ dependencies { testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test' testCompile group: 'org.springframework.security', name: 'spring-security-test' + testCompile group: 'org.springframework.restdocs', name: 'spring-restdocs-mockmvc' + + asciidoctor group: 'org.springframework.restdocs', name: 'spring-restdocs-asciidoctor' } defaultTasks 'clean', 'build' @@ -81,7 +87,15 @@ jacocoTestReport { } } -test.finalizedBy jacocoTestReport +test { + outputs.dir snippetsDir + finalizedBy jacocoTestReport +} + +asciidoctor { + inputs.dir snippetsDir + dependsOn test +} checkstyle { toolVersion = checkstyleVersion diff --git a/etc/pmd/ruleset.xml b/etc/pmd/ruleset.xml index 01b1a71..0b5750e 100644 --- a/etc/pmd/ruleset.xml +++ b/etc/pmd/ruleset.xml @@ -51,6 +51,12 @@ + + + + + + diff --git a/src/docs/asciidoc/_includes/http-response-error.adoc b/src/docs/asciidoc/_includes/http-response-error.adoc new file mode 100644 index 0000000..bba7511 --- /dev/null +++ b/src/docs/asciidoc/_includes/http-response-error.adoc @@ -0,0 +1,16 @@ +[source,http,options="nowrap"] +---- +HTTP/1.1 404 +Content-Length: 213 +Content-Type: application/json;charset=UTF-8 + +{ + "timestamp": "2018-12-12T13:16:11.771539Z", + "method": "GET", + "path": "/api/...", + "status": 404, + "statusText": "Not Found", + "exceptionClass": "java.util.NoSuchElementException", + "exceptionMessage": "No value present" +} +---- \ No newline at end of file diff --git a/src/docs/asciidoc/_includes/response-fields-error.adoc b/src/docs/asciidoc/_includes/response-fields-error.adoc new file mode 100644 index 0000000..28f6c68 --- /dev/null +++ b/src/docs/asciidoc/_includes/response-fields-error.adoc @@ -0,0 +1,32 @@ +|=== +|Path|Type|Description + +|`+timestamp+` +|`+String+` +|The time the error occurred. + +|`+method+` +|`+String+` +|The HTTP method. + +|`+path+` +|`+String+` +|The request context path. + +|`+status+` +|`+Number+` +|The response HTTP status code. + +|`+statusText+` +|`+String+` +|The response HTTP status text. + +|`+exceptionClass+` +|`+String+` +|The exception class. + +|`+exceptionMessage+` +|`+String+` +|The exception message. + +|=== \ No newline at end of file diff --git a/src/docs/asciidoc/create-greeting.adoc b/src/docs/asciidoc/create-greeting.adoc new file mode 100644 index 0000000..689070f --- /dev/null +++ b/src/docs/asciidoc/create-greeting.adoc @@ -0,0 +1,40 @@ += API Endpoint - Create Greeting +LeanStacks; +:doctype: book +:icons: font +:source-highlighter: highlightjs +:includedir: _includes + +== Create a new greeting + +=== POST /api/greetings + +Create a new greeting. + +=== Request Body Parameters + +include::{snippets}/create-greeting/request-fields.adoc[] + +=== Response Body Parameters + +include::{snippets}/create-greeting/response-fields.adoc[] + +=== Example Request + +Using cURL: + +include::{snippets}/create-greeting/curl-request.adoc[] + +The HTTP request: + +include::{snippets}/create-greeting/http-request.adoc[] + +=== Example Response + +include::{snippets}/create-greeting/http-response.adoc[] + +=== Example Error Response + +include::{includedir}/http-response-error.adoc[] + +include::{includedir}/response-fields-error.adoc[] diff --git a/src/docs/asciidoc/delete-greeting.adoc b/src/docs/asciidoc/delete-greeting.adoc new file mode 100644 index 0000000..4895c16 --- /dev/null +++ b/src/docs/asciidoc/delete-greeting.adoc @@ -0,0 +1,32 @@ += API Endpoint - Delete Greeting +LeanStacks; +:doctype: book +:icons: font +:source-highlighter: highlightjs +:includedir: _includes + +== Delete a greeting + +=== DELETE /api/greetings/{id} + +Delete all information about a specific greeting. + +=== Example Request + +Using cURL: + +include::{snippets}/delete-greeting/curl-request.adoc[] + +The HTTP request: + +include::{snippets}/delete-greeting/http-request.adoc[] + +=== Example Response + +include::{snippets}/delete-greeting/http-response.adoc[] + +=== Example Error Response + +include::{includedir}/http-response-error.adoc[] + +include::{includedir}/response-fields-error.adoc[] diff --git a/src/docs/asciidoc/get-greeting.adoc b/src/docs/asciidoc/get-greeting.adoc new file mode 100644 index 0000000..14444bd --- /dev/null +++ b/src/docs/asciidoc/get-greeting.adoc @@ -0,0 +1,40 @@ += API Endpoint - Get Greeting +LeanStacks; +:doctype: book +:icons: font +:source-highlighter: highlightjs +:includedir: _includes + +== Get a specific greeting + +=== GET /api/greetings/{id} + +Get the information about a specific greeting. + +=== Path Parameters + +include::{snippets}/get-greeting/path-parameters.adoc[] + +=== Response Body Parameters + +include::{snippets}/get-greeting/response-fields.adoc[] + +=== Example Request + +Using cURL: + +include::{snippets}/get-greeting/curl-request.adoc[] + +The HTTP request: + +include::{snippets}/get-greeting/http-request.adoc[] + +=== Example Response + +include::{snippets}/get-greeting/http-response.adoc[] + +=== Example Error Response + +include::{includedir}/http-response-error.adoc[] + +include::{includedir}/response-fields-error.adoc[] diff --git a/src/docs/asciidoc/get-greetings.adoc b/src/docs/asciidoc/get-greetings.adoc new file mode 100644 index 0000000..1d3e6db --- /dev/null +++ b/src/docs/asciidoc/get-greetings.adoc @@ -0,0 +1,36 @@ += API Endpoint - Get Greetings +LeanStacks; +:doctype: book +:icons: font +:source-highlighter: highlightjs +:includedir: _includes + +== Get a list of greetings + +=== GET /api/greetings + +Get a list of all greetings. + +=== Response Body Parameters + +include::{snippets}/get-greeting/response-fields.adoc[] + +=== Example Request + +Using cURL: + +include::{snippets}/get-greetings/curl-request.adoc[] + +The HTTP request: + +include::{snippets}/get-greetings/http-request.adoc[] + +=== Example Response + +include::{snippets}/get-greetings/http-response.adoc[] + +=== Example Error Response + +include::{includedir}/http-response-error.adoc[] + +include::{includedir}/response-fields-error.adoc[] diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc new file mode 100644 index 0000000..db1f63d --- /dev/null +++ b/src/docs/asciidoc/index.adoc @@ -0,0 +1,27 @@ += Overview +:doctype: book +:icons: font +:source-highlighter: highlightjs + +Review the available resources from the LeanStacks API. Click any endpoint for additional information. + +=== Greetings + +[cols="2,4,4",options="header"] +|=== + +| HTTP method | Endpoint | Function + +| GET | <> | Get a list of greetings + +| GET | <> | Get a specific greeting + +| POST | <> | Create a greeting + +| PUT | <> | Update a greeting + +| DELETE | <> | Delete a greeting + +| POST | <> | Send a greeting + +|=== diff --git a/src/docs/asciidoc/send-greeting.adoc b/src/docs/asciidoc/send-greeting.adoc new file mode 100644 index 0000000..9a89133 --- /dev/null +++ b/src/docs/asciidoc/send-greeting.adoc @@ -0,0 +1,44 @@ += API Endpoint - Send Greeting +LeanStacks; +:doctype: book +:icons: font +:source-highlighter: highlightjs +:includedir: _includes + +== Send a greeting + +=== POST /api/greetings/{id}/send + +Email a specific greeting synchronously or asynchronously. + +=== Path Parameters + +include::{snippets}/send-greeting/path-parameters.adoc[] + +=== Request Parameters + +include::{snippets}/send-greeting/request-parameters.adoc[] + +=== Response Body Parameters + +include::{snippets}/send-greeting/response-fields.adoc[] + +=== Example Request + +Using cURL: + +include::{snippets}/send-greeting/curl-request.adoc[] + +The HTTP request: + +include::{snippets}/send-greeting/http-request.adoc[] + +=== Example Response + +include::{snippets}/send-greeting/http-response.adoc[] + +=== Example Error Response + +include::{includedir}/http-response-error.adoc[] + +include::{includedir}/response-fields-error.adoc[] diff --git a/src/docs/asciidoc/update-greeting.adoc b/src/docs/asciidoc/update-greeting.adoc new file mode 100644 index 0000000..87f875c --- /dev/null +++ b/src/docs/asciidoc/update-greeting.adoc @@ -0,0 +1,44 @@ += API Endpoint - Update Greeting +LeanStacks; +:doctype: book +:icons: font +:source-highlighter: highlightjs +:includedir: _includes + +== Update a greeting + +=== PUT /api/greetings/{id} + +Update the information about a specific greeting. + +=== Path Parameters + +include::{snippets}/update-greeting/path-parameters.adoc[] + +=== Request Body Parameters + +include::{snippets}/update-greeting/request-fields.adoc[] + +=== Response Body Parameters + +include::{snippets}/update-greeting/response-fields.adoc[] + +=== Example Request + +Using cURL: + +include::{snippets}/update-greeting/curl-request.adoc[] + +The HTTP request: + +include::{snippets}/update-greeting/http-request.adoc[] + +=== Example Response + +include::{snippets}/update-greeting/http-response.adoc[] + +=== Example Error Response + +include::{includedir}/http-response-error.adoc[] + +include::{includedir}/response-fields-error.adoc[] diff --git a/src/main/java/com/leanstacks/ws/web/api/ExceptionDetail.java b/src/main/java/com/leanstacks/ws/web/api/ExceptionDetail.java index 0d9c577..de636ca 100644 --- a/src/main/java/com/leanstacks/ws/web/api/ExceptionDetail.java +++ b/src/main/java/com/leanstacks/ws/web/api/ExceptionDetail.java @@ -1,5 +1,7 @@ package com.leanstacks.ws.web.api; +import java.time.Instant; + /** * The ExceptionDetail class models information about a web service request which results in an Exception. This * information may be returned to the client. @@ -10,15 +12,15 @@ public class ExceptionDetail { /** - * A timestamp expressed in milliseconds. + * The time the exception occurred. */ - private long timestamp; + private Instant timestamp; /** * The HTTP method (e.g. GET, POST, etc.) */ private String method = ""; /** - * The web service servlet path. + * The web service context path. */ private String path = ""; /** @@ -42,24 +44,24 @@ public class ExceptionDetail { * Construct an ExceptionDetail. */ public ExceptionDetail() { - this.timestamp = System.currentTimeMillis(); + this.timestamp = Instant.now(); } /** * Returns the timestamp attribute value. * - * @return A long. + * @return An Instant. */ - public long getTimestamp() { + public Instant getTimestamp() { return timestamp; } /** * Sets the timestamp attribute value. * - * @param timestamp A long. + * @param timestamp An Instant. */ - public void setTimestamp(final long timestamp) { + public void setTimestamp(final Instant timestamp) { this.timestamp = timestamp; } diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties index c46bbcf..e5281f9 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -4,9 +4,9 @@ ## # Profile Configuration -# profiles: hsqldb, mysql, batch, docs +# profiles: hsqldb, mysql, batch ## -spring.profiles.active=hsqldb,batch,docs +spring.profiles.active=hsqldb,batch ## # Web Server Configuration @@ -59,6 +59,7 @@ logging.level.com.leanstacks.ws=DEBUG logging.level.org.springboot=INFO logging.level.org.springframework=INFO logging.level.org.springframework.security=INFO +logging.level.org.springframework.restdocs=DEBUG # Uncomment the 2 hibernate appenders below to show SQL and params in logs logging.level.org.hibernate.SQL=DEBUG #logging.level.org.hibernate.type.descriptor.sql=TRACE diff --git a/src/test/java/com/leanstacks/ws/AbstractDocTest.java b/src/test/java/com/leanstacks/ws/AbstractDocTest.java new file mode 100644 index 0000000..ab4a84e --- /dev/null +++ b/src/test/java/com/leanstacks/ws/AbstractDocTest.java @@ -0,0 +1,75 @@ +package com.leanstacks.ws; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import com.leanstacks.ws.util.RequestContext; + +/** + * The AbstractDocTest class is the parent of all JUnit test classes which create Spring REST Docs. This class + * configures the test ApplicationContext and test runner environment to facilitate the creation of API documentation + * via Spring REST Docs. + * + * @author Matt Warman + */ +public abstract class AbstractDocTest { + + /** + * A MockMvc instance configured with Spring REST Docs configuration. + */ + protected transient MockMvc mockMvc; + + /** + * A WebApplicationContext instance. + */ + @Autowired + private transient WebApplicationContext context; + + /** + * A JUnit 4.x Rule for Spring REST Documentation generation. + */ + @Rule + public transient JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); + + /** + * Perform set up activities before each unit test. Invoked by the JUnit framework. + */ + @Before + public void before() { + RequestContext.setUsername(AbstractTest.USERNAME); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation).uris().withScheme("https") + .withHost("api.leanstacks.net").withPort(443).and().operationPreprocessors() + .withRequestDefaults(prettyPrint()).withResponseDefaults(prettyPrint())) + .build(); + doBeforeEachTest(); + } + + /** + * Perform initialization tasks before the execution of each test method. + */ + public abstract void doBeforeEachTest(); + + /** + * Perform clean up activities after each unit test. Invoked by the JUnit framework. + */ + @After + public void after() { + doAfterEachTest(); + } + + /** + * Perform clean up tasks after the execution of each test method. + */ + public abstract void doAfterEachTest(); + +} diff --git a/src/test/java/com/leanstacks/ws/web/api/CreateGreetingDocTest.java b/src/test/java/com/leanstacks/ws/web/api/CreateGreetingDocTest.java new file mode 100644 index 0000000..c90d2ea --- /dev/null +++ b/src/test/java/com/leanstacks/ws/web/api/CreateGreetingDocTest.java @@ -0,0 +1,92 @@ +package com.leanstacks.ws.web.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.PayloadDocumentation; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import com.leanstacks.ws.AbstractDocTest; +import com.leanstacks.ws.RestControllerTest; + +/** + *

+ * Generate REST API documentation for the GreetingController. + *

+ *

+ * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class + * responsible for unit testing functionality. + *

+ * + * @author Matt Warman + */ +@RunWith(SpringRunner.class) +@RestControllerTest +public class CreateGreetingDocTest extends AbstractDocTest { + + /** + * The HTTP request body content. + */ + private static final String REQUEST_BODY = "{ \"text\": \"Bonjour le monde!\" }"; + + @Override + public void doBeforeEachTest() { + // perform test initialization + } + + @Override + public void doAfterEachTest() { + // perform test cleanup + } + + /** + * Generate API documentation for POST /api/greetings. + * + * @throws Exception Thrown if documentation generation failure occurs. + */ + @Test + public void documentCreateGreeting() throws Exception { + + // Generate API Documentation + final MvcResult result = this.mockMvc + .perform(RestDocumentationRequestBuilders.post("/api/greetings").contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON).content(REQUEST_BODY)) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(MockMvcRestDocumentation.document("create-greeting", + PayloadDocumentation.relaxedRequestFields(PayloadDocumentation.fieldWithPath("text") + .description("The text.").type(JsonFieldType.STRING)), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation + .fieldWithPath("id") + .description( + "The identifier. Used to reference specific greetings in API requests.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("referenceId") + .description("The supplementary identifier.").type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("text").description("The text.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("version").description("The entity version.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("createdBy").description("The entity creator.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("createdAt").description("The creation timestamp.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("updatedBy").description("The last modifier.") + .type(JsonFieldType.STRING).optional(), + PayloadDocumentation.fieldWithPath("updatedAt") + .description("The last modification timestamp.").type(JsonFieldType.STRING) + .optional()))) + .andReturn(); + + // Perform a simple, standard JUnit assertion to satisfy PMD rule + Assert.assertEquals("failure - expected HTTP status 201", 201, result.getResponse().getStatus()); + + } + +} diff --git a/src/test/java/com/leanstacks/ws/web/api/DeleteGreetingDocTest.java b/src/test/java/com/leanstacks/ws/web/api/DeleteGreetingDocTest.java new file mode 100644 index 0000000..478024d --- /dev/null +++ b/src/test/java/com/leanstacks/ws/web/api/DeleteGreetingDocTest.java @@ -0,0 +1,58 @@ +package com.leanstacks.ws.web.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import com.leanstacks.ws.AbstractDocTest; +import com.leanstacks.ws.RestControllerTest; + +/** + *

+ * Generate REST API documentation for the GreetingController. + *

+ *

+ * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class + * responsible for unit testing functionality. + *

+ * + * @author Matt Warman + */ +@RunWith(SpringRunner.class) +@RestControllerTest +public class DeleteGreetingDocTest extends AbstractDocTest { + + @Override + public void doBeforeEachTest() { + // perform test initialization + } + + @Override + public void doAfterEachTest() { + // perform test cleanup + } + + /** + * Generate API documentation for DELETE /api/greetings/{id}. + * + * @throws Exception Thrown if documentation generation failure occurs. + */ + @Test + public void documentDeleteGreeting() throws Exception { + + // Generate API Documentation + final MvcResult result = this.mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/greetings/{id}", 1)) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(MockMvcRestDocumentation.document("delete-greeting")).andReturn(); + + // Perform a simple, standard JUnit assertion to satisfy PMD rule + Assert.assertEquals("failure - expected HTTP status 204", 204, result.getResponse().getStatus()); + + } + +} diff --git a/src/test/java/com/leanstacks/ws/web/api/GetGreetingDocTest.java b/src/test/java/com/leanstacks/ws/web/api/GetGreetingDocTest.java new file mode 100644 index 0000000..306a658 --- /dev/null +++ b/src/test/java/com/leanstacks/ws/web/api/GetGreetingDocTest.java @@ -0,0 +1,87 @@ +package com.leanstacks.ws.web.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.PayloadDocumentation; +import org.springframework.restdocs.request.RequestDocumentation; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import com.leanstacks.ws.AbstractDocTest; +import com.leanstacks.ws.RestControllerTest; + +/** + *

+ * Generate REST API documentation for the GreetingController. + *

+ *

+ * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class + * responsible for unit testing functionality. + *

+ * + * @author Matt Warman + */ +@RunWith(SpringRunner.class) +@RestControllerTest +public class GetGreetingDocTest extends AbstractDocTest { + + @Override + public void doBeforeEachTest() { + // perform test initialization + } + + @Override + public void doAfterEachTest() { + // perform test cleanup + } + + /** + * Generate API documentation for GET /api/greetings/{id}. + * + * @throws Exception Thrown if documentation generation failure occurs. + */ + @Test + public void documentGetGreeting() throws Exception { + + // Generate API Documentation + final MvcResult result = this.mockMvc + .perform(RestDocumentationRequestBuilders + .get("/api/greetings/{id}", "0").accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("get-greeting", + RequestDocumentation.pathParameters( + RequestDocumentation.parameterWithName("id").description("The greeting identifier.")), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation.fieldWithPath("id") + .description( + "The identifier. Used to reference specific greetings in API requests.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("referenceId") + .description("The supplementary identifier.").type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("text").description("The text.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("version").description("The entity version.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("createdBy").description("The entity creator.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("createdAt").description("The creation timestamp.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("updatedBy").description("The last modifier.") + .type(JsonFieldType.STRING).optional(), + PayloadDocumentation.fieldWithPath("updatedAt") + .description("The last modification timestamp.").type(JsonFieldType.STRING) + .optional()))) + .andReturn(); + + // Perform a simple, standard JUnit assertion to satisfy PMD rule + Assert.assertEquals("failure - expected HTTP status 200", 200, result.getResponse().getStatus()); + + } + +} diff --git a/src/test/java/com/leanstacks/ws/web/api/GetGreetingsDocTest.java b/src/test/java/com/leanstacks/ws/web/api/GetGreetingsDocTest.java new file mode 100644 index 0000000..7b95b0c --- /dev/null +++ b/src/test/java/com/leanstacks/ws/web/api/GetGreetingsDocTest.java @@ -0,0 +1,75 @@ +package com.leanstacks.ws.web.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.PayloadDocumentation; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import com.leanstacks.ws.AbstractDocTest; +import com.leanstacks.ws.RestControllerTest; + +/** + *

+ * Generate REST API documentation for the GreetingController. + *

+ *

+ * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class + * responsible for unit testing functionality. + *

+ * + * @author Matt Warman + */ +@RunWith(SpringRunner.class) +@RestControllerTest +public class GetGreetingsDocTest extends AbstractDocTest { + + @Override + public void doBeforeEachTest() { + // perform test initialization + } + + @Override + public void doAfterEachTest() { + // perform test cleanup + } + + /** + * Generate API documentation for GET /api/greetings. + * + * @throws Exception Thrown if documentation generation failure occurs. + */ + @Test + public void documentGetGreetings() throws Exception { + + // Generate API Documentation + final MvcResult result = this.mockMvc + .perform(RestDocumentationRequestBuilders.get("/api/greetings").accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("get-greetings", + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation.fieldWithPath("[].id").description( + "The identifier. Used to reference specific greetings in API requests."), + PayloadDocumentation.fieldWithPath("[].referenceId") + .description("The supplementary identifier."), + PayloadDocumentation.fieldWithPath("[].text").description("The text."), + PayloadDocumentation.fieldWithPath("[].version").description("The entity version."), + PayloadDocumentation.fieldWithPath("[].createdBy").description("The entity creator."), + PayloadDocumentation.fieldWithPath("[].createdAt") + .description("The creation timestamp."), + PayloadDocumentation.fieldWithPath("[].updatedBy").description("The last modifier."), + PayloadDocumentation.fieldWithPath("[].updatedAt") + .description("The last modification timestamp.")))) + .andReturn(); + + // Perform a simple, standard JUnit assertion to satisfy PMD rule + Assert.assertEquals("failure - expected HTTP status 200", 200, result.getResponse().getStatus()); + + } + +} diff --git a/src/test/java/com/leanstacks/ws/web/api/SendGreetingDocTest.java b/src/test/java/com/leanstacks/ws/web/api/SendGreetingDocTest.java new file mode 100644 index 0000000..f139b0f --- /dev/null +++ b/src/test/java/com/leanstacks/ws/web/api/SendGreetingDocTest.java @@ -0,0 +1,90 @@ +package com.leanstacks.ws.web.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.PayloadDocumentation; +import org.springframework.restdocs.request.RequestDocumentation; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import com.leanstacks.ws.AbstractDocTest; +import com.leanstacks.ws.RestControllerTest; + +/** + *

+ * Generate REST API documentation for the GreetingController. + *

+ *

+ * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class + * responsible for unit testing functionality. + *

+ * + * @author Matt Warman + */ +@RunWith(SpringRunner.class) +@RestControllerTest +public class SendGreetingDocTest extends AbstractDocTest { + + @Override + public void doBeforeEachTest() { + // perform test initialization + } + + @Override + public void doAfterEachTest() { + // perform test cleanup + } + + /** + * Generate API documentation for POST /api/greetings/{id}/send. + * + * @throws Exception Thrown if documentation generation failure occurs. + */ + @Test + public void documentSendGreeting() throws Exception { + + // Generate API Documentation + final MvcResult result = this.mockMvc + .perform(RestDocumentationRequestBuilders + .post("/api/greetings/{id}/send?wait=true", 1).accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("send-greeting", + RequestDocumentation.pathParameters( + RequestDocumentation.parameterWithName("id").description("The greeting identifier.")), + RequestDocumentation.requestParameters(RequestDocumentation.parameterWithName("wait") + .description("Optional. Boolean. Wait for email to be sent.").optional()), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation + .fieldWithPath("id") + .description( + "The identifier. Used to reference specific greetings in API requests.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("referenceId") + .description("The supplementary identifier.").type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("text").description("The text.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("version").description("The entity version.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("createdBy").description("The entity creator.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("createdAt").description("The creation timestamp.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("updatedBy").description("The last modifier.") + .type(JsonFieldType.STRING).optional(), + PayloadDocumentation.fieldWithPath("updatedAt") + .description("The last modification timestamp.").type(JsonFieldType.STRING) + .optional()))) + .andReturn(); + + // Perform a simple, standard JUnit assertion to satisfy PMD rule + Assert.assertEquals("failure - expected HTTP status 200", 200, result.getResponse().getStatus()); + + } + +} diff --git a/src/test/java/com/leanstacks/ws/web/api/UpdateGreetingDocTest.java b/src/test/java/com/leanstacks/ws/web/api/UpdateGreetingDocTest.java new file mode 100644 index 0000000..6b31a86 --- /dev/null +++ b/src/test/java/com/leanstacks/ws/web/api/UpdateGreetingDocTest.java @@ -0,0 +1,94 @@ +package com.leanstacks.ws.web.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.PayloadDocumentation; +import org.springframework.restdocs.request.RequestDocumentation; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import com.leanstacks.ws.AbstractDocTest; +import com.leanstacks.ws.RestControllerTest; + +/** + *

+ * Generate REST API documentation for the GreetingController. + *

+ *

+ * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class + * responsible for unit testing functionality. + *

+ * + * @author Matt Warman + */ +@RunWith(SpringRunner.class) +@RestControllerTest +public class UpdateGreetingDocTest extends AbstractDocTest { + + /** + * The HTTP request body content. + */ + private static final String REQUEST_BODY = "{ \"text\": \"Bonjour le monde!\" }"; + + @Override + public void doBeforeEachTest() { + // perform test initialization + } + + @Override + public void doAfterEachTest() { + // perform test cleanup + } + + /** + * Generate API documentation for PUT /api/greetings/{id}. + * + * @throws Exception Thrown if documentation generation failure occurs. + */ + @Test + public void documentUpdateGreeting() throws Exception { + + // Generate API Documentation + final MvcResult result = this.mockMvc.perform(RestDocumentationRequestBuilders.put("/api/greetings/{id}", 1) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).content(REQUEST_BODY)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("update-greeting", + RequestDocumentation.pathParameters( + RequestDocumentation.parameterWithName("id").description("The greeting identifier.")), + PayloadDocumentation.relaxedRequestFields(PayloadDocumentation.fieldWithPath("text") + .description("The text.").type(JsonFieldType.STRING)), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation + .fieldWithPath("id") + .description( + "The identifier. Used to reference specific greetings in API requests.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("referenceId") + .description("The supplementary identifier.").type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("text").description("The text.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("version").description("The entity version.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("createdBy").description("The entity creator.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("createdAt").description("The creation timestamp.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("updatedBy").description("The last modifier.") + .type(JsonFieldType.STRING).optional(), + PayloadDocumentation.fieldWithPath("updatedAt") + .description("The last modification timestamp.").type(JsonFieldType.STRING) + .optional()))) + .andReturn(); + + // Perform a simple, standard JUnit assertion to satisfy PMD rule + Assert.assertEquals("failure - expected HTTP status 200", 200, result.getResponse().getStatus()); + + } + +} From 336fbc2ace6dd9d31e2a2a81a842306d179066a3 Mon Sep 17 00:00:00 2001 From: Matt Warman Date: Sat, 15 Dec 2018 07:18:30 -0500 Subject: [PATCH 2/4] See #48. README --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e247eaf..f7a51ab 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ The project contains unit and integration test examples for standard components The project illustrates the use of Spring Boot Actuator for application monitoring and management. The application demonstrates the recording of custom metrics and the creation of custom health checks. Also, custom Maven and Gradle project attributes are incorporated into the Actuator info endpoint. #### API Documentation Generator -The project includes [Springfox](http://springfox.github.io/springfox/) Swagger integration to automatically generate API docs for the RESTful web service endpoints. This feature may be activated using the *"docs"* Spring profile. +The project includes [Spring REST Docs](https://spring.io/projects/spring-restdocs) integration to automatically generate API docs for the RESTful web service endpoints. This feature may be activated using the *"asciidoctor"* Gradle task. #### Executable Jar The Maven and Gradle builds produce a fully executable Spring Boot Jar file. The Jar file may be executed directly from the command line without the *"java -jar"* command and may be installed on servers as a Linux service. @@ -279,6 +279,26 @@ java -jar build/libs/example-1.0.0.jar --spring.profiles.active=mysql,batch ./example-1.0.0.jar --spring.profiles.active=mysql,batch ``` +#### asciidoctor + +The `asciidoctor` Gradle task performs the following workflow steps: + +* compiles Java classes to the /build directory +* copies all resources to the /build directory +* executes the unit test suites +* generates [Asciidoctor](https://asciidoctor.org/) snippets in the /build/generated-snippets directory +* generates HTML API Docs in the /build/asciidoc/html5 directory + +The `asciidoctor` Gradle task generates API documentation using [Spring REST Docs](https://spring.io/projects/spring-restdocs). + +To execute the `asciidoctor` Gradle task, type the following command at a terminal prompt in the project base directory. + +``` +./gradlew clean asciidoctor +``` + +The API documentation is placed in the /build/asciidoc/html5 directory and the root document is named `index.html`. + #### encodePassword The `encodePassword` Gradle task executes the `BCryptPasswordEncoderUtil` utility class to encode password values which may be included in the sample database scripts. The clear text password values are passed as a Gradle `-P` property arguments on the command line. From be899c8bb4f5f966252850b9690b75915987ec71 Mon Sep 17 00:00:00 2001 From: Matt Warman Date: Sat, 15 Dec 2018 07:24:23 -0500 Subject: [PATCH 3/4] See #48. Remove Swagger --- build.gradle | 4 +- pom.xml | 13 --- .../leanstacks/ws/ApiDocsConfiguration.java | 88 ------------------- .../com/leanstacks/ws/model/Greeting.java | 6 -- .../ws/model/TransactionalEntity.java | 37 -------- .../ws/security/SecurityConfiguration.java | 30 ------- .../ws/web/api/GreetingController.java | 67 ++------------ .../RestResponseEntityExceptionHandler.java | 2 +- .../ws/web/docs/ApiDocsController.java | 28 ------ .../resources/config/application-docs.yaml | 19 ---- 10 files changed, 7 insertions(+), 287 deletions(-) delete mode 100644 src/main/java/com/leanstacks/ws/ApiDocsConfiguration.java delete mode 100644 src/main/java/com/leanstacks/ws/web/docs/ApiDocsController.java delete mode 100644 src/main/resources/config/application-docs.yaml diff --git a/build.gradle b/build.gradle index 322faec..55fe8fe 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,6 @@ ext { jacocoVersion = '0.8.2' checkstyleVersion = '8.14' pmdVersion = '6.9.0' - swaggerVersion = '2.7.0' } group = 'com.leanstacks' @@ -40,8 +39,6 @@ dependencies { compile group: 'com.github.ben-manes.caffeine', name: 'caffeine' compile group: 'org.liquibase', name: 'liquibase-core' - compile group: 'io.springfox', name: 'springfox-swagger2', version: swaggerVersion - compile group: 'io.springfox', name: 'springfox-swagger-ui', version: swaggerVersion runtime group: 'org.hsqldb', name: 'hsqldb' runtime group: 'mysql', name: 'mysql-connector-java' @@ -49,6 +46,7 @@ dependencies { testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test' testCompile group: 'org.springframework.security', name: 'spring-security-test' testCompile group: 'org.springframework.restdocs', name: 'spring-restdocs-mockmvc' + testCompile group: 'com.google.guava', name: 'guava', version: '27.0.1-jre' asciidoctor group: 'org.springframework.restdocs', name: 'spring-restdocs-asciidoctor' } diff --git a/pom.xml b/pom.xml index b617fcb..d6d8add 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,6 @@ UTF-8 11 - 2.7.0 @@ -66,18 +65,6 @@ - - - io.springfox - springfox-swagger2 - ${swagger.version} - - - io.springfox - springfox-swagger-ui - ${swagger.version} - - org.springframework.boot diff --git a/src/main/java/com/leanstacks/ws/ApiDocsConfiguration.java b/src/main/java/com/leanstacks/ws/ApiDocsConfiguration.java deleted file mode 100644 index 1d54a54..0000000 --- a/src/main/java/com/leanstacks/ws/ApiDocsConfiguration.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.leanstacks.ws; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; - -import com.google.common.base.Predicate; - -import springfox.documentation.builders.ApiInfoBuilder; -import springfox.documentation.builders.PathSelectors; -import springfox.documentation.service.ApiInfo; -import springfox.documentation.service.Contact; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spring.web.plugins.Docket; -import springfox.documentation.swagger2.annotations.EnableSwagger2; - -/** - * The ApiDocsConfiguration class provides configuration beans for the Swagger API documentation generator. - * - * @author Matt Warman - */ -@Profile("docs") -@Configuration -@EnableSwagger2 -public class ApiDocsConfiguration { - - /** - * The project version. - */ - public static final String PROJECT_VERSION = "2.3.0"; - /** - * The project contact name. - */ - public static final String PROJECT_CONTACT_NAME = "LeanStacks.com"; - /** - * The project contact URL. - */ - public static final String PROJECT_CONTACT_URL = "http://www.leanstacks.com/"; - - /** - * Create a Contact class to be used by Springfox's Swagger API Documentation framework. - * - * @return A Contact instance. - */ - private Contact contact() { - return new Contact(PROJECT_CONTACT_NAME, PROJECT_CONTACT_URL, null); - } - - /** - * Create an ApiInfo class to be used by Springfox's Swagger API Documentation framework. - * - * @return An ApiInfo instance. - */ - private ApiInfo apiInfo() { - - // @formatter:off - return new ApiInfoBuilder() - .title("Project Skeleton for Spring Boot Web Services") - .description("The Spring Boot web services starter project provides a foundation " - + "to rapidly construct a RESTful web services application.") - .contact(contact()) - .version(PROJECT_VERSION) - .build(); - // @formatter:on - - } - - /** - * Create a Docket class to be used by Springfox's Swagger API Documentation framework. See - * http://springfox.github.io/springfox/ for more information. - * - * @return A Docket instance. - */ - @Bean - public Docket docket() { - final Predicate paths = PathSelectors.ant("/api/**"); - - // @formatter:off - return new Docket(DocumentationType.SWAGGER_2) - .apiInfo(apiInfo()) - .select() - .paths(paths) - .build(); - // @formatter:on - - } - -} diff --git a/src/main/java/com/leanstacks/ws/model/Greeting.java b/src/main/java/com/leanstacks/ws/model/Greeting.java index f63b01d..663c965 100644 --- a/src/main/java/com/leanstacks/ws/model/Greeting.java +++ b/src/main/java/com/leanstacks/ws/model/Greeting.java @@ -3,8 +3,6 @@ import javax.persistence.Entity; import javax.validation.constraints.NotNull; -import io.swagger.annotations.ApiModelProperty; - /** * The Greeting class is an entity model object. * @@ -18,10 +16,6 @@ public class Greeting extends TransactionalEntity { /** * The text value. */ - @ApiModelProperty(value = "The actual text of the Greeting.", - required = true, - position = 100, - example = "Hello World!") @NotNull private String text; diff --git a/src/main/java/com/leanstacks/ws/model/TransactionalEntity.java b/src/main/java/com/leanstacks/ws/model/TransactionalEntity.java index cef678f..6230c78 100644 --- a/src/main/java/com/leanstacks/ws/model/TransactionalEntity.java +++ b/src/main/java/com/leanstacks/ws/model/TransactionalEntity.java @@ -15,8 +15,6 @@ import com.leanstacks.ws.util.RequestContext; -import io.swagger.annotations.ApiModelProperty; - /** * The parent class for all transactional persistent entities. * @@ -33,11 +31,6 @@ public class TransactionalEntity implements Serializable { /** * The primary key identifier. */ - @ApiModelProperty(value = "Primary key identifier", - required = false, - position = 1, - readOnly = true, - example = "1") @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -45,65 +38,35 @@ public class TransactionalEntity implements Serializable { /** * A secondary unique identifier which may be used as a reference to this entity by external systems. */ - @ApiModelProperty(value = "External reference identifier", - required = false, - position = 2, - readOnly = true, - example = "a1b2c3d4") @NotNull private String referenceId = UUID.randomUUID().toString(); /** * The entity instance version used for optimistic locking. */ - @ApiModelProperty(value = "Version of this model object since creation", - required = false, - position = Integer.MAX_VALUE - 500, - readOnly = true, - example = "1") @Version private Integer version; /** * A reference to the entity or process which created this entity instance. */ - @ApiModelProperty(value = "Identifies the object creator", - required = false, - position = Integer.MAX_VALUE - 400, - readOnly = true, - example = "user") @NotNull private String createdBy; /** * The timestamp when this entity instance was created. */ - @ApiModelProperty(value = "The object creation timestamp", - required = false, - position = Integer.MAX_VALUE - 300, - readOnly = true, - example = "1499418339522") @NotNull private Instant createdAt; /** * A reference to the entity or process which most recently updated this entity instance. */ - @ApiModelProperty(value = "Identifies the object updater", - required = false, - position = Integer.MAX_VALUE - 200, - readOnly = true, - example = "usertoo") private String updatedBy; /** * The timestamp when this entity instance was most recently updated. */ - @ApiModelProperty(value = "The object update timestamp", - required = false, - position = Integer.MAX_VALUE - 100, - readOnly = true, - example = "1499418343681") private Instant updatedAt; public Long getId() { diff --git a/src/main/java/com/leanstacks/ws/security/SecurityConfiguration.java b/src/main/java/com/leanstacks/ws/security/SecurityConfiguration.java index 4586d86..b45fe69 100644 --- a/src/main/java/com/leanstacks/ws/security/SecurityConfiguration.java +++ b/src/main/java/com/leanstacks/ws/security/SecurityConfiguration.java @@ -5,7 +5,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; 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.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; @@ -183,33 +182,4 @@ public AuthenticationEntryPoint actuatorAuthenticationEntryPoint() { } - /** - * This inner class configures the WebSecurityConfigurerAdapter instance for any remaining context paths not handled - * by other adapters. - * - * @author Matt Warman - */ - @Profile("docs") - @Configuration - @Order(3) - public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(final HttpSecurity http) throws Exception { - - // @formatter:off - - http - .csrf().disable() - .authorizeRequests() - .anyRequest().authenticated() - .and() - .formLogin(); - - // @formatter:on - - } - - } - } diff --git a/src/main/java/com/leanstacks/ws/web/api/GreetingController.java b/src/main/java/com/leanstacks/ws/web/api/GreetingController.java index a06b3ef..b54d2a5 100644 --- a/src/main/java/com/leanstacks/ws/web/api/GreetingController.java +++ b/src/main/java/com/leanstacks/ws/web/api/GreetingController.java @@ -24,11 +24,6 @@ import com.leanstacks.ws.service.EmailService; import com.leanstacks.ws.service.GreetingService; -import io.swagger.annotations.ApiImplicitParam; -import io.swagger.annotations.ApiImplicitParams; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; - /** * The GreetingController class is a RESTful web service controller. The @RestController annotation informs * Spring that each @RequestMapping method returns a @ResponseBody. @@ -62,15 +57,6 @@ public class GreetingController { * * @return A List of Greeting objects. */ - @ApiOperation(value = "${GreetingController.getGreetings.title}", - notes = "${GreetingController.getGreetings.notes}", - response = Greeting.class, - responseContainer = "List") - @ApiImplicitParams(@ApiImplicitParam(name = "Authorization", - value = "Basic auth_creds", - required = true, - dataType = "string", - paramType = "header")) @GetMapping public List getGreetings() { logger.info("> getGreetings"); @@ -93,16 +79,8 @@ public List getGreetings() { * @param id A Long URL path variable containing the Greeting primary key identifier. * @return A Greeting object, if found, and a HTTP status code as described in the method comment. */ - @ApiOperation(value = "${GreetingController.getGreeting.title}", - notes = "${GreetingController.getGreeting.notes}", - response = Greeting.class) - @ApiImplicitParams(@ApiImplicitParam(name = "Authorization", - value = "Basic auth_creds", - required = true, - dataType = "string", - paramType = "header")) @GetMapping("/{id}") - public Greeting getGreeting(@ApiParam("Greeting ID") @PathVariable final Long id) { + public Greeting getGreeting(@PathVariable final Long id) { logger.info("> getGreeting"); final Optional greetingOptional = greetingService.findOne(id); @@ -124,15 +102,6 @@ public Greeting getGreeting(@ApiParam("Greeting ID") @PathVariable final Long id * @param greeting The Greeting object to be created. * @return A Greeting object, if created successfully, and a HTTP status code as described in the method comment. */ - @ApiOperation(value = "${GreetingController.createGreeting.title}", - notes = "${GreetingController.createGreeting.notes}", - response = Greeting.class, - code = 201) - @ApiImplicitParams(@ApiImplicitParam(name = "Authorization", - value = "Basic auth_creds", - required = true, - dataType = "string", - paramType = "header")) @PostMapping @ResponseStatus(HttpStatus.CREATED) public Greeting createGreeting(@RequestBody final Greeting greeting) { @@ -158,17 +127,8 @@ public Greeting createGreeting(@RequestBody final Greeting greeting) { * @param greeting The Greeting object to be updated. * @return A Greeting object, if updated successfully, and a HTTP status code as described in the method comment. */ - @ApiOperation(value = "${GreetingController.updateGreeting.title}", - notes = "${GreetingController.updateGreeting.notes}", - response = Greeting.class) - @ApiImplicitParams(@ApiImplicitParam(name = "Authorization", - value = "Basic auth_creds", - required = true, - dataType = "string", - paramType = "header")) @PutMapping("/{id}") - public Greeting updateGreeting(@ApiParam("Greeting ID") @PathVariable("id") final Long id, - @RequestBody final Greeting greeting) { + public Greeting updateGreeting(@PathVariable("id") final Long id, @RequestBody final Greeting greeting) { logger.info("> updateGreeting"); greeting.setId(id); @@ -191,17 +151,9 @@ public Greeting updateGreeting(@ApiParam("Greeting ID") @PathVariable("id") fina * * @param id A Long URL path variable containing the Greeting primary key identifier. */ - @ApiOperation(value = "${GreetingController.deleteGreeting.title}", - notes = "${GreetingController.deleteGreeting.notes}", - code = 204) - @ApiImplicitParams(@ApiImplicitParam(name = "Authorization", - value = "Basic auth_creds", - required = true, - dataType = "string", - paramType = "header")) @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) - public void deleteGreeting(@ApiParam("Greeting ID") @PathVariable("id") final Long id) { + public void deleteGreeting(@PathVariable("id") final Long id) { logger.info("> deleteGreeting"); greetingService.delete(id); @@ -223,18 +175,9 @@ public void deleteGreeting(@ApiParam("Greeting ID") @PathVariable("id") final Lo * transmission. * @return A Greeting object, if found, and a HTTP status code as described in the method comment. */ - @ApiOperation(value = "${GreetingController.sendGreeting.title}", - notes = "${GreetingController.sendGreeting.notes}", - response = Greeting.class) - @ApiImplicitParams(@ApiImplicitParam(name = "Authorization", - value = "Basic auth_creds", - required = true, - dataType = "string", - paramType = "header")) @PostMapping("/{id}/send") - public Greeting sendGreeting(@ApiParam("Greeting ID") @PathVariable("id") final Long id, - @ApiParam("Wait for Response") @RequestParam(value = "wait", - defaultValue = "false") final boolean waitForAsyncResult) { + public Greeting sendGreeting(@PathVariable("id") final Long id, @RequestParam(value = "wait", + defaultValue = "false") final boolean waitForAsyncResult) { logger.info("> sendGreeting"); diff --git a/src/main/java/com/leanstacks/ws/web/api/RestResponseEntityExceptionHandler.java b/src/main/java/com/leanstacks/ws/web/api/RestResponseEntityExceptionHandler.java index 281d77d..f39a691 100644 --- a/src/main/java/com/leanstacks/ws/web/api/RestResponseEntityExceptionHandler.java +++ b/src/main/java/com/leanstacks/ws/web/api/RestResponseEntityExceptionHandler.java @@ -74,7 +74,7 @@ public ResponseEntity handleNoSuchElementException(final NoSuchElementEx public ResponseEntity handleEmptyResultDataAccessException(final EmptyResultDataAccessException ex, final WebRequest request) { logger.info("> handleEmptyResultDataAccessException"); - logger.warn("- EmptyResultDataAccessException: ", ex); + logger.info("- EmptyResultDataAccessException: ", ex); final ExceptionDetail detail = new ExceptionDetailBuilder().exception(ex).httpStatus(HttpStatus.NOT_FOUND) .webRequest(request).build(); logger.info("< handleEmptyResultDataAccessException"); diff --git a/src/main/java/com/leanstacks/ws/web/docs/ApiDocsController.java b/src/main/java/com/leanstacks/ws/web/docs/ApiDocsController.java deleted file mode 100644 index 21594d1..0000000 --- a/src/main/java/com/leanstacks/ws/web/docs/ApiDocsController.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.leanstacks.ws.web.docs; - -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -/** - * The ApiDocsController is a Spring MVC web controller class which serves the - * Swagger user interface HTML page. - * - * @author Matt Warman - */ -@Profile("docs") -@Controller -public class ApiDocsController { - - /** - * Request handler to serve the Swagger user interface HTML page configured - * to the mapped context path. - * - * @return A String name of the Swagger user interface HTML page name. - */ - @RequestMapping("/docs") - public String getSwaggerApiDocsPage() { - return "redirect:swagger-ui.html"; - } - -} diff --git a/src/main/resources/config/application-docs.yaml b/src/main/resources/config/application-docs.yaml deleted file mode 100644 index 0666f6e..0000000 --- a/src/main/resources/config/application-docs.yaml +++ /dev/null @@ -1,19 +0,0 @@ -GreetingController: - getGreetings: - title: 'Find all Greetings' - notes: 'Retrieves a Collection of Greeting objects from the data store and returns them as JSON.' - getGreeting: - title: 'Find Greeting by ID' - notes: 'Retrieves a Greeting object from the data store by its Identifier and returns it as JSON.' - createGreeting: - title: 'Create a Greeting' - notes: 'Creates a Greeting object in the data store and returns it as JSON.' - updateGreeting: - title: 'Update a Greeting' - notes: 'Updates a Greeting object in the data store and returns it as JSON.' - deleteGreeting: - title: 'Delete a Greeting' - notes: 'Deletes a Greeting object from the data store.' - sendGreeting: - title: 'Send a Greeting' - notes: 'Send a Greeting object via Email and returns it as JSON.' From 117b43c4de2ca7d5c3455fa325de90fcffab63d9 Mon Sep 17 00:00:00 2001 From: Matt Warman Date: Sat, 15 Dec 2018 08:31:13 -0500 Subject: [PATCH 4/4] See #48. Maven dependencies for Spring REST Docs --- pom.xml | 12 ++++++++++++ src/test/java/com/leanstacks/ws/AbstractDocTest.java | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d6d8add..abc307e 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,18 @@ spring-security-test test + + com.google.guava + guava + 27.0.1-jre + test + + + + + org.springframework.restdocs + spring-restdocs-mockmvc + diff --git a/src/test/java/com/leanstacks/ws/AbstractDocTest.java b/src/test/java/com/leanstacks/ws/AbstractDocTest.java index ab4a84e..e452d40 100644 --- a/src/test/java/com/leanstacks/ws/AbstractDocTest.java +++ b/src/test/java/com/leanstacks/ws/AbstractDocTest.java @@ -35,7 +35,9 @@ public abstract class AbstractDocTest { private transient WebApplicationContext context; /** - * A JUnit 4.x Rule for Spring REST Documentation generation. + * A JUnit 4.x Rule for Spring REST Documentation generation. Note that the snippet output directory is only + * provided because this project contains both 'build.gradle' and 'pom.xml' files. Spring REST Docs uses those files + * to auto-detect the build system and automatically sets certain configuration values which cannot be overridden. */ @Rule public transient JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets");