package org.dcache.http;
import com.google.common.base.Splitter;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Futures;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.util.CharsetUtil;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.ArgumentMatcher;
import org.python.google.common.collect.Lists;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import diskCacheV111.util.FsPath;
import diskCacheV111.vehicles.HttpProtocolInfo;
import org.dcache.pool.movers.IoMode;
import org.dcache.pool.movers.NettyTransferService;
import org.dcache.util.Checksum;
import org.dcache.util.ChecksumType;
import org.dcache.vehicles.FileAttributes;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static io.netty.handler.codec.http.HttpHeaders.Names.*;
import static io.netty.handler.codec.http.HttpHeaders.Values.BYTES;
import static io.netty.handler.codec.http.HttpMethod.*;
import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.mock;
/**
* This class provides unit-tests for how the pool responses to HTTP requests
*/
public class HttpPoolRequestHandlerTests
{
/* A constant UUID chosen to avoid using random data */
private static final UUID SOME_UUID =
UUID.fromString("49571502-60ca-49cd-bfe4-306bfe68037c");
/* Just some UUID that is different from SOME_UUID */
private static final UUID ANOTHER_UUID =
UUID.fromString("f92e2faf-29d7-416c-9637-0ed7ba73fc36");
private static final String DIGEST = "Digest";
private static final String CONTENT_DISPOSITION = "Content-Disposition";
private static final int SOME_CHUNK_SIZE = 4096;
private HttpPoolRequestHandler _handler;
private NettyTransferService<HttpProtocolInfo> _server;
private Map<String,FileInfo> _files;
private List<Object> _additionalWrites;
private HttpResponse _response;
private EmbeddedChannel _channel;
@Before
public void setup()
{
_server = mock(NettyTransferService.class);
_handler = new HttpPoolRequestHandler(_server, SOME_CHUNK_SIZE);
_channel = new EmbeddedChannel(_handler);
_files = Maps.newHashMap();
_additionalWrites = new ArrayList<>();
}
@Test
public void shouldIncludeContentLengthForErrorResponse() throws Exception
{
whenClientMakes(a(OPTIONS).forUri("/path/to/file"));
assertThat(_response, hasHeader(CONTENT_LENGTH));
}
@Test
public void shouldGiveErrorIfRequestHasWrongUuid() throws Exception
{
givenPoolHas(file("/path/to/file").withSize(100));
givenDoorHasOrganisedReadOf(file("/path/to/file").with(SOME_UUID));
whenClientMakes(a(GET).
forUri("/path/to/file?dcache-http-uuid="+ANOTHER_UUID));
assertThat(_response.getStatus(), is(BAD_REQUEST));
assertThat(_response, hasHeader(CONTENT_LENGTH));
}
@Ignore("it's the mover (which is mocked) that verifies path")
@Test
public void shouldGiveErrorIfRequestHasWrongPath() throws Exception
{
givenPoolHas(file("/path/to/file").withSize(100));
givenDoorHasOrganisedReadOf(file("/path/to/file").with(SOME_UUID));
whenClientMakes(a(GET).
forUri("/path/to/another-file?dcache-http-uuid=" + SOME_UUID));
assertThat(_response.getStatus(), is(BAD_REQUEST));
assertThat(_response, hasHeader(CONTENT_LENGTH));
}
@Test
public void shouldDeliverCompleteFileIfReceivesRequestForWholeFile()
throws Exception
{
givenPoolHas(file("/path/to/file").withSize(100));
givenDoorHasOrganisedReadOf(file("/path/to/file").with(SOME_UUID));
whenClientMakes(a(GET).
forUri("/path/to/file?dcache-http-uuid="+SOME_UUID));
assertThat(_response.getStatus(), is(OK));
assertThat(_response, hasHeader(CONTENT_LENGTH, "100"));
assertThat(_response, hasHeader(CONTENT_DISPOSITION,
"attachment;filename=file"));
assertThat(_response, not(hasHeader(DIGEST)));
assertThat(_response, hasHeader(ACCEPT_RANGES, BYTES));
assertThat(_response, not(hasHeader(CONTENT_RANGE)));
assertThat(_additionalWrites, hasSize(2));
assertThat(_additionalWrites.get(0), isCompleteRead("/path/to/file"));
assertThat(_additionalWrites.get(1), instanceOf(LastHttpContent.class));
}
@Test
public void shouldDeliverCompleteFileWithChecksumIfReceivesRequestForWholeFileWithChecksum()
throws Exception
{
givenPoolHas(file("/path/to/file").withSize(100));
givenDoorHasOrganisedReadOf(file("/path/to/file").with(SOME_UUID).
withAdler32("03da0195"));
whenClientMakes(a(GET).
forUri("/path/to/file?dcache-http-uuid="+SOME_UUID));
assertThat(_response.getStatus(), is(OK));
assertThat(_response, hasHeader(CONTENT_LENGTH, "100"));
assertThat(_response, hasHeader(CONTENT_DISPOSITION,
"attachment;filename=file"));
assertThat(_response, hasHeader(DIGEST, "adler32=03da0195"));
assertThat(_response, hasHeader(ACCEPT_RANGES, BYTES));
assertThat(_response, not(hasHeader(CONTENT_RANGE)));
assertThat(_additionalWrites, hasSize(2));
assertThat(_additionalWrites.get(0), isCompleteRead("/path/to/file"));
assertThat(_additionalWrites.get(1), instanceOf(LastHttpContent.class));
}
@Test
public void shouldDeliverCompleteFileIfReceivesRequestForWholeFileWithQuestionMark()
throws Exception
{
givenPoolHas(file("/path/to/file?here").withSize(100));
givenDoorHasOrganisedReadOf(file("/path/to/file?here")
.with(SOME_UUID));
whenClientMakes(a(GET).
forUri("/path/to/file%3Fhere?dcache-http-uuid="+SOME_UUID));
assertThat(_response.getStatus(), is(OK));
assertThat(_response, hasHeader(CONTENT_LENGTH, "100"));
assertThat(_response, hasHeader(CONTENT_DISPOSITION,
"attachment;filename=\"file?here\""));
assertThat(_response, not(hasHeader(DIGEST)));
assertThat(_response, hasHeader(ACCEPT_RANGES, BYTES));
assertThat(_response, not(hasHeader(CONTENT_RANGE)));
assertThat(_additionalWrites, hasSize(2));
assertThat(_additionalWrites.get(0),
isCompleteRead("/path/to/file?here"));
assertThat(_additionalWrites.get(1), instanceOf(LastHttpContent.class));
}
@Test
public void shouldDeliverCompleteFileIfReceivesRequestForWholeFileWithBackslashQuote()
throws Exception
{
givenPoolHas(file("/path/to/file\\\"here").withSize(100));
givenDoorHasOrganisedReadOf(file("/path/to/file\\\"here")
.with(SOME_UUID));
whenClientMakes(a(GET).
forUri("/path/to/file%5C%22here?dcache-http-uuid="+SOME_UUID));
assertThat(_response.getStatus(), is(OK));
assertThat(_response, hasHeader(CONTENT_LENGTH, "100"));
assertThat(_response, hasHeader(CONTENT_DISPOSITION,
"attachment;filename=\"file\\\\\\\"here\""));
assertThat(_response, not(hasHeader(DIGEST)));
assertThat(_response, hasHeader(ACCEPT_RANGES, BYTES));
assertThat(_response, not(hasHeader(CONTENT_RANGE)));
assertThat(_additionalWrites, hasSize(2));
assertThat(_additionalWrites.get(0),
isCompleteRead("/path/to/file\\\"here"));
assertThat(_additionalWrites.get(1), instanceOf(LastHttpContent.class));
}
@Test
public void shouldDeliverCompleteFileIfReceivesRequestForWholeFileWithNonAsciiName()
throws Exception
{
// 0x16A0 0x16C7 0x16BB is the three-rune word from the start of Rune
// poem, available from http://www.ragweedforge.com/poems.html, in
// UTF-16. The same word, in UTF-8, is represented by the 9-byte
// sequence 0xe1 0x9a 0xa0 0xe1 0x9b 0x87 0xe1 0x9a 0xbb.
givenPoolHas(file("/path/to/\u16A0\u16C7\u16BB").withSize(100));
givenDoorHasOrganisedReadOf(file("/path/to/\u16A0\u16C7\u16BB")
.with(SOME_UUID));
whenClientMakes(a(GET).
forUri("/path/to/%E1%9A%A0%E1%9B%87%E1%9A%BB?dcache-http-uuid="
+ SOME_UUID));
assertThat(_response.getStatus(), is(OK));
assertThat(_response, hasHeader(CONTENT_LENGTH, "100"));
assertThat(_response, hasHeader(CONTENT_DISPOSITION,
"attachment;filename*=UTF-8''%E1%9A%A0%E1%9B%87%E1%9A%BB"));
assertThat(_response, not(hasHeader(DIGEST)));
assertThat(_response, hasHeader(ACCEPT_RANGES, BYTES));
assertThat(_response, not(hasHeader(CONTENT_RANGE)));
assertThat(_additionalWrites, hasSize(2));
assertThat(_additionalWrites.get(0),
isCompleteRead("/path/to/\u16A0\u16C7\u16BB"));
assertThat(_additionalWrites.get(1), instanceOf(LastHttpContent.class));
}
@Test
public void shouldDeliverPartialFileIfReceivesRequestWithSingleRange()
throws Exception
{
givenPoolHas(file("/path/to/file").withSize(1024));
givenDoorHasOrganisedReadOf(file("/path/to/file").with(SOME_UUID));
whenClientMakes(a(GET).withHeader("Range", "bytes=0-499").
forUri("/path/to/file?dcache-http-uuid="+SOME_UUID));
assertThat(_response.getStatus(), is(PARTIAL_CONTENT));
assertThat(_response, hasHeader(ACCEPT_RANGES, "bytes"));
assertThat(_response, hasHeader(CONTENT_LENGTH, "500"));
assertThat(_response, hasHeader(CONTENT_RANGE, "bytes 0-499/1024"));
assertThat(_response, not(hasHeader(DIGEST)));
assertThat(_response, not(hasHeader(CONTENT_DISPOSITION)));
assertThat(_additionalWrites, hasSize(2));
assertThat(_additionalWrites.get(0),
isPartialRead("/path/to/file", 0, 499));
assertThat(_additionalWrites.get(1), instanceOf(LastHttpContent.class));
}
@Test
public void shouldDeliverPartialFileIfReceivesRequestWithSingleRangeForFileWithChecksum()
throws Exception
{
givenPoolHas(file("/path/to/file").withSize(1024));
givenDoorHasOrganisedReadOf(file("/path/to/file").with(SOME_UUID).
withAdler32("03da0195"));
whenClientMakes(a(GET).withHeader("Range", "bytes=0-499").
forUri("/path/to/file?dcache-http-uuid="+SOME_UUID));
assertThat(_response.getStatus(), is(PARTIAL_CONTENT));
assertThat(_response, hasHeader(ACCEPT_RANGES, "bytes"));
assertThat(_response, hasHeader(CONTENT_LENGTH, "500"));
assertThat(_response, hasHeader(CONTENT_RANGE, "bytes 0-499/1024"));
assertThat(_response, hasHeader(DIGEST, "adler32=03da0195"));
assertThat(_response, not(hasHeader(CONTENT_DISPOSITION)));
assertThat(_additionalWrites, hasSize(2));
assertThat(_additionalWrites.get(0),
isPartialRead("/path/to/file", 0, 499));
assertThat(_additionalWrites.get(1), instanceOf(LastHttpContent.class));
}
@Test
public void shouldDeliverAvailableDataIfReceivesRequestWithSingleRangeButTooBig()
throws Exception
{
givenPoolHas(file("/path/to/file").withSize(100));
givenDoorHasOrganisedReadOf(file("/path/to/file").with(SOME_UUID));
whenClientMakes(a(GET).withHeader("Range", "bytes=0-1024").
forUri("/path/to/file?dcache-http-uuid="+SOME_UUID));
assertThat(_response.getStatus(), is(PARTIAL_CONTENT));
assertThat(_response, hasHeader(ACCEPT_RANGES, "bytes"));
assertThat(_response, hasHeader(CONTENT_LENGTH, "100"));
assertThat(_response, hasHeader(CONTENT_RANGE, "bytes 0-99/100"));
assertThat(_response, not(hasHeader(DIGEST)));
assertThat(_response, not(hasHeader(CONTENT_DISPOSITION)));
assertThat(_additionalWrites, hasSize(2));
assertThat(_additionalWrites.get(0),
isCompleteRead("/path/to/file"));
assertThat(_additionalWrites.get(1), instanceOf(LastHttpContent.class));
}
@Test
public void shouldDeliverPartialFileIfReceivesRequestWithMultipleRanges()
throws Exception
{
givenPoolHas(file("/path/to/file").withSize(1024));
givenDoorHasOrganisedReadOf(file("/path/to/file").with(SOME_UUID).
withAdler32("03da0195"));
whenClientMakes(a(GET).withHeader("Range", "bytes=0-0,-1").
forUri("/path/to/file?dcache-http-uuid="+SOME_UUID));
assertThat(_response.getStatus(), is(PARTIAL_CONTENT));
assertThat(_response, hasHeader(ACCEPT_RANGES, "bytes"));
assertThat(_response, hasHeader(CONTENT_TYPE,
"multipart/byteranges; boundary=\"__AAAAAAAAAAAAAAAA__\""));
assertThat(_response, hasHeader(DIGEST, "adler32=03da0195"));
assertThat(_response, hasHeader(CONTENT_LENGTH, "154"));
assertThat(_response, not(hasHeader(CONTENT_RANGE)));
assertThat(_response, not(hasHeader(CONTENT_DISPOSITION)));
assertThat(_additionalWrites, hasSize(5));
assertThat(_additionalWrites.get(0), isMultipart().
emptyLine().
line("--__AAAAAAAAAAAAAAAA__").
line("Content-Range: bytes 0-0/1024").
emptyLine());
assertThat(_additionalWrites.get(1),
isPartialRead("/path/to/file", 0, 0));
assertThat(_additionalWrites.get(2), isMultipart().
emptyLine().
line("--__AAAAAAAAAAAAAAAA__").
line("Content-Range: bytes 1023-1023/1024").
emptyLine());
assertThat(_additionalWrites.get(3),
isPartialRead("/path/to/file", 1023, 1023));
assertThat(_additionalWrites.get(4), isMultipart().
emptyLine().
line("--__AAAAAAAAAAAAAAAA__--"));
}
@Test
public void shouldRejectDeleteRequests() throws Exception
{
whenClientMakes(a(DELETE).forUri("/path/to/file"));
assertThat(_response.getStatus(), is(NOT_IMPLEMENTED));
assertThat(_response, hasHeader(CONTENT_LENGTH));
}
@Test
public void shouldRejectConnectRequests() throws Exception
{
whenClientMakes(a(CONNECT).forUri("/path/to/file"));
assertThat(_response.getStatus(), is(NOT_IMPLEMENTED));
assertThat(_response, hasHeader(CONTENT_LENGTH));
}
@Test
public void shouldAcceptHeadRequests() throws Exception
{
givenPoolHas(file("/path/to/file").withSize(1024));
givenDoorHasOrganisedReadOf(file("/path/to/file").with(SOME_UUID));
whenClientMakes(a(HEAD)
.forUri("/path/to/file?dcache-http-uuid=" + SOME_UUID));
assertThat(_response.getStatus(), is(OK));
assertThat(_response, hasHeader(CONTENT_LENGTH));
assertThat(_response, hasHeader(ACCEPT_RANGES, BYTES));
}
@Test
public void shouldRejectOptionsRequests() throws Exception
{
whenClientMakes(a(OPTIONS).forUri("/path/to/file"));
assertThat(_response.getStatus(), is(NOT_IMPLEMENTED));
assertThat(_response, hasHeader(CONTENT_LENGTH));
}
@Test
public void shouldRejectPatchRequests() throws Exception
{
whenClientMakes(a(PATCH).forUri("/path/to/file"));
assertThat(_response.getStatus(), is(NOT_IMPLEMENTED));
assertThat(_response, hasHeader(CONTENT_LENGTH));
}
@Test
public void shouldRejectPostRequests() throws Exception
{
whenClientMakes(a(POST).forUri("/path/to/file"));
assertThat(_response.getStatus(), is(NOT_IMPLEMENTED));
assertThat(_response, hasHeader(CONTENT_LENGTH));
}
@Test
public void shouldAcceptPutRequests() throws Exception
{
givenDoorHasOrganisedWriteOf(file("/path/to/file").with(SOME_UUID));
whenClientMakes(a(PUT)
.forUri("/path/to/file?dcache-http-uuid=" + SOME_UUID));
assertThat(_response.getStatus(), is(CREATED));
}
@Test
public void shouldRejectPutOnRead() throws Exception
{
givenPoolHas(file("/path/to/file").withSize(1024));
givenDoorHasOrganisedReadOf(file("/path/to/file").with(SOME_UUID));
whenClientMakes(a(PUT)
.forUri("/path/to/file?dcache-http-uuid=" + SOME_UUID));
assertThat(_response.getStatus(), is(METHOD_NOT_ALLOWED));
}
@Test
public void shouldRejectGetOnWrite() throws Exception
{
givenDoorHasOrganisedWriteOf(file("/path/to/file").with(SOME_UUID));
whenClientMakes(a(GET)
.forUri("/path/to/file?dcache-http-uuid=" + SOME_UUID));
assertThat(_response.getStatus(), is(METHOD_NOT_ALLOWED));
}
@Test
public void shouldRejectTraceRequests() throws Exception
{
whenClientMakes(a(TRACE).forUri("/path/to/file"));
assertThat(_response.getStatus(), is(NOT_IMPLEMENTED));
assertThat(_response, hasHeader(CONTENT_LENGTH));
}
private void givenPoolHas(FileInfo file)
{
_files.put(file.getPath(), file);
}
private void givenDoorHasOrganisedReadOf(final FileInfo file)
throws URISyntaxException, IOException
{
String path = file.getPath();
file.withSize(sizeOfFile(file));
NettyTransferService<HttpProtocolInfo>.NettyMoverChannel channel =
mock(NettyTransferService.NettyMoverChannel.class);
given(channel.size()).willReturn(file.getSize());
given(channel.getIoMode()).willReturn(IoMode.READ);
given(channel.getProtocolInfo())
.willReturn(new HttpProtocolInfo("Http", 1, 1,
new InetSocketAddress((InetAddress) null, 0),
null, null, path,
new URI("http", "localhost", path, null)));
given(channel.getFileAttributes()).willReturn(file.getFileAttributes());
given(channel.release()).willReturn(Futures.immediateCheckedFuture(null));
given(_server.openFile(eq(file.getUuid()), anyBoolean())).willReturn(channel);
}
private void givenDoorHasOrganisedWriteOf(final FileInfo file)
throws URISyntaxException
{
String path = file.getPath();
NettyTransferService<HttpProtocolInfo>.NettyMoverChannel channel =
mock(NettyTransferService.NettyMoverChannel.class);
given(channel.getIoMode()).willReturn(IoMode.WRITE);
given(channel.getProtocolInfo())
.willReturn(new HttpProtocolInfo("Http", 1, 1,
new InetSocketAddress((InetAddress) null, 0),
null, null, path,
new URI("http", "localhost", path, null)));
given(channel.release()).willReturn(Futures.immediateCheckedFuture(null));
given(_server.openFile(eq(file.getUuid()), anyBoolean())).willReturn(channel);
}
private long sizeOfFile(FileInfo file)
{
checkState(_files.containsKey(file.getPath()),
"missing file: " + file.getPath());
return _files.get(file.getPath()).getSize();
}
private static FileInfo file(String path)
{
return new FileInfo(path);
}
/**
* Information about some ficticious file in the pool's repository.
* The methods allow declaration of information via chaining method calls.
*/
private static class FileInfo
{
private final String _path;
private long _size;
private UUID _uuid;
private FileAttributes _attributes = new FileAttributes();
public FileInfo(String path)
{
_path = path;
}
public FileInfo withSize(long size)
{
_size = size;
return this;
}
public FileInfo with(UUID uuid)
{
_uuid = uuid;
return this;
}
public FileInfo withAdler32(String value)
{
Checksum checksum = new Checksum(ChecksumType.ADLER32, value);
_attributes.setChecksums(Sets.newHashSet(checksum));
return this;
}
public String getPath()
{
return _path;
}
public String getFileName()
{
return FsPath.create(_path).name();
}
public UUID getUuid()
{
checkState(_uuid != null, "uuid has not been defined");
return _uuid;
}
public long getSize()
{
return _size;
}
public URI getUri()
{
return URI.create(_path);
}
public FileAttributes getFileAttributes()
{
return _attributes;
}
}
private static RequestInfo a(HttpMethod method)
{
return new RequestInfo(method);
}
/**
* Class to hold information about an incoming HTTP request. Various
* methods allow chaining of the declaration to add additional information.
*/
private static class RequestInfo
{
private HttpMethod _method;
private HttpVersion _version = HTTP_1_1;
private String _uri;
private Multimap<String,String> _headers = ArrayListMultimap.create();
public RequestInfo(HttpMethod type)
{
_method = type;
}
public RequestInfo using(HttpVersion version)
{
_version = version;
return this;
}
public RequestInfo withHeader(String header, String value)
{
_headers.put(header, value);
return this;
}
public RequestInfo forUri(String uri)
{
_uri = uri;
return this;
}
public String getUri()
{
checkState(_uri != null, "URI has not been specified in test");
return _uri;
}
public Multimap<String,String> getHeaders()
{
return _headers;
}
public HttpVersion getProtocolVersion()
{
return _version;
}
public HttpMethod getMethod()
{
return _method;
}
}
private void whenClientMakes(RequestInfo info) throws Exception
{
_channel.writeInbound(buildRequest(info));
_response = (HttpResponse) _channel.readOutbound();
_additionalWrites.addAll(_channel.outboundMessages());
_channel.outboundMessages().clear();
if (!(Iterables.getLast(_additionalWrites, _response) instanceof LastHttpContent)) {
throw new RuntimeException("Reply lacks LastHttpContent.");
}
}
private HttpRequest buildRequest(RequestInfo info)
{
HttpRequest request = new DefaultFullHttpRequest(info.getProtocolVersion(), info.getMethod(), info.getUri());
for(Map.Entry<String,Collection<String>> entry : info.getHeaders().asMap().entrySet()) {
request.headers().set(entry.getKey(), Lists.newArrayList(entry.getValue()));
}
return request;
}
private static URI withPath(String path)
{
return argThat(new UriPathMatcher(path));
}
/**
* A custom argument matcher that matches if the URI has a path
* equal to the path supplied when constructing this object.
*/
private static class UriPathMatcher extends ArgumentMatcher<URI>
{
private final String _path;
public UriPathMatcher(String path)
{
checkArgument(path != null, "path cannot be null");
_path = path;
}
@Override
public boolean matches(Object uri) {
return _path.equals(((URI) uri).getPath());
}
}
private static HttpResponseHeaderMatcher hasHeader(String name,
String value)
{
return new HttpResponseHeaderMatcher(name, value);
}
private static HttpResponseHeaderMatcher hasHeader(String name)
{
return new HttpResponseHeaderMatcher(name);
}
/**
* A Matcher that checks whether an HttpResponse object contains the
* header specified. It either matches a header+value tuple or if the
* response has at least one header irrespective of the value(s).
*/
private static class HttpResponseHeaderMatcher extends
BaseMatcher<HttpResponse>
{
private final String _name;
private final String _value;
/**
* Create a Matcher that matches only if the response has the specified
* header with specified value.
*/
public HttpResponseHeaderMatcher(String name, String value)
{
_name = name;
_value = value;
}
/**
* Create a Matcher that matches if the response contains at least one
* header of the specified type, irrespective of what value(s) the
* header has.
*/
public HttpResponseHeaderMatcher(String name)
{
this(name, null);
}
@Override
public boolean matches(Object o)
{
if(!(o instanceof HttpResponse)) {
return false;
}
HttpResponse response = (HttpResponse) o;
if(!response.headers().contains(_name)) {
return false;
}
if(_value != null) {
List<String> values = response.headers().getAll(_name);
return values.contains(_value);
} else {
return true;
}
}
@Override
public void describeTo(Description d)
{
if(_value == null) {
d.appendText("At least one header '");
d.appendText(_name);
d.appendText("'");
} else {
d.appendText("Header '");
d.appendText(_name);
d.appendText("' with value '");
d.appendText(_value);
d.appendText("'");
}
}
}
private FileReadSizeMatcher isCompleteRead(String path)
{
return new FileReadSizeMatcher(path, 0, sizeOfFile(file(path)) - 1);
}
private FileReadSizeMatcher isPartialRead(String path,
long lower, long upper)
{
return new FileReadSizeMatcher(path, lower, upper);
}
/**
* This class provides a Matcher for assertThat statements. It
* checks whether one of the written objects is from a file and, if so,
* whether it is all of that file or a partial read.
*/
private static class FileReadSizeMatcher extends BaseMatcher<Object>
{
private static final long DUMMY_VALUE = -1;
private final long _lower;
private final long _upper;
private final String _path;
/**
* Create a Matcher that matches only if the read was for part of
* the contents of the specified file.
*/
public FileReadSizeMatcher(String path, long lower, long upper)
{
_lower = lower;
_upper = upper;
_path = path;
}
@Override
public boolean matches(Object o)
{
if(!(o instanceof ReusableChunkedNioFile)) {
return false;
}
ReusableChunkedNioFile ci = (ReusableChunkedNioFile) o;
NettyTransferService<HttpProtocolInfo>.NettyMoverChannel channel =
(NettyTransferService<HttpProtocolInfo>.NettyMoverChannel) ci.getChannel();
if(!_path.equals(channel.getProtocolInfo().getPath())) {
return false;
}
return ci.getOffset() == _lower && ci.getEndOffset() == _upper + 1;
}
@Override
public void describeTo(Description d)
{
d.appendText("match a read from ");
d.appendValue(_lower);
d.appendText(" to ");
d.appendValue(_upper);
}
}
private MultipartMatcher isMultipart()
{
return new MultipartMatcher();
}
/**
* This class provides a Matcher that matches if the supplied Object is
* a HeapChannelBuffer containing lines separated by CR-LF 2-byte
* sequences. The sequence must end with a CR-LF combination.
*
* The matcher also checks the values of these lines. The expected
* lines are specified by successive calls to {@code #line} or
* {@code #emptyLine}. These calls may be chained.
*/
private static class MultipartMatcher extends BaseMatcher<Object>
{
private static final String CRLF = "\r\n";
private List<String> _expectedLines = new ArrayList<>();
public MultipartMatcher emptyLine()
{
_expectedLines.add("");
return this;
}
public MultipartMatcher line(String line)
{
_expectedLines.add(line);
return this;
}
@Override
public boolean matches(Object o)
{
if (o instanceof HttpContent) {
o = ((HttpContent) o).content();
}
if(!(o instanceof ByteBuf)) {
return false;
}
String rawData = ((ByteBuf) o).toString(CharsetUtil.UTF_8);
if (!rawData.endsWith(CRLF)) {
return false;
}
String data = rawData.substring(0, rawData.length()-CRLF.length());
return Iterators.elementsEqual(_expectedLines.iterator(),
Splitter.on(CRLF).split(data).iterator());
}
@Override
public void describeTo(Description d)
{
d.appendValueList("A multipart header with lines: ", ", ", ".",
_expectedLines);
}
}
}