/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.hypermedia;
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import groovy.text.Template;
import groovy.text.TemplateEngine;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.mvc.ActuatorMediaTypes;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrint;
import org.springframework.boot.test.context.SpringBootContextLoader;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = SpringBootHypermediaApplication.class, loader = SpringBootContextLoader.class)
@WebAppConfiguration
@TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true",
"endpoints.health.sensitive=true", "endpoints.actuator.enabled=false",
"management.security.enabled=false" })
@DirtiesContext
@AutoConfigureRestDocs(EndpointDocumentation.RESTDOCS_OUTPUT_DIR)
@AutoConfigureMockMvc(print = MockMvcPrint.NONE)
public class EndpointDocumentation {
static final String RESTDOCS_OUTPUT_DIR = "target/generated-snippets";
static final File LOG_FILE = new File("target/logs/spring.log");
private static final Set<String> SKIPPED = Collections
.<String>unmodifiableSet(new HashSet<>(
Arrays.asList("/docs", "/logfile", "/heapdump", "/auditevents")));
@Autowired
private MvcEndpoints mvcEndpoints;
@Autowired
private TemplateEngine templates;
@Autowired
private MockMvc mockMvc;
@BeforeClass
public static void clearLog() {
LOG_FILE.delete();
}
@Test
public void logfile() throws Exception {
this.mockMvc.perform(get("/application/logfile").accept(MediaType.TEXT_PLAIN))
.andExpect(status().isOk()).andDo(document("logfile"));
}
@Test
public void partialLogfile() throws Exception {
FileCopyUtils.copy(getClass().getResourceAsStream("log.txt"),
new FileOutputStream(LOG_FILE));
this.mockMvc
.perform(get("/application/logfile").accept(MediaType.TEXT_PLAIN)
.header(HttpHeaders.RANGE, "bytes=0-1024"))
.andExpect(status().isPartialContent())
.andDo(document("partial-logfile"));
}
@Test
public void singleLogger() throws Exception {
this.mockMvc
.perform(get("/application/loggers/org.springframework.boot")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andDo(document("single-logger"));
}
@Test
public void setLogger() throws Exception {
this.mockMvc
.perform(post("/application/loggers/org.springframework.boot")
.contentType(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON)
.content("{\"configuredLevel\": \"DEBUG\"}"))
.andExpect(status().isOk()).andDo(document("set-logger"));
}
@Test
public void auditEvents() throws Exception {
this.mockMvc
.perform(get("/application/auditevents").param("after", "2016-11-01T10:00:00+0000")
.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
.andExpect(status().isOk()).andDo(document("auditevents"));
}
@Test
public void auditEventsByPrincipal() throws Exception {
this.mockMvc
.perform(get("/application/auditevents").param("principal", "admin")
.param("after", "2016-11-01T10:00:00+0000")
.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
.andExpect(status().isOk())
.andDo(document("auditevents/filter-by-principal"));
}
@Test
public void auditEventsByPrincipalAndType() throws Exception {
this.mockMvc
.perform(get("/application/auditevents").param("principal", "admin")
.param("after", "2016-11-01T10:00:00+0000")
.param("type", "AUTHENTICATION_SUCCESS")
.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
.andExpect(status().isOk())
.andDo(document("auditevents/filter-by-principal-and-type"));
}
@Test
public void endpoints() throws Exception {
final File docs = new File("src/main/asciidoc");
final Map<String, Object> model = new LinkedHashMap<>();
final List<EndpointDoc> endpoints = new ArrayList<>();
model.put("endpoints", endpoints);
for (MvcEndpoint endpoint : getEndpoints()) {
final String endpointPath = (StringUtils.hasText(endpoint.getPath())
? endpoint.getPath() : "/");
if (!SKIPPED.contains(endpointPath)) {
String output = endpointPath.substring(1);
output = output.length() > 0 ? output : "./";
this.mockMvc
.perform(get("/application" + endpointPath)
.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
.andExpect(status().isOk()).andDo(document(output))
.andDo(new ResultHandler() {
@Override
public void handle(MvcResult mvcResult) throws Exception {
EndpointDoc endpoint = new EndpointDoc(docs,
endpointPath);
endpoints.add(endpoint);
}
});
}
}
File file = new File(RESTDOCS_OUTPUT_DIR + "/endpoints.adoc");
file.getParentFile().mkdirs();
PrintWriter writer = new PrintWriter(file, "UTF-8");
try {
Template template = this.templates.createTemplate(
new File("src/restdoc/resources/templates/endpoints.adoc.tpl"));
template.make(model).writeTo(writer);
}
finally {
writer.close();
}
}
private Collection<? extends MvcEndpoint> getEndpoints() {
List<? extends MvcEndpoint> endpoints = new ArrayList<>(
this.mvcEndpoints.getEndpoints());
Collections.sort(endpoints, new Comparator<MvcEndpoint>() {
@Override
public int compare(MvcEndpoint o1, MvcEndpoint o2) {
return o1.getPath().compareTo(o2.getPath());
}
});
return endpoints;
}
public static class EndpointDoc {
private String path;
private String custom;
private String title;
public EndpointDoc(File rootDir, String path) {
this.title = path;
this.path = path.equals("/") ? "" : path;
String custom = path.substring(1) + ".adoc";
if (new File(rootDir, custom).exists()) {
this.custom = custom;
}
}
public String getTitle() {
return this.title;
}
public String getPath() {
return this.path;
}
public String getCustom() {
return this.custom;
}
}
}