Updated Documents Helper

This commit is contained in:
EstebanSP 2020-09-28 11:27:49 -06:00 committed by GitHub
parent b1d990cdf7
commit 42744a1b38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 2660 additions and 0 deletions

View File

@ -0,0 +1,22 @@
.idea
*.class
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.ear
# exclude jar for gradle wrapper
!gradle/wrapper/*.jar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# build files
**/target
target
.gradle
build

View File

@ -0,0 +1,207 @@
# Selling Partner API Documents Helper
This library provides helper classes that you can use to work with encrypted documents. Specifically, they help with:
* Encrypting and uploading a document
* Downloading an encrypted document and reading its decrypted contents
Note: Its the developers responsibility to always maintain encryption at rest. Unencrypted document content should never be stored on disk, even temporarily, because documents can contain sensitive information.
The helper classes provided in this library are built to assist with this.
## Example usage
### Upload
```java
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.nio.charset.StandardCharsets;
import com.amazon.spapi.documents.UploadHelper;
import com.amazon.spapi.documents.UploadSpecification;
import com.amazon.spapi.documents.exception.CryptoException;
import com.amazon.spapi.documents.exception.HttpResponseException;
import com.amazon.spapi.documents.impl.AESCryptoStreamFactory;
/* We want to maintain encryption at rest, so do not write unencrypted data to disk. This is bad:
InputStream source = new FileInputStream(new File("/path/to/data.txt"));
Instead, if your data can fit in memory, you can create an InputStream from a String (see encryptAndUpload_fromString()).
Otherwise, you can pipe data into an InputStream using Piped streams (see encryptAndUpload_fromPipedInputStream()).
*/
public class UploadExample {
private final UploadHelper uploadHelper = new UploadHelper.Builder().build();
public void encryptAndUpload_fromString(String key, String initializationVector, String url) {
AESCryptoStreamFactory aesCryptoStreamFactory =
new AESCryptoStreamFactory.Builder(key, initializationVector)
.build();
String contentType = String.format("text/plain; charset=%s", StandardCharsets.UTF_8);
// The character set must be the same one that is specified in contentType.
try (InputStream source = new ByteArrayInputStream("my document contents".getBytes(StandardCharsets.UTF_8))) {
UploadSpecification uploadSpec =
new UploadSpecification.Builder(contentType, aesCryptoStreamFactory, source, url)
.build();
uploadHelper.upload(uploadSpec);
} catch (CryptoException | HttpResponseException | IOException e) {
// Handle exception.
}
}
public void encryptAndUpload_fromPipedInputStream(String key, String initializationVector, String url) {
AESCryptoStreamFactory aesCryptoStreamFactory =
new AESCryptoStreamFactory.Builder(key, initializationVector)
.build();
String contentType = String.format("text/plain; charset=%s", StandardCharsets.UTF_8);
try (PipedInputStream source = new PipedInputStream()) {
new Thread (
new Runnable() {
public void run() {
try (PipedOutputStream documentContents = new PipedOutputStream(source)) {
// The character set must be the same one that is specified in contentType.
documentContents.write("my document contents\n".getBytes(StandardCharsets.UTF_8));
documentContents.write("more document contents".getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
// Handle exception.
}
}
}
).start();
UploadSpecification uploadSpec =
new UploadSpecification.Builder(contentType, aesCryptoStreamFactory, source, url)
.build();
uploadHelper.upload(uploadSpec);
} catch (CryptoException | HttpResponseException | IOException e) {
// Handle exception.
}
}
}
```
### Download
```java
import java.io.BufferedReader;
import java.io.IOException;
import com.amazon.spapi.documents.CompressionAlgorithm;
import com.amazon.spapi.documents.DownloadBundle;
import com.amazon.spapi.documents.DownloadHelper;
import com.amazon.spapi.documents.DownloadSpecification;
import com.amazon.spapi.documents.exception.CryptoException;
import com.amazon.spapi.documents.exception.HttpResponseException;
import com.amazon.spapi.documents.exception.MissingCharsetException;
import com.amazon.spapi.documents.impl.AESCryptoStreamFactory;
public class DownloadExample {
final DownloadHelper downloadHelper = new DownloadHelper.Builder().build();
public void downloadAndDecrypt(String key, String initializationVector, String url, String compressionAlgorithm) {
AESCryptoStreamFactory aesCryptoStreamFactory =
new AESCryptoStreamFactory.Builder(key, initializationVector).build();
DownloadSpecification downloadSpec = new DownloadSpecification.Builder(aesCryptoStreamFactory, url)
.withCompressionAlgorithm(CompressionAlgorithm.fromEquivalent(compressionAlgorithm))
.build();
try (DownloadBundle downloadBundle = downloadHelper.download(downloadSpec)) {
// This example assumes that the downloaded document has a charset in the content type, e.g.
// text/plain; charset=UTF-8
try (BufferedReader reader = downloadBundle.newBufferedReader()) {
String line;
do {
line = reader.readLine();
// Process the decrypted line.
} while (line != null);
}
} catch (CryptoException | HttpResponseException | IOException | MissingCharsetException e) {
// Handle exception here.
}
}
}
```
## Requirements
Building the Selling Partner API Documents Helper requires:
1. Java 1.8+
2. Maven/Gradle
## Installation
To install the Selling Partner API Documents Helper to your local Maven repository, simply execute:
```shell
mvn clean install
```
To deploy it to a remote Maven repository instead, configure the settings of the repository and execute:
```shell
mvn clean deploy
```
Refer to the [OSSRH Guide](http://central.sonatype.org/pages/ossrh-guide.html) for more information.
### Maven users
Add this dependency to your project's POM:
```xml
<dependency>
<groupId>com.amazon.sellingpartnerapi</groupId>
<artifactId>sellingpartner-api-documents-helper-java</artifactId>
<version>1.0.0</version>
</dependency>
```
### Gradle users
Add this dependency to your project's build file:
```groovy
implementation "com.amazon.sellingpartnerapi:sellingpartner-api-documents-helper-java:1.0.0"
```
### Others
At first generate the JAR by executing:
```shell
mvn clean package
```
Then manually install the following JARs:
* `target/sellingpartner-api-documents-helper-java.jar`
* `target/lib/*.jar`
## License
Swagger Codegen templates are subject to the [Swagger Codegen License](https://github.com/swagger-api/swagger-codegen#license).
All other work licensed as follows:
Copyright Amazon.com Inc. or its affiliates.
All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this library 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.

View File

@ -0,0 +1,73 @@
plugins {
id 'idea'
id 'eclipse'
id 'java'
}
group = 'com.amazon.sellingpartnerapi'
version = '1.0.0'
repositories {
jcenter()
mavenCentral()
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
/*sourceSets {
main {
java {
srcDirs = ['src/main/java']
}
resources {
srcDirs = ['src/main/resources']
}
}
test {
java {
srcDirs = ['src/test/java']
}
resources {
srcDirs = ['src/test/resources']
}
}
}*/
task execute(type: JavaExec) {
main = System.getProperty('mainClass')
classpath = sourceSets.main.runtimeClasspath
}
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
}
}
dependencies {
testImplementation(platform('org.junit:junit-bom:5.7.0'))
testImplementation('org.junit.jupiter:junit-jupiter')
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.7.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.0")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.7.0")
implementation 'com.squareup.okhttp:okhttp:2.7.5'
implementation 'com.google.guava:guava:28.2-jre'
// https://mvnrepository.com/artifact/org.threeten/threetenbp
implementation group: 'org.threeten', name: 'threetenbp', version: '1.3.5'
// https://mvnrepository.com/artifact/junit/junit
implementation 'org.junit.jupiter:junit-jupiter-migrationsupport:5.5.1'
implementation 'org.mockito:mockito-core:3.0.0'
implementation 'org.mockito:mockito-inline:3.0.0'
implementation 'org.apache.directory.studio:org.apache.commons.io:2.4'
}

View File

@ -0,0 +1,6 @@
#Wed Sep 23 13:37:56 CST 2020
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.amazon.sellingpartnerapi</groupId>
<artifactId>sellingpartner-api-documents-helper-java</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp</artifactId>
<version>2.7.5</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>logging-interceptor</artifactId>
<version>2.7.5</version>
</dependency>
<dependency>
<groupId>io.gsonfire</groupId>
<artifactId>gson-fire</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.io</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
<dependency>
<groupId>org.threeten</groupId>
<artifactId>threetenbp</artifactId>
<version>1.3.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.platform/junit-platform-commons -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-commons</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.3.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-migrationsupport</artifactId>
<version>5.5.1</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven-plugin-version>1.0.0</maven-plugin-version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

@ -0,0 +1 @@
rootProject.name = "sellingpartner-api-documents-helper-java"

View File

@ -0,0 +1,36 @@
package com.amazon.spapi.documents;
/**
* The compression algorithm.
*/
public enum CompressionAlgorithm {
GZIP;
/**
* Convert from any equivalent enum value. If the specified enum value is null, return null.
*
* @param val The equivalent enum value to convert
* @return This enum's equivalent to the specified enum value
*/
public static <T extends Enum> CompressionAlgorithm fromEquivalent(T val) {
if (val != null) {
return CompressionAlgorithm.valueOf(val.toString());
}
return null;
}
/**
* Convert from a string. If the specified string is null, return null.
*
* @param val The value to convert.
* @return This enum's equivalent to the specified string
*/
public static CompressionAlgorithm fromEquivalent(String val) {
if (val != null) {
return CompressionAlgorithm.valueOf(val);
}
return null;
}
}

