Add a dateValue HeaderResultMatcher

HTTP headers such as "Expires", "Last-Modified" all use date
strings like "Tue, 21 Jul 2015 10:00:00 GMT". Prior to this commit,
there was no way to match those header values, besides formatting dates
manually.

This commit introduces a new HeaderResultMatcher to test those date
headers using a long timestamp:

```
this.mockMvc.perform(get("/persons/1").header("If-Modified-Since", now))
  .andExpect(status().isNotModified())
  .andExpect(header().dateValue("Last-Modified", timestamp));
```

Issue: SPR-13263
This commit is contained in:
Brian Clozel 2015-07-22 11:22:44 +02:00
parent ed20b3771c
commit cf2aed9d00
2 changed files with 80 additions and 27 deletions

View File

@ -24,6 +24,11 @@ import org.springframework.test.web.servlet.ResultMatcher;
import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.MatcherAssert.*;
import static org.springframework.test.util.AssertionErrors.*; import static org.springframework.test.util.AssertionErrors.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
/** /**
* Factory for response header assertions. * Factory for response header assertions.
* <p>An instance of this class is usually accessed via * <p>An instance of this class is usually accessed via
@ -31,6 +36,7 @@ import static org.springframework.test.util.AssertionErrors.*;
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Sam Brannen * @author Sam Brannen
* @author Brian Clozel
* @since 3.2 * @since 3.2
*/ */
public class HeaderResultMatchers { public class HeaderResultMatchers {
@ -96,4 +102,26 @@ public class HeaderResultMatchers {
}; };
} }
/**
* Assert the primary value of the named response header as a date String,
* using the preferred date format described in RFC 7231.
* <p>The {@link ResultMatcher} returned by this method throws an {@link AssertionError}
* if the response does not contain the specified header, or if the supplied
* {@code value} does not match the primary value.
*
* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a>
* @since 4.2
*/
public ResultMatcher dateValue(final String name, final long value) {
return new ResultMatcher() {
@Override
public void match(MvcResult result) {
SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("GMT"));
assertTrue("Response does not contain header " + name, result.getResponse().containsHeader(name));
assertEquals("Response header " + name, format.format(new Date(value)), result.getResponse().getHeader(name));
}
};
}
} }

View File

