blob: c1fd8416c5f2004afa1f21d25660c6c2919e7059 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.jackrabbit.oak.blob.cloud.azure.blobstorage;
import static org.apache.commons.codec.binary.Hex.encodeHexString;
import static org.apache.commons.io.FileUtils.copyInputStreamToFile;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import com.google.common.base.Strings;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.microsoft.azure.storage.StorageException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.jackrabbit.core.data.DataIdentifier;
import org.apache.jackrabbit.core.data.DataRecord;
import org.apache.jackrabbit.core.data.DataStoreException;
import org.apache.jackrabbit.oak.spi.blob.SharedBackend;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.URISyntaxException;
import java.security.DigestOutputStream;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.jcr.RepositoryException;
/**
* Test {@link AzureDataStore} with AzureDataStore and local cache on.
* It requires to pass azure config file via system property or system properties by prefixing with 'ds.'.
* See details @ {@link AzureDataStoreUtils}.
* For e.g. -Dconfig=/opt/cq/azure.properties. Sample azure properties located at
* src/test/resources/azure.properties
*/
public class AzureDataStoreTest {
protected static final Logger LOG = LoggerFactory.getLogger(AzureDataStoreTest.class);
@Rule
public TemporaryFolder folder = new TemporaryFolder(new File("target"));
private Properties props;
private static byte[] testBuffer = "test".getBytes();
private AzureDataStore ds;
private AzureBlobStoreBackend backend;
private String container;
Random randomGen = new Random();
@BeforeClass
public static void assumptions() {
assumeTrue(AzureDataStoreUtils.isAzureConfigured());
}
@Before
public void setup() throws IOException, RepositoryException, URISyntaxException, InvalidKeyException, StorageException {
props = AzureDataStoreUtils.getAzureConfig();
container = String.valueOf(randomGen.nextInt(9999)) + "-" + String.valueOf(randomGen.nextInt(9999))
+ "-test";
props.setProperty(AzureConstants.AZURE_BLOB_CONTAINER_NAME, container);
ds = new AzureDataStore();
ds.setProperties(props);
ds.setCacheSize(0); // Turn caching off so we don't get weird test results due to caching
ds.init(folder.newFolder().getAbsolutePath());
backend = (AzureBlobStoreBackend) ds.getBackend();
}
@After
public void teardown() throws InvalidKeyException, URISyntaxException, StorageException {
ds = null;
try {
AzureDataStoreUtils.deleteContainer(container);
} catch (Exception ignore) {}
}
private void validateRecord(final DataRecord record,
final String contents,
final DataRecord rhs)
throws DataStoreException, IOException {
validateRecord(record, contents, rhs.getIdentifier(), rhs.getLength(), rhs.getLastModified());
}
private void validateRecord(final DataRecord record,
final String contents,
final DataIdentifier identifier,
final long length,
final long lastModified)
throws DataStoreException, IOException {
validateRecord(record, contents, identifier, length, lastModified, true);
}
private void validateRecord(final DataRecord record,
final String contents,
final DataIdentifier identifier,
final long length,
final long lastModified,
final boolean lastModifiedEquals)
throws DataStoreException, IOException {
assertEquals(record.getLength(), length);
if (lastModifiedEquals) {
assertEquals(record.getLastModified(), lastModified);
} else {
assertTrue(record.getLastModified() > lastModified);
}
assertTrue(record.getIdentifier().toString().equals(identifier.toString()));
StringWriter writer = new StringWriter();
org.apache.commons.io.IOUtils.copy(record.getStream(), writer, "utf-8");
assertTrue(writer.toString().equals(contents));
}
private static InputStream randomStream(int seed, int size) {
Random r = new Random(seed);
byte[] data = new byte[size];
r.nextBytes(data);
return new ByteArrayInputStream(data);
}
private static String getIdForInputStream(final InputStream in)
throws NoSuchAlgorithmException, IOException {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
OutputStream output = new DigestOutputStream(new NullOutputStream(), digest);
try {
IOUtils.copyLarge(in, output);
} finally {
IOUtils.closeQuietly(output);
IOUtils.closeQuietly(in);
}
return encodeHexString(digest.digest());
}
@Test
public void testCreateAndDeleteBlobHappyPath() throws DataStoreException, IOException {
final DataRecord uploadedRecord = ds.addRecord(new ByteArrayInputStream(testBuffer));
DataIdentifier identifier = uploadedRecord.getIdentifier();
assertTrue(backend.exists(identifier));
assertTrue(0 != uploadedRecord.getLastModified());
assertEquals(testBuffer.length, uploadedRecord.getLength());
final DataRecord retrievedRecord = ds.getRecord(identifier);
validateRecord(retrievedRecord, new String(testBuffer), uploadedRecord);
ds.deleteRecord(identifier);
assertFalse(backend.exists(uploadedRecord.getIdentifier()));
}
@Test
public void testCreateAndReUploadBlob() throws DataStoreException, IOException {
final DataRecord createdRecord = ds.addRecord(new ByteArrayInputStream(testBuffer));
DataIdentifier identifier1 = createdRecord.getIdentifier();
assertTrue(backend.exists(identifier1));
final DataRecord record1 = ds.getRecord(identifier1);
validateRecord(record1, new String(testBuffer), createdRecord);
try { Thread.sleep(1001); } catch (InterruptedException e) { }
final DataRecord updatedRecord = ds.addRecord(new ByteArrayInputStream(testBuffer));
DataIdentifier identifier2 = updatedRecord.getIdentifier();
assertTrue(backend.exists(identifier2));
assertTrue(identifier1.toString().equals(identifier2.toString()));
validateRecord(record1, new String(testBuffer), createdRecord);
ds.deleteRecord(identifier1);
assertFalse(backend.exists(createdRecord.getIdentifier()));
}
@Test
public void testListBlobs() throws DataStoreException, IOException {
final Set<DataIdentifier> identifiers = Sets.newHashSet();
final Set<String> testStrings = Sets.newHashSet("test1", "test2", "test3");
for (String s : testStrings) {
identifiers.add(ds.addRecord(new ByteArrayInputStream(s.getBytes())).getIdentifier());
}
Iterator<DataIdentifier> iter = ds.getAllIdentifiers();
while (iter.hasNext()) {
DataIdentifier identifier = iter.next();
assertTrue(identifiers.contains(identifier));
ds.deleteRecord(identifier);
}
}
////
// Backend Tests
////
private void validateRecordData(final SharedBackend backend,
final DataIdentifier identifier,
int expectedSize,
final InputStream expected) throws IOException, DataStoreException {
byte[] blobData = new byte[expectedSize];
backend.read(identifier).read(blobData);
byte[] expectedData = new byte[expectedSize];
expected.read(expectedData);
for (int i=0; i<expectedSize; i++) {
assertEquals(expectedData[i], blobData[i]);
}
}
// Write (Backend)
@Test
public void testBackendWriteDifferentSizedRecords() throws IOException, NoSuchAlgorithmException, DataStoreException {
// Sizes are chosen as follows:
// 0 - explicitly test zero-size file
// 10 - very small file
// 1000 - under 4K (a reasonably expected stream buffer size)
// 4100 - over 4K but under 8K and 16K (other reasonably expected stream buffer sizes)
// 16500 - over 8K and 16K but under 64K (another reasonably expected stream buffer size)
// 66000 - over 64K but under 128K (probably the largest reasonably expected stream buffer size)
// 132000 - over 128K
for (int size : Lists.newArrayList(0, 10, 1000, 4100, 16500, 66000, 132000)) {
File testFile = folder.newFile();
copyInputStreamToFile(randomStream(size, size), testFile);
DataIdentifier identifier = new DataIdentifier(getIdForInputStream(new FileInputStream(testFile)));
backend.write(identifier, testFile);
assertTrue(backend.exists(identifier));
validateRecordData(backend, identifier, size, new FileInputStream(testFile));
backend.deleteRecord(identifier);
assertFalse(backend.exists(identifier));
}
}
@Test
public void testBackendWriteRecordNullIdentifierThrowsNullPointerException() throws IOException, DataStoreException{
DataIdentifier identifier = null;
File testFile = folder.newFile();
copyInputStreamToFile(randomStream(0, 10), testFile);
try {
backend.write(identifier, testFile);
fail();
} catch (NullPointerException e) {
assertEquals("identifier", e.getMessage());
}
}
@Test
public void testBackendWriteRecordNullFileThrowsNullPointerException() throws DataStoreException {
File testFile = null;
DataIdentifier identifier = new DataIdentifier("fake");
try {
backend.write(identifier, testFile);
fail();
}
catch (NullPointerException e) {
assertTrue("file".equals(e.getMessage()));
}
}
@Test
public void testBackendWriteRecordFileNotFoundThrowsException() throws IOException, NoSuchAlgorithmException {
File testFile = folder.newFile();
copyInputStreamToFile(randomStream(0, 10), testFile);
DataIdentifier identifier = new DataIdentifier(getIdForInputStream(new FileInputStream(testFile)));
assertTrue(testFile.delete());
try {
backend.write(identifier, testFile);
fail();
} catch (DataStoreException e) {
assertTrue(e.getCause() instanceof FileNotFoundException);
}
}
// Read (Backend)
@Test
public void testBackendReadRecordNullIdentifier() throws DataStoreException {
DataIdentifier identifier = null;
try {
backend.read(identifier);
fail();
}
catch (NullPointerException e) {
assert("identifier".equals(e.getMessage()));
}
}
@Test
public void testBackendReadRecordInvalidIdentifier() {
DataIdentifier identifier = new DataIdentifier("fake");
try {
backend.read(identifier);
fail();
}
catch (DataStoreException e) { }
}
// Delete (Backend)
@Test
public void testBackendDeleteRecordNullIdentifier() throws DataStoreException {
DataIdentifier identifier = null;
try {
backend.deleteRecord(identifier);
fail();
}
catch (NullPointerException e) {
assert("identifier".equals(e.getMessage()));
}
}
@Test
public void testBackendDeleteRecordInvalidIdentifier() throws DataStoreException {
DataIdentifier identifier = new DataIdentifier("fake");
backend.deleteRecord(identifier); // We don't care if the identifier is invalid; this is a noop
}
// Exists (Backend)
@Test
public void testBackendNotCreatedRecordDoesNotExist() throws DataStoreException {
assertFalse(backend.exists(new DataIdentifier(("fake"))));
}
@Test
public void testBackendRecordExistsNullIdentifierThrowsNullPointerException() throws DataStoreException {
try {
DataIdentifier nullIdentifier = null;
backend.exists(nullIdentifier);
fail();
}
catch (NullPointerException e) { }
}
// GetAllIdentifiers (Backend)
@Test
public void testBackendGetAllIdentifiersNoRecordsReturnsNone() throws DataStoreException {
Iterator<DataIdentifier> allIdentifiers = backend.getAllIdentifiers();
assertFalse(allIdentifiers.hasNext());
}
@Test
public void testBackendGetAllIdentifiers() throws DataStoreException, IOException, NoSuchAlgorithmException {
for (int expectedRecCount : Lists.newArrayList(1, 2, 5)) {
final List<DataIdentifier> ids = Lists.newArrayList();
for (int i=0; i<expectedRecCount; i++) {
File testfile = folder.newFile();
copyInputStreamToFile(randomStream(i, 10), testfile);
DataIdentifier identifier = new DataIdentifier(getIdForInputStream(new FileInputStream(testfile)));
backend.write(identifier, testfile);
ids.add(identifier);
}
int actualRecCount = Iterators.size(backend.getAllIdentifiers());
for (DataIdentifier identifier : ids) {
backend.deleteRecord(identifier);
}
assertEquals(expectedRecCount, actualRecCount);
}
}
// GetRecord (Backend)
@Test
public void testBackendGetRecord() throws IOException, DataStoreException {
String recordData = "testData";
DataRecord record = ds.addRecord(new ByteArrayInputStream(recordData.getBytes()));
DataRecord retrievedRecord = backend.getRecord(record.getIdentifier());
validateRecord(record, recordData, retrievedRecord);
}
@Test
public void testBackendGetRecordNullIdentifierThrowsNullPointerException() throws DataStoreException {
try {
DataIdentifier identifier = null;
backend.getRecord(identifier);
fail();
}
catch (NullPointerException e) {
assertTrue("identifier".equals(e.getMessage()));
}
}
@Test
public void testBackendGetRecordInvalidIdentifierThrowsDataStoreException() {
try {
backend.getRecord(new DataIdentifier("invalid"));
fail();
}
catch (DataStoreException e) {
}
}
// GetAllRecords (Backend)
@Test
public void testBackendGetAllRecordsReturnsAll() throws DataStoreException, IOException {
for (int recCount : Lists.newArrayList(0, 1, 2, 5)) {
Map<DataIdentifier, String> addedRecords = Maps.newHashMap();
if (0 < recCount) {
for (int i = 0; i < recCount; i++) {
String data = String.format("testData%d", i);
DataRecord record = ds.addRecord(new ByteArrayInputStream(data.getBytes()));
addedRecords.put(record.getIdentifier(), data);
}
}
Iterator<DataRecord> iter = backend.getAllRecords();
List<DataIdentifier> identifiers = Lists.newArrayList();
int actualCount = 0;
while (iter.hasNext()) {
DataRecord record = iter.next();
identifiers.add(record.getIdentifier());
assertTrue(addedRecords.containsKey(record.getIdentifier()));
StringWriter writer = new StringWriter();
IOUtils.copy(record.getStream(), writer);
assertTrue(writer.toString().equals(addedRecords.get(record.getIdentifier())));
actualCount++;
}
for (DataIdentifier identifier : identifiers) {
ds.deleteRecord(identifier);
}
assertEquals(recCount, actualCount);
}
}
// AddMetadataRecord (Backend)
@Test
public void testBackendAddMetadataRecordsFromInputStream() throws DataStoreException, IOException, NoSuchAlgorithmException {
for (boolean fromInputStream : Lists.newArrayList(false, true)) {
String prefix = String.format("%s.META.", getClass().getSimpleName());
for (int count : Lists.newArrayList(1, 3)) {
Map<String, String> records = Maps.newHashMap();
for (int i = 0; i < count; i++) {
String recordName = String.format("%sname.%d", prefix, i);
String data = String.format("testData%d", i);
records.put(recordName, data);
if (fromInputStream) {
backend.addMetadataRecord(new ByteArrayInputStream(data.getBytes()), recordName);
}
else {
File testFile = folder.newFile();
copyInputStreamToFile(new ByteArrayInputStream(data.getBytes()), testFile);
backend.addMetadataRecord(testFile, recordName);
}
}
assertEquals(count, backend.getAllMetadataRecords(prefix).size());
for (Map.Entry<String, String> entry : records.entrySet()) {
DataRecord record = backend.getMetadataRecord(entry.getKey());
StringWriter writer = new StringWriter();
IOUtils.copy(record.getStream(), writer);
backend.deleteMetadataRecord(entry.getKey());
assertTrue(writer.toString().equals(entry.getValue()));
}
assertEquals(0, backend.getAllMetadataRecords(prefix).size());
}
}
}
@Test
public void testBackendAddMetadataRecordFileNotFoundThrowsDataStoreException() throws IOException {
File testFile = folder.newFile();
copyInputStreamToFile(randomStream(0, 10), testFile);
testFile.delete();
try {
backend.addMetadataRecord(testFile, "name");
fail();
}
catch (DataStoreException e) {
assertTrue(e.getCause() instanceof FileNotFoundException);
}
}
@Test
public void testBackendAddMetadataRecordNullInputStreamThrowsNullPointerException() throws DataStoreException {
try {
backend.addMetadataRecord((InputStream)null, "name");
fail();
}
catch (NullPointerException e) {
assertTrue("input".equals(e.getMessage()));
}
}
@Test
public void testBackendAddMetadataRecordNullFileThrowsNullPointerException() throws DataStoreException {
try {
backend.addMetadataRecord((File)null, "name");
fail();
}
catch (NullPointerException e) {
assertTrue("input".equals(e.getMessage()));
}
}
@Test
public void testBackendAddMetadataRecordNullEmptyNameThrowsIllegalArgumentException() throws DataStoreException, IOException {
final String data = "testData";
for (boolean fromInputStream : Lists.newArrayList(false, true)) {
for (String name : Lists.newArrayList(null, "")) {
try {
if (fromInputStream) {
backend.addMetadataRecord(new ByteArrayInputStream(data.getBytes()), name);
} else {
File testFile = folder.newFile();
copyInputStreamToFile(new ByteArrayInputStream(data.getBytes()), testFile);
backend.addMetadataRecord(testFile, name);
}
fail();
} catch (IllegalArgumentException e) {
assertTrue("name".equals(e.getMessage()));
}
}
}
}
// GetMetadataRecord (Backend)
@Test
public void testBackendGetMetadataRecordInvalidName() throws DataStoreException {
backend.addMetadataRecord(randomStream(0, 10), "testRecord");
assertNull(backend.getMetadataRecord("invalid"));
for (String name : Lists.newArrayList("", null)) {
try {
backend.getMetadataRecord(name);
fail("Expect to throw");
} catch(Exception e) {}
}
backend.deleteMetadataRecord("testRecord");
}
// GetAllMetadataRecords (Backend)
@Test
public void testBackendGetAllMetadataRecordsPrefixMatchesAll() throws DataStoreException {
assertEquals(0, backend.getAllMetadataRecords("").size());
String prefixAll = "prefix1";
String prefixSome = "prefix1.prefix2";
String prefixOne = "prefix1.prefix3";
String prefixNone = "prefix4";
backend.addMetadataRecord(randomStream(1, 10), String.format("%s.testRecord1", prefixAll));
backend.addMetadataRecord(randomStream(2, 10), String.format("%s.testRecord2", prefixSome));
backend.addMetadataRecord(randomStream(3, 10), String.format("%s.testRecord3", prefixSome));
backend.addMetadataRecord(randomStream(4, 10), String.format("%s.testRecord4", prefixOne));
backend.addMetadataRecord(randomStream(5, 10), "prefix5.testRecord5");
assertEquals(5, backend.getAllMetadataRecords("").size());
assertEquals(4, backend.getAllMetadataRecords(prefixAll).size());
assertEquals(2, backend.getAllMetadataRecords(prefixSome).size());
assertEquals(1, backend.getAllMetadataRecords(prefixOne).size());
assertEquals(0, backend.getAllMetadataRecords(prefixNone).size());
backend.deleteAllMetadataRecords("");
assertEquals(0, backend.getAllMetadataRecords("").size());
}
@Test
public void testBackendGetAllMetadataRecordsNullPrefixThrowsNullPointerException() {
try {
backend.getAllMetadataRecords(null);
fail();
}
catch (NullPointerException e) {
assertTrue("prefix".equals(e.getMessage()));
}
}
// DeleteMetadataRecord (Backend)
@Test
public void testBackendDeleteMetadataRecord() throws DataStoreException {
backend.addMetadataRecord(randomStream(0, 10), "name");
for (String name : Lists.newArrayList("invalid", "", null)) {
if (Strings.isNullOrEmpty(name)) {
try {
backend.deleteMetadataRecord(name);
}
catch (IllegalArgumentException e) { }
}
else {
assertFalse(backend.deleteMetadataRecord(name));
}
}
assertTrue(backend.deleteMetadataRecord("name"));
}
// MetadataRecordExists (Backend)
@Test
public void testBackendMetadataRecordExists() throws DataStoreException {
backend.addMetadataRecord(randomStream(0, 10), "name");
for (String name : Lists.newArrayList("invalid", "", null)) {
if (Strings.isNullOrEmpty(name)) {
try {
backend.metadataRecordExists(name);
}
catch (IllegalArgumentException e) { }
}
else {
assertFalse(backend.metadataRecordExists(name));
}
}
assertTrue(backend.metadataRecordExists("name"));
}
// DeleteAllMetadataRecords (Backend)
@Test
public void testBackendDeleteAllMetadataRecordsPrefixMatchesAll() throws DataStoreException {
String prefixAll = "prefix1";
String prefixSome = "prefix1.prefix2";
String prefixOne = "prefix1.prefix3";
String prefixNone = "prefix4";
Map<String, Integer> prefixCounts = Maps.newHashMap();
prefixCounts.put(prefixAll, 4);
prefixCounts.put(prefixSome, 2);
prefixCounts.put(prefixOne, 1);
prefixCounts.put(prefixNone, 0);
for (Map.Entry<String, Integer> entry : prefixCounts.entrySet()) {
backend.addMetadataRecord(randomStream(1, 10), String.format("%s.testRecord1", prefixAll));
backend.addMetadataRecord(randomStream(2, 10), String.format("%s.testRecord2", prefixSome));
backend.addMetadataRecord(randomStream(3, 10), String.format("%s.testRecord3", prefixSome));
backend.addMetadataRecord(randomStream(4, 10), String.format("%s.testRecord4", prefixOne));
int preCount = backend.getAllMetadataRecords("").size();
backend.deleteAllMetadataRecords(entry.getKey());
int deletedCount = preCount - backend.getAllMetadataRecords("").size();
assertEquals(entry.getValue().intValue(), deletedCount);
backend.deleteAllMetadataRecords("");
}
}
@Test
public void testBackendDeleteAllMetadataRecordsNoRecordsNoChange() {
assertEquals(0, backend.getAllMetadataRecords("").size());
backend.deleteAllMetadataRecords("");
assertEquals(0, backend.getAllMetadataRecords("").size());
}
@Test
public void testBackendDeleteAllMetadataRecordsNullPrefixThrowsNullPointerException() {
try {
backend.deleteAllMetadataRecords(null);
fail();
}
catch (NullPointerException e) {
assertTrue("prefix".equals(e.getMessage()));
}
}
@Test
public void testSecret() throws Exception {
byte[] data = new byte[4096];
randomGen.nextBytes(data);
DataRecord rec = ds.addRecord(new ByteArrayInputStream(data));
assertEquals(data.length, rec.getLength());
String ref = rec.getReference();
String id = rec.getIdentifier().toString();
assertNotNull(ref);
byte[] refKey = backend.getOrCreateReferenceKey();
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec(refKey, "HmacSHA1"));
byte[] hash = mac.doFinal(id.getBytes("UTF-8"));
String calcRef = id + ':' + encodeHexString(hash);
assertEquals("getReference() not equal", calcRef, ref);
DataRecord refRec = ds.getMetadataRecord("reference.key");
assertNotNull("Reference data record null", refRec);
byte[] refDirectFromBackend = IOUtils.toByteArray(refRec.getStream());
LOG.warn("Ref direct from backend {}", refDirectFromBackend);
assertTrue("refKey in memory not equal to the metadata record",
Arrays.equals(refKey, refDirectFromBackend));
}
}