/*
Copyright (c) 2015 LinkedIn Corp.
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 com.linkedin.multipart;
import com.linkedin.data.ByteString;
import com.linkedin.multipart.exceptions.MultiPartReaderFinishedException;
import com.linkedin.r2.filter.R2Constants;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.mail.BodyPart;
import javax.mail.Header;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static com.linkedin.multipart.utils.MIMETestUtils.*;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.times;
/**
* Unit tests that mock out R2 and test the draining behavior of {@link com.linkedin.multipart.MultiPartMIMEReader}.
*
* @author Karim Vidhani
*/
public class TestMIMEReaderDrain extends AbstractMIMEUnitTest
{
MultiPartMIMEDrainReaderCallbackImpl _currentMultiPartMIMEReaderCallback;
MimeMultipart _currentMimeMultipartBody;
//This test will perform a drain without registering a callback. It functions different then other drain tests
//located in this class in terms of setup, assertions and verifies.
@Test
public void testDrainAllWithoutCallbackRegistered() throws Exception
{
mockR2AndWrite(ByteString.copy("Some multipart mime payload. It doesn't need to be pretty".getBytes()),
1, "multipart/mixed; boundary=----abcdefghijk");
MultiPartMIMEReader reader = MultiPartMIMEReader.createAndAcquireStream(_streamRequest);
try
{
reader.drainAllParts(); //The first should succeed.
reader.drainAllParts(); //The second should fail.
Assert.fail();
}
catch (MultiPartReaderFinishedException multiPartReaderFinishedException)
{
}
Assert.assertTrue(reader.haveAllPartsFinished());
//mock verifies
verify(_readHandle, times(1)).cancel();
verify(_streamRequest, times(1)).getEntityStream();
verify(_streamRequest, times(1)).getHeader(HEADER_CONTENT_TYPE);
verify(_entityStream, times(1)).setReader(isA(MultiPartMIMEReader.R2MultiPartMIMEReader.class));
verifyNoMoreInteractions(_streamRequest);
verifyNoMoreInteractions(_entityStream);
verifyNoMoreInteractions(_readHandle);
}
///////////////////////////////////////////////////////////////////////////////////////
@DataProvider(name = "allTypesOfBodiesDataSource")
public Object[][] allTypesOfBodiesDataSource() throws Exception
{
final List<MimeBodyPart> bodyPartList = new ArrayList<MimeBodyPart>();
bodyPartList.add(SMALL_DATA_SOURCE);
bodyPartList.add(LARGE_DATA_SOURCE);
bodyPartList.add(HEADER_LESS_BODY);
bodyPartList.add(BODY_LESS_BODY);
bodyPartList.add(BYTES_BODY);
bodyPartList.add(PURELY_EMPTY_BODY);
bodyPartList.add(PURELY_EMPTY_BODY);
bodyPartList.add(BYTES_BODY);
bodyPartList.add(BODY_LESS_BODY);
bodyPartList.add(HEADER_LESS_BODY);
bodyPartList.add(LARGE_DATA_SOURCE);
bodyPartList.add(SMALL_DATA_SOURCE);
return new Object[][]
{
{1, bodyPartList}, {R2Constants.DEFAULT_DATA_CHUNK_SIZE, bodyPartList}
};
}
@Test(dataProvider = "allTypesOfBodiesDataSource")
public void testSingleAllNoCallback(final int chunkSize, final List<MimeBodyPart> bodyPartList) throws Exception
{
executeRequestWithDrainStrategy(chunkSize, bodyPartList, SINGLE_ALL_NO_CALLBACK, "onFinished");
//Single part drains all individually but doesn't use a callback:
List<SinglePartMIMEDrainReaderCallbackImpl> singlePartMIMEReaderCallbacks =
_currentMultiPartMIMEReaderCallback.getSinglePartMIMEReaderCallbacks();
Assert.assertEquals(singlePartMIMEReaderCallbacks.size(), 0);
}
@Test(dataProvider = "allTypesOfBodiesDataSource")
public void testDrainAllWithCallbackRegistered(final int chunkSize, final List<MimeBodyPart> bodyPartList) throws Exception
{
executeRequestWithDrainStrategy(chunkSize, bodyPartList, TOP_ALL_WITH_CALLBACK, "onDrainComplete");
//Top level drains all after registering a callback and being invoked for the first time on onNewPart().
List<SinglePartMIMEDrainReaderCallbackImpl> singlePartMIMEReaderCallbacks =
_currentMultiPartMIMEReaderCallback.getSinglePartMIMEReaderCallbacks();
Assert.assertEquals(singlePartMIMEReaderCallbacks.size(), 0);
}
@Test(dataProvider = "allTypesOfBodiesDataSource")
public void testSinglePartialTopRemaining(final int chunkSize, final List<MimeBodyPart> bodyPartList) throws Exception
{
//Execute the request, verify the correct header came back to ensure the server took the proper drain actions
//and return the payload so we can assert deeper.
executeRequestWithDrainStrategy(chunkSize, bodyPartList, SINGLE_PARTIAL_TOP_REMAINING, "onDrainComplete");
//Single part drains the first 6 then the top level drains all of remaining
List<SinglePartMIMEDrainReaderCallbackImpl> singlePartMIMEReaderCallbacks =
_currentMultiPartMIMEReaderCallback.getSinglePartMIMEReaderCallbacks();
Assert.assertEquals(singlePartMIMEReaderCallbacks.size(), 6);
for (int i = 0; i < singlePartMIMEReaderCallbacks.size(); i++)
{
//Actual
final SinglePartMIMEDrainReaderCallbackImpl currentCallback = singlePartMIMEReaderCallbacks.get(i);
//Expected
final BodyPart currentExpectedPart = _currentMimeMultipartBody.getBodyPart(i);
//Construct expected headers and verify they match
final Map<String, String> expectedHeaders = new HashMap<String, String>();
@SuppressWarnings("unchecked")
final Enumeration<Header> allHeaders = currentExpectedPart.getAllHeaders();
while (allHeaders.hasMoreElements())
{
final Header header = allHeaders.nextElement();
expectedHeaders.put(header.getName(), header.getValue());
}
Assert.assertEquals(currentCallback.getHeaders(), expectedHeaders);
//Verify that the bodies are empty
Assert.assertNull(currentCallback.getFinishedData());
}
}
@Test(dataProvider = "allTypesOfBodiesDataSource")
public void testSingleAlternateTopRemaining(final int chunkSize, final List<MimeBodyPart> bodyPartList)
throws Exception
{
//Execute the request, verify the correct header came back to ensure the server took the proper drain actions
//and return the payload so we can assert deeper.
executeRequestWithDrainStrategy(chunkSize, bodyPartList, SINGLE_ALTERNATE_TOP_REMAINING, "onDrainComplete");
//Single part alternates between consumption and draining the first 6 parts, then top level drains all of remaining.
//This means that parts 0, 2, 4 will be consumed and parts 1, 3, 5 will be drained.
List<SinglePartMIMEDrainReaderCallbackImpl> singlePartMIMEReaderCallbacks =
_currentMultiPartMIMEReaderCallback.getSinglePartMIMEReaderCallbacks();
Assert.assertEquals(singlePartMIMEReaderCallbacks.size(), 6);
//First the consumed
for (int i = 0; i < singlePartMIMEReaderCallbacks.size(); i = i + 2)
{
//Actual
final SinglePartMIMEDrainReaderCallbackImpl currentCallback = singlePartMIMEReaderCallbacks.get(i);
//Expected
final BodyPart currentExpectedPart = _currentMimeMultipartBody.getBodyPart(i);
//Construct expected headers and verify they match
final Map<String, String> expectedHeaders = new HashMap<String, String>();
@SuppressWarnings("unchecked")
final Enumeration<Header> allHeaders = currentExpectedPart.getAllHeaders();
while (allHeaders.hasMoreElements())
{
final Header header = allHeaders.nextElement();
expectedHeaders.put(header.getName(), header.getValue());
}
Assert.assertEquals(currentCallback.getHeaders(), expectedHeaders);
//Verify the body matches
if (currentExpectedPart.getContent() instanceof byte[])
{
Assert.assertEquals(currentCallback.getFinishedData().copyBytes(), currentExpectedPart.getContent());
}
else
{
//Default is String
Assert.assertEquals(new String(currentCallback.getFinishedData().copyBytes()), currentExpectedPart.getContent());
}
}
//Then the drained
for (int i = 1; i < singlePartMIMEReaderCallbacks.size(); i = i + 2)
{
//Actual
final SinglePartMIMEDrainReaderCallbackImpl currentCallback = singlePartMIMEReaderCallbacks.get(i);
//Expected
final BodyPart currentExpectedPart = _currentMimeMultipartBody.getBodyPart(i);
//Construct expected headers and verify they match
final Map<String, String> expectedHeaders = new HashMap<String, String>();
@SuppressWarnings("unchecked")
final Enumeration<Header> allHeaders = currentExpectedPart.getAllHeaders();
while (allHeaders.hasMoreElements())
{
final Header header = allHeaders.nextElement();
expectedHeaders.put(header.getName(), header.getValue());
}
Assert.assertEquals(currentCallback.getHeaders(), expectedHeaders);
//Verify that the bodies are empty
Assert.assertNull(currentCallback.getFinishedData(), null);
}
}
@Test(dataProvider = "allTypesOfBodiesDataSource")
public void testSingleAll(final int chunkSize, final List<MimeBodyPart> bodyPartList) throws Exception
{
//Execute the request, verify the correct header came back to ensure the server took the proper drain actions
//and return the payload so we can assert deeper.
executeRequestWithDrainStrategy(chunkSize, bodyPartList, SINGLE_ALL, "onFinished");
//Single part drains all, one by one
List<SinglePartMIMEDrainReaderCallbackImpl> singlePartMIMEReaderCallbacks =
_currentMultiPartMIMEReaderCallback.getSinglePartMIMEReaderCallbacks();
Assert.assertEquals(singlePartMIMEReaderCallbacks.size(), 12);
//Verify everything was drained
for (int i = 0; i < singlePartMIMEReaderCallbacks.size(); i++)
{
//Actual
final SinglePartMIMEDrainReaderCallbackImpl currentCallback = singlePartMIMEReaderCallbacks.get(i);
//Expected
final BodyPart currentExpectedPart = _currentMimeMultipartBody.getBodyPart(i);
//Construct expected headers and verify they match
final Map<String, String> expectedHeaders = new HashMap<String, String>();
@SuppressWarnings("unchecked")
final Enumeration<Header> allHeaders = currentExpectedPart.getAllHeaders();
while (allHeaders.hasMoreElements())
{
final Header header = allHeaders.nextElement();
expectedHeaders.put(header.getName(), header.getValue());
}
Assert.assertEquals(currentCallback.getHeaders(), expectedHeaders);
//Verify that the bodies are empty
Assert.assertNull(currentCallback.getFinishedData());
}
}
@Test(dataProvider = "allTypesOfBodiesDataSource")
public void testSingleAlternate(final int chunkSize, final List<MimeBodyPart> bodyPartList) throws Exception
{
//Execute the request, verify the correct header came back to ensure the server took the proper drain actions
//and return the payload so we can assert deeper.
executeRequestWithDrainStrategy(chunkSize, bodyPartList, SINGLE_ALTERNATE, "onFinished");
//Single part alternates between consumption and draining for all 12 parts.
//This means that parts 0, 2, 4, etc.. will be consumed and parts 1, 3, 5, etc... will be drained.
List<SinglePartMIMEDrainReaderCallbackImpl> singlePartMIMEReaderCallbacks =
_currentMultiPartMIMEReaderCallback.getSinglePartMIMEReaderCallbacks();
Assert.assertEquals(singlePartMIMEReaderCallbacks.size(), 12);
//First the consumed
for (int i = 0; i < singlePartMIMEReaderCallbacks.size(); i = i + 2)
{
//Actual
final SinglePartMIMEDrainReaderCallbackImpl currentCallback = singlePartMIMEReaderCallbacks.get(i);
//Expected
final BodyPart currentExpectedPart = _currentMimeMultipartBody.getBodyPart(i);
//Construct expected headers and verify they match
final Map<String, String> expectedHeaders = new HashMap<String, String>();
@SuppressWarnings("unchecked")
final Enumeration<Header> allHeaders = currentExpectedPart.getAllHeaders();
while (allHeaders.hasMoreElements())
{
final Header header = allHeaders.nextElement();
expectedHeaders.put(header.getName(), header.getValue());
}
Assert.assertEquals(currentCallback.getHeaders(), expectedHeaders);
//Verify the body matches
if (currentExpectedPart.getContent() instanceof byte[])
{
Assert.assertEquals(currentCallback.getFinishedData().copyBytes(), currentExpectedPart.getContent());
}
else
{
//Default is String
Assert.assertEquals(new String(currentCallback.getFinishedData().copyBytes()), currentExpectedPart.getContent());
}
}
//Then the drained
for (int i = 1; i < singlePartMIMEReaderCallbacks.size(); i = i + 2)
{
//Actual
final SinglePartMIMEDrainReaderCallbackImpl currentCallback = singlePartMIMEReaderCallbacks.get(i);
//Expected
final BodyPart currentExpectedPart = _currentMimeMultipartBody.getBodyPart(i);
//Construct expected headers and verify they match
final Map<String, String> expectedHeaders = new HashMap<String, String>();
@SuppressWarnings("unchecked")
final Enumeration<Header> allHeaders = currentExpectedPart.getAllHeaders();
while (allHeaders.hasMoreElements())
{
final Header header = allHeaders.nextElement();
expectedHeaders.put(header.getName(), header.getValue());
}
Assert.assertEquals(currentCallback.getHeaders(), expectedHeaders);
//Verify that the bodies are empty
Assert.assertNull(currentCallback.getFinishedData());
}
}
///////////////////////////////////////////////////////////////////////////////////////
private void executeRequestWithDrainStrategy(final int chunkSize, final List<MimeBodyPart> bodyPartList,
final String drainStrategy, final String serverHeaderPrefix) throws Exception
{
MimeMultipart multiPartMimeBody = new MimeMultipart();
//Add your body parts
for (final MimeBodyPart bodyPart : bodyPartList)
{
multiPartMimeBody.addBodyPart(bodyPart);
}
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
multiPartMimeBody.writeTo(byteArrayOutputStream);
final ByteString requestPayload = ByteString.copy(byteArrayOutputStream.toByteArray());
_currentMimeMultipartBody = multiPartMimeBody;
mockR2AndWrite(requestPayload, chunkSize, multiPartMimeBody.getContentType());
final CountDownLatch latch = new CountDownLatch(1);
MultiPartMIMEReader reader = MultiPartMIMEReader.createAndAcquireStream(_streamRequest);
_currentMultiPartMIMEReaderCallback = new MultiPartMIMEDrainReaderCallbackImpl(latch, drainStrategy, reader);
reader.registerReaderCallback(_currentMultiPartMIMEReaderCallback);
latch.await(_testTimeout, TimeUnit.MILLISECONDS);
Assert.assertEquals(_currentMultiPartMIMEReaderCallback.getResponseHeaders().get(DRAIN_HEADER), serverHeaderPrefix + drainStrategy);
try
{
reader.drainAllParts();
Assert.fail();
}
catch (MultiPartReaderFinishedException multiPartReaderFinishedException)
{
}
Assert.assertTrue(reader.haveAllPartsFinished());
//mock verifies
verify(_streamRequest, times(1)).getEntityStream();
verify(_streamRequest, times(1)).getHeader(HEADER_CONTENT_TYPE);
verify(_entityStream, times(1)).setReader(isA(MultiPartMIMEReader.R2MultiPartMIMEReader.class));
final int expectedRequests = (int) Math.ceil((double) requestPayload.length() / chunkSize);
//One more expected request because we have to make the last call to get called onDone().
verify(_readHandle, times(expectedRequests + 1)).request(1);
verifyNoMoreInteractions(_streamRequest);
verifyNoMoreInteractions(_entityStream);
verifyNoMoreInteractions(_readHandle);
}
private static class SinglePartMIMEDrainReaderCallbackImpl implements SinglePartMIMEReaderCallback
{
final MultiPartMIMEReader.SinglePartMIMEReader _singlePartMIMEReader;
final ByteArrayOutputStream _byteArrayOutputStream = new ByteArrayOutputStream();
Map<String, String> _headers;
ByteString _finishedData = null;
static int partCounter = 0;
SinglePartMIMEDrainReaderCallbackImpl(final MultiPartMIMEReader.SinglePartMIMEReader singlePartMIMEReader)
{
_singlePartMIMEReader = singlePartMIMEReader;
_headers = singlePartMIMEReader.dataSourceHeaders();
}
public Map<String, String> getHeaders()
{
return _headers;
}
public ByteString getFinishedData()
{
return _finishedData;
}
@Override
public void onPartDataAvailable(ByteString partData)
{
try
{
_byteArrayOutputStream.write(partData.copyBytes());
}
catch (IOException ioException)
{
Assert.fail();
}
_singlePartMIMEReader.requestPartData();
}
@Override
public void onFinished()
{
partCounter++;
_finishedData = ByteString.copy(_byteArrayOutputStream.toByteArray());
}
//Delegate to the top level for now for these two
@Override
public void onDrainComplete()
{
partCounter++;
}
@Override
public void onStreamError(Throwable throwable)
{
//MultiPartMIMEReader will end up calling onStreamError(e) on our top level callback
//which will fail the test
}
}
private static class MultiPartMIMEDrainReaderCallbackImpl implements MultiPartMIMEReaderCallback
{
final CountDownLatch _latch;
final String _drainValue;
final MultiPartMIMEReader _reader;
final Map<String, String> _responseHeaders = new HashMap<String, String>();
final List<SinglePartMIMEDrainReaderCallbackImpl> _singlePartMIMEReaderCallbacks = new ArrayList<SinglePartMIMEDrainReaderCallbackImpl>();
MultiPartMIMEDrainReaderCallbackImpl(final CountDownLatch latch, final String drainValue,
final MultiPartMIMEReader reader)
{
_latch = latch;
_drainValue = drainValue;
_reader = reader;
}
public List<SinglePartMIMEDrainReaderCallbackImpl> getSinglePartMIMEReaderCallbacks()
{
return _singlePartMIMEReaderCallbacks;
}
public Map<String, String> getResponseHeaders()
{
return _responseHeaders;
}
@Override
public void onNewPart(MultiPartMIMEReader.SinglePartMIMEReader singlePartMIMEReader)
{
if (_drainValue.equalsIgnoreCase(SINGLE_ALL_NO_CALLBACK))
{
singlePartMIMEReader.drainPart();
return;
}
if (_drainValue.equalsIgnoreCase(TOP_ALL_WITH_CALLBACK))
{
_reader.drainAllParts();
return;
}
if (_drainValue.equalsIgnoreCase(SINGLE_PARTIAL_TOP_REMAINING) && _singlePartMIMEReaderCallbacks.size() == 6)
{
_reader.drainAllParts();
return;
}
if (_drainValue.equalsIgnoreCase(SINGLE_ALTERNATE_TOP_REMAINING) && _singlePartMIMEReaderCallbacks.size() == 6)
{
_reader.drainAllParts();
return;
}
//Now we know we have to either consume or drain individually using a registered callback, so we
//register with the SinglePartReader and take appropriate action based on the drain strategy:
SinglePartMIMEDrainReaderCallbackImpl singlePartMIMEReaderCallback =
new SinglePartMIMEDrainReaderCallbackImpl(singlePartMIMEReader);
singlePartMIMEReader.registerReaderCallback(singlePartMIMEReaderCallback);
_singlePartMIMEReaderCallbacks.add(singlePartMIMEReaderCallback);
if (_drainValue.equalsIgnoreCase(SINGLE_ALL) || _drainValue.equalsIgnoreCase(SINGLE_PARTIAL_TOP_REMAINING))
{
singlePartMIMEReader.drainPart();
return;
}
if (_drainValue.equalsIgnoreCase(SINGLE_ALTERNATE) || _drainValue.equalsIgnoreCase(SINGLE_ALTERNATE_TOP_REMAINING))
{
if (SinglePartMIMEDrainReaderCallbackImpl.partCounter % 2 == 1)
{
singlePartMIMEReader.drainPart();
}
else
{
singlePartMIMEReader.requestPartData();
}
}
}
@Override
public void onFinished()
{
//Happens for SINGLE_ALL_NO_CALLBACK, SINGLE_ALL and SINGLE_ALTERNATE
_responseHeaders.put(DRAIN_HEADER, "onFinished" + _drainValue);
_latch.countDown();
}
@Override
public void onDrainComplete()
{
//Happens for TOP_ALL, SINGLE_PARTIAL_TOP_REMAINING and SINGLE_ALTERNATE_TOP_REMAINING
_responseHeaders.put(DRAIN_HEADER, "onDrainComplete" + _drainValue);
_latch.countDown();
}
@Override
public void onStreamError(Throwable throwable)
{
Assert.fail();
}
}
}