/* * ModeShape (http://www.modeshape.org) * * 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.modeshape.jcr.value.binary; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.CopyObjectRequest; import com.amazonaws.services.s3.model.ListObjectsRequest; import com.amazonaws.services.s3.model.ObjectListing; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.S3Object; import com.amazonaws.services.s3.model.S3ObjectSummary; import com.amazonaws.util.IOUtils; import com.amazonaws.util.StringInputStream; import org.easymock.Capture; import org.easymock.EasyMockRunner; import org.easymock.EasyMockSupport; import org.easymock.Mock; import org.easymock.TestSubject; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import org.modeshape.jcr.value.BinaryKey; import org.modeshape.jcr.value.BinaryValue; import static org.easymock.EasyMock.capture; import static org.easymock.EasyMock.eq; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.expectLastCall; import static org.easymock.EasyMock.isA; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; /** * Unit test for the S3 Binary Store. * All calls to S3 are handled by a mock S3 client and verified. * * @author bbranan */ @RunWith(EasyMockRunner.class) public class S3BinaryStoreTest extends EasyMockSupport { private static final String BUCKET = "MOCK_BUCKET"; private static final String TEST_KEY = "test-key"; private static final String TEST_MIME = "text/plain"; private static final String TEST_CONTENT = "test-content"; @Mock private AmazonS3Client s3Client; @Mock private ObjectListing objectListing; @TestSubject private S3BinaryStore s3BinaryStore = new S3BinaryStore(BUCKET, s3Client); @After public void tearDown() { verifyAll(); } @Test public void testGetStoredMimeType() throws BinaryStoreException { ObjectMetadata objMeta = new ObjectMetadata(); objMeta.setContentType(TEST_MIME); objMeta.addUserMetadata(S3BinaryStore.USER_MIME_TYPE_KEY, String.valueOf(true)); expect(s3Client.getObjectMetadata(BUCKET, TEST_KEY)).andReturn(objMeta); replayAll(); BinaryValue binaryValue = createBinaryValue(TEST_KEY, TEST_CONTENT); String mimeType = s3BinaryStore.getStoredMimeType(binaryValue); assertEquals(TEST_MIME, mimeType); } private BinaryValue createBinaryValue(String key, String content) { return new StoredBinaryValue(s3BinaryStore, new BinaryKey(key), content.length()); } @Test public void testStoreMimeType() throws BinaryStoreException { expect(s3Client.getObjectMetadata(BUCKET, TEST_KEY)) .andReturn(new ObjectMetadata()); Capture<CopyObjectRequest> copyRequestCapture = Capture.newInstance(); expect(s3Client.copyObject(capture(copyRequestCapture))).andReturn(null); replayAll(); BinaryValue binaryValue = createBinaryValue(TEST_KEY, TEST_CONTENT); s3BinaryStore.storeMimeType(binaryValue, TEST_MIME); CopyObjectRequest copyRequest = copyRequestCapture.getValue(); assertEquals(BUCKET, copyRequest.getSourceBucketName()); assertEquals(BUCKET, copyRequest.getDestinationBucketName()); assertEquals(TEST_KEY, copyRequest.getSourceKey()); assertEquals(TEST_KEY, copyRequest.getDestinationKey()); assertEquals(TEST_MIME, copyRequest.getNewObjectMetadata().getContentType()); } /** * Ensures that an execption is thrown if an attempt is made to store an * extracted text value which exceeds the capacity of S3 */ @Test public void testStoreExtractedTextTooLong() throws BinaryStoreException { StringBuilder textBuilder = new StringBuilder(); for(int i=0; i<2001; i++) { textBuilder.append("a"); } String extractedText = textBuilder.toString(); replayAll(); BinaryValue binaryValue = createBinaryValue(TEST_KEY, TEST_CONTENT); try { s3BinaryStore.storeExtractedText(binaryValue, extractedText); fail("Exception expected due to long extracted text value"); } catch(BinaryStoreException e) { assertNotNull(e); } } @Test public void testStoreExtractedText() throws BinaryStoreException { String extractedText = "text-that-has-been-extracted"; expect(s3Client.getObjectMetadata(BUCKET, TEST_KEY)) .andReturn(new ObjectMetadata()); Capture<CopyObjectRequest> copyRequestCapture = Capture.newInstance(); expect(s3Client.copyObject(capture(copyRequestCapture))).andReturn(null); replayAll(); BinaryValue binaryValue = createBinaryValue(TEST_KEY, TEST_CONTENT); s3BinaryStore.storeExtractedText(binaryValue, extractedText); CopyObjectRequest copyRequest = copyRequestCapture.getValue(); assertEquals(BUCKET, copyRequest.getSourceBucketName()); assertEquals(BUCKET, copyRequest.getDestinationBucketName()); assertEquals(TEST_KEY, copyRequest.getSourceKey()); assertEquals(TEST_KEY, copyRequest.getDestinationKey()); assertEquals(extractedText, copyRequest.getNewObjectMetadata() .getUserMetadata() .get(s3BinaryStore.EXTRACTED_TEXT_KEY)); } @Test public void testGetExtractedText() throws BinaryStoreException { String extractedText = "text-that-has-been-extracted"; ObjectMetadata objMeta = new ObjectMetadata(); Map<String, String> userMeta = new HashMap<>(); userMeta.put(s3BinaryStore.EXTRACTED_TEXT_KEY, extractedText); objMeta.setUserMetadata(userMeta); expect(s3Client.getObjectMetadata(BUCKET, TEST_KEY)).andReturn(objMeta); replayAll(); BinaryValue binaryValue = createBinaryValue(TEST_KEY, TEST_CONTENT); String extractValue = s3BinaryStore.getExtractedText(binaryValue); assertEquals(extractedText, extractValue); } /* * Tests storing new content */ @Test public void testStoreValue() throws BinaryStoreException, UnsupportedEncodingException { String valueToStore = "value-to-store"; expect(s3Client.doesObjectExist(eq(BUCKET), isA(String.class))).andReturn(false); Capture<ObjectMetadata> objMetaCapture = Capture.newInstance(); expect(s3Client.putObject(eq(BUCKET), isA(String.class), isA(InputStream.class), capture(objMetaCapture))) .andReturn(null); replayAll(); s3BinaryStore.storeValue(new StringInputStream(valueToStore), false); ObjectMetadata objMeta = objMetaCapture.getValue(); assertEquals(String.valueOf(false), objMeta.getUserMetadata().get(s3BinaryStore.UNUSED_KEY)); } /* * Tests storing content which already exists. Ensures that the call to set the * markAsUnread property is made. */ @Test public void testStoreValueExisting() throws BinaryStoreException, IOException { String valueToStore = "value-to-store"; expect(s3Client.doesObjectExist(eq(BUCKET), isA(String.class))).andReturn(true); expect(s3Client.getObjectMetadata(eq(BUCKET), isA(String.class))) .andReturn(new ObjectMetadata()); ObjectMetadata objMeta = new ObjectMetadata(); Map<String, String> userMeta = new HashMap<>(); userMeta.put(s3BinaryStore.UNUSED_KEY, String.valueOf(true)); objMeta.setUserMetadata(userMeta); Capture<CopyObjectRequest> copyRequestCapture = Capture.newInstance(); expect(s3Client.copyObject(capture(copyRequestCapture))).andReturn(null); replayAll(); s3BinaryStore.storeValue(new StringInputStream(valueToStore), true); ObjectMetadata newObjMeta = copyRequestCapture.getValue().getNewObjectMetadata(); assertEquals(String.valueOf(true), newObjMeta.getUserMetadata().get(s3BinaryStore.UNUSED_KEY)); } @Test public void testGetInputStream() throws BinaryStoreException, IOException { S3Object s3Object = new S3Object(); s3Object.setObjectContent(new StringInputStream(TEST_CONTENT)); expect(s3Client.getObject(BUCKET, TEST_KEY)).andReturn(s3Object); replayAll(); InputStream resultStream = s3BinaryStore.getInputStream(new BinaryKey(TEST_KEY)); assertEquals(TEST_CONTENT, IOUtils.toString(resultStream)); } @Test public void testMarkAsUsed() throws BinaryStoreException { ObjectMetadata objMeta = new ObjectMetadata(); Map<String, String> userMeta = new HashMap<>(); // Existing value of unused property set to true (so file is considered not used) userMeta.put(s3BinaryStore.UNUSED_KEY, String.valueOf(true)); objMeta.setUserMetadata(userMeta); expect(s3Client.getObjectMetadata(eq(BUCKET), isA(String.class))) .andReturn(objMeta); Capture<CopyObjectRequest> copyRequestCapture = Capture.newInstance(); expect(s3Client.copyObject(capture(copyRequestCapture))).andReturn(null); replayAll(); s3BinaryStore.markAsUsed(Collections.singleton(new BinaryKey(TEST_KEY))); ObjectMetadata newObjMeta = copyRequestCapture.getValue().getNewObjectMetadata(); assertEquals(String.valueOf(false), newObjMeta.getUserMetadata().get(s3BinaryStore.UNUSED_KEY)); } @Test public void testMarkAsUnused() throws BinaryStoreException { ObjectMetadata objMeta = new ObjectMetadata(); Map<String, String> userMeta = new HashMap<>(); // Existing value of unused property set to false (so file is considered used) userMeta.put(s3BinaryStore.UNUSED_KEY, String.valueOf(false)); objMeta.setUserMetadata(userMeta); expect(s3Client.getObjectMetadata(eq(BUCKET), isA(String.class))) .andReturn(objMeta); Capture<CopyObjectRequest> copyRequestCapture = Capture.newInstance(); expect(s3Client.copyObject(capture(copyRequestCapture))).andReturn(null); replayAll(); s3BinaryStore.markAsUnused(Collections.singleton(new BinaryKey(TEST_KEY))); ObjectMetadata newObjMeta = copyRequestCapture.getValue().getNewObjectMetadata(); assertEquals(String.valueOf(true), newObjMeta.getUserMetadata().get(s3BinaryStore.UNUSED_KEY)); } /* * Tests setting the unused property given that the existing value is already set * to the preferred value. No update calls should occur. */ @Test public void testMarkAsUnusedNoChangeNeeded() throws BinaryStoreException { ObjectMetadata objMeta = new ObjectMetadata(); Map<String, String> userMeta = new HashMap<>(); // Existing value of unused property set to true, so file is already considered // to be not used. No change should be needed. userMeta.put(s3BinaryStore.UNUSED_KEY, String.valueOf(true)); objMeta.setUserMetadata(userMeta); expect(s3Client.getObjectMetadata(eq(BUCKET), isA(String.class))) .andReturn(objMeta); replayAll(); s3BinaryStore.markAsUnused(Collections.singleton(new BinaryKey(TEST_KEY))); } @Test public void testRemoveValuesUnusedLongerThan() throws BinaryStoreException { String usedObjectKey = "used-object"; String unusedNewKey = "unused-new"; String unusedOldKey = "unused-old"; // List of objects, one with unused=false, // one with unused=true but updated within the hour, // one with unused=true and last updated over a week ago (which should be removed) List<S3ObjectSummary> objectList = new ArrayList<>(); S3ObjectSummary usedObject = new S3ObjectSummary(); usedObject.setKey(usedObjectKey); objectList.add(usedObject); S3ObjectSummary unusedNewObject = new S3ObjectSummary(); unusedNewObject.setKey(unusedNewKey); objectList.add(unusedNewObject); S3ObjectSummary unusedOldObject = new S3ObjectSummary(); unusedOldObject.setKey(unusedOldKey); objectList.add(unusedOldObject); // Expect request to get object list expect(s3Client.listObjects(isA(ListObjectsRequest.class))) .andReturn(objectListing); expect(objectListing.getObjectSummaries()).andReturn(objectList); expect(objectListing.isTruncated()).andReturn(false); // Request for used object ObjectMetadata usedObjMeta = new ObjectMetadata(); usedObjMeta.setUserMetadata( Collections.singletonMap(s3BinaryStore.UNUSED_KEY, String.valueOf(false))); usedObjMeta.setLastModified(new Date()); expect(s3Client.getObjectMetadata(BUCKET, usedObjectKey)).andReturn(usedObjMeta); // Request for unused object with recent update ObjectMetadata unusedNewObjMeta = new ObjectMetadata(); unusedNewObjMeta.setUserMetadata( Collections.singletonMap(s3BinaryStore.UNUSED_KEY, String.valueOf(true))); unusedNewObjMeta.setLastModified(new Date()); expect(s3Client.getObjectMetadata(BUCKET, unusedNewKey)).andReturn(unusedNewObjMeta); // Request for unused object with old update ObjectMetadata unusedOldObjMeta = new ObjectMetadata(); unusedOldObjMeta.setUserMetadata( Collections.singletonMap(s3BinaryStore.UNUSED_KEY, String.valueOf(true))); // Last modified 8 days ago unusedOldObjMeta.setLastModified(new Date(System.currentTimeMillis() - 691200000)); expect(s3Client.getObjectMetadata(BUCKET, unusedOldKey)).andReturn(unusedOldObjMeta); // Expect one delete s3Client.deleteObject(BUCKET, unusedOldKey); expectLastCall(); replayAll(); s3BinaryStore.removeValuesUnusedLongerThan(7, TimeUnit.DAYS); } @Test public void testGetAllBinaryKeys() throws BinaryStoreException { List<S3ObjectSummary> objectList = new ArrayList<>(); for(int i=0; i< 101; i++) { S3ObjectSummary object = new S3ObjectSummary(); object.setKey(String.valueOf(i)); objectList.add(object); } // First request to get objects (incomplete list returned) expect(s3Client.listObjects(isA(ListObjectsRequest.class))) .andReturn(objectListing); expect(objectListing.getObjectSummaries()).andReturn(objectList); expect(objectListing.isTruncated()).andReturn(true); // Second request to get more objects expect(s3Client.listNextBatchOfObjects(objectListing)) .andReturn(objectListing); expect(objectListing.getObjectSummaries()).andReturn(objectList); expect(objectListing.isTruncated()).andReturn(false); replayAll(); Iterable<BinaryKey> allKeys = s3BinaryStore.getAllBinaryKeys(); int keyCount = 0; for(BinaryKey key : allKeys) { keyCount++; } assertEquals(202, keyCount); // Expecting two sets of 101 objects } }