View File

@ -0,0 +1,29 @@
package com.amazon.spapi.documents;
import com.amazon.spapi.documents.exception.CryptoException;
import java.io.InputStream;
/**
* Crypto stream factory interface.
*/
public interface CryptoStreamFactory {
/**
* Create a new {@link InputStream} that decrypts a stream of encrypted data.
*
* @param source The source for encrypted data to decrypt
* @return A new {@link InputStream} from which decrypted data can be read
* @throws CryptoException Crypto exception
*/
InputStream newDecryptStream(InputStream source) throws CryptoException;
/**
* Create a new {@link InputStream} that encrypts a stream of unencrypted data.
*
* @param source The source for unencrypted data to encrypt
* @return A new {@link InputStream} from which encrypted data can be read
* @throws CryptoException Crypto exception
*/
InputStream newEncryptStream(InputStream source) throws CryptoException;
}

View File

@ -0,0 +1,153 @@
package com.amazon.spapi.documents;
import com.amazon.spapi.documents.exception.CryptoException;
import com.amazon.spapi.documents.exception.MissingCharsetException;
import com.squareup.okhttp.MediaType;
import org.apache.commons.io.IOUtils;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.zip.GZIPInputStream;
/**
* Helper that contains and provides access to the downloaded contents of an encrypted document. {@link #close()} must
* be called to delete the temporary file that contains the encrypted document contents.
*
* Multiple independent streams and readers (from which unencrypted data can be read while maintaining encryption at
* rest on the filesystem) can be opened from an instance of {@link DownloadBundle}, but once {@link #close()} is
* called the behavior of any open streams or readers from this instance will be unspecified as the underlying
* temporary file will be deleted.
*/
public class DownloadBundle implements AutoCloseable {
private final CompressionAlgorithm compressionAlgorithm;
private final String contentType;
private final CryptoStreamFactory cryptoStreamFactory;
private final File document;
DownloadBundle(CompressionAlgorithm compressionAlgorithm, String contentType,
CryptoStreamFactory cryptoStreamFactory, File document) {
this.compressionAlgorithm = compressionAlgorithm;
this.contentType = contentType;
this.cryptoStreamFactory = cryptoStreamFactory;
this.document = document;
}
/**
* The content type of the unencrypted document contents.
*
* @return The content type
*/
public String getContentType() {
return contentType;
}
/**
* Open an {@link InputStream} that allows the caller to read the decompressed (if applicable) and decrypted
* contents of the downloaded document. It is the responsibility of the caller to close the returned
* {@link InputStream}. The {@link InputStream}'s operation will be unspecified once this {@link DownloadBundle} is
* closed.
*
* @return An {@link InputStream} that decompresses and decrypts the document's contents
* @throws CryptoException Crypto exception
* @throws IOException IO exception
*/
public InputStream newInputStream() throws CryptoException, IOException {
Closeable closeThis = null;
try {
InputStream inputStream = new FileInputStream(document);
closeThis = inputStream;
inputStream = cryptoStreamFactory.newDecryptStream(inputStream);
closeThis = inputStream;
if (compressionAlgorithm != null) {
switch (compressionAlgorithm) {
case GZIP:
inputStream = new GZIPInputStream(inputStream);
closeThis = inputStream;
}
}
closeThis = null;
return inputStream;
} finally {
IOUtils.closeQuietly(closeThis);
}
}
/**
* Open a {@link BufferedReader} that allows the caller to read the decompressed (if applicable) and decrypted
* contents of the downloaded document. The character set is parsed from {@link #getContentType()}. If the character
* set could not be parsed, this method will fail with {@link MissingCharsetException}.
*
* It is the responsibility of the caller to close the returned {@link BufferedReader}. This {@link BufferedReader}
* will become invalid once this {@link DownloadBundle} is closed.
*
* @return A {@link BufferedReader} that decompresses and decrypts the document's contents
* @throws CryptoException Crypto exception
* @throws IOException IO exception
* @throws MissingCharsetException The character set could not be parsed from {@link #getContentType()}
*/
public BufferedReader newBufferedReader() throws CryptoException, IOException, MissingCharsetException {
return newBufferedReader(null);
}
/**
* Open a {@link BufferedReader} that allows the caller to read the decompressed (if applicable) and decrypted
* contents of the downloaded document. The character set is parsed from {@link #getContentType()}. If the character
* set could not be parsed and <code>defaultCharset</code> is specified, this method will attempt to open the reader
* with <code>defaultCharset</code>. Otherwise, this method will fail with {@link MissingCharsetException}.
*
* It is the responsibility of the caller to close the returned {@link BufferedReader}. This {@link BufferedReader}
* will become invalid once this {@link DownloadBundle} is closed.
*
* @param defaultCharset The default charset to use if a charset cannot be parsed from the content type.
* @return A {@link BufferedReader} that decompresses and decrypts the document's contents
* @throws CryptoException Crypto exception
* @throws IOException IO exception
* @throws MissingCharsetException The character set could not be parsed from {@link #getContentType()} and
* <code>defaultCharset</code> was not specified.
*/
public BufferedReader newBufferedReader(Charset defaultCharset) throws CryptoException, IOException,
MissingCharsetException {
String contentType = getContentType();
Charset charset = MediaType.parse(contentType).charset();
if (charset == null) {
charset = defaultCharset;
}
if (charset == null) {
throw new MissingCharsetException(String.format(
"Could not parse character set from content type '%s' and no default provided", contentType));
}
Closeable closeThis = null;
try {
InputStream inputStream = newInputStream();
closeThis = inputStream;
InputStreamReader reader = new InputStreamReader(inputStream, charset);
closeThis = reader;
BufferedReader bufferedReader = new BufferedReader(reader);
closeThis = null;
return bufferedReader;
} finally {
IOUtils.closeQuietly(closeThis);
}
}
/**
* Closes this {@link DownloadBundle}, deleting the temporary file containing the encrypted document contents.
*/
public void close() {
document.delete();
}
}

View File

@ -0,0 +1,128 @@
package com.amazon.spapi.documents;
import com.amazon.spapi.documents.exception.HttpResponseException;
import com.amazon.spapi.documents.impl.OkHttpTransferClient;
import java.io.File;
import java.io.IOException;
/**
* Helper for downloading encrypted documents.
*/
public class DownloadHelper {
private final HttpTransferClient httpTransferClient;
private final String tmpFilePrefix;
private final String tmpFileSuffix;
private final File tmpFileDirectory;
private DownloadHelper(HttpTransferClient httpTransferClient, String tmpFilePrefix, String tmpFileSuffix,
File tmpFileDirectory) {
this.httpTransferClient = httpTransferClient;
this.tmpFilePrefix = tmpFilePrefix;
this.tmpFileSuffix = tmpFileSuffix;
this.tmpFileDirectory = tmpFileDirectory;
}
/**
* Download the specified document's encrypted contents to a temporary file on disk. It is the responsibility of the
* caller to call <code>close</code> on the returned {@link AutoCloseable} {@link DownloadBundle}.
*
* Common reasons for receiving a 403 response include:
* <li> The signed URL has expired
*
* @param spec The specification for the download
* @return The closeable {@link DownloadBundle}
* @throws HttpResponseException On failure HTTP response
* @throws IOException IO Exception
*/
public DownloadBundle download(DownloadSpecification spec) throws HttpResponseException, IOException {
File tmpFile = File.createTempFile(tmpFilePrefix, tmpFileSuffix, tmpFileDirectory);
try {
tmpFile.deleteOnExit();
String contentType = httpTransferClient.download(spec.getUrl(), tmpFile);
return new DownloadBundle(
spec.getCompressionAlgorithm(), contentType, spec.getCryptoStreamFactory(), tmpFile);
} catch (Exception e) {
tmpFile.delete();
throw e;
}
}
/**
* Use this to create an instance of a {@link DownloadHelper}.
*/
public static class Builder {
private HttpTransferClient httpTransferClient = null;
private String tmpFilePrefix = "SPAPI";
private String tmpFileSuffix = null;
private File tmpFileDirectory = null;
/**
* The HTTP transfer client.
*
* @param httpTransferClient The HTTP transfer client
* @return this
*/
public Builder withHttpTransferClient(HttpTransferClient httpTransferClient) {
this.httpTransferClient = httpTransferClient;
return this;
}
/**
* The tmp file prefix. If not specified, defaults to <code>"SPAPI"</code>.
*
* @param tmpFilePrefix The prefix string to be used in generating the tmp file's name; must be at least three
* characters long
* @return this
*/
public Builder withTmpFilePrefix(String tmpFilePrefix) {
if (tmpFilePrefix.length() < 3) {
throw new IllegalArgumentException("Prefix string too short");
}
this.tmpFilePrefix = tmpFilePrefix;
return this;
}
/**
* The tmp file suffix. If not specified, defaults to null.
*
* @param tmpFileSuffix The suffix string to be used in generating the file's name; may be <code>null</code>, in
* which case the suffix <code>".tmp"</code> will be used
* @return this
*/
public Builder withTmpFileSuffix(String tmpFileSuffix) {
this.tmpFileSuffix = tmpFileSuffix;
return this;
}
/**
* The tmp file directory. If not specified, defaults to null.
*
* @param tmpFileDirectory The directory in which the file is to be created, or <code>null</code> if the default
* temporary-file directory is to be used
* @return this
*/
public Builder withTmpFileDirectory(File tmpFileDirectory) {
this.tmpFileDirectory = tmpFileDirectory;
return this;
}
/**
* Create the helper.
*
* @return The helper
*/
public DownloadHelper build() {
if (httpTransferClient == null) {
httpTransferClient = new OkHttpTransferClient.Builder().build();
}
return new DownloadHelper(httpTransferClient, tmpFilePrefix, tmpFileSuffix, tmpFileDirectory);
}
}
}