@ -19,13 +19,13 @@ package org.springframework.test.web.servlet.samples.standalone.resultmatchers;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.test.web.Person; import org.springframework.test.web.Person;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.WebRequest; import org.springframework.web.context.request.WebRequest;
import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.CoreMatchers.*;
@ -34,11 +34,17 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
/** /**
* Examples of expectations on response header values. * Examples of expectations on response header values.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Sam Brannen * @author Sam Brannen
* @author Brian Clozel
*/ */
public class HeaderAssertionTests { public class HeaderAssertionTests {
@ -50,6 +56,12 @@ public class HeaderAssertionTests {
private final long currentTime = System.currentTimeMillis(); private final long currentTime = System.currentTimeMillis();
private String now;
private String oneMinuteAgo;
private String oneSecondLater;
private MockMvc mockMvc; private MockMvc mockMvc;
private PersonController personController; private PersonController personController;
@ -57,6 +69,11 @@ public class HeaderAssertionTests {
@Before @Before
public void setup() { public void setup() {
SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
this.now = dateFormat.format(new Date(currentTime));
this.oneMinuteAgo = dateFormat.format(new Date(currentTime - (1000 * 60)));
this.oneSecondLater = dateFormat.format(new Date(currentTime + 1000));
this.personController = new PersonController(); this.personController = new PersonController();
this.personController.setStubTimestamp(currentTime); this.personController.setStubTimestamp(currentTime);
this.mockMvc = standaloneSetup(this.personController).build(); this.mockMvc = standaloneSetup(this.personController).build();
@ -64,32 +81,38 @@ public class HeaderAssertionTests {
@Test @Test
public void stringWithCorrectResponseHeaderValue() throws Exception { public void stringWithCorrectResponseHeaderValue() throws Exception {
this.mockMvc.perform(get("/persons/1").header(IF_MODIFIED_SINCE, currentTime - (1000 * 60)))// this.mockMvc.perform(get("/persons/1").header(IF_MODIFIED_SINCE, oneMinuteAgo))//
.andExpect(header().string(LAST_MODIFIED, String.valueOf(currentTime))); .andExpect(header().string(LAST_MODIFIED, now));
} }
@Test @Test
public void stringWithMatcherAndCorrectResponseHeaderValue() throws Exception { public void stringWithMatcherAndCorrectResponseHeaderValue() throws Exception {
this.mockMvc.perform(get("/persons/1").header(IF_MODIFIED_SINCE, currentTime - (1000 * 60)))// this.mockMvc.perform(get("/persons/1").header(IF_MODIFIED_SINCE, oneMinuteAgo))//
.andExpect(header().string(LAST_MODIFIED, equalTo(String.valueOf(currentTime)))); .andExpect(header().string(LAST_MODIFIED, equalTo(now)));
}
@Test
public void dateValueWithCorrectResponseHeaderValue() throws Exception {
this.mockMvc.perform(get("/persons/1").header(IF_MODIFIED_SINCE, oneMinuteAgo))//
.andExpect(header().dateValue(LAST_MODIFIED, currentTime));
} }
@Test @Test
public void longValueWithCorrectResponseHeaderValue() throws Exception { public void longValueWithCorrectResponseHeaderValue() throws Exception {
this.mockMvc.perform(get("/persons/1").header(IF_MODIFIED_SINCE, currentTime - (1000 * 60)))// this.mockMvc.perform(get("/persons/1"))//
.andExpect(header().longValue(LAST_MODIFIED, currentTime)); .andExpect(header().longValue("X-Rate-Limiting", 42));
} }
@Test @Test
public void stringWithMissingResponseHeader() throws Exception { public void stringWithMissingResponseHeader() throws Exception {
this.mockMvc.perform(get("/persons/1").header(IF_MODIFIED_SINCE, currentTime))// this.mockMvc.perform(get("/persons/1").header(IF_MODIFIED_SINCE, now))//
.andExpect(status().isNotModified())// .andExpect(status().isNotModified())//
.andExpect(header().string("X-Custom-Header", (String) null)); .andExpect(header().string("X-Custom-Header", (String) null));
} }
@Test @Test
public void stringWithMatcherAndMissingResponseHeader() throws Exception { public void stringWithMatcherAndMissingResponseHeader() throws Exception {
this.mockMvc.perform(get("/persons/1").header(IF_MODIFIED_SINCE, currentTime))// this.mockMvc.perform(get("/persons/1").header(IF_MODIFIED_SINCE, now))//
.andExpect(status().isNotModified())// .andExpect(status().isNotModified())//
.andExpect(header().string("X-Custom-Header", nullValue())); .andExpect(header().string("X-Custom-Header", nullValue()));
} }
@ -97,7 +120,7 @@ public class HeaderAssertionTests {
@Test @Test
public void longValueWithMissingResponseHeader() throws Exception { public void longValueWithMissingResponseHeader() throws Exception {
try { try {
this.mockMvc.perform(get("/persons/1").header(IF_MODIFIED_SINCE, currentTime))// this.mockMvc.perform(get("/persons/1").header(IF_MODIFIED_SINCE, now))//
.andExpect(status().isNotModified())// .andExpect(status().isNotModified())//
.andExpect(header().longValue("X-Custom-Header", 99L)); .andExpect(header().longValue("X-Custom-Header", 99L));
@ -129,26 +152,28 @@ public class HeaderAssertionTests {
@Test @Test
public void stringWithIncorrectResponseHeaderValue() throws Exception { public void stringWithIncorrectResponseHeaderValue() throws Exception {
long unexpected = currentTime + 1000; assertIncorrectResponseHeaderValue(header().string(LAST_MODIFIED, oneSecondLater), oneSecondLater);
assertIncorrectResponseHeaderValue(header().string(LAST_MODIFIED, String.valueOf(unexpected)), unexpected);
} }
@Test @Test
public void stringWithMatcherAndIncorrectResponseHeaderValue() throws Exception { public void stringWithMatcherAndIncorrectResponseHeaderValue() throws Exception {
long unexpected = currentTime + 1000; assertIncorrectResponseHeaderValue(header().string(LAST_MODIFIED, equalTo(oneSecondLater)), oneSecondLater);
assertIncorrectResponseHeaderValue(header().string(LAST_MODIFIED, equalTo(String.valueOf(unexpected))),
unexpected);
} }
@Test @Test
public void longValueWithIncorrectResponseHeaderValue() throws Exception { public void dateValueWithIncorrectResponseHeaderValue() throws Exception {
long unexpected = currentTime + 1000; long unexpected = currentTime + 1000;
assertIncorrectResponseHeaderValue(header().longValue(LAST_MODIFIED, unexpected), unexpected); assertIncorrectResponseHeaderValue(header().dateValue(LAST_MODIFIED, unexpected), oneSecondLater);
} }
private void assertIncorrectResponseHeaderValue(ResultMatcher resultMatcher, long unexpected) throws Exception { @Test(expected = AssertionError.class)
public void longValueWithIncorrectResponseHeaderValue() throws Exception {
this.mockMvc.perform(get("/persons/1")).andExpect(header().longValue("X-Rate-Limiting", 1));
}
private void assertIncorrectResponseHeaderValue(ResultMatcher resultMatcher, String unexpected) throws Exception {
try { try {
this.mockMvc.perform(get("/persons/1").header(IF_MODIFIED_SINCE, currentTime - (1000 * 60)))// this.mockMvc.perform(get("/persons/1").header(IF_MODIFIED_SINCE, oneMinuteAgo))//
.andExpect(resultMatcher); .andExpect(resultMatcher);
fail(EXPECTED_ASSERTION_ERROR_MSG); fail(EXPECTED_ASSERTION_ERROR_MSG);
@ -162,8 +187,8 @@ public class HeaderAssertionTests {
// We don't use assertEquals() since we cannot control the formatting // We don't use assertEquals() since we cannot control the formatting
// produced by JUnit or Hamcrest. // produced by JUnit or Hamcrest.
assertMessageContains(e, "Response header " + LAST_MODIFIED); assertMessageContains(e, "Response header " + LAST_MODIFIED);
assertMessageContains(e, String.valueOf(unexpected)); assertMessageContains(e, unexpected);
assertMessageContains(e, String.valueOf(currentTime)); assertMessageContains(e, now);
} }
} }
@ -186,12 +211,12 @@ public class HeaderAssertionTests {
} }
@RequestMapping("/persons/{id}") @RequestMapping("/persons/{id}")
@ResponseBody public ResponseEntity<Person> showEntity(@PathVariable long id, WebRequest request) {
public Person showEntity(@PathVariable long id, WebRequest request) { return ResponseEntity
if (request.checkNotModified(calculateLastModified(id))) { .ok()
return null; .lastModified(calculateLastModified(id))
} .header("X-Rate-Limiting", "42")
return new Person("Jason"); .body(new Person("Jason"));
} }
private long calculateLastModified(long id) { private long calculateLastModified(long id) {