diff --git a/.gitignore b/.gitignore
index ae7ce98..8fce833 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,9 @@ s3proxy.iml
.project
.settings
+# MAC stuff
+.DS_Store
+
# below is default github .ignore for java
*.class
# Mobile Tools for Java (J2ME)
diff --git a/pom.xml b/pom.xml
index af50a47..42ccac7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -344,7 +344,8 @@
junit
junit
4.12
- test
+
+ provided
com.fasterxml.jackson.dataformat
diff --git a/src/main/java/org/gaul/s3proxy/junit/S3ProxyRule.java b/src/main/java/org/gaul/s3proxy/junit/S3ProxyRule.java
new file mode 100644
index 0000000..9523454
--- /dev/null
+++ b/src/main/java/org/gaul/s3proxy/junit/S3ProxyRule.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2014-2018 Andrew Gaul
+ *
+ * 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.gaul.s3proxy.junit;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.Files;
+import java.util.Properties;
+
+import com.google.common.annotations.Beta;
+
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.gaul.s3proxy.AuthenticationType;
+import org.gaul.s3proxy.S3Proxy;
+import org.jclouds.ContextBuilder;
+import org.jclouds.blobstore.BlobStore;
+import org.jclouds.blobstore.BlobStoreContext;
+import org.jclouds.blobstore.domain.StorageMetadata;
+import org.junit.rules.ExternalResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A JUnit Rule that manages an S3Proxy instance which tests can use as an S3
+ * API endpoint.
+ */
+@Beta
+public final class S3ProxyRule extends ExternalResource {
+ private static final Logger logger = LoggerFactory.getLogger(
+ S3ProxyRule.class);
+
+ private static final String LOCALHOST = "127.0.0.1";
+
+ private final String accessKey;
+ private final String secretKey;
+ private final String endpointFormat;
+ private final S3Proxy s3Proxy;
+
+ private BlobStoreContext blobStoreContext;
+ private URI endpointUri;
+ private File blobStoreLocation;
+
+ public static final class Builder {
+ private AuthenticationType authType = AuthenticationType.NONE;
+ private String accessKey;
+ private String secretKey;
+ private String secretStorePath;
+ private String secretStorePassword;
+ private int port = -1;
+ private boolean ignoreUnknownHeaders;
+ private String blobStoreProvider = "filesystem";
+
+ private Builder() { }
+
+ public Builder withCredentials(AuthenticationType authType,
+ String accessKey, String secretKey) {
+ this.authType = authType;
+ this.accessKey = accessKey;
+ this.secretKey = secretKey;
+ return this;
+ }
+
+ public Builder withCredentials(String accessKey, String secretKey) {
+ return withCredentials(AuthenticationType.AWS_V2_OR_V4, accessKey,
+ secretKey);
+ }
+
+ public Builder withSecretStore(String path, String password) {
+ secretStorePath = path;
+ secretStorePassword = password;
+ return this;
+ }
+
+ public Builder withPort(int port) {
+ this.port = port;
+ return this;
+ }
+
+ public Builder withBlobStoreProvider(String blobStoreProvider) {
+ this.blobStoreProvider = blobStoreProvider;
+ return this;
+ }
+
+ public Builder ignoreUnknownHeaders() {
+ ignoreUnknownHeaders = true;
+ return this;
+ }
+
+ public S3ProxyRule build() {
+ return new S3ProxyRule(this);
+ }
+ }
+
+ private S3ProxyRule(Builder builder) {
+ accessKey = builder.accessKey;
+ secretKey = builder.secretKey;
+
+ Properties properties = new Properties();
+ try {
+ blobStoreLocation = Files.createTempDirectory("S3ProxyRule")
+ .toFile();
+ properties.setProperty("jclouds.filesystem.basedir",
+ blobStoreLocation.getCanonicalPath());
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to initialize Blob Store", e);
+ }
+
+ blobStoreContext = ContextBuilder.newBuilder(
+ builder.blobStoreProvider)
+ .credentials(accessKey, secretKey)
+ .overrides(properties).build(BlobStoreContext.class);
+
+ S3Proxy.Builder s3ProxyBuilder = S3Proxy.builder()
+ .blobStore(blobStoreContext.getBlobStore())
+ .awsAuthentication(builder.authType, accessKey, secretKey)
+ .ignoreUnknownHeaders(builder.ignoreUnknownHeaders);
+
+ if (builder.secretStorePath != null ||
+ builder.secretStorePassword != null) {
+ s3ProxyBuilder.keyStore(builder.secretStorePath,
+ builder.secretStorePassword);
+ }
+
+ int port = builder.port < 0 ? 0 : builder.port;
+ endpointFormat = "http://%s:%d";
+ String endpoint = String.format(endpointFormat, LOCALHOST, port);
+ s3ProxyBuilder.endpoint(URI.create(endpoint));
+
+ s3Proxy = s3ProxyBuilder.build();
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Override
+ protected void before() throws Throwable {
+ logger.debug("S3 proxy is starting");
+ s3Proxy.start();
+ while (!s3Proxy.getState().equals(AbstractLifeCycle.STARTED)) {
+ Thread.sleep(10);
+ }
+ endpointUri = URI.create(String.format(endpointFormat, LOCALHOST,
+ s3Proxy.getPort()));
+ logger.debug("S3 proxy is running");
+ }
+
+ @Override
+ protected void after() {
+ logger.debug("S3 proxy is stopping");
+ try {
+ s3Proxy.stop();
+ BlobStore blobStore = blobStoreContext.getBlobStore();
+ for (StorageMetadata metadata : blobStore.list()) {
+ blobStore.deleteContainer(metadata.getName());
+ }
+ blobStoreContext.close();
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to stop S3 proxy", e);
+ }
+ FileUtils.deleteQuietly(blobStoreLocation);
+ logger.debug("S3 proxy has stopped");
+ }
+
+ public URI getUri() {
+ return endpointUri;
+ }
+
+ public String getAccessKey() {
+ return accessKey;
+ }
+
+ public String getSecretKey() {
+ return secretKey;
+ }
+
+}
diff --git a/src/test/java/org/gaul/s3proxy/junit/S3ProxyRuleTest.java b/src/test/java/org/gaul/s3proxy/junit/S3ProxyRuleTest.java
new file mode 100644
index 0000000..0ed177d
--- /dev/null
+++ b/src/test/java/org/gaul/s3proxy/junit/S3ProxyRuleTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2014-2018 Andrew Gaul
+ *
+ * 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.gaul.s3proxy.junit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration;
+import com.amazonaws.regions.Regions;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3ClientBuilder;
+import com.amazonaws.services.s3.model.Bucket;
+import com.amazonaws.services.s3.model.S3ObjectSummary;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * This is an example of how one would use the S3Proxy JUnit rule in a unit
+ * test as opposed to a proper test of the S3ProxyRule class.
+ */
+public class S3ProxyRuleTest {
+
+ private static final String MY_TEST_BUCKET = "my-test-bucket";
+
+ public @Rule TemporaryFolder temporaryFolder = new TemporaryFolder();
+ public @Rule S3ProxyRule s3Proxy = S3ProxyRule
+ .builder()
+ .withCredentials("access", "secret")
+ .build();
+
+ private AmazonS3 s3Client;
+
+ @Before
+ public final void setUp() throws Exception {
+ s3Client = AmazonS3ClientBuilder
+ .standard()
+ .withCredentials(
+ new AWSStaticCredentialsProvider(
+ new BasicAWSCredentials(
+ s3Proxy.getAccessKey(), s3Proxy.getSecretKey())))
+ .withEndpointConfiguration(
+ new EndpointConfiguration(
+ s3Proxy.getUri().toString(), Regions.US_EAST_1.getName()))
+ .build();
+
+ s3Client.createBucket(MY_TEST_BUCKET);
+ }
+
+ @Test
+ public final void listBucket() {
+ List buckets = s3Client.listBuckets();
+ assertThat(buckets).hasSize(1);
+ assertThat(buckets.get(0).getName())
+ .isEqualTo(MY_TEST_BUCKET);
+ }
+
+ @Test
+ public final void uploadFile() throws Exception {
+ String testInput = "content";
+ s3Client.putObject(MY_TEST_BUCKET, "file.txt", testInput);
+
+ List summaries = s3Client
+ .listObjects(MY_TEST_BUCKET)
+ .getObjectSummaries();
+ assertThat(summaries).hasSize(1);
+ assertThat(summaries.get(0).getKey()).isEqualTo("file.txt");
+ assertThat(summaries.get(0).getSize()).isEqualTo(testInput.length());
+ }
+
+}