/** * */ package org.archive.wayback.replay; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.util.Arrays; import java.util.zip.GZIPInputStream; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import junit.framework.TestCase; import org.archive.io.warc.TestWARCReader; import org.archive.io.warc.TestWARCRecordInfo; import org.archive.io.warc.WARCRecord; import org.archive.io.warc.WARCRecordInfo; import org.archive.wayback.ResultURIConverter; import org.archive.wayback.core.CaptureSearchResult; import org.archive.wayback.core.CaptureSearchResults; import org.archive.wayback.core.Resource; import org.archive.wayback.core.WaybackRequest; import org.archive.wayback.resourcestore.resourcefile.WarcResource; import org.easymock.Capture; import org.easymock.CaptureType; import org.easymock.EasyMock; /** * unit test for {@link TransparentReplayRenderer} * * @contributor kenji * */ public class TransparentReplayRendererTest extends TestCase { TransparentReplayRenderer cut; HttpServletRequest request; HttpServletResponse response; WaybackRequest wbRequest; CaptureSearchResult result = new CaptureSearchResult(); ResultURIConverter uriConverter; // unused in TransparentReplayRenderer. CaptureSearchResults results = null; /* (non-Javadoc) * @see junit.framework.TestCase#setUp() */ protected void setUp() throws Exception { super.setUp(); //HttpHeaderProcessor httpHeaderProcessor = new IdentityHttpHeaderProcessor(); HttpHeaderProcessor httpHeaderProcessor = new RedirectRewritingHttpHeaderProcessor(); cut = new TransparentReplayRenderer(httpHeaderProcessor); // unused in TransparentReplayRenderer wbRequest = null; //new WaybackRequest(); // use test fixture version as we want to focus on TransparentReplayRenderer behavior. uriConverter = EasyMock.createMock(ResultURIConverter.class); // result is only used in HttpHeaderOperation.processHeaders() results = new CaptureSearchResults(); request = EasyMock.createNiceMock(HttpServletRequest.class); response = EasyMock.createMock(HttpServletResponse.class); } public static class TestServletOutputStream extends ServletOutputStream { ByteArrayOutputStream out = new ByteArrayOutputStream(); @Override public void write(int b) throws IOException { out.write(b); } public byte[] getBytes() { return out.toByteArray(); } public String getString() { try { return out.toString("UTF-8"); } catch (UnsupportedEncodingException ex) { throw new RuntimeException("unexpected UnsupportedEncodingException", ex); } } } public void testRenderResource_BasicCapture() throws Exception { final String ct = "image/gif"; WARCRecordInfo recinfo = TestWARCRecordInfo.createHttpResponse(ct, TestWARCRecordInfo.PAYLOAD_GIF); TestWARCReader ar = new TestWARCReader(recinfo); WARCRecord rec = ar.get(0); Resource payloadResource = new WarcResource(rec, ar); payloadResource.parseHeaders(); Resource headersResource = payloadResource; TestServletOutputStream servletOutput = new TestServletOutputStream(); response.setStatus(200); EasyMock.expect(response.getOutputStream()).andReturn(servletOutput); response.setHeader("Content-Type", ct); // ??? RedirectRewritingHttpHeaderProcessor drops Content-Length header. is this really // it is supposed to do? //response.setHeader("Content-Length", Integer.toString(payloadBytes.length)); response.setHeader(EasyMock.<String>notNull(), EasyMock.<String>notNull()); EasyMock.expectLastCall().anyTimes(); EasyMock.replay(response); cut.renderResource(request, response, wbRequest, result, headersResource, payloadResource, uriConverter, results); EasyMock.verify(response); byte[] content = servletOutput.getBytes(); assertTrue("servlet output", Arrays.equals(TestWARCRecordInfo.PAYLOAD_GIF, content)); } /** * test replay of capture with {@code Content-Encoding: gzip}. * TransparentReplayRenderer copies original, compressed payload to the output. * * TODO: should render uncompressed content if client cannot handle * {@code Content-Encoding: gzip}. * * @throws Exception */ public void testRenderResource_CompressedCapture() throws Exception { final String ct = "image/gif"; WARCRecordInfo recinfo = new TestWARCRecordInfo( TestWARCRecordInfo.buildCompressedHttpResponseBlock(ct, TestWARCRecordInfo.PAYLOAD_GIF)); TestWARCReader ar = new TestWARCReader(recinfo); WARCRecord rec = ar.get(0); Resource payloadResource = new WarcResource(rec, ar); payloadResource.parseHeaders(); Resource headersResource = payloadResource; TestServletOutputStream servletOutput = new TestServletOutputStream(); response.setStatus(200); EasyMock.expect(response.getOutputStream()).andReturn(servletOutput); response.setHeader("Content-Type", ct); response.setHeader("Content-Encoding", "gzip"); // ??? RedirectRewritingHttpHeaderProcessor drops Content-Length header. is this really // what it is supposed to do? //response.setHeader("Content-Length", Integer.toString(payloadBytes.length)); response.setHeader(EasyMock.<String>notNull(), EasyMock.<String>notNull()); EasyMock.expectLastCall().anyTimes(); EasyMock.replay(response); cut.renderResource(request, response, wbRequest, result, headersResource, payloadResource, uriConverter, results); EasyMock.verify(response); // content is the original gzip-compressed bytes for PAYLOAD_GIF. InputStream zis = new GZIPInputStream(new ByteArrayInputStream(servletOutput.getBytes())); byte[] content = new byte[TestWARCRecordInfo.PAYLOAD_GIF.length]; zis.read(content); assertTrue("servlet output", Arrays.equals(TestWARCRecordInfo.PAYLOAD_GIF, content)); } public void testRenderResource_Redirect() throws Exception { String location = "http://www.example.com/index.html"; WARCRecordInfo recinfo = new TestWARCRecordInfo(TestWARCRecordInfo.buildHttpRedirectResponseBlock(location)); TestWARCReader ar = new TestWARCReader(recinfo); WARCRecord rec = ar.get(0); Resource payloadResource = new WarcResource(rec, ar); payloadResource.parseHeaders(); final String originalUrl = "http://www.example.com/"; final String captureTimestamp = "20130101123456"; result.setOriginalUrl(originalUrl); result.setCaptureTimestamp(captureTimestamp); // makeReplayURI() is called through RedirectRewritingHttpHeaderProcessor. // TODO: perhaps HttpheaderProcessor is the right class to make fixture? EasyMock.expect(uriConverter.makeReplayURI(captureTimestamp, location)) .andReturn("/web/" + captureTimestamp + "/" + location); TestServletOutputStream servletOutput = new TestServletOutputStream(); response.setStatus(302); EasyMock.expect(response.getOutputStream()).andReturn(servletOutput); response.setHeader("Content-Type", "text/html"); response.setHeader(EasyMock.eq("Location"), EasyMock.matches("/web/" + captureTimestamp + "/" + location)); // RedirectRewritingHttpHeaderProcessor drops Content-Length. // response.setHeader("Content-Length", "0"); response.setHeader(EasyMock.<String>notNull(), EasyMock.<String>notNull()); EasyMock.expectLastCall().anyTimes(); EasyMock.replay(response, uriConverter); cut.renderResource(request, response, wbRequest, result, payloadResource, payloadResource, uriConverter, results); EasyMock.verify(response, uriConverter); byte[] content = servletOutput.getBytes(); assertEquals("payload length", 0, content.length); } /** * test replay of capture with {@code Transfer-Encoding: chunked}. * * <p>TransparentReplayRenderer writes out chunk-decoded payload, because * {@link WarcResource} always decodes chunked-entity. Point of this test * is that response never have {@code Transfer-Encoding: chunked} header, * even when initialized with {@link IdentityHttpHeaderProcessor}. * so, this is not really a unit test for TransparentReplayRenderer, but * a multi-component test placed here for convenience.</p> * <p>This test does not use member object {@code cut}, in order to test * with {@link IdentityHttpHeaderProcessor}.</p> * * @throws Exception */ public void testRenderResource_Chunked() throws Exception { final String ct = "text/xml"; final String payload = "<?xml version=\"1.0\"?>\n" + "<payload name=\"archive\">\n" + " <inside/>\n" + "</payload>\n"; final byte[] recordBytes = TestWARCRecordInfo.buildHttpResponseBlock( "200 OK", ct, payload.getBytes("UTF-8"), true); //System.out.println(new String(recordBytes, "UTF-8")); WARCRecordInfo recinfo = new TestWARCRecordInfo(recordBytes); TestWARCReader ar = new TestWARCReader(recinfo); WARCRecord rec = ar.get(0); Resource payloadResource = new WarcResource(rec, ar); payloadResource.parseHeaders(); Resource headersResource = payloadResource; TestServletOutputStream servletOutput = new TestServletOutputStream(); // expectations response.setStatus(200); EasyMock.expect(response.getOutputStream()).andReturn(servletOutput); // capture setHeader() call for "Transfer-Encoding" Capture<String> transferEncodingCapture = new Capture<String>(CaptureType.FIRST); response.setHeader(EasyMock.eq("Transfer-Encoding"), EasyMock.capture(transferEncodingCapture)); EasyMock.expectLastCall().anyTimes(); response.setHeader(EasyMock.<String>anyObject(), EasyMock.<String>anyObject()); EasyMock.expectLastCall().anyTimes(); EasyMock.replay(response); // creating separate test object to use IdentityHttpHeaderProcessor TransparentReplayRenderer cut2 = new TransparentReplayRenderer(new IdentityHttpHeaderProcessor()); cut2.renderResource(request, response, wbRequest, result, headersResource, payloadResource, uriConverter, results); EasyMock.verify(response); assertFalse("Transfer-Encoding header must not be set", transferEncodingCapture.hasCaptured()); // content is the original gzip-compressed bytes for PAYLOAD_GIF. String output = new String(servletOutput.getBytes(), "UTF-8"); assertEquals(payload, output); } protected byte[] buildTestBytes(int length) { byte[] bytes = new byte[length]; for (int i = 0; i < length; i++) { bytes[i] = (byte)(i & 0xff); } return bytes; } public void testRenderResource_Range_From200() throws Exception { final String ct = "application/octet-stream"; final byte[] payload = buildTestBytes(1024); final byte[] recordBytes = TestWARCRecordInfo.buildHttpResponseBlock( "200 OK", ct, payload, false); WARCRecordInfo recinfo = new TestWARCRecordInfo(recordBytes); TestWARCReader ar = new TestWARCReader(recinfo); WARCRecord rec = ar.get(0); Resource payloadResource = new WarcResource(rec, ar); payloadResource.parseHeaders(); final String range = "bytes=10-19"; // range request EasyMock.expect(request.getHeader(EasyMock.matches("(?i)range$"))) .andStubReturn(range); TestServletOutputStream servletOutput = new TestServletOutputStream(); // expectations response.setStatus(206); EasyMock.expect(response.getOutputStream()).andReturn(servletOutput); Capture<String> contentRange = new Capture<String>(CaptureType.FIRST); response.setHeader(EasyMock.matches("(?i)content-range$"), EasyMock.capture(contentRange)); EasyMock.expectLastCall().once(); Capture<String> contentLength = new Capture<String>(CaptureType.FIRST); response.setHeader(EasyMock.matches("(?i)content-length$"), EasyMock.capture(contentLength)); EasyMock.expectLastCall().once(); response.setHeader(EasyMock.<String>anyObject(), EasyMock.<String>anyObject()); EasyMock.expectLastCall().anyTimes(); EasyMock.replay(request, response, uriConverter); cut.renderResource(request, response, wbRequest, result, payloadResource, uriConverter, results); EasyMock.verify(response); assertTrue(contentRange.hasCaptured()); String contentRangeValue = contentRange.getValue(); assertEquals("bytes 10-19/1024", contentRangeValue); assertTrue(contentLength.hasCaptured()); assertEquals("10", contentLength.getValue()); byte[] outputBytes = servletOutput.getBytes(); assertEquals(10, outputBytes.length); assertEquals(10, outputBytes[0]); assertEquals(19, outputBytes[9]); } public void testRenderResource_Range_From200_All() throws Exception { final String ct = "application/octet-stream"; final byte[] payload = buildTestBytes(1024); final byte[] recordBytes = TestWARCRecordInfo.buildHttpResponseBlock( "200 OK", ct, payload, false); WARCRecordInfo recinfo = new TestWARCRecordInfo(recordBytes); TestWARCReader ar = new TestWARCReader(recinfo); WARCRecord rec = ar.get(0); Resource payloadResource = new WarcResource(rec, ar); payloadResource.parseHeaders(); final String range = "bytes=0-"; // range request EasyMock.expect(request.getHeader(EasyMock.matches("(?i)range$"))) .andStubReturn(range); TestServletOutputStream servletOutput = new TestServletOutputStream(); // expectations response.setStatus(206); EasyMock.expect(response.getOutputStream()).andReturn(servletOutput); Capture<String> contentRange = new Capture<String>(CaptureType.FIRST); response.setHeader(EasyMock.matches("(?i)content-range$"), EasyMock.capture(contentRange)); EasyMock.expectLastCall().once(); Capture<String> contentLength = new Capture<String>(CaptureType.FIRST); response.setHeader(EasyMock.matches("(?i)content-length$"), EasyMock.capture(contentLength)); EasyMock.expectLastCall().once(); response.setHeader(EasyMock.<String>anyObject(), EasyMock.<String>anyObject()); EasyMock.expectLastCall().anyTimes(); EasyMock.replay(request, response, uriConverter); cut.renderResource(request, response, wbRequest, result, payloadResource, uriConverter, results); EasyMock.verify(response); assertTrue(contentRange.hasCaptured()); String contentRangeValue = contentRange.getValue(); assertEquals("bytes 0-1023/1024", contentRangeValue); assertTrue(contentLength.hasCaptured()); assertEquals("1024", contentLength.getValue()); byte[] outputBytes = servletOutput.getBytes(); assertEquals(1024, outputBytes.length); assertEquals(0, outputBytes[0]); assertEquals(-1, outputBytes[1023]); } public void testRenderResource_Range_From200_Tail() throws Exception { final String ct = "application/octet-stream"; final byte[] payload = buildTestBytes(1024); final byte[] recordBytes = TestWARCRecordInfo.buildHttpResponseBlock( "200 OK", ct, payload, false); WARCRecordInfo recinfo = new TestWARCRecordInfo(recordBytes); TestWARCReader ar = new TestWARCReader(recinfo); WARCRecord rec = ar.get(0); Resource payloadResource = new WarcResource(rec, ar); payloadResource.parseHeaders(); final String range = "bytes=-10"; // range request EasyMock.expect(request.getHeader(EasyMock.matches("(?i)range$"))) .andStubReturn(range); TestServletOutputStream servletOutput = new TestServletOutputStream(); // expectations response.setStatus(206); EasyMock.expect(response.getOutputStream()).andReturn(servletOutput); Capture<String> contentRange = new Capture<String>(CaptureType.FIRST); response.setHeader(EasyMock.matches("(?i)content-range$"), EasyMock.capture(contentRange)); EasyMock.expectLastCall().once(); Capture<String> contentLength = new Capture<String>(CaptureType.FIRST); response.setHeader(EasyMock.matches("(?i)content-length$"), EasyMock.capture(contentLength)); EasyMock.expectLastCall().once(); response.setHeader(EasyMock.<String>anyObject(), EasyMock.<String>anyObject()); EasyMock.expectLastCall().anyTimes(); EasyMock.replay(request, response, uriConverter); cut.renderResource(request, response, wbRequest, result, payloadResource, uriConverter, results); EasyMock.verify(response); assertTrue(contentRange.hasCaptured()); String contentRangeValue = contentRange.getValue(); assertEquals("bytes 1014-1023/1024", contentRangeValue); assertTrue(contentLength.hasCaptured()); assertEquals("10", contentLength.getValue()); byte[] outputBytes = servletOutput.getBytes(); assertEquals(10, outputBytes.length); assertEquals(-10, outputBytes[0]); assertEquals(-1, outputBytes[9]); } protected static byte[] buildPartialContentResponseBlock(String ctype, byte[] payloadBytes, long startPos, long instanceSize) throws IOException { assertTrue(startPos + payloadBytes.length <= instanceSize); final String CRLF = "\r\n"; final String contentRange = String.format("bytes %d-%d/%d", startPos, startPos + payloadBytes.length - 1, instanceSize); ByteArrayOutputStream blockbuf = new ByteArrayOutputStream(); Writer bw = new OutputStreamWriter(blockbuf); bw.write("HTTP/1.0 206 Partial Content" + CRLF); bw.write("Content-Type: " + ctype + CRLF); bw.write("Content-Range: " + contentRange + CRLF); bw.write("Content-Length: " + payloadBytes.length + CRLF); bw.write(CRLF); bw.close(); blockbuf.write(payloadBytes); return blockbuf.toByteArray(); } public void testRenderResource_Range_From206() throws Exception { final String ct = "application/octet-stream"; final byte[] payload = buildTestBytes(1024); final byte[] recordBytes = buildPartialContentResponseBlock(ct, payload, 10, 1040); WARCRecordInfo recinfo = new TestWARCRecordInfo(recordBytes); TestWARCReader ar = new TestWARCReader(recinfo); WARCRecord rec = ar.get(0); Resource payloadResource = new WarcResource(rec, ar); payloadResource.parseHeaders(); final String range = "bytes=12-1032"; // range request EasyMock.expect(request.getHeader(EasyMock.matches("(?i)range$"))) .andStubReturn(range); TestServletOutputStream servletOutput = new TestServletOutputStream(); // expectations response.setStatus(206); EasyMock.expect(response.getOutputStream()).andReturn(servletOutput); Capture<String> contentRange = new Capture<String>(CaptureType.FIRST); response.setHeader(EasyMock.matches("(?i)content-range$"), EasyMock.capture(contentRange)); EasyMock.expectLastCall().once(); Capture<String> contentLength = new Capture<String>(CaptureType.FIRST); response.setHeader(EasyMock.matches("(?i)content-length$"), EasyMock.capture(contentLength)); EasyMock.expectLastCall().once(); response.setHeader(EasyMock.<String>anyObject(), EasyMock.<String>anyObject()); EasyMock.expectLastCall().anyTimes(); EasyMock.replay(request, response, uriConverter); cut.renderResource(request, response, wbRequest, result, payloadResource, uriConverter, results); EasyMock.verify(response); assertTrue(contentRange.hasCaptured()); String contentRangeValue = contentRange.getValue(); assertEquals("bytes 12-1032/1040", contentRangeValue); assertTrue(contentLength.hasCaptured()); assertEquals("1021", contentLength.getValue()); byte[] outputBytes = servletOutput.getBytes(); assertEquals(1021, outputBytes.length); assertEquals(2, outputBytes[0]); assertEquals(-2, outputBytes[1020]); } }