View File

@ -0,0 +1,76 @@
package com.amazon.spapi.documents;
import com.google.common.base.Preconditions;
/**
* Specification for {@link DownloadHelper#download(DownloadSpecification)}.
*/
public class DownloadSpecification {
private final CompressionAlgorithm compressionAlgorithm;
private final CryptoStreamFactory cryptoStreamFactory;
private final String url;
private DownloadSpecification(CompressionAlgorithm compressionAlgorithm, CryptoStreamFactory cryptoStreamFactory,
String url) {
this.compressionAlgorithm = compressionAlgorithm;
this.cryptoStreamFactory = cryptoStreamFactory;
this.url = url;
}
CompressionAlgorithm getCompressionAlgorithm() {
return compressionAlgorithm;
}
CryptoStreamFactory getCryptoStreamFactory() {
return cryptoStreamFactory;
}
String getUrl() {
return url;
}
/**
* Use this to create an instance of a {@link DownloadSpecification}.
*/
public static class Builder {
private final CryptoStreamFactory cryptoStreamFactory;
private final String url;
private CompressionAlgorithm compressionAlgorithm = null;
/**
* Create the builder.
*
* @param cryptoStreamFactory The crypto stream factory
* @param url The url to download the encrypted document from
*/
public Builder(CryptoStreamFactory cryptoStreamFactory, String url) {
Preconditions.checkArgument(cryptoStreamFactory != null, "cryptoStreamFactory is required");
Preconditions.checkArgument(url != null, "url is required");
this.cryptoStreamFactory = cryptoStreamFactory;
this.url = url;
}
/**
* The compression algorithm.
*
* @param compressionAlgorithm The compression algorithm
* @return this
*/
public Builder withCompressionAlgorithm(CompressionAlgorithm compressionAlgorithm) {
this.compressionAlgorithm = compressionAlgorithm;
return this;
}
/**
* Create the specification.
*
* @return The specification
*/
public DownloadSpecification build() {
return new DownloadSpecification(compressionAlgorithm, cryptoStreamFactory, url);
}
}
}

View File

@ -0,0 +1,34 @@
package com.amazon.spapi.documents;
import com.amazon.spapi.documents.exception.HttpResponseException;
import java.io.File;
import java.io.IOException;
/**
* HTTP transfer client. Implementations of this interface must be thread-safe and reusable for multiple requests.
*/
public interface HttpTransferClient {
/**
* Perform an HTTP GET on the specified <code>url</code>, storing the response body to <code>destination</code>.
*
* @param url The url to perform an HTTP GET on
* @param destination The file to write the HTTP GET response body to
* @return The Content-Type header value extracted from the response to the HTTP GET
* @throws HttpResponseException On failure HTTP response
* @throws IOException IO exception
*/
String download(String url, File destination) throws HttpResponseException, IOException;
/**
* Perform an HTTP PUT on the specified <code>url</code>, uploading the contents of <code>source</code> to the body
* of the request.
*
* @param url The url to perform an HTTP PUT on
* @param contentType The Content-Type header to be used for the HTTP PUT request
* @param source The file to read the HTTP PUT body from
* @throws HttpResponseException On failure HTTP response
* @throws IOException IO exception
*/
void upload(String url, String contentType, File source) throws HttpResponseException, IOException;
}

View File

@ -0,0 +1,131 @@
package com.amazon.spapi.documents;
import com.amazon.spapi.documents.exception.CryptoException;
import com.amazon.spapi.documents.exception.HttpResponseException;
import com.amazon.spapi.documents.impl.OkHttpTransferClient;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
/**
* Helper class for encrypting and uploading documents.
*/
public class UploadHelper {
private final HttpTransferClient httpTransferClient;
private final String tmpFilePrefix;
private final String tmpFileSuffix;
private final File tmpFileDirectory;
private UploadHelper(HttpTransferClient httpTransferClient, String tmpFilePrefix, String tmpFileSuffix,
File tmpFileDirectory) {
this.httpTransferClient = httpTransferClient;
this.tmpFilePrefix = tmpFilePrefix;
this.tmpFileSuffix = tmpFileSuffix;
this.tmpFileDirectory = tmpFileDirectory;
}
/**
* Perform the specified upload. This method will buffer the encrypted contents of the document in a temporary file
* before uploading to the specified url.
*
* Common reasons for receiving a 403 response include:
* <li> The signed URL has expired
* <li> {@link UploadSpecification#getContentType()} does not match the content type the URL was signed with
*
* @param spec The specification for the upload
* @throws CryptoException Crypto exception
* @throws HttpResponseException On failure HTTP response
* @throws IOException IO exception
*/
public void upload(UploadSpecification spec) throws CryptoException, HttpResponseException, IOException {
File tmpFile = File.createTempFile(tmpFilePrefix, tmpFileSuffix, tmpFileDirectory);
try {
tmpFile.deleteOnExit();
try (InputStream inputStream = spec.getCryptoStreamFactory().newEncryptStream(spec.getSource())) {
FileUtils.copyInputStreamToFile(inputStream, tmpFile);
}
httpTransferClient.upload(spec.getUrl(), spec.getContentType(), tmpFile);
} finally {
tmpFile.delete();
}
}
/**
* Use this to create an instance of an {@link UploadHelper}.
*/
public static class Builder {
private HttpTransferClient httpTransferClient = null;
private String tmpFilePrefix = "SPAPI";
private String tmpFileSuffix = null;
private File tmpFileDirectory = null;
/**
* The HTTP transfer client.
*
* @param httpTransferClient The HTTP transfer client.
* @return this
*/
public Builder withHttpTransferClient(HttpTransferClient httpTransferClient) {
this.httpTransferClient = httpTransferClient;
return this;
}
/**
* The tmp file prefix. If not specified, defaults to <code>"SPAPI"</code>.
*
* @param tmpFilePrefix The prefix string to be used in generating the tmp file's name; must be at least three
* characters long
* @return this
*/
public Builder withTmpFilePrefix(String tmpFilePrefix) {
if (tmpFilePrefix.length() < 3) {
throw new IllegalArgumentException("Prefix string too short");
}
this.tmpFilePrefix = tmpFilePrefix;
return this;
}
/**
* The tmp file suffix. If not specified, defaults to null.
*
* @param tmpFileSuffix The suffix string to be used in generating the file's name; may be <code>null</code>, in
* which case the suffix <code>".tmp"</code> will be used
* @return this
*/
public Builder withTmpFileSuffix(String tmpFileSuffix) {
this.tmpFileSuffix = tmpFileSuffix;
return this;
}
/**
* The tmp file directory. If not specified, defaults to null.
*
* @param tmpFileDirectory The directory in which the file is to be created, or <code>null</code> if the default
* temporary-file directory is to be used
* @return this
*/
public Builder withTmpFileDirectory(File tmpFileDirectory) {
this.tmpFileDirectory = tmpFileDirectory;
return this;
}
/**
* Create the helper.
*
* @return The helper.
*/
public UploadHelper build() {
if (httpTransferClient == null) {
httpTransferClient = new OkHttpTransferClient.Builder().build();
}
return new UploadHelper(httpTransferClient, tmpFilePrefix, tmpFileSuffix, tmpFileDirectory);
}
}
}

View File

@ -0,0 +1,79 @@
package com.amazon.spapi.documents;
import com.google.common.base.Preconditions;
import java.io.InputStream;
/**
* Specification for {@link UploadHelper#upload(UploadSpecification)}.
*/
public class UploadSpecification {
private final String contentType;
private final CryptoStreamFactory cryptoStreamFactory;
private final InputStream source;
private final String url;
private UploadSpecification(String contentType, CryptoStreamFactory cryptoStreamFactory, InputStream source,
String url) {
this.contentType = contentType;
this.cryptoStreamFactory = cryptoStreamFactory;
this.source = source;
this.url = url;
}
String getContentType() {
return contentType;
}
CryptoStreamFactory getCryptoStreamFactory() {
return cryptoStreamFactory;
}
InputStream getSource() {
return source;
}
String getUrl() {
return url;
}
/**
* Use this to create an instance of an {@link UploadSpecification}.
*/
public static class Builder {
private final String contentType;
private final CryptoStreamFactory cryptoStreamFactory;
private final InputStream source;
private final String url;
/**
* Create the builder.
*
* @param contentType The content type of the document to upload
* @param cryptoStreamFactory The crypto stream factory
* @param source The source of the unencrypted data to upload
* @param url The url
*/
public Builder(String contentType, CryptoStreamFactory cryptoStreamFactory, InputStream source, String url) {
Preconditions.checkArgument(contentType != null, "contentType is required");
Preconditions.checkArgument(cryptoStreamFactory != null, "cryptoStreamFactory is required");
Preconditions.checkArgument(source != null, "source is required");
Preconditions.checkArgument(url != null, "url is required");
this.contentType = contentType;
this.cryptoStreamFactory = cryptoStreamFactory;
this.source = source;
this.url = url;
}
/**
* Create the specification.
*
* @return The specification
*/
public UploadSpecification build() {
return new UploadSpecification(contentType, cryptoStreamFactory, source, url);
}
}
}

View File

@ -0,0 +1,13 @@
package com.amazon.spapi.documents.exception;
/**
* Crypto exception.
*/
public class CryptoException extends Exception {
/**
* {@inheritDoc}
*/
public CryptoException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,63 @@
package com.amazon.spapi.documents.exception;
/**
* The details of an HTTP response that indicates failure.
*/
public class HttpResponseException extends Exception {
private final String body;
private final int code;
/**
* {@inheritDoc}
*
* @param message The {@link Exception} message
* @param body The body
* @param code The HTTP status code
*/
public HttpResponseException(String message, String body, int code) {
super(message);
this.body = body;
this.code = code;
}
/**
* {@inheritDoc}
*
* @param message The {@link Exception} message
* @param cause The {@link Exception} cause
* @param body The body
* @param code The HTTP status code
*/
public HttpResponseException(String message, Throwable cause, String body, int code) {
super(message, cause);
this.body = body;
this.code = code;
}
/**
* The body. To ensure that a remote server cannot overwhelm heap memory, the body may have been truncated.
*
* @return The body
*/
public String getBody() {
return body;
}
/**
* The HTTP status code
*
* @return The HTTP status code
*/
public int getCode() {
return code;
}
@Override
public String toString() {
return super.toString() + " {code="
+ getCode()
+ ", body="
+ getBody()
+ '}';
}
}

View File

@ -0,0 +1,13 @@
package com.amazon.spapi.documents.exception;
/**
* Missing charset exception.
*/
public class MissingCharsetException extends Exception {
/**
* {@inheritDoc}
*/
public MissingCharsetException(String message) {
super(message);
}
}

View File

@ -0,0 +1,96 @@
package com.amazon.spapi.documents.impl;
import com.amazon.spapi.documents.CryptoStreamFactory;
import com.amazon.spapi.documents.exception.CryptoException;
import com.google.common.base.Preconditions;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
* A crypto stream factory implementing AES encryption.
*/
public class AESCryptoStreamFactory implements CryptoStreamFactory {
private static final String ENCRYPTION_ALGORITHM = "AES/CBC/PKCS5Padding";
private final Key key;
private final byte[] initializationVector;
private AESCryptoStreamFactory(Key key, byte[] initializationVector) {
this.key = key;
this.initializationVector = initializationVector;
}
private Cipher createInitializedCipher(int mode) throws CryptoException {
try {
Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
cipher.init(mode, key, new IvParameterSpec(initializationVector));
return cipher;
} catch (InvalidKeyException | InvalidAlgorithmParameterException | NoSuchAlgorithmException |
NoSuchPaddingException e) {
throw new CryptoException(e);
}
}
/**
* {@inheritDoc}
*/
@Override
public InputStream newDecryptStream(InputStream source) throws CryptoException {
return new CipherInputStream(source, createInitializedCipher(Cipher.DECRYPT_MODE));
}
/**
* {@inheritDoc}
*/
@Override
public InputStream newEncryptStream(InputStream source) throws CryptoException {
return new CipherInputStream(source, createInitializedCipher(Cipher.ENCRYPT_MODE));
}
/**
* Use this to create an instance of an {@link AESCryptoStreamFactory}.
*/
public static class Builder {
private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder();
private static final String REQUIRED_KEY_ALGORITHM = "AES";
private final String key;
private final String initializationVector;
/**
* Create the builder.
*
* @param key The key
* @param initializationVector The initialization vector
*/
public Builder(String key, String initializationVector) {
Preconditions.checkArgument(key != null, "key is required");
Preconditions.checkArgument(initializationVector != null, "initializationVector is required");
this.key = key;
this.initializationVector = initializationVector;
}
/**
* Create the crypto stream factory.
*
* @return the crypto stream factory
*/
public AESCryptoStreamFactory build() {
Key convertedKey = new SecretKeySpec(BASE64_DECODER.decode(key), REQUIRED_KEY_ALGORITHM);
byte[] convertedInitializationVector = BASE64_DECODER.decode(initializationVector);
return new AESCryptoStreamFactory(convertedKey, convertedInitializationVector);
}
}
}

View File

@ -0,0 +1,136 @@
package com.amazon.spapi.documents.impl;
import com.amazon.spapi.documents.HttpTransferClient;
import com.amazon.spapi.documents.exception.HttpResponseException;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
/**
* HTTP transfer client utilizing OkHttp.
*/
public class OkHttpTransferClient implements HttpTransferClient {
private static final String CONTENT_TYPE_HEADER = "Content-Type";
private final OkHttpClient client;
private final int maxErrorBodyLen;
private OkHttpTransferClient(OkHttpClient client, int maxErrorBodyLen) {
this.client = client;
this.maxErrorBodyLen = maxErrorBodyLen;
}
private HttpResponseException createResponseException(Response response) {
String body = "";
if (maxErrorBodyLen > 0) {
try (Reader bodyReader = response.body().charStream()) {
char[] buf = new char[maxErrorBodyLen];
int charsRead = IOUtils.read(bodyReader, buf);
if (charsRead > 0) {
body = new String(buf, 0, charsRead);
}
} catch (Exception e) {
// Ignore any failures reading the body so that the original failure is not lost
}
}
return new HttpResponseException(response.message(), body, response.code());
}
/**
* {@inheritDoc}
*/
@Override
public String download(String url, File destination) throws HttpResponseException, IOException {
Request request = new Request.Builder()
.url(url)
.get()
.build();
Response response = client.newCall(request).execute();
try {
if (!response.isSuccessful()) {
throw createResponseException(response);
}
FileUtils.copyInputStreamToFile(response.body().byteStream(), destination);
} finally {
IOUtils.closeQuietly(response.body());
}
return response.header(CONTENT_TYPE_HEADER);
}
/**
* {@inheritDoc}
*/
@Override
public void upload(String url, String contentType, File source) throws HttpResponseException, IOException {
Request request = new Request.Builder()
.url(url)
.put(RequestBody.create(MediaType.parse(contentType), source))
.build();
Response response = client.newCall(request).execute();
try {
if (!response.isSuccessful()) {
throw createResponseException(response);
}
} finally {
IOUtils.closeQuietly(response.body());
}
}
/**
* Use this to create an instance of an {@link OkHttpTransferClient}.
*/
public static class Builder {
private OkHttpClient client = null;
private int maxErrorBodyLen = 4096;
/**
* The {@link OkHttpClient}. If not specified, a new instance of {@link OkHttpClient} will be created using the
* no-arg constructor.
*
* @param client The client
* @return this
*/
public Builder withClient(OkHttpClient client) {
this.client = client;
return this;
}
/**
* When an HTTP response indicates failure, the maximum number of characters for
* {@link HttpResponseException#getBody()}. Default <code>4096</code>.
*
* @param maxErrorBodyLen The maximum number of characters to extract from the response body on failure
* @return this
*/
public Builder withMaxErrorBodyLen(int maxErrorBodyLen) {
this.maxErrorBodyLen = maxErrorBodyLen;
return this;
}
/**
* Create the client.
*
* @return The client
*/
public OkHttpTransferClient build() {
if (client == null) {
client = new OkHttpClient();
}
return new OkHttpTransferClient(client, maxErrorBodyLen);
}
}
}

View File

@ -0,0 +1,46 @@
package com.amazon.spapi.documents;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CompressionAlgorithmTest {
public enum MyEnum {
GZIP, NOT_GZIP
}
@Test
public void testFromEquivalent() {
assertEquals(CompressionAlgorithm.GZIP, CompressionAlgorithm.fromEquivalent(MyEnum.GZIP));
}
@Test
public void testFromEquivalentNull() {
MyEnum myEnum = null;
assertNull(CompressionAlgorithm.fromEquivalent(myEnum));
}
@Test
public void testNotEquivalent() {
Assertions.assertThrows(IllegalArgumentException.class,
() -> CompressionAlgorithm.fromEquivalent(MyEnum.NOT_GZIP));
}
@Test
public void testFromString() {
assertEquals(CompressionAlgorithm.GZIP, CompressionAlgorithm.fromEquivalent("GZIP"));
}
@Test
public void testFromStringNull() {
String val = null;
assertNull(CompressionAlgorithm.fromEquivalent(val));
}
@Test
public void testFromStringUnsupportedValue() {
Assertions.assertThrows(IllegalArgumentException.class,
() -> CompressionAlgorithm.fromEquivalent("NOT_GZIP"));
}
}

View File

@ -0,0 +1,36 @@
package com.amazon.spapi.documents;
import com.amazon.spapi.documents.impl.AESCryptoStreamFactory;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.FileNotFoundException;
import static org.junit.jupiter.api.Assertions.*;
class DownloadBundleTest {
private static String KEY = "sxx/wImF6BFndqSAz56O6vfiAh8iD9P297DHfFgujec=";
private static String VECTOR = "7S2tn363v0wfCfo1IX2Q1A==";
private DownloadBundle createBadFileBundle() throws Exception {
String contentType = "text/xml; charset=UTF-8";
CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
File file = File.createTempFile("foo", null, null);
file.delete();
return new DownloadBundle(null, contentType, cryptoStreamFactory, file);
}
@Test
public void testNewInputStreamBadFile() throws Exception {
DownloadBundle downloadBundle = createBadFileBundle();
assertThrows(FileNotFoundException.class, () -> downloadBundle.newInputStream());
}
@Test
public void testNewReaderBadFile() throws Exception {
DownloadBundle downloadBundle = createBadFileBundle();
assertThrows(FileNotFoundException.class, () -> downloadBundle.newBufferedReader());
}
}

View File

@ -0,0 +1,403 @@
package com.amazon.spapi.documents;
import com.amazon.spapi.documents.exception.MissingCharsetException;
import com.amazon.spapi.documents.impl.AESCryptoStreamFactory;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.zip.GZIPOutputStream;
import static org.junit.jupiter.api.Assertions.*;
class DownloadHelperTest {
private static String KEY = "sxx/wImF6BFndqSAz56O6vfiAh8iD9P297DHfFgujec=";
private static String VECTOR = "7S2tn363v0wfCfo1IX2Q1A==";
private Answer getTransferAnswer(String contentType, CryptoStreamFactory cryptoStreamFactory, String fileContents,
boolean isGZipped) {
return new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
File file = invocation.getArgument(1);
byte[] dataToWrite = fileContents.getBytes(StandardCharsets.UTF_8);
if (isGZipped) {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(dataToWrite.length);
GZIPOutputStream zipStream = new GZIPOutputStream(byteStream);
zipStream.write(dataToWrite);
zipStream.close();
dataToWrite = byteStream.toByteArray();
}
// Write encrypted contents to file to mimic downloading an encrypted file
InputStream source = new ByteArrayInputStream(dataToWrite);
try (InputStream inputStream = cryptoStreamFactory.newEncryptStream(source)) {
FileUtils.copyInputStreamToFile(inputStream, file);
}
return contentType;
}
};
}
@Test
public void testSuccess() throws Exception {
String contentType = "text/xml; charset=UTF-8";
CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
String url = "http://www.abc.com/123";
String expectedFileRegex = "SPAPI.*\\.tmp$";
String expectContents = "Hello World";
HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
ArgumentCaptor<File> fileArg = ArgumentCaptor.forClass(File.class);
Mockito.doReturn(contentType).when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
Mockito.doAnswer(getTransferAnswer(contentType, cryptoStreamFactory, expectContents, false))
.when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
DownloadHelper helper = new DownloadHelper.Builder()
.withHttpTransferClient(httpTransferClient)
.build();
DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url).build();
File file;
try (DownloadBundle bundle = helper.download(spec)) {
assertEquals(contentType, bundle.getContentType());
file = fileArg.getValue();
assertTrue(file.exists());
assertTrue(file.getName().matches(expectedFileRegex));
try (InputStream contents = bundle.newInputStream()) {
assertTrue(IOUtils.contentEquals(contents, new ByteArrayInputStream(expectContents.getBytes(
StandardCharsets.UTF_8))));
}
}
assertFalse(file.exists());
Mockito.verify(httpTransferClient).download(Mockito.eq(url), Mockito.any());
}
@Test
public void testSuccessWithCompression() throws Exception {
String contentType = "text/xml; charset=UTF-8";
CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
String url = "http://www.abc.com/123";
String expectedFileRegex = "SPAPI.*\\.tmp$";
String expectContents = "Hello World";
HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
ArgumentCaptor<File> fileArg = ArgumentCaptor.forClass(File.class);
Mockito.doReturn(contentType).when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
Mockito.doAnswer(getTransferAnswer(contentType, cryptoStreamFactory, expectContents, true))
.when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
DownloadHelper helper = new DownloadHelper.Builder()
.withHttpTransferClient(httpTransferClient)
.build();
DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url)
.withCompressionAlgorithm(CompressionAlgorithm.GZIP)
.build();
File file;
try (DownloadBundle bundle = helper.download(spec)) {
assertEquals(contentType, bundle.getContentType());
file = fileArg.getValue();
assertTrue(file.exists());
assertTrue(file.getName().matches(expectedFileRegex));
try (InputStream contents = bundle.newInputStream()) {
assertTrue(IOUtils.contentEquals(contents, new ByteArrayInputStream(expectContents.getBytes(
StandardCharsets.UTF_8))));
}
try (BufferedReader contents = bundle.newBufferedReader()) {
assertTrue(IOUtils.contentEquals(contents, new StringReader(expectContents)));
}
assertTrue(file.exists());
}
assertFalse(file.exists());
Mockito.verify(httpTransferClient).download(Mockito.eq(url), Mockito.any());
}
@Test
public void testSuccessWithCustomTempFileParams() throws Exception {
String contentType = "text/xml; charset=UTF-8";
CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
String url = "http://www.abc.com/123";
String expectedFileRegex = "NOTSPAPI.*\\.NOTtmp$";
String expectContents = "Hello World";
String expectedFilePath = "spapitmp";
HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
ArgumentCaptor<File> fileArg = ArgumentCaptor.forClass(File.class);
Mockito.doReturn(contentType).when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
Mockito.doAnswer(getTransferAnswer(contentType, cryptoStreamFactory, expectContents, false))
.when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
File tmpDir = new File(Paths.get(System.getProperty("java.io.tmpdir"), expectedFilePath)
.toAbsolutePath().toString());
tmpDir.mkdirs();
tmpDir.deleteOnExit();
DownloadHelper helper = new DownloadHelper.Builder()
.withHttpTransferClient(httpTransferClient)
.withTmpFilePrefix("NOTSPAPI")
.withTmpFileSuffix(".NOTtmp")
.withTmpFileDirectory(tmpDir)
.build();
DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url).build();
File file;
try (DownloadBundle bundle = helper.download(spec)) {
assertEquals(contentType, bundle.getContentType());
file = fileArg.getValue();
assertTrue(file.exists());
assertEquals(expectedFilePath, file.getParentFile().getName());
assertTrue(file.getName().matches(expectedFileRegex));
try (BufferedReader contents = bundle.newBufferedReader()) {
assertTrue(IOUtils.contentEquals(contents, new StringReader(expectContents)));
}
try (InputStream contents = bundle.newInputStream()) {
assertTrue(IOUtils.contentEquals(contents, new ByteArrayInputStream(expectContents.getBytes(
StandardCharsets.UTF_8))));
}
assertTrue(file.exists());
}
assertFalse(file.exists());
Mockito.verify(httpTransferClient).download(Mockito.eq(url), Mockito.any());
}
@Test
public void testReaderBadCharset() throws Exception {
String contentType = "text/xml; charset=NOTSUPPORTED";
CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
String url = "http://www.abc.com/123";
String expectedFileRegex = "NOTSPAPI.*\\.NOTtmp$";
String expectContents = "Hello World";
String expectedFilePath = "spapitmp";
HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
ArgumentCaptor<File> fileArg = ArgumentCaptor.forClass(File.class);
Mockito.doReturn(contentType).when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
Mockito.doAnswer(getTransferAnswer(contentType, cryptoStreamFactory, expectContents, false))
.when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
File tmpDir = new File(Paths.get(System.getProperty("java.io.tmpdir"), expectedFilePath)
.toAbsolutePath().toString());
tmpDir.mkdirs();
tmpDir.deleteOnExit();
DownloadHelper helper = new DownloadHelper.Builder()
.withHttpTransferClient(httpTransferClient)
.withTmpFilePrefix("NOTSPAPI")
.withTmpFileSuffix(".NOTtmp")
.withTmpFileDirectory(tmpDir)
.build();
DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url).build();
File file;
try (DownloadBundle bundle = helper.download(spec)) {
assertEquals(contentType, bundle.getContentType());
file = fileArg.getValue();
assertTrue(file.exists());
assertEquals(expectedFilePath, file.getParentFile().getName());
assertTrue(file.getName().matches(expectedFileRegex));
assertThrows(UnsupportedCharsetException.class, () -> bundle.newBufferedReader());
try (InputStream contents = bundle.newInputStream()) {
assertTrue(IOUtils.contentEquals(contents, new ByteArrayInputStream(expectContents.getBytes(
StandardCharsets.UTF_8))));
}
assertTrue(file.exists());
}
assertFalse(file.exists());
Mockito.verify(httpTransferClient).download(Mockito.eq(url), Mockito.any());
}
@Test
public void testMissingCharsetNoDefault() throws Exception {
String contentType = "text/xml";
CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
String url = "http://www.abc.com/123";
String expectedFileRegex = "NOTSPAPI.*\\.NOTtmp$";
String expectContents = "Hello World";
String expectedFilePath = "spapitmp";
HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
ArgumentCaptor<File> fileArg = ArgumentCaptor.forClass(File.class);
Mockito.doReturn(contentType).when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
Mockito.doAnswer(getTransferAnswer(contentType, cryptoStreamFactory, expectContents, false))
.when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
File tmpDir = new File(Paths.get(System.getProperty("java.io.tmpdir"), expectedFilePath)
.toAbsolutePath().toString());
tmpDir.mkdirs();
tmpDir.deleteOnExit();
DownloadHelper helper = new DownloadHelper.Builder()
.withHttpTransferClient(httpTransferClient)
.withTmpFilePrefix("NOTSPAPI")
.withTmpFileSuffix(".NOTtmp")
.withTmpFileDirectory(tmpDir)
.build();
DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url).build();
File file;
try (DownloadBundle bundle = helper.download(spec)) {
assertEquals(contentType, bundle.getContentType());
file = fileArg.getValue();
assertTrue(file.exists());
assertEquals(expectedFilePath, file.getParentFile().getName());
assertTrue(file.getName().matches(expectedFileRegex));
assertThrows(MissingCharsetException.class, () -> bundle.newBufferedReader());
try (InputStream contents = bundle.newInputStream()) {
assertTrue(IOUtils.contentEquals(contents, new ByteArrayInputStream(expectContents.getBytes(
StandardCharsets.UTF_8))));
}
assertTrue(file.exists());
}
assertFalse(file.exists());
Mockito.verify(httpTransferClient).download(Mockito.eq(url), Mockito.any());
}
@Test
public void testSuccessMissingBadCharsetDefaultProvided() throws Exception {
String contentType = "text/xml";
CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
String url = "http://www.abc.com/123";
String expectedFileRegex = "NOTSPAPI.*\\.NOTtmp$";
String expectContents = "Hello World";
String expectedFilePath = "spapitmp";
HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
ArgumentCaptor<File> fileArg = ArgumentCaptor.forClass(File.class);
Mockito.doReturn(contentType).when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
Mockito.doAnswer(getTransferAnswer(contentType, cryptoStreamFactory, expectContents, false))
.when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
File tmpDir = new File(Paths.get(System.getProperty("java.io.tmpdir"), expectedFilePath)
.toAbsolutePath().toString());
tmpDir.mkdirs();
tmpDir.deleteOnExit();
DownloadHelper helper = new DownloadHelper.Builder()
.withHttpTransferClient(httpTransferClient)
.withTmpFilePrefix("NOTSPAPI")
.withTmpFileSuffix(".NOTtmp")
.withTmpFileDirectory(tmpDir)
.build();
DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url).build();
File file;
try (DownloadBundle bundle = helper.download(spec)) {
assertEquals(contentType, bundle.getContentType());
file = fileArg.getValue();
assertTrue(file.exists());
assertEquals(expectedFilePath, file.getParentFile().getName());
assertTrue(file.getName().matches(expectedFileRegex));
try (InputStream contents = bundle.newInputStream()) {
assertTrue(IOUtils.contentEquals(contents, new ByteArrayInputStream(expectContents.getBytes(
StandardCharsets.UTF_8))));
}
try (BufferedReader contents = bundle.newBufferedReader(StandardCharsets.UTF_8)) {
assertTrue(IOUtils.contentEquals(contents, new StringReader(expectContents)));
}
assertTrue(file.exists());
}
assertFalse(file.exists());
Mockito.verify(httpTransferClient).download(Mockito.eq(url), Mockito.any());
}
@Test
public void testBadUrlDownload() throws Exception {
CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
String url = "sdfkajsdfiefi";
Path path = Files.createTempDirectory("SPAPI");
DownloadHelper helper = new DownloadHelper.Builder()
.withTmpFileDirectory(path.toFile())
.build();
DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url)
.build();
try {
assertThrows(IllegalArgumentException.class, () -> helper.download(spec));
} finally {
// This will throw if the directory is not empty, indicating that the temp file was not removed
Files.delete(path);
}
}
@Test
public void testInvalidTmpFilePrefix() {
DownloadHelper.Builder builder = new DownloadHelper.Builder();
assertThrows(IllegalArgumentException.class, () -> builder
.withTmpFilePrefix("A"));
}
}

View File

@ -0,0 +1,53 @@
package com.amazon.spapi.documents;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.*;
class DownloadSpecificationTest {
@Test
public void testBuilderConstructorMissingCryptoStreamFactory() throws Exception {
CryptoStreamFactory cryptoStreamFactory = null;
String url = "https://www.amazon.com";
assertThrows(IllegalArgumentException.class, () ->
new DownloadSpecification.Builder(cryptoStreamFactory, url));
}
@Test
public void testBuilderConstructorMissingUrl() throws Exception {
CryptoStreamFactory cryptoStreamFactory = Mockito.mock(CryptoStreamFactory.class);
String url = null;
assertThrows(IllegalArgumentException.class, () ->
new DownloadSpecification.Builder(cryptoStreamFactory, url));
}
@Test
public void testSuccess() throws Exception {
CryptoStreamFactory cryptoStreamFactory = Mockito.mock(CryptoStreamFactory.class);
String url = "http://abc.com/123";
DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url).build();
assertNull(spec.getCompressionAlgorithm());
assertSame(cryptoStreamFactory, spec.getCryptoStreamFactory());
assertEquals(url, spec.getUrl());
}
@Test
public void testCompressionAlgorithm() throws Exception {
CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.GZIP;
CryptoStreamFactory cryptoStreamFactory = Mockito.mock(CryptoStreamFactory.class);
String url = "http://abc.com/123";
DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url)
.withCompressionAlgorithm(compressionAlgorithm)
.build();
assertEquals(compressionAlgorithm, spec.getCompressionAlgorithm());
assertSame(cryptoStreamFactory, spec.getCryptoStreamFactory());
assertEquals(url, spec.getUrl());
}
}

View File

@ -0,0 +1,171 @@
package com.amazon.spapi.documents;
import com.amazon.spapi.documents.impl.AESCryptoStreamFactory;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.junit.jupiter.api.Assertions.*;
class UploadHelperTest {
private static String KEY = "sxx/wImF6BFndqSAz56O6vfiAh8iD9P297DHfFgujec=";
private static String VECTOR = "7S2tn363v0wfCfo1IX2Q1A==";
class FileCapture {
volatile boolean existed = false;
volatile File file = null;
}
private Answer getAnswer(FileCapture fileCapture, Exception e) {
return new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
File tmpFile = (File)invocation.getArgument(2);
fileCapture.existed = tmpFile.exists();
fileCapture.file = tmpFile;
if (e != null) {
throw e;
}
return null;
}
};
}
private Answer getAnswer(FileCapture fileCapture) {
return getAnswer(fileCapture, null);
}
@Test
public void testSuccess() throws Exception {
String contentType = "text/xml; charset=UTF-8";
CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
InputStream source = new ByteArrayInputStream(new byte[0]);
String url = "http://www.abc.com/123";
String expectedFileRegex = "SPAPI.*\\.tmp$";
HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
FileCapture fileCapture = new FileCapture();
Mockito.doAnswer(getAnswer(fileCapture)).when(httpTransferClient)
.upload(Mockito.eq(url), Mockito.eq(contentType), Mockito.any());
UploadHelper helper = new UploadHelper.Builder()
.withHttpTransferClient(httpTransferClient)
.build();
UploadSpecification spec = new UploadSpecification.Builder(contentType, cryptoStreamFactory, source, url)
.build();
helper.upload(spec);
assertTrue(fileCapture.file.getName().matches(expectedFileRegex));
assertTrue(fileCapture.existed);
assertFalse(fileCapture.file.exists());
Mockito.verify(httpTransferClient).upload(Mockito.eq(url), Mockito.eq(contentType), Mockito.any());
}
@Test
public void testSuccessWithCustomTmpFileParams() throws Exception {
String contentType = "text/xml; charset=UTF-8";
CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
InputStream source = new ByteArrayInputStream(new byte[0]);
String url = "http://www.abc.com/123";
String expectedFileRegex = "NOTSPAPI.*\\.NOTtmp$";
String expectedFilePath = "spapitmp";
HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
FileCapture fileCapture = new FileCapture();
Mockito.doAnswer(getAnswer(fileCapture)).when(httpTransferClient)
.upload(Mockito.eq(url), Mockito.eq(contentType), Mockito.any());
File tmpDir = new File(Paths.get(System.getProperty("java.io.tmpdir"), expectedFilePath)
.toAbsolutePath().toString());
tmpDir.mkdirs();
UploadHelper helper = new UploadHelper.Builder()
.withHttpTransferClient(httpTransferClient)
.withTmpFilePrefix("NOTSPAPI")
.withTmpFileSuffix(".NOTtmp")
.withTmpFileDirectory(tmpDir)
.build();
UploadSpecification spec = new UploadSpecification.Builder(contentType, cryptoStreamFactory, source, url)
.build();
helper.upload(spec);
assertTrue(fileCapture.file.getName().matches(expectedFileRegex));
assertEquals(expectedFilePath, fileCapture.file.getParentFile().getName());
assertTrue(fileCapture.existed);
assertFalse(fileCapture.file.exists());
Mockito.verify(httpTransferClient).upload(Mockito.eq(url), Mockito.eq(contentType), Mockito.any());
}
@Test
public void testFileDeletedOnFailure() throws Exception {
String contentType = "text/xml; charset=UTF-8";
CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
InputStream source = new ByteArrayInputStream(new byte[0]);
String url = "http://www.abc.com/123";
HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
FileCapture fileCapture = new FileCapture();
Mockito.doAnswer(getAnswer(fileCapture, new IllegalArgumentException())).when(httpTransferClient)
.upload(Mockito.eq(url), Mockito.eq(contentType), Mockito.any());
UploadHelper helper = new UploadHelper.Builder()
.withHttpTransferClient(httpTransferClient)
.build();
UploadSpecification spec = new UploadSpecification.Builder(contentType, cryptoStreamFactory, source, url)
.build();
assertThrows(IllegalArgumentException.class, () -> helper.upload(spec));
assertTrue(fileCapture.existed);
assertFalse(fileCapture.file.exists());
Mockito.verify(httpTransferClient).upload(Mockito.eq(url), Mockito.eq(contentType), Mockito.any());
}
@Test
public void testBadUrlFailedUpload() throws Exception {
String contentType = "text/xml; charset=UTF-8";
CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
InputStream source = new ByteArrayInputStream(new byte[0]);
String url = "sdfkajsdfiefi";
Path path = Files.createTempDirectory("SPAPI");
try {
UploadHelper helper = new UploadHelper.Builder()
.withTmpFileDirectory(path.toFile())
.build();
UploadSpecification spec = new UploadSpecification.Builder(contentType, cryptoStreamFactory, source, url)
.build();
assertThrows(IllegalArgumentException.class, () -> helper.upload(spec));
} finally {
// This will throw if the directory is not empty, indicating that the temp file was not removed
Files.delete(path);
}
}
@Test
public void testInvalidTmpFilePrefix() {
UploadHelper.Builder builder = new UploadHelper.Builder();
assertThrows(IllegalArgumentException.class, () -> builder
.withTmpFilePrefix("A"));
}
}

View File

@ -0,0 +1,71 @@
package com.amazon.spapi.documents;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import static org.junit.jupiter.api.Assertions.*;
class UploadSpecificationTest {
@Test
public void testBuilderConstructorMissingContentType() throws Exception {
String contentType = null;
CryptoStreamFactory cryptoStreamFactory = Mockito.mock(CryptoStreamFactory.class);
InputStream source = new ByteArrayInputStream(new byte[0]);
String url = "https://www.amazon.com";
assertThrows(IllegalArgumentException.class, () -> new UploadSpecification.Builder(
contentType, cryptoStreamFactory, source, url));
}
@Test
public void testBuilderConstructorMissingCryptoStreamFactory() throws Exception {
String contentType = "text/xml; charset=UTF-8";
CryptoStreamFactory cryptoStreamFactory = null;
InputStream source = new ByteArrayInputStream(new byte[0]);
String url = "https://www.amazon.com";
assertThrows(IllegalArgumentException.class, () -> new UploadSpecification.Builder(
contentType, cryptoStreamFactory, source, url));
}
@Test
public void testBuilderConstructorMissingSource() throws Exception {
String contentType = "text/xml; charset=UTF-8";
CryptoStreamFactory cryptoStreamFactory = Mockito.mock(CryptoStreamFactory.class);
InputStream source = null;
String url = "https://www.amazon.com";
assertThrows(IllegalArgumentException.class, () -> new UploadSpecification.Builder(
contentType, cryptoStreamFactory, source, url));
}
@Test
public void testBuilderConstructorMissingUrl() throws Exception {
String contentType = "text/xml; charset=UTF-8";
CryptoStreamFactory cryptoStreamFactory = Mockito.mock(CryptoStreamFactory.class);
InputStream source = new ByteArrayInputStream(new byte[0]);
String url = null;
assertThrows(IllegalArgumentException.class, () -> new UploadSpecification.Builder(
contentType, cryptoStreamFactory, source, url));
}
@Test
public void testSuccess() throws Exception {
String contentType = "text/xml; charset=UTF-8";
CryptoStreamFactory cryptoStreamFactory = Mockito.mock(CryptoStreamFactory.class);
InputStream source = new ByteArrayInputStream(new byte[0]);
String url = "http://abc.com/123";
UploadSpecification spec = new UploadSpecification.Builder(contentType, cryptoStreamFactory, source, url)
.build();
assertEquals(contentType, spec.getContentType());
assertSame(cryptoStreamFactory, spec.getCryptoStreamFactory());
assertSame(source, spec.getSource());
assertEquals(url, spec.getUrl());
}
}

View File

@ -0,0 +1,15 @@
package com.amazon.spapi.documents.exception;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CryptoExceptionTest {
@Test
public void testConstructor() {
Throwable throwable = new RuntimeException();
CryptoException exception = new CryptoException(throwable);
assertSame(throwable, exception.getCause());
}
}

View File

@ -0,0 +1,54 @@
package com.amazon.spapi.documents.exception;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class HttpResponseExceptionTest {
@Test
public void testConstructor() {
String message = "This is the message";
String body = "This is the body";
int code = 403;
String expectToString = "com.amazon.spapi.documents.exception.HttpResponseException: " +
"This is the message {code=403, body=This is the body}";
HttpResponseException exception = new HttpResponseException(message, body, code);
assertEquals(message, exception.getMessage());
assertEquals(body, exception.getBody());
assertEquals(code, exception.getCode());
assertEquals(expectToString, exception.toString());
}
@Test
public void testConstructorNullBody() {
String message = "This is the message";
String body = null;
int code = 403;
String expectToString = "com.amazon.spapi.documents.exception.HttpResponseException: " +
"This is the message {code=403, body=null}";
HttpResponseException exception = new HttpResponseException(message, body, code);
assertEquals(message, exception.getMessage());
assertEquals(body, exception.getBody());
assertEquals(code, exception.getCode());
assertEquals(expectToString, exception.toString());
}
@Test
public void testConstructorCause() {
String message = "This is the message";
Throwable cause = new RuntimeException();
String body = "This is the body";
int code = 403;
HttpResponseException exception = new HttpResponseException(message, cause, body, code);
assertEquals(message, exception.getMessage());
assertSame(cause, exception.getCause());
assertEquals(body, exception.getBody());
assertEquals(code, exception.getCode());
}
}

View File

@ -0,0 +1,15 @@
package com.amazon.spapi.documents.exception;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MissingCharsetExceptionTest {
@Test
public void testConstructor() {
String message = "This is the message";
MissingCharsetException exception = new MissingCharsetException(message);
assertEquals(message, exception.getMessage());
}
}

View File

@ -0,0 +1,69 @@
package com.amazon.spapi.documents.impl;
import com.amazon.spapi.documents.exception.CryptoException;
import com.google.common.io.ByteStreams;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import static org.junit.jupiter.api.Assertions.*;
class AESCryptoStreamFactoryTest {
private static String KEY = "sxx/wImF6BFndqSAz56O6vfiAh8iD9P297DHfFgujec=";
private static String VECTOR = "7S2tn363v0wfCfo1IX2Q1A==";
@Test
public void testBuilderConstructorMissingInitializationVector() {
String key = "DEF";
String initializationVector = null;
assertThrows(IllegalArgumentException.class, () ->
new AESCryptoStreamFactory.Builder(key, initializationVector));
}
@Test
public void testBuilderConstructorMissingKey() {
String key = null;
String initializationVector = "ABC";
assertThrows(IllegalArgumentException.class, () ->
new AESCryptoStreamFactory.Builder(key, initializationVector));
}
@Test
public void testBadKey() throws Exception {
byte[] decodedKey = Base64.getDecoder().decode(KEY);
String encodedKey = Base64.getEncoder().encodeToString(
Arrays.copyOfRange(decodedKey, 2, decodedKey.length-1));
AESCryptoStreamFactory aesCryptoStreamFactory = new AESCryptoStreamFactory.Builder(encodedKey, VECTOR).build();
try (InputStream inputStream = new ByteArrayInputStream("Hello World!".getBytes(StandardCharsets.UTF_8))) {
assertThrows(CryptoException.class, () -> aesCryptoStreamFactory.newDecryptStream(inputStream));
}
}
@Test
public void testEncryptDecrypt() throws Exception {
String stringContent = "Hello World!";
byte[] byteContent = stringContent.getBytes(StandardCharsets.UTF_8);
AESCryptoStreamFactory aesCryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
try (InputStream encryptStream =
aesCryptoStreamFactory.newEncryptStream(new ByteArrayInputStream(byteContent))) {
byte[] encryptedContent = ByteStreams.toByteArray(encryptStream);
try (InputStream decryptStream =
aesCryptoStreamFactory.newDecryptStream(new ByteArrayInputStream(encryptedContent))) {
byte[] decryptedContent = ByteStreams.toByteArray(decryptStream);
assertArrayEquals(decryptedContent, byteContent);
assertFalse(Arrays.equals(encryptedContent, decryptedContent));
}
}
}
}

View File

@ -0,0 +1,331 @@
package com.amazon.spapi.documents.impl;
import com.amazon.spapi.documents.exception.HttpResponseException;
import com.squareup.okhttp.Call;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ResponseBody;
import okio.Buffer;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.platform.commons.util.ReflectionUtils;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
class OkHttpTransferClientTest {
@Test
public void testDefaultBuilder() throws Exception {
OkHttpTransferClient helper = new OkHttpTransferClient.Builder().build();
OkHttpClient client = (OkHttpClient)ReflectionUtils.tryToReadFieldValue(OkHttpTransferClient.class,
"client", helper).get();
}
@Test
public void testDownloadSuccess() throws Exception {
String contentType = "text/xml; charset=UTF-8";
String url = "https://www.s3-amazon.com/123";
String content = "Hello world!";
OkHttpClient client = Mockito.mock(OkHttpClient.class);
Call call = Mockito.mock(Call.class);
ArgumentCaptor<Request> requestArg = ArgumentCaptor.forClass(Request.class);
Mockito.doReturn(call).when(client).newCall(requestArg.capture());
Response response = Mockito.mock(Response.class);
Mockito.doReturn(response).when(call).execute();
Mockito.doReturn(contentType).when(response).header("Content-Type");
Mockito.doReturn(true).when(response).isSuccessful();
ResponseBody responseBody = Mockito.mock(ResponseBody.class);
Mockito.doReturn(responseBody).when(response).body();
Mockito.doReturn(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)))
.when(responseBody).byteStream();
OkHttpTransferClient helper = new OkHttpTransferClient.Builder()
.withClient(client)
.build();
File tmpFile = File.createTempFile("foo", null);
try {
assertEquals(contentType, helper.download(url, tmpFile));
assertTrue(IOUtils.contentEquals(new FileInputStream(tmpFile),
new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))));
} finally {
tmpFile.delete();
}
assertEquals(url, requestArg.getValue().urlString());
assertEquals("GET", requestArg.getValue().method());
Mockito.verify(responseBody).close();
}
@Test
public void testDownloadIOException() throws Exception {
String url = "https://www.s3-amazon.com/123";
OkHttpClient client = Mockito.mock(OkHttpClient.class);
Call call = Mockito.mock(Call.class);
ArgumentCaptor<Request> requestArg = ArgumentCaptor.forClass(Request.class);
Mockito.doReturn(call).when(client).newCall(requestArg.capture());
Mockito.doThrow(new IOException()).when(call).execute();
OkHttpTransferClient helper = new OkHttpTransferClient.Builder()
.withClient(client)
.build();
File tmpFile = File.createTempFile("foo", null);
try {
Assertions.assertThrows(IOException.class, () -> helper.download(url, tmpFile));
} finally {
tmpFile.delete();
}
}
private void failureResponseMocks(Response response, ResponseBody responseBody,
int httpStatusCode, String message, String bodyContent) throws Exception {
Mockito.doReturn(httpStatusCode).when(response).code();
Mockito.doReturn(message).when(response).message();
Mockito.doReturn(new StringReader(bodyContent)).when(responseBody)
.charStream();
}
@Test
public void testDownloadFailure() throws Exception {
String url = "https://www.s3-amazon.com/123";
String responseBodyText = "This is the response body";
int maxErrorBodyLen = 5;
int httpStatusCode = 403;
String message = "Forbidden";
OkHttpClient client = Mockito.mock(OkHttpClient.class);
Call call = Mockito.mock(Call.class);
ArgumentCaptor<Request> requestArg = ArgumentCaptor.forClass(Request.class);
Mockito.doReturn(call).when(client).newCall(requestArg.capture());
Response response = Mockito.mock(Response.class);
Mockito.doReturn(response).when(call).execute();
ResponseBody responseBody = Mockito.mock(ResponseBody.class);
Mockito.doReturn(responseBody).when(response).body();
Mockito.doNothing().when(responseBody).close();
failureResponseMocks(response, responseBody, httpStatusCode, message, responseBodyText);
Mockito.doReturn(false).when(response).isSuccessful();
OkHttpTransferClient helper = new OkHttpTransferClient.Builder()
.withClient(client)
.withMaxErrorBodyLen(maxErrorBodyLen)
.build();
File tmpFile = File.createTempFile("foo", null);
try {
helper.download(url, tmpFile);
fail("Expected exception");
} catch(HttpResponseException e) {
assertEquals(httpStatusCode, e.getCode());
assertEquals(message, e.getMessage());
assertEquals("This ", e.getBody());
} finally {
tmpFile.delete();
}
Mockito.verify(responseBody).close();
}
@Test
public void testFailureInBodyExtraction() throws Exception {
String url = "https://www.s3-amazon.com/123";
int maxErrorBodyLen = 5;
int httpStatusCode = 403;
String message = "Forbidden";
OkHttpClient client = Mockito.mock(OkHttpClient.class);
Call call = Mockito.mock(Call.class);
ArgumentCaptor<Request> requestArg = ArgumentCaptor.forClass(Request.class);
Mockito.doReturn(call).when(client).newCall(requestArg.capture());
Response response = Mockito.mock(Response.class);
Mockito.doReturn(response).when(call).execute();
ResponseBody responseBody = Mockito.mock(ResponseBody.class);
Mockito.doReturn(responseBody).when(response).body();
Mockito.doNothing().when(responseBody).close();
Mockito.doReturn(httpStatusCode).when(response).code();
Mockito.doReturn(message).when(response).message();
Mockito.doThrow(new IOException()).when(responseBody).charStream();
Mockito.doReturn(false).when(response).isSuccessful();
OkHttpTransferClient helper = new OkHttpTransferClient.Builder()
.withClient(client)
.withMaxErrorBodyLen(maxErrorBodyLen)
.build();
File tmpFile = File.createTempFile("foo", null);
try {
helper.download(url, tmpFile);
fail("Expected exception");
} catch(HttpResponseException e) {
assertEquals(httpStatusCode, e.getCode());
assertEquals(message, e.getMessage());
assertEquals("", e.getBody());
} finally {
tmpFile.delete();
}
Mockito.verify(responseBody).close();
}
@Test
public void testUploadSuccess() throws Exception {
String contentType = "text/xml; charset=UTF-8";
String url = "https://www.s3-amazon.com/123";
String content = "Hello world!";
OkHttpClient client = Mockito.mock(OkHttpClient.class);
Call call = Mockito.mock(Call.class);
ArgumentCaptor<Request> requestArg = ArgumentCaptor.forClass(Request.class);
Mockito.doReturn(call).when(client).newCall(requestArg.capture());
Response response = Mockito.mock(Response.class);
Mockito.doReturn(response).when(call).execute();
Mockito.doReturn(true).when(response).isSuccessful();
ResponseBody responseBody = Mockito.mock(ResponseBody.class);
Mockito.doReturn(responseBody).when(response).body();
Mockito.doNothing().when(responseBody).close();
OkHttpTransferClient helper = new OkHttpTransferClient.Builder()
.withClient(client)
.build();
File tmpFile = File.createTempFile("foo", null);
try {
try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile)) {
fileOutputStream.write(content.getBytes(StandardCharsets.UTF_8));
}
helper.upload(url, contentType, tmpFile);
Buffer buffer = new Buffer();
requestArg.getValue().body().writeTo(buffer);
assertEquals(content, buffer.readString(StandardCharsets.UTF_8));
assertEquals(url, requestArg.getValue().urlString());
assertEquals("PUT", requestArg.getValue().method());
assertEquals(contentType, requestArg.getValue().body().contentType().toString());
} finally {
tmpFile.delete();
}
Mockito.verify(responseBody).close();
}
@Test
public void testUploadIOException() throws Exception {
String contentType = "text/xml; charset=UTF-8";
String url = "https://www.s3-amazon.com/123";
String content = "Hello world!";
OkHttpClient client = Mockito.mock(OkHttpClient.class);
Call call = Mockito.mock(Call.class);
ArgumentCaptor<Request> requestArg = ArgumentCaptor.forClass(Request.class);
Mockito.doReturn(call).when(client).newCall(requestArg.capture());
Mockito.doThrow(new IOException()).when(call).execute();
OkHttpTransferClient helper = new OkHttpTransferClient.Builder()
.withClient(client)
.build();
File tmpFile = File.createTempFile("foo", null);
try {
try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile)) {
fileOutputStream.write(content.getBytes(StandardCharsets.UTF_8));
}
Assertions.assertThrows(IOException.class, () -> helper.upload(url, contentType, tmpFile));
} finally {
tmpFile.delete();
}
}
@Test
public void testUploadFailure() throws Exception {
String contentType = "text/xml; charset=UTF-8";
String url = "https://www.s3-amazon.com/123";
String content = "Hello world!";
String responseBodyText = "This is the response body";
int maxErrorBodyLen = 5;
int httpStatusCode = 403;
String message = "Forbidden";
OkHttpClient client = Mockito.mock(OkHttpClient.class);
Call call = Mockito.mock(Call.class);
ArgumentCaptor<Request> requestArg = ArgumentCaptor.forClass(Request.class);
Mockito.doReturn(call).when(client).newCall(requestArg.capture());
Response response = Mockito.mock(Response.class);
Mockito.doReturn(response).when(call).execute();
Mockito.doReturn(false).when(response).isSuccessful();
ResponseBody responseBody = Mockito.mock(ResponseBody.class);
Mockito.doReturn(responseBody).when(response).body();
Mockito.doNothing().when(responseBody).close();
failureResponseMocks(response, responseBody, httpStatusCode, message, responseBodyText);
OkHttpTransferClient helper = new OkHttpTransferClient.Builder()
.withClient(client)
.withMaxErrorBodyLen(maxErrorBodyLen)
.build();
File tmpFile = File.createTempFile("foo", null);
try {
try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile)) {
fileOutputStream.write(content.getBytes(StandardCharsets.UTF_8));
}
helper.upload(url, contentType, tmpFile);
fail("Expected exception");
} catch(HttpResponseException e) {
assertEquals(httpStatusCode, e.getCode());
assertEquals(message, e.getMessage());
assertEquals("This ", e.getBody());
} finally {
tmpFile.delete();
}
Mockito.verify(responseBody).close();
}
}