├── .github └── workflows │ └── ci-cd.yml ├── .gitignore ├── CHANGES.md ├── LICENSE ├── README.md ├── pom.xml ├── settings.xml ├── src ├── main │ ├── java │ │ └── com │ │ │ └── tinify │ │ │ ├── AccountException.java │ │ │ ├── Client.java │ │ │ ├── ClientException.java │ │ │ ├── ConnectionException.java │ │ │ ├── Exception.java │ │ │ ├── Options.java │ │ │ ├── Result.java │ │ │ ├── ResultMeta.java │ │ │ ├── ServerException.java │ │ │ ├── Source.java │ │ │ ├── TLSContext.java │ │ │ └── Tinify.java │ └── resources │ │ └── cacert.pem └── test │ ├── java │ └── com │ │ └── tinify │ │ ├── ClientEndpointTest.java │ │ ├── ClientErrorTest.java │ │ ├── ClientTest.java │ │ ├── Integration.java │ │ ├── ResultMetaTest.java │ │ ├── ResultTest.java │ │ ├── SourceTest.java │ │ └── TinifyTest.java │ └── resources │ ├── dummy.png │ └── voormedia.png └── update-cacert.sh /.github/workflows/ci-cd.yml: -------------------------------------------------------------------------------- 1 | name: Java CI/CD 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | java: [11, 17, 21] 11 | distribution: ["temurin", "adopt"] 12 | os: [ubuntu-latest, macOS-latest, windows-latest] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up JDK ${{ matrix.java }} 17 | uses: actions/setup-java@v4 18 | with: 19 | distribution: ${{ matrix.distribution }} 20 | java-version: ${{ matrix.java }} 21 | cache: 'maven' 22 | - name: Build with Maven 23 | run: mvn test --file pom.xml -B 24 | 25 | test-oracle: 26 | runs-on: ${{ matrix.os }} 27 | strategy: 28 | matrix: 29 | java: [17, 21] 30 | distribution: ["oracle"] 31 | os: [ubuntu-latest, macOS-latest, windows-latest] 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Set up JDK ${{ matrix.java }} 36 | uses: actions/setup-java@v4 37 | with: 38 | distribution: ${{ matrix.distribution }} 39 | java-version: ${{ matrix.java }} 40 | cache: 'maven' 41 | - name: Build with Maven 42 | run: mvn test --file pom.xml -B 43 | 44 | test-java-8: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: actions/setup-java@v4 49 | with: 50 | distribution: 'adopt' 51 | java-version: '8' 52 | cache: 'maven' 53 | - name: Build with Maven 54 | run: mvn test --file pom.xml -B 55 | 56 | integration-test: 57 | if: github.event_name == 'push' 58 | runs-on: ubuntu-latest 59 | strategy: 60 | matrix: 61 | java: [11, 17, 21] 62 | os: [ubuntu-latest, macOS-latest, windows-latest] 63 | needs: 64 | - "test" 65 | - "test-java-8" 66 | - "test-oracle" 67 | steps: 68 | - uses: actions/checkout@v4 69 | - uses: actions/setup-java@v4 70 | with: 71 | distribution: 'temurin' 72 | java-version: '${{ matrix.java }}' 73 | cache: 'maven' 74 | - name: Build with Maven 75 | env: 76 | TINIFY_KEY: ${{ secrets.TINIFY_KEY }} 77 | run: mvn -Pintegration integration-test -B 78 | 79 | adopt-integration-test-java-8: 80 | if: github.event_name == 'push' 81 | runs-on: ubuntu-latest 82 | needs: 83 | - "test-java-8" 84 | - "test-oracle" 85 | - "test" 86 | steps: 87 | - uses: actions/checkout@v3 88 | - uses: actions/setup-java@v3 89 | with: 90 | distribution: 'adopt' 91 | java-version: '8' 92 | cache: 'maven' 93 | - name: Build with Maven 94 | env: 95 | TINIFY_KEY: ${{ secrets.TINIFY_KEY }} 96 | run: mvn -Pintegration integration-test -B 97 | 98 | record-dependency-graph: 99 | if: github.event_name == 'push' 100 | runs-on: ubuntu-latest 101 | steps: 102 | - uses: actions/checkout@v3 103 | - name: Submit Dependency Snapshot 104 | uses: advanced-security/maven-dependency-submission-action@v3 105 | 106 | publish: 107 | if: | 108 | github.repository == 'tinify/tinify-java' && 109 | startsWith(github.ref, 'refs/tags') && 110 | github.event_name == 'push' 111 | needs: 112 | - "adopt-integration-test-java-8" 113 | - "integration-test" 114 | runs-on: "ubuntu-latest" 115 | steps: 116 | - uses: actions/checkout@v3 117 | - name: Set up JDK ${{ matrix.java }} 118 | uses: actions/setup-java@v3 119 | with: 120 | distribution: 'temurin' 121 | java-version: 11 122 | - name: Setup GPG 123 | run: | 124 | # GPG exported with 125 | # gpg -a --export-secret-keys "info@tinify.com" | base64 > gpg.base64 126 | mkdir -p ./private-keys-v1.d 127 | echo -n "${GPG_KEY_BASE64}" | base64 --decode > ./private-keys-v1.d/gpg.asc 128 | echo -n "${GPG_PASSPHRASE}" | gpg --batch --yes --passphrase-fd 0 --import ./private-keys-v1.d/gpg.asc 129 | gpg -k 130 | env: 131 | GPG_KEY_BASE64: ${{ secrets.GPG_KEY_BASE64 }} 132 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 133 | - name: Check if properly tagged 134 | run: | 135 | PACKAGE_VERSION="$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)"; 136 | CURRENT_TAG="${GITHUB_REF#refs/*/}"; 137 | if [[ "${PACKAGE_VERSION}" != "${CURRENT_TAG}" ]]; then 138 | >&2 echo "Tag mismatch" 139 | >&2 echo "Version in pom.xml (${PACKAGE_VERSION}) does not match the current tag=${CURRENT_TAG}" 140 | >&2 echo "Skipping deploy" 141 | exit 1; 142 | fi 143 | - name: 'Build & publish' 144 | run: | 145 | mvn clean \ 146 | compile \ 147 | org.apache.felix:maven-bundle-plugin:bundle \ 148 | deploy \ 149 | -Dmaven.test.skip=true \ 150 | -P release \ 151 | --settings settings.xml \ 152 | --no-transfer-progress \ 153 | --batch-mode 154 | env: 155 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 156 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 157 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 158 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target 3 | *.iml 4 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 1.8.8 2 | * Fixed transient dependencies for okhttp 3 | 4 | ## 1.8.7 5 | * Fixed Import-Package manifest for OSGI deployments 6 | 7 | ## 1.8.6 8 | * Fixed Import-Package manifest for OSGI deployments 9 | 10 | ## 1.8.5 11 | * Upgrade Okhttp to version 4.12.0 12 | 13 | ## 1.8.4 14 | * Upgrade Okio to version 3.7.0 15 | 16 | ## 1.8.3 17 | * Fix for requests with empty body 18 | 19 | ## 1.8.2 20 | * Fixed Import-Package manifest for OSGI deployments 21 | 22 | ## 1.8.0 23 | * Added new property extension(). 24 | * Added new methods convert(new Options().with("type", "image/webp")) and 25 | transform(new Options().with("background", "black")). 26 | 27 | ## 1.7.0 28 | * Updated dependent libraries. 29 | * Minimum java version is 1.8. 30 | 31 | ## 1.6.1 32 | * Fixes to depedency paths in OSGi imported packages. 33 | 34 | ## 1.6.0 35 | * As of this version dependencies are bundled making this package a valid OSGi bundle. 36 | 37 | ## 1.5.1 38 | * Properly close response body for requests where body is unused. 39 | * Migrate internals to OkHttp3 (minimum version required is 3.3.0). 40 | 41 | ## 1.5.0 42 | * Retry failed requests by default. 43 | 44 | ## 1.4.1 45 | * Bugfix: Avoid java.security.cert.CertPathValidatorException on Android. 46 | 47 | ## 1.4.0 48 | * Support for HTTP proxies. 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013-2022 Tinify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Maven Central](https://img.shields.io/maven-central/v/com.tinify/tinify.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.tinify%22%20AND%20a%3A%22tinify%22) 2 | [![MIT License](http://img.shields.io/badge/license-MIT-green.svg) ](https://github.com/tinify/tinify-java/blob/main/LICENSE) 3 | [![Java CI/CD](https://github.com/tinify/tinify-java/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/tinify/tinify-java/actions/workflows/ci-cd.yml) 4 | 5 | # Tinify API client for Java 6 | 7 | Java client for the Tinify API, used for [TinyPNG](https://tinypng.com) and [TinyJPG](https://tinyjpg.com). Tinify compresses your images intelligently. Read more at [http://tinify.com](http://tinify.com). 8 | 9 | ## Documentation 10 | 11 | [Go to the documentation for the Java client](https://tinypng.com/developers/reference/java). 12 | 13 | ## Installation 14 | 15 | Install the API client via Maven: 16 | 17 | ```xml 18 | 19 | com.tinify 20 | tinify 21 | 1.8.8 22 | 23 | ``` 24 | 25 | ## Usage 26 | 27 | ```java 28 | import com.tinify.*; 29 | import java.io.IOException; 30 | 31 | public class Compress { 32 | public static void main(String[] args) throws java.io.IOException { 33 | Tinify.setKey("YOUR_API_KEY"); 34 | Tinify.fromFile("unoptimized.png").toFile("optimized.png"); 35 | } 36 | } 37 | ``` 38 | 39 | ## Running tests 40 | 41 | ``` 42 | mvn test 43 | ``` 44 | 45 | ### Integration tests 46 | 47 | ``` 48 | TINIFY_KEY=$YOUR_API_KEY mvn -Pintegration integration-test 49 | ``` 50 | 51 | ## License 52 | 53 | This software is licensed under the MIT License. [View the license](LICENSE). 54 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.tinify 5 | tinify 6 | 1.8.8 7 | Tinify 8 | Java client for the Tinify API. Tinify compresses your images intelligently. Read more at https://tinify.com. 9 | https://tinify.com 10 | bundle 11 | 12 | UTF-8 13 | UTF-8 14 | 4.12.0 15 | 2.9.0 16 | 17 | 18 | 19 | MIT License 20 | http://www.opensource.org/licenses/mit-license.php 21 | 22 | 23 | 24 | 25 | Jippe Holwerda 26 | jippeholwerda@voormedia.com 27 | Voormedia B.V. 28 | http://voormedia.com 29 | 30 | 31 | Jari van den Berg 32 | jarivandenberg@voormedia.com 33 | Voormedia B.V. 34 | http://voormedia.com 35 | 36 | 37 | Rolf Timmermans 38 | rolftimmermans@voormedia.com 39 | Voormedia B.V. 40 | http://voormedia.com 41 | 42 | 43 | 44 | scm:git:https://github.com/tinify/tinify-java.git 45 | scm:git:https://github.com/tinify/tinify-java.git 46 | scm:git:https://github.com/tinify/tinify-java.git 47 | 48 | 49 | 50 | ossrh 51 | https://oss.sonatype.org/content/repositories/snapshots 52 | 53 | 54 | 55 | 56 | release 57 | 58 | 59 | 60 | org.apache.maven.plugins 61 | maven-source-plugin 62 | 3.2.0 63 | 64 | 65 | attach-sources 66 | 67 | jar-no-fork 68 | 69 | 70 | 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-javadoc-plugin 75 | 3.3.1 76 | 77 | 78 | attach-javadocs 79 | 80 | jar 81 | 82 | 83 | 84 | 85 | 86 | org.apache.maven.plugins 87 | maven-gpg-plugin 88 | 3.0.1 89 | 90 | 91 | sign-artifacts 92 | verify 93 | 94 | sign 95 | 96 | 97 | 98 | 99 | --pinentry-mode 100 | loopback 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | integration 111 | 112 | 113 | 114 | org.apache.maven.plugins 115 | maven-surefire-plugin 116 | 3.0.0 117 | 118 | true 119 | 120 | 121 | 122 | integration-tests 123 | integration-test 124 | 125 | test 126 | 127 | 128 | false 129 | 130 | **/Integration.java 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | com.squareup.okhttp3 143 | okhttp 144 | ${okhttp.version} 145 | compile 146 | 147 | 148 | org.jetbrains.kotlin 149 | kotlin-stdlib-jdk8 150 | 151 | 152 | 153 | 154 | com.squareup.okio 155 | okio 156 | 3.7.0 157 | compile 158 | 159 | 160 | com.google.code.gson 161 | gson 162 | 2.9.0 163 | compile 164 | 165 | 166 | org.jmockit 167 | jmockit 168 | 1.49 169 | test 170 | 171 | 172 | junit 173 | junit 174 | 4.13.2 175 | test 176 | 177 | 178 | org.hamcrest 179 | hamcrest-all 180 | 1.3 181 | test 182 | 183 | 184 | com.squareup.okhttp3 185 | mockwebserver 186 | ${okhttp.version} 187 | test 188 | 189 | 190 | commons-codec 191 | commons-codec 192 | 1.15 193 | test 194 | 195 | 196 | 197 | 198 | 199 | org.apache.maven.plugins 200 | maven-dependency-plugin 201 | 2.5.1 202 | 203 | 204 | copy-agent 205 | process-test-classes 206 | 207 | copy 208 | 209 | 210 | 211 | 212 | org.jmockit 213 | jmockit 214 | 1.49 215 | ${project.build.directory}/agents 216 | jmockit-1.49.jar 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | org.apache.maven.plugins 225 | maven-surefire-plugin 226 | 3.0.0 227 | 228 | -javaagent:"${project.build.directory}/agents/jmockit-1.49.jar" 229 | 230 | 231 | 232 | org.apache.maven.plugins 233 | maven-compiler-plugin 234 | 3.11.0 235 | 236 | 1.8 237 | 1.8 238 | 239 | -Xlint 240 | 241 | 242 | 243 | 244 | org.apache.maven.plugins 245 | maven-jar-plugin 246 | 3.2.2 247 | 248 | 249 | 250 | true 251 | true 252 | 253 | 254 | 255 | 256 | 257 | org.sonatype.plugins 258 | nexus-staging-maven-plugin 259 | 1.6.13 260 | true 261 | 262 | ossrh 263 | https://oss.sonatype.org/ 264 | true 265 | 266 | 267 | 268 | org.apache.felix 269 | maven-bundle-plugin 270 | 3.5.1 271 | true 272 | 273 | 274 | !com.tinify.*,!org.bouncycastle.jsse.*,!com.experian.b2b.global.mvdam-project.*,!android.*,!javax.annotation.meta.*,!org.conscrypt.*;resolution:=optional,* 275 | 276 | 277 | *;scope=compile|runtime 278 | 279 | true 280 | 281 | 282 | 283 | 284 | org.apache.maven.plugins 285 | maven-dependency-plugin 286 | 3.6.1 287 | 288 | 289 | 290 | 291 | -------------------------------------------------------------------------------- /settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ossrh 5 | ${env.SONATYPE_USERNAME} 6 | ${env.SONATYPE_PASSWORD} 7 | 8 | 9 | 10 | 11 | ossrh 12 | 13 | true 14 | 15 | 16 | 14B5D60C 17 | ${env.GPG_PASSPHRASE} 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/java/com/tinify/AccountException.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | public class AccountException extends Exception { 4 | public AccountException() { 5 | super(); 6 | } 7 | 8 | public AccountException(final Throwable t) { 9 | super(t); 10 | } 11 | 12 | public AccountException(final String message) { 13 | super(message); 14 | } 15 | 16 | public AccountException(final String message, final String type, final int status) { 17 | super(message, type, status); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/tinify/Client.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import okhttp3.OkHttpClient; 6 | import okhttp3.Authenticator; 7 | import okhttp3.Credentials; 8 | import okhttp3.Headers; 9 | import okhttp3.HttpUrl; 10 | import okhttp3.MediaType; 11 | import okhttp3.Request; 12 | import okhttp3.RequestBody; 13 | import okhttp3.ResponseBody; 14 | import okhttp3.Route; 15 | 16 | import java.io.IOException; 17 | import java.net.Proxy; 18 | import java.net.InetSocketAddress; 19 | import java.net.Proxy.Type; 20 | import java.net.URL; 21 | import java.util.concurrent.TimeUnit; 22 | 23 | public class Client { 24 | public class Response { 25 | public Headers headers; 26 | public byte[] body; 27 | 28 | public Response(Headers headers, byte[] body) { 29 | this.headers = headers; 30 | this.body = body; 31 | } 32 | } 33 | 34 | private OkHttpClient client; 35 | private String credentials; 36 | private String userAgent; 37 | 38 | public static final MediaType JSON 39 | = MediaType.parse("application/json; charset=utf-8"); 40 | 41 | public static final String API_ENDPOINT = "https://api.tinify.com"; 42 | 43 | public static final short RETRY_COUNT = 1; 44 | public static final short RETRY_DELAY = 500; 45 | 46 | public static final String USER_AGENT = "Tinify/" 47 | + Client.class.getPackage().getImplementationVersion() 48 | + " Java/" + System.getProperty("java.version") 49 | + " (" + System.getProperty("java.vendor") 50 | + ", " + System.getProperty("os.name") + " " + System.getProperty("os.arch") + ")"; 51 | 52 | public enum Method { 53 | POST, 54 | GET 55 | } 56 | 57 | public Client(final String key) { 58 | this(key, null, null); 59 | } 60 | 61 | public Client(final String key, final String appIdentifier) { 62 | this(key, appIdentifier, null); 63 | } 64 | 65 | public Client(final String key, final String appIdentifier, final String proxy) { 66 | OkHttpClient.Builder builder = new OkHttpClient.Builder(); 67 | 68 | if (proxy != null) { 69 | try { 70 | URL url = new URL(proxy); 71 | Proxy proxyAddress = createProxyAddress(url); 72 | Authenticator proxyAuthenticator = createProxyAuthenticator(url); 73 | 74 | if (proxyAddress != null) { 75 | builder.proxy(proxyAddress); 76 | if (proxyAuthenticator != null) { 77 | builder.proxyAuthenticator(proxyAuthenticator); 78 | } 79 | } 80 | } catch (java.lang.Exception e) { 81 | throw new ConnectionException("Invalid proxy: " + e.getMessage(), e); 82 | } 83 | } 84 | 85 | builder.sslSocketFactory(TLSContext.socketFactory, TLSContext.trustManager); 86 | builder.connectTimeout(0, TimeUnit.SECONDS); 87 | builder.readTimeout(0, TimeUnit.SECONDS); 88 | builder.writeTimeout(0, TimeUnit.SECONDS); 89 | 90 | client = builder.build(); 91 | 92 | credentials = Credentials.basic("api", key); 93 | if (appIdentifier == null) { 94 | userAgent = USER_AGENT; 95 | } else { 96 | userAgent = USER_AGENT + " " + appIdentifier; 97 | } 98 | } 99 | 100 | public final Response request(final Method method, final String endpoint) throws Exception { 101 | /* OkHttp does not support null request bodies if the method is POST. */ 102 | if (method.equals(Method.POST)) { 103 | return request(method, endpoint, RequestBody.create(null, new byte[] {})); 104 | } else { 105 | return request(method, endpoint, (RequestBody) null); 106 | } 107 | } 108 | 109 | public final Response request(final Method method, final String endpoint, final Options options) throws Exception { 110 | /* OkHttp does not support null request bodies if the method is POST. */ 111 | if (method.equals(Method.GET)) { 112 | return request(method, endpoint, options.isEmpty() ? null : RequestBody.create(JSON, options.toJson())); 113 | } else { 114 | return request(method, endpoint, RequestBody.create(JSON, options.toJson())); 115 | } 116 | } 117 | 118 | public final Response request(final Method method, final String endpoint, final byte[] body) throws Exception { 119 | return request(method, endpoint, RequestBody.create(null, body)); 120 | } 121 | 122 | private Proxy createProxyAddress(final URL proxy) { 123 | if (proxy == null) return null; 124 | 125 | String host = proxy.getHost(); 126 | int port = proxy.getPort(); 127 | 128 | if (port < 0) { 129 | port = proxy.getDefaultPort(); 130 | } 131 | 132 | return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port)); 133 | } 134 | 135 | private Authenticator createProxyAuthenticator(final URL proxy) { 136 | if (proxy == null) return null; 137 | 138 | String user = proxy.getUserInfo(); 139 | if (user == null) return null; 140 | 141 | final String username, password; 142 | int c = user.indexOf(':'); 143 | if (0 < c) { 144 | username = user.substring(0, c); 145 | password = user.substring(c + 1); 146 | } else { 147 | username = user; 148 | password = null; 149 | } 150 | 151 | return new Authenticator() { 152 | @Override public Request authenticate(Route route, okhttp3.Response response) throws IOException { 153 | String credential = Credentials.basic(username, password); 154 | return response.request().newBuilder().header("Proxy-Authorization", credential).build(); 155 | } 156 | }; 157 | } 158 | 159 | private Response request(final Method method, final String endpoint, final RequestBody body) throws Exception { 160 | HttpUrl url; 161 | if (endpoint.startsWith("https")) { 162 | url = HttpUrl.parse(endpoint); 163 | } else { 164 | url = HttpUrl.parse(API_ENDPOINT + endpoint); 165 | } 166 | 167 | for (short retries = RETRY_COUNT; retries >= 0; retries--) { 168 | if (retries < RETRY_COUNT) { 169 | try { 170 | Thread.sleep(RETRY_DELAY); 171 | } catch(InterruptedException ex) { 172 | Thread.currentThread().interrupt(); 173 | } 174 | } 175 | 176 | Request request = new Request.Builder() 177 | .header("Authorization", credentials) 178 | .header("User-Agent", userAgent) 179 | .url(url) 180 | .method(method.toString(), body) 181 | .build(); 182 | 183 | Response response; 184 | int status; 185 | 186 | try { 187 | okhttp3.Response res = client.newCall(request).execute(); 188 | status = res.code(); 189 | response = new Response(res.headers(), res.body().bytes()); 190 | } catch (java.lang.Exception e) { 191 | if (retries > 0) continue; 192 | throw new ConnectionException("Error while connecting: " + e.getMessage(), e); 193 | } 194 | 195 | String compressionCount = response.headers.get("Compression-Count"); 196 | if (compressionCount != null && !compressionCount.isEmpty()) { 197 | Tinify.setCompressionCount(Integer.valueOf(compressionCount)); 198 | } 199 | 200 | if (status >= 200 && status < 300) { 201 | return response; 202 | } 203 | 204 | Exception.Data data; 205 | Gson gson = new Gson(); 206 | try { 207 | data = gson.fromJson(new String(response.body), Exception.Data.class); 208 | if (data == null) { 209 | data = new Exception.Data(); 210 | data.setMessage("Error while parsing response: received empty body"); 211 | data.setError("ParseError"); 212 | } 213 | } catch (com.google.gson.JsonParseException e) { 214 | data = new Exception.Data(); 215 | data.setMessage("Error while parsing response: " + e.getMessage()); 216 | data.setError("ParseError"); 217 | } 218 | 219 | if (retries > 0 && status >= 500) continue; 220 | throw Exception.create( 221 | data.getMessage(), 222 | data.getError(), 223 | status); 224 | } 225 | 226 | return null; 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/main/java/com/tinify/ClientException.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | public class ClientException extends Exception { 4 | public ClientException() { 5 | super(); 6 | } 7 | 8 | public ClientException(final Throwable t) { 9 | super(t); 10 | } 11 | 12 | public ClientException(final String message, final String type, final int status) { 13 | super(message, type, status); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/tinify/ConnectionException.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | public class ConnectionException extends Exception { 4 | public ConnectionException() { 5 | super(); 6 | } 7 | 8 | public ConnectionException(final Throwable t) { 9 | super(t); 10 | } 11 | 12 | public ConnectionException(final String message, final Throwable t) { super(message, t); } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/tinify/Exception.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | public class Exception extends RuntimeException { 4 | public static class Data { 5 | private String message; 6 | private String error; 7 | 8 | public void setMessage(final String message) { 9 | this.message = message; 10 | } 11 | 12 | public final String getMessage() { 13 | return message; 14 | } 15 | 16 | public void setError(final String error) { 17 | this.error = error; 18 | } 19 | 20 | public final String getError() { 21 | return error; 22 | } 23 | } 24 | 25 | public static Exception create(final String message, final String type, final int status) { 26 | if (status == 401 || status == 429) { 27 | return new AccountException(message, type, status); 28 | } else if (status >= 400 && status <= 499) { 29 | return new ClientException(message, type, status); 30 | } else if (status >= 500 && status <= 599) { 31 | return new ServerException(message, type, status); 32 | } else { 33 | return new Exception(message, type, status); 34 | } 35 | } 36 | 37 | int status = 0; 38 | 39 | public Exception() { 40 | super(); 41 | } 42 | 43 | public Exception(final Throwable t) { 44 | super(t); 45 | } 46 | 47 | public Exception(final String message) { 48 | super(message); 49 | } 50 | 51 | public Exception(final String message, final Throwable t) { 52 | super(message, t); 53 | } 54 | 55 | public Exception(final String message, final String type, final int status) { 56 | super(message + " (HTTP " + status + "/" + type + ")"); 57 | this.status = status; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/tinify/Options.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | public final class Options { 9 | private Map options; 10 | 11 | public Options() { 12 | this.options = new HashMap<>(); 13 | } 14 | 15 | public Options(Options options) { 16 | this.options = new HashMap<>(options.options); 17 | } 18 | 19 | public Options with(final String key, final Object value) { 20 | this.options.put(key, value); 21 | return this; 22 | } 23 | 24 | public Options with(final String key, final Options options) { 25 | this.options.put(key, options.options); 26 | return this; 27 | } 28 | 29 | public final String toJson() { 30 | Gson gson = new Gson(); 31 | return gson.toJson(this.options); 32 | } 33 | 34 | public boolean isEmpty() { 35 | return this.options.isEmpty(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/tinify/Result.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Paths; 6 | import okhttp3.Headers; 7 | 8 | public class Result extends ResultMeta { 9 | private final byte[] data; 10 | 11 | public Result(final Headers meta, final byte[] data) { 12 | super(meta); 13 | this.data = data; 14 | } 15 | 16 | public void toFile(final String path) throws IOException { 17 | Files.write(Paths.get(path), toBuffer()); 18 | } 19 | 20 | public final byte[] toBuffer() { 21 | return data; 22 | } 23 | 24 | public final Integer size() { 25 | String value = meta.get("content-length"); 26 | return (value == null) ? null : Integer.parseInt(value); 27 | } 28 | 29 | public final String mediaType() { 30 | return meta.get("content-type"); 31 | } 32 | 33 | public final String extension() { 34 | return meta.get("content-type") == null ? null : meta.get("content-type").replaceFirst(".*/(.*)", "$1"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/tinify/ResultMeta.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | import okhttp3.Headers; 4 | 5 | public class ResultMeta { 6 | protected final Headers meta; 7 | 8 | public ResultMeta(final Headers meta) { 9 | this.meta = meta; 10 | } 11 | 12 | public final Integer width() { 13 | String value = meta.get("image-width"); 14 | return (value == null) ? null : Integer.parseInt(value); 15 | } 16 | 17 | public final Integer height() { 18 | String value = meta.get("image-height"); 19 | return (value == null) ? null : Integer.parseInt(value); 20 | } 21 | 22 | public final String location() { 23 | return meta.get("location"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/tinify/ServerException.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | public class ServerException extends Exception { 4 | public ServerException() { 5 | super(); 6 | } 7 | 8 | public ServerException(final Throwable t) { 9 | super(t); 10 | } 11 | 12 | public ServerException(final String message, final String type, final int status) { 13 | super(message, type, status); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/tinify/Source.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Paths; 6 | 7 | public class Source { 8 | private String url; 9 | private Options commands; 10 | 11 | public static Source fromFile(final String path) throws IOException { 12 | return fromBuffer(Files.readAllBytes(Paths.get(path))); 13 | } 14 | 15 | public static Source fromBuffer(final byte[] buffer) { 16 | Client.Response response = Tinify.client().request(Client.Method.POST, "/shrink", buffer); 17 | return new Source(response.headers.get("location"), new Options()); 18 | } 19 | 20 | public static Source fromUrl(final String url) { 21 | Options body = new Options().with("source", new Options().with("url", url)); 22 | Client.Response response = Tinify.client().request(Client.Method.POST, "/shrink", body); 23 | return new Source(response.headers.get("location"), new Options()); 24 | } 25 | 26 | public Source(final String url, final Options commands) { 27 | this.url = url; 28 | this.commands = commands; 29 | } 30 | 31 | public final Source preserve(final String... options) { 32 | return new Source(url, new Options(commands).with("preserve", options)); 33 | } 34 | 35 | public final Source resize(final Options options) { 36 | return new Source(url, new Options(commands).with("resize", options)); 37 | } 38 | 39 | public final Source convert(final Options options) { 40 | return new Source(url, new Options(commands).with("convert", options)); 41 | } 42 | 43 | public final Source transform(final Options options) { 44 | return new Source(url, new Options(commands).with("transform", options)); 45 | } 46 | 47 | public final ResultMeta store(final Options options) { 48 | Options params = new Options(commands).with("store", options); 49 | Client.Response response = Tinify.client().request(Client.Method.POST, url, params); 50 | return new ResultMeta(response.headers); 51 | } 52 | 53 | public final Result result() throws IOException { 54 | Client.Response response; 55 | if (commands == null || commands.isEmpty()) { 56 | response = Tinify.client().request(Client.Method.GET, url); 57 | } else { 58 | response = Tinify.client().request(Client.Method.POST, url, commands); 59 | } 60 | 61 | /* No need for try(Response response = ...): body().bytes() calls close(). */ 62 | return new Result(response.headers, response.body); 63 | } 64 | 65 | public void toFile(final String path) throws IOException { 66 | result().toFile(path); 67 | } 68 | 69 | public final byte[] toBuffer() throws IOException { 70 | return result().toBuffer(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/tinify/TLSContext.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | import javax.net.ssl.KeyManager; 4 | import javax.net.ssl.KeyManagerFactory; 5 | import javax.net.ssl.SSLSocketFactory; 6 | import javax.net.ssl.TrustManager; 7 | import javax.net.ssl.TrustManagerFactory; 8 | import javax.net.ssl.X509TrustManager; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.security.GeneralSecurityException; 12 | import java.security.KeyStore; 13 | import java.security.cert.Certificate; 14 | import java.security.cert.CertificateFactory; 15 | import java.util.Collection; 16 | 17 | public class TLSContext { 18 | public static SSLSocketFactory socketFactory; 19 | public static X509TrustManager trustManager; 20 | 21 | static { 22 | try { 23 | KeyStore keyStore = certificateKeyStore(); 24 | 25 | KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( 26 | KeyManagerFactory.getDefaultAlgorithm()); 27 | keyManagerFactory.init(keyStore, null); 28 | 29 | TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( 30 | TrustManagerFactory.getDefaultAlgorithm()); 31 | trustManagerFactory.init(keyStore); 32 | 33 | KeyManager[] keyManagers = keyManagerFactory.getKeyManagers(); 34 | TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); 35 | 36 | if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { 37 | throw new AssertionError("Unexpected default trust managers."); 38 | } 39 | 40 | javax.net.ssl.SSLContext sslContext = javax.net.ssl.SSLContext.getInstance("TLS"); 41 | sslContext.init(keyManagers, trustManagers, null); 42 | 43 | socketFactory = sslContext.getSocketFactory(); 44 | trustManager = (X509TrustManager) trustManagers[0]; 45 | 46 | } catch (GeneralSecurityException err) { 47 | throw new AssertionError("Unexpected error while configuring TLS. No TLS available?", err); 48 | } 49 | } 50 | 51 | private static KeyStore certificateKeyStore() throws GeneralSecurityException { 52 | try { 53 | KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 54 | 55 | /* By convention, a null InputStream creates an empty key store. */ 56 | keyStore.load(null, null); 57 | 58 | CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); 59 | Collection certificates = certificateFactory.generateCertificates(certificateStream()); 60 | 61 | int index = 0; 62 | for (Certificate certificate : certificates) { 63 | String certificateAlias = Integer.toString(index++); 64 | keyStore.setCertificateEntry(certificateAlias, certificate); 65 | } 66 | 67 | if (keyStore.size() == 0) { 68 | /* The resource stream was empty, no certificates were found. */ 69 | throw new AssertionError("Unable to load any CA certificates."); 70 | } 71 | 72 | return keyStore; 73 | } catch (IOException err) { 74 | throw new AssertionError(err); 75 | } 76 | } 77 | 78 | private static InputStream certificateStream() throws IOException { 79 | return TLSContext.class.getResourceAsStream("/cacert.pem"); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/tinify/Tinify.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | 6 | public class Tinify { 7 | private static String key; 8 | private static String appIdentifier; 9 | private static String proxy; 10 | private static int compressionCount = 0; 11 | private static Client client; 12 | 13 | public static Client client() { 14 | if (key == null) { 15 | throw new AccountException("Provide an API key with Tinify.setKey(...)"); 16 | } 17 | if (client != null) { 18 | return client; 19 | } else { 20 | synchronized (Tinify.class) { 21 | if (client == null) { 22 | client = new Client(key, appIdentifier, proxy); 23 | } 24 | } 25 | return client; 26 | } 27 | } 28 | 29 | public static void setKey(final String key) { 30 | Tinify.key = key; 31 | client = null; 32 | } 33 | 34 | public static void setProxy(final String proxy) { 35 | Tinify.proxy = proxy; 36 | client = null; 37 | } 38 | 39 | public static void setAppIdentifier(final String identifier) { 40 | Tinify.appIdentifier = identifier; 41 | client = null; 42 | } 43 | 44 | public static Source fromFile(final String path) throws IOException { 45 | return Source.fromFile(path); 46 | } 47 | 48 | public static Source fromBuffer(final byte[] buffer) { 49 | return Source.fromBuffer(buffer); 50 | } 51 | 52 | public static Source fromUrl(final String url) { 53 | return Source.fromUrl(url); 54 | } 55 | 56 | public static boolean validate() { 57 | try { 58 | client().request(Client.Method.POST, "/shrink"); 59 | } catch (AccountException ex) { 60 | if (ex.status == 429) return true; 61 | throw ex; 62 | } catch (ClientException ex) { 63 | return true; 64 | } 65 | return false; 66 | } 67 | 68 | public static String key() { 69 | return key; 70 | } 71 | 72 | public static String proxy() { 73 | return proxy; 74 | } 75 | 76 | public static String appIdentifier() { 77 | return appIdentifier; 78 | } 79 | 80 | public static void setCompressionCount(final int count) { 81 | compressionCount = count; 82 | } 83 | 84 | public static int compressionCount() { 85 | return compressionCount; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/com/tinify/ClientEndpointTest.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | import okhttp3.OkHttpClient; 4 | import okhttp3.Request; 5 | import mockit.Expectations; 6 | import mockit.Mocked; 7 | import mockit.Verifications; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | 11 | import java.io.IOException; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | 15 | public class ClientEndpointTest { 16 | Client subject; 17 | String key = "key"; 18 | 19 | @Mocked 20 | OkHttpClient httpClient; 21 | 22 | @Before 23 | public void setup() throws IOException { 24 | subject = new Client(key); 25 | } 26 | 27 | @Test 28 | public void requestShouldAddApiEndpoint() throws Exception { 29 | new Expectations() {{ 30 | httpClient.newCall((Request) any); 31 | result = new ConnectionException(); 32 | }}; 33 | 34 | try { 35 | subject.request(Client.Method.POST, "/shrink", new byte[] {}); 36 | } catch(Exception e) { 37 | // not interested in result 38 | } 39 | new Verifications() {{ 40 | Request request; 41 | httpClient.newCall(request = withCapture()); 42 | assertEquals("https://api.tinify.com/shrink", request.url().toString()); 43 | }}; 44 | } 45 | 46 | @Test 47 | public void requestShouldNotAddApiEndpoint() throws Exception { 48 | final String url = "https://api.tinify.com/output/13259adadfq3.png"; 49 | 50 | new Expectations() {{ 51 | httpClient.newCall((Request) any); 52 | result = new ConnectionException(); 53 | }}; 54 | 55 | try { 56 | subject.request(Client.Method.POST, url, new byte[] {}); 57 | } catch(Exception e) { 58 | // not interested in result 59 | } 60 | 61 | new Verifications() {{ 62 | Request request; 63 | httpClient.newCall(request = withCapture()); 64 | assertEquals(url, request.url().toString()); 65 | }}; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/com/tinify/ClientErrorTest.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | import okhttp3.mockwebserver.MockWebServer; 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import java.io.IOException; 7 | import java.util.logging.Level; 8 | import java.util.logging.Logger; 9 | 10 | public class ClientErrorTest { 11 | Client subject; 12 | MockWebServer server; 13 | String key = "key"; 14 | 15 | @Before 16 | public void setup() throws IOException { 17 | Logger.getLogger(MockWebServer.class.getName()).setLevel(Level.WARNING); 18 | server = new MockWebServer(); 19 | subject = new Client(key); 20 | server.start(); 21 | } 22 | 23 | @After 24 | public void tearDown() throws IOException { 25 | server.shutdown(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/tinify/ClientTest.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | import com.google.gson.Gson; 4 | import mockit.Invocation; 5 | import mockit.Mock; 6 | import mockit.MockUp; 7 | import okhttp3.HttpUrl; 8 | import okhttp3.Call; 9 | import okhttp3.Response; 10 | import okhttp3.mockwebserver.MockResponse; 11 | import okhttp3.mockwebserver.MockWebServer; 12 | import okhttp3.mockwebserver.RecordedRequest; 13 | import org.apache.commons.codec.binary.Base64; 14 | import org.junit.After; 15 | import org.junit.Before; 16 | import org.junit.Test; 17 | 18 | import java.io.IOException; 19 | import java.net.URISyntaxException; 20 | import java.nio.file.Files; 21 | import java.nio.file.Paths; 22 | import java.util.HashMap; 23 | import java.util.concurrent.TimeUnit; 24 | import java.util.logging.Level; 25 | import java.util.logging.Logger; 26 | 27 | import static org.junit.Assert.assertEquals; 28 | import static org.junit.Assert.fail; 29 | 30 | public class ClientTest { 31 | Client subject; 32 | MockWebServer server; 33 | String key = "key"; 34 | 35 | @Before 36 | public void setup() throws IOException { 37 | Logger.getLogger(MockWebServer.class.getName()).setLevel(Level.WARNING); 38 | 39 | server = new MockWebServer(); 40 | server.start(); 41 | subject = new Client(key); 42 | new MockUp() 43 | { 44 | @Mock 45 | @SuppressWarnings("unused") 46 | HttpUrl parse(Invocation inv, String url) 47 | { 48 | if (url.contains("localhost")) { 49 | return inv.proceed(); 50 | } else { 51 | return new HttpUrl.Builder() 52 | .scheme("http") 53 | .host(server.getHostName()) 54 | .port(server.getPort()) 55 | .encodedPath("/shrink") 56 | .build(); 57 | } 58 | } 59 | }; 60 | } 61 | 62 | @After 63 | public void tearDown() throws IOException { 64 | server.shutdown(); 65 | } 66 | 67 | public void enqueuShrink() { 68 | server.enqueue(new MockResponse() 69 | .setResponseCode(201) 70 | .addHeader("Location", "https://api.tinify.com/foo.png") 71 | .addHeader("Compression-Count", 12)); 72 | } 73 | 74 | @Test 75 | public void requestWhenValidShouldIssueRequest() throws Exception, InterruptedException { 76 | enqueuShrink(); 77 | subject.request(Client.Method.POST, "/shrink"); 78 | String credentials = new String(Base64.encodeBase64(("api:" + key).getBytes())); 79 | 80 | RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS); 81 | assertEquals("Basic " + credentials, request.getHeader("Authorization")); 82 | } 83 | 84 | @Test 85 | public void requestWhenValidShouldIssueRequestToEndpoint() throws Exception, InterruptedException { 86 | enqueuShrink(); 87 | subject.request(Client.Method.POST, "/shrink"); 88 | RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS); 89 | assertEquals("/shrink", request.getPath()); 90 | } 91 | 92 | @Test 93 | public void requestWhenValidShouldIssueRequestWithMethod() throws Exception, InterruptedException { 94 | enqueuShrink(); 95 | subject.request(Client.Method.POST, "/shrink"); 96 | RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS); 97 | assertEquals("POST", request.getMethod()); 98 | } 99 | 100 | @Test 101 | public void requestWhenValidShouldReturnResponse() throws Exception, InterruptedException, IOException, URISyntaxException { 102 | enqueuShrink(); 103 | 104 | byte[] body = Files.readAllBytes( 105 | Paths.get(getClass().getResource("/voormedia.png").toURI())); 106 | 107 | Client.Response response = subject.request(Client.Method.POST, "/shrink", body); 108 | assertEquals("https://api.tinify.com/foo.png", response.headers.get("Location")); 109 | } 110 | 111 | @Test 112 | public void requestWhenValidShouldIssueRequestWithoutBodyWhenOptionsAreEmpty() throws Exception, InterruptedException { 113 | enqueuShrink(); 114 | subject.request(Client.Method.GET, "/shrink", new Options()); 115 | RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS); 116 | assertEquals(0, request.getBody().size()); 117 | } 118 | 119 | @Test 120 | public void requestWhenValidShouldIssueRequestWithoutContentTypeWhenOptionsAreEmpty() throws Exception, InterruptedException { 121 | enqueuShrink(); 122 | subject.request(Client.Method.GET, "/shrink", new Options()); 123 | RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS); 124 | assertEquals(null, request.getHeader("Content-Type")); 125 | } 126 | 127 | @Test 128 | public void requestWhenValidShouldIssueRequestWithJSONBody() throws Exception, InterruptedException { 129 | enqueuShrink(); 130 | subject.request(Client.Method.POST, "/shrink", new Options().with("hello", "world")); 131 | RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS); 132 | Gson gson = new Gson(); 133 | assertEquals("world", gson.fromJson(request.getBody().readUtf8(), HashMap.class).get("hello")); 134 | assertEquals("application/json; charset=utf-8", request.getHeader("Content-Type")); 135 | } 136 | 137 | @Test 138 | public void requestWhenValidShouldIssueRequestWithUserAgent() throws Exception, InterruptedException { 139 | enqueuShrink(); 140 | subject.request(Client.Method.POST, "/shrink", new byte[] {}); 141 | RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS); 142 | assertEquals(Client.USER_AGENT, request.getHeader("User-Agent")); 143 | } 144 | 145 | @Test 146 | public void requestWhenValidShouldUpdateCompressionCount() throws Exception { 147 | enqueuShrink(); 148 | subject.request(Client.Method.POST, "/shrink", new byte[] {}); 149 | assertEquals(12, Tinify.compressionCount()); 150 | } 151 | 152 | @Test 153 | public void requestWhenValidWithAppIdShouldIssueRequestWithUserAgent() throws Exception, InterruptedException { 154 | enqueuShrink(); 155 | Client client = new Client(key, "TestApp/0.1"); 156 | client.request(Client.Method.POST, "/shrink"); 157 | RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS); 158 | assertEquals(Client.USER_AGENT + " TestApp/0.1", request.getHeader("User-Agent")); 159 | } 160 | 161 | @Test 162 | public void requestWhenValidWithProxyShouldIssueRequestWithProxyAuthorization() throws Exception, InterruptedException { 163 | server.enqueue(new MockResponse().setResponseCode(407)); 164 | enqueuShrink(); 165 | Client client = new Client(key, null, "http://user:pass@" + server.getHostName() + ":" + server.getPort()); 166 | client.request(Client.Method.POST, "/shrink"); 167 | RecordedRequest request1 = server.takeRequest(5, TimeUnit.SECONDS); 168 | RecordedRequest request2 = server.takeRequest(5, TimeUnit.SECONDS); 169 | assertEquals("Basic dXNlcjpwYXNz", request2.getHeader("Proxy-Authorization")); 170 | } 171 | 172 | @Test 173 | public void requestWithTimeoutOnceShouldReturnResponse() throws Exception { 174 | new MockUp() { 175 | int count = 1; 176 | 177 | @Mock 178 | public Response execute(Invocation inv) throws IOException { 179 | if (count == 0) { 180 | return inv.proceed(); 181 | } else { 182 | count--; 183 | throw new java.net.SocketTimeoutException("SocketTimeoutException"); 184 | } 185 | } 186 | }; 187 | 188 | server.enqueue(new MockResponse() 189 | .setResponseCode(201) 190 | .setBody("ok")); 191 | 192 | Client.Response response = new Client(key).request(Client.Method.POST, "/shrink"); 193 | assertEquals("ok", new String(response.body)); 194 | } 195 | 196 | @Test(expected = ConnectionException.class) 197 | public void requestWithTimeoutRepeatedlyShouldThrowConnectionException() throws Exception { 198 | new MockUp() { 199 | @Mock 200 | public Response execute(Invocation inv) throws IOException { 201 | throw new java.net.SocketTimeoutException("SocketTimeoutException"); 202 | } 203 | }; 204 | 205 | new Client(key).request(Client.Method.POST, "http://shrink"); 206 | } 207 | 208 | @Test 209 | public void requestWithTimeoutRepeatedlyShouldThrowExceptionWithMessage() throws Exception { 210 | new MockUp() { 211 | @Mock 212 | public Response execute(Invocation inv) throws IOException { 213 | throw new java.net.SocketTimeoutException("SocketTimeoutException"); 214 | } 215 | }; 216 | 217 | try { 218 | new Client(key).request(Client.Method.POST, "http://shrink"); 219 | fail("Expected an Exception to be thrown"); 220 | } catch (ConnectionException e) { 221 | assertEquals("Error while connecting: SocketTimeoutException", e.getMessage()); 222 | } 223 | } 224 | 225 | @Test 226 | public void requestWithSocketErrorOnceShouldReturnResponse() throws Exception { 227 | new MockUp() { 228 | int count = 1; 229 | 230 | @Mock 231 | public Response execute(Invocation inv) throws IOException { 232 | if (count == 0) { 233 | return inv.proceed(); 234 | } else { 235 | count--; 236 | throw new java.net.UnknownHostException("UnknownHostException"); 237 | } 238 | } 239 | }; 240 | 241 | server.enqueue(new MockResponse() 242 | .setResponseCode(201) 243 | .setBody("ok")); 244 | 245 | Client.Response response = new Client(key).request(Client.Method.POST, "/shrink"); 246 | assertEquals("ok", new String(response.body)); 247 | } 248 | 249 | @Test(expected = ConnectionException.class) 250 | public void requestWithSocketErrorRepeatedlyShouldThrowConnectionException() throws Exception { 251 | new MockUp() { 252 | @Mock 253 | public Response execute(Invocation inv) throws IOException { 254 | throw new java.net.UnknownHostException("UnknownHostException"); 255 | } 256 | }; 257 | 258 | new Client(key).request(Client.Method.POST, "http://shrink"); 259 | } 260 | 261 | @Test 262 | public void requestWithSocketErrorRepeatedlyShouldThrowExceptionWithMessage() throws Exception { 263 | new MockUp() { 264 | @Mock 265 | public Response execute(Invocation inv) throws IOException { 266 | throw new java.net.UnknownHostException("UnknownHostException"); 267 | } 268 | }; 269 | 270 | try { 271 | new Client(key).request(Client.Method.POST, "http://shrink"); 272 | fail("Expected an Exception to be thrown"); 273 | } catch (ConnectionException e) { 274 | assertEquals("Error while connecting: UnknownHostException", e.getMessage()); 275 | } 276 | } 277 | 278 | @Test 279 | public void requestWithUnexpectedExceptionOnceShouldReturnResponse() throws Exception { 280 | new MockUp() { 281 | int count = 1; 282 | 283 | @Mock 284 | public Response execute(Invocation inv) throws IOException { 285 | if (count == 0) { 286 | return inv.proceed(); 287 | } else { 288 | count--; 289 | throw new RuntimeException("Some exception"); 290 | } 291 | } 292 | }; 293 | 294 | server.enqueue(new MockResponse() 295 | .setResponseCode(201) 296 | .setBody("ok")); 297 | 298 | Client.Response response = new Client(key).request(Client.Method.POST, "/shrink"); 299 | assertEquals("ok", new String(response.body)); 300 | } 301 | 302 | @Test(expected = ConnectionException.class) 303 | public void requestWithUnexpectedExceptionRepeatedlyShouldThrowConnectionException() throws Exception { 304 | new MockUp() { 305 | @Mock 306 | public Response execute(Invocation inv) throws IOException { 307 | throw new RuntimeException("Some exception"); 308 | } 309 | }; 310 | 311 | new Client(key).request(Client.Method.POST, "http://shrink"); 312 | } 313 | 314 | @Test 315 | public void requestWithUnexpectedExceptionRepeatedlyShouldThrowExceptionWithMessage() throws Exception { 316 | new MockUp() { 317 | @Mock 318 | public Response execute(Invocation inv) throws IOException { 319 | throw new RuntimeException("Some exception"); 320 | } 321 | }; 322 | 323 | try { 324 | new Client(key).request(Client.Method.POST, "http://shrink"); 325 | fail("Expected an Exception to be thrown"); 326 | } catch (ConnectionException e) { 327 | assertEquals("Error while connecting: Some exception", e.getMessage()); 328 | } 329 | } 330 | 331 | @Test 332 | public void requestWithServerErrorOnceShouldReturnResponse() throws Exception { 333 | server.enqueue(new MockResponse() 334 | .setResponseCode(584) 335 | .setBody("{'error':'InternalServerError','message':'Oops!'}")); 336 | server.enqueue(new MockResponse() 337 | .setResponseCode(201) 338 | .setBody("ok")); 339 | 340 | Client.Response response = new Client(key).request(Client.Method.POST, "/shrink"); 341 | assertEquals("ok", new String(response.body)); 342 | } 343 | 344 | @Test(expected = ServerException.class) 345 | public void requestWithServerErrorRepeatedlyShouldThrowServerException() throws Exception { 346 | server.enqueue(new MockResponse() 347 | .setResponseCode(584) 348 | .setBody("{'error':'InternalServerError','message':'Oops!'}")); 349 | server.enqueue(new MockResponse() 350 | .setResponseCode(584) 351 | .setBody("{'error':'InternalServerError','message':'Oops!'}")); 352 | 353 | new Client(key).request(Client.Method.POST, "/shrink"); 354 | } 355 | 356 | @Test 357 | public void requestWithServerErrorRepeatedlyShouldThrowExceptionWithMessage() throws Exception { 358 | server.enqueue(new MockResponse() 359 | .setResponseCode(584) 360 | .setBody("{'error':'InternalServerError','message':'Oops!'}")); 361 | server.enqueue(new MockResponse() 362 | .setResponseCode(584) 363 | .setBody("{'error':'InternalServerError','message':'Oops!'}")); 364 | 365 | try { 366 | new Client(key).request(Client.Method.POST, "/shrink"); 367 | fail("Expected an Exception to be thrown"); 368 | } catch (Exception e) { 369 | assertEquals("Oops! (HTTP 584/InternalServerError)", e.getMessage()); 370 | } 371 | } 372 | 373 | @Test 374 | public void requestWithBadServerResponseOnceShouldReturnResponse() throws Exception { 375 | server.enqueue(new MockResponse() 376 | .setResponseCode(543) 377 | .setBody("")); 378 | server.enqueue(new MockResponse() 379 | .setResponseCode(201) 380 | .setBody("ok")); 381 | 382 | Client.Response response = new Client(key).request(Client.Method.POST, "/shrink"); 383 | assertEquals("ok", new String(response.body)); 384 | } 385 | 386 | @Test(expected = ServerException.class) 387 | public void requestWithBadServerResponseRepeatedlyShouldThrowServerException() throws Exception { 388 | server.enqueue(new MockResponse() 389 | .setResponseCode(543) 390 | .setBody("")); 391 | server.enqueue(new MockResponse() 392 | .setResponseCode(543) 393 | .setBody("")); 394 | 395 | new Client(key).request(Client.Method.POST, "/shrink"); 396 | } 397 | 398 | @Test 399 | public void requestWithBlankServerResponseRepeatedlyShouldThrowServerException() throws Exception { 400 | server.enqueue(new MockResponse() 401 | .setResponseCode(543) 402 | .setBody("")); 403 | server.enqueue(new MockResponse() 404 | .setResponseCode(543) 405 | .setBody("")); 406 | try { 407 | new Client(key).request(Client.Method.POST, "/shrink"); 408 | fail("Expected an Exception to be thrown"); 409 | } catch (Exception e) { 410 | assertEquals("Error while parsing response: received empty body (HTTP 543/ParseError)", e.getMessage()); 411 | } 412 | } 413 | 414 | @Test 415 | public void requestWithBadServerResponseRepeatedlyShouldThrowExceptionWithMessage() throws Exception { 416 | server.enqueue(new MockResponse() 417 | .setResponseCode(543) 418 | .setBody("")); 419 | server.enqueue(new MockResponse() 420 | .setResponseCode(543) 421 | .setBody("")); 422 | 423 | try { 424 | new Client(key).request(Client.Method.POST, "/shrink"); 425 | fail("Expected an Exception to be thrown"); 426 | } catch (Exception e) { 427 | assertEquals("Error while parsing response: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $ (HTTP 543/ParseError)", e.getMessage()); 428 | } 429 | } 430 | 431 | @Test(expected = ClientException.class) 432 | public void requestWithClientErrorShouldThrowClientException() throws Exception { 433 | server.enqueue(new MockResponse() 434 | .setResponseCode(492) 435 | .setBody("{'error':'BadRequest','message':'Oops!'}")); 436 | 437 | new Client(key).request(Client.Method.POST, "/shrink"); 438 | } 439 | 440 | @Test 441 | public void requestWithClientErrorShouldThrowExceptionWithMessage() throws Exception { 442 | server.enqueue(new MockResponse() 443 | .setResponseCode(492) 444 | .setBody("{'error':'BadRequest','message':'Oops!'}")); 445 | 446 | try { 447 | new Client(key).request(Client.Method.POST, "/shrink"); 448 | fail("Expected an Exception to be thrown"); 449 | } catch (Exception e) { 450 | assertEquals("Oops! (HTTP 492/BadRequest)", e.getMessage()); 451 | } 452 | } 453 | 454 | @Test(expected = AccountException.class) 455 | public void requestWithBadCredentialsShouldThrowAccountException() throws Exception { 456 | server.enqueue(new MockResponse() 457 | .setResponseCode(401) 458 | .setBody("{'error':'Unauthorized','message':'Oops!'}")); 459 | 460 | new Client(key).request(Client.Method.POST, "/shrink"); 461 | } 462 | 463 | @Test 464 | public void requestWithBadCredentialsShouldThrowExceptionWithMessage() throws Exception { 465 | server.enqueue(new MockResponse() 466 | .setResponseCode(401) 467 | .setBody("{'error':'Unauthorized','message':'Oops!'}")); 468 | 469 | try { 470 | new Client(key).request(Client.Method.POST, "/shrink"); 471 | fail("Expected an Exception to be thrown"); 472 | } catch (Exception e) { 473 | assertEquals("Oops! (HTTP 401/Unauthorized)", e.getMessage()); 474 | } 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /src/test/java/com/tinify/Integration.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | import org.junit.BeforeClass; 4 | import org.junit.Test; 5 | 6 | import java.net.URISyntaxException; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.io.File; 10 | import java.nio.file.Paths; 11 | 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static org.hamcrest.Matchers.containsString; 14 | import static org.hamcrest.Matchers.equalTo; 15 | import static org.hamcrest.Matchers.greaterThan; 16 | import static org.hamcrest.Matchers.is; 17 | import static org.hamcrest.Matchers.lessThan; 18 | import static org.hamcrest.Matchers.not; 19 | 20 | public class Integration { 21 | private static Source optimized; 22 | 23 | @BeforeClass 24 | public static void setup() throws java.io.IOException, URISyntaxException { 25 | String key = System.getenv().get("TINIFY_KEY"); 26 | String proxy = System.getenv().get("TINIFY_PROXY"); 27 | if (key == null) { 28 | System.out.println("Set the TINIFY_KEY environment variable."); 29 | System.exit(1); 30 | } 31 | 32 | Tinify.setKey(key); 33 | Tinify.setProxy(proxy); 34 | 35 | String unoptimizedPath = Paths.get(Integration.class.getResource("/voormedia.png").toURI()).toAbsolutePath().toString(); 36 | optimized = Tinify.fromFile(unoptimizedPath); 37 | } 38 | 39 | @Test 40 | public void shouldCompressFromFile() throws java.lang.Exception { 41 | Path tempFile = Files.createTempFile("tinify_", null); 42 | tempFile.toFile().deleteOnExit(); 43 | 44 | Result result = optimized.result(); 45 | result.toFile(tempFile.toString()); 46 | 47 | long size = new File(tempFile.toString()).length(); 48 | String contents = new String(Files.readAllBytes(Paths.get(tempFile.toString()))); 49 | 50 | assertThat(result.width(), is(equalTo(137))); 51 | assertThat(result.height(), is(equalTo(21))); 52 | 53 | assertThat(size, greaterThan((long) 1000)); 54 | assertThat(size, lessThan((long) 1500)); 55 | 56 | /* width == 137 */ 57 | assertThat(contents, containsString(new String(new byte[] {0, 0, 0, (byte)0x89}))); 58 | assertThat(contents, not(containsString(("Copyright Voormedia")))); 59 | } 60 | 61 | @Test 62 | public void shouldCompressFromUrl() throws java.lang.Exception { 63 | Path tempFile = Files.createTempFile("tinify_", null); 64 | tempFile.toFile().deleteOnExit(); 65 | 66 | optimized = Tinify.fromUrl("https://raw.githubusercontent.com/tinify/tinify-java/master/src/test/resources/voormedia.png"); 67 | 68 | Result result = optimized.result(); 69 | result.toFile(tempFile.toString()); 70 | 71 | long size = new File(tempFile.toString()).length(); 72 | String contents = new String(Files.readAllBytes(Paths.get(tempFile.toString()))); 73 | 74 | assertThat(result.width(), is(equalTo(137))); 75 | assertThat(result.height(), is(equalTo(21))); 76 | 77 | assertThat(size, greaterThan((long) 1000)); 78 | assertThat(size, lessThan((long) 1500)); 79 | 80 | /* width == 137 */ 81 | assertThat(contents, containsString(new String(new byte[] {0, 0, 0, (byte)0x89}))); 82 | assertThat(contents, not(containsString(("Copyright Voormedia")))); 83 | } 84 | 85 | @Test 86 | public void shouldResize() throws java.lang.Exception { 87 | Path tempFile = Files.createTempFile("tinify_", null); 88 | tempFile.toFile().deleteOnExit(); 89 | 90 | Options options = new Options() 91 | .with("method", "fit") 92 | .with("width", 50) 93 | .with("height", 20); 94 | Result result = optimized.resize(options).result(); 95 | result.toFile(tempFile.toString()); 96 | 97 | long size = new File(tempFile.toString()).length(); 98 | String contents = new String(Files.readAllBytes(Paths.get(tempFile.toString()))); 99 | 100 | assertThat(result.width(), is(equalTo(50))); 101 | assertThat(result.height(), is(equalTo(8))); 102 | 103 | assertThat(size, greaterThan((long) 500)); 104 | assertThat(size, lessThan((long) 1000)); 105 | 106 | /* width == 50 */ 107 | assertThat(contents, containsString(new String(new byte[] {0, 0, 0, (byte)0x32}))); 108 | assertThat(contents, not(containsString(("Copyright Voormedia")))); 109 | } 110 | 111 | @Test 112 | public void shouldPreserveMetadata() throws java.lang.Exception { 113 | Path tempFile = Files.createTempFile("tinify_", null); 114 | tempFile.toFile().deleteOnExit(); 115 | 116 | Result result = optimized.preserve("copyright", "creation").result(); 117 | result.toFile(tempFile.toString()); 118 | 119 | long size = new File(tempFile.toString()).length(); 120 | String contents = new String(Files.readAllBytes(Paths.get(tempFile.toString()))); 121 | 122 | assertThat(result.width(), is(equalTo(137))); 123 | assertThat(result.height(), is(equalTo(21))); 124 | 125 | assertThat(size, greaterThan((long) 1000)); 126 | assertThat(size, lessThan((long) 2000)); 127 | 128 | /* width == 137 */ 129 | assertThat(contents, containsString(new String(new byte[] {0, 0, 0, (byte)0x89}))); 130 | assertThat(contents, containsString(("Copyright Voormedia"))); 131 | } 132 | 133 | @Test 134 | public void shouldConvertFile() throws java.lang.Exception { 135 | Path tempFile = Files.createTempFile("tinify_", null); 136 | tempFile.toFile().deleteOnExit(); 137 | 138 | Result result = optimized.convert(new Options().with("type", "image/webp")).result(); 139 | result.toFile(tempFile.toString()); 140 | 141 | long size = new File(tempFile.toString()).length(); 142 | 143 | assertThat(result.width(), is(equalTo(137))); 144 | assertThat(result.height(), is(equalTo(21))); 145 | assertThat(result.mediaType(), is(equalTo("image/webp"))); 146 | 147 | assertThat(size, greaterThan((long) 1000)); 148 | assertThat(size, lessThan((long) 2000)); 149 | } 150 | 151 | @Test 152 | public void shouldTransformFile() throws java.lang.Exception { 153 | Path tempFile = Files.createTempFile("tinify_", null); 154 | tempFile.toFile().deleteOnExit(); 155 | Result result = optimized.transform(new Options().with("background", "black")).result(); 156 | result.toFile(tempFile.toString()); 157 | 158 | long size = new File(tempFile.toString()).length(); 159 | String contents = new String(Files.readAllBytes(Paths.get(tempFile.toString()))); 160 | 161 | assertThat(result.width(), is(equalTo(137))); 162 | assertThat(result.height(), is(equalTo(21))); 163 | 164 | assertThat(size, greaterThan((long) 1000)); 165 | assertThat(size, lessThan((long) 2000)); 166 | 167 | /* width == 137 */ 168 | assertThat(contents, containsString(new String(new byte[] {0, 0, 0, (byte)0x89}))); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/test/java/com/tinify/ResultMetaTest.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import java.util.HashMap; 7 | import okhttp3.Headers; 8 | 9 | import static org.hamcrest.CoreMatchers.equalTo; 10 | import static org.hamcrest.CoreMatchers.is; 11 | import static org.hamcrest.CoreMatchers.nullValue; 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | 14 | public class ResultMetaTest { 15 | ResultMeta subject; 16 | 17 | @Before 18 | public void setup() { 19 | HashMap meta = new HashMap<>(); 20 | meta.put("Image-Width", "100"); 21 | meta.put("Image-Height", "60"); 22 | meta.put("Location", "https://example.com/image.png"); 23 | 24 | subject = new ResultMeta(Headers.of(meta)); 25 | } 26 | 27 | @Test 28 | public void withMetadataWidthShouldReturnImageWidth() { 29 | assertThat(subject.width(), is(equalTo(100))); 30 | } 31 | 32 | @Test 33 | public void withMetadataHeightShouldReturnImageHeight() { 34 | assertThat(subject.height(), is(equalTo(60))); 35 | } 36 | 37 | @Test 38 | public void withMetadataLocationShouldReturnLocation() { 39 | assertThat(subject.location(), is(equalTo("https://example.com/image.png"))); 40 | } 41 | 42 | @Test 43 | public void withoutMetadataWidthShouldReturnNull() { 44 | subject = new ResultMeta(Headers.of()); 45 | assertThat(subject.width(), is(nullValue())); 46 | } 47 | 48 | @Test 49 | public void withoutMetadataHeightShouldReturnNull() { 50 | subject = new ResultMeta(Headers.of()); 51 | assertThat(subject.height(), is(nullValue())); 52 | } 53 | 54 | @Test 55 | public void withoutMetadataLocationShouldReturnNull() { 56 | subject = new ResultMeta(Headers.of()); 57 | assertThat(subject.location(), is(nullValue())); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/com/tinify/ResultTest.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import java.util.HashMap; 7 | import okhttp3.Headers; 8 | 9 | import static org.hamcrest.CoreMatchers.equalTo; 10 | import static org.hamcrest.CoreMatchers.is; 11 | import static org.hamcrest.CoreMatchers.nullValue; 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | 14 | public class ResultTest { 15 | Result subject; 16 | 17 | @Before 18 | public void setup() { 19 | HashMap meta = new HashMap<>(); 20 | meta.put("Image-Width", "100"); 21 | meta.put("Image-Height", "60"); 22 | meta.put("Content-Length", "450"); 23 | meta.put("Content-Type", "image/png"); 24 | 25 | subject = new Result(Headers.of(meta), "image data".getBytes()); 26 | } 27 | 28 | @Test 29 | public void withMetaAndDataWidthShouldReturnImageWidth() { 30 | assertThat(subject.width(), is(equalTo(100))); 31 | } 32 | 33 | @Test 34 | public void withMetaAndDataHeightShouldReturnImageHeight() { 35 | assertThat(subject.height(), is(equalTo(60))); 36 | } 37 | 38 | @Test 39 | public void withMetaAndDataLocationShouldReturnNull() { 40 | assertThat(subject.location(), is(nullValue())); 41 | } 42 | 43 | @Test 44 | public void withMetaAndDataSizeShouldReturnContentLength() { 45 | assertThat(subject.size(), is(equalTo(450))); 46 | } 47 | 48 | @Test 49 | public void withMetaAndDataMediaTypeShouldReturnContentType() { 50 | assertThat(subject.mediaType(), is(equalTo("image/png"))); 51 | } 52 | 53 | @Test 54 | public void withMetaAndDataToBufferShouldReturnImageData() { 55 | assertThat(subject.toBuffer(), is(equalTo("image data".getBytes()))); 56 | } 57 | 58 | @Test 59 | public void withMetaAndDataMediaTypeShouldReturnExtension() { 60 | assertThat(subject.extension(), is(equalTo("png"))); 61 | } 62 | 63 | @Test 64 | public void withoutMetaAndDataWidthShouldReturnNull() { 65 | subject = new Result(Headers.of(), null); 66 | assertThat(subject.width(), is(nullValue())); 67 | } 68 | 69 | @Test 70 | public void withoutMetaAndDataHeightShouldReturnNull() { 71 | subject = new Result(Headers.of(), null); 72 | assertThat(subject.height(), is(nullValue())); 73 | } 74 | 75 | @Test 76 | public void withoutMetaAndDataLocationShouldReturnNull() { 77 | subject = new Result(Headers.of(), null); 78 | assertThat(subject.location(), is(nullValue())); 79 | } 80 | 81 | @Test 82 | public void withoutMetaAndDataSizeShouldReturnNull() { 83 | subject = new Result(Headers.of(), null); 84 | assertThat(subject.size(), is(nullValue())); 85 | } 86 | 87 | @Test 88 | public void withoutMetaAndDataContentTypeShouldReturnNull() { 89 | subject = new Result(Headers.of(), null); 90 | assertThat(subject.mediaType(), is(nullValue())); 91 | } 92 | 93 | @Test 94 | public void withoutMetaAndDataToBufferShouldReturnNull() { 95 | subject = new Result(Headers.of(), null); 96 | assertThat(subject.toBuffer(), is(nullValue())); 97 | } 98 | 99 | @Test 100 | public void withoutMetaAndDataMediaTypeShouldReturnNull() { 101 | subject = new Result(Headers.of(), null); 102 | assertThat(subject.extension(), is(nullValue())); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/com/tinify/SourceTest.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | import com.google.gson.Gson; 4 | import mockit.Invocation; 5 | import mockit.Mock; 6 | import mockit.MockUp; 7 | import okhttp3.HttpUrl; 8 | import okhttp3.mockwebserver.MockResponse; 9 | import okhttp3.mockwebserver.MockWebServer; 10 | import okhttp3.mockwebserver.RecordedRequest; 11 | import org.junit.After; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | 15 | import java.io.IOException; 16 | import java.net.URISyntaxException; 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.nio.file.Paths; 20 | import java.util.Map; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.logging.Level; 23 | import java.util.logging.Logger; 24 | 25 | import static org.hamcrest.MatcherAssert.assertThat; 26 | import static org.hamcrest.Matchers.equalTo; 27 | import static org.hamcrest.Matchers.is; 28 | import static org.hamcrest.Matchers.isA; 29 | import static org.junit.Assert.assertEquals; 30 | 31 | public class SourceTest { 32 | MockWebServer server; 33 | 34 | @Before 35 | public void setup() throws IOException { 36 | Logger.getLogger(MockWebServer.class.getName()).setLevel(Level.WARNING); 37 | 38 | server = new MockWebServer(); 39 | server.start(); 40 | new MockUp() 41 | { 42 | @Mock 43 | @SuppressWarnings("unused") 44 | HttpUrl parse(Invocation inv, String url) 45 | { 46 | if (url.contains("localhost")) { 47 | return inv.proceed(); 48 | } else { 49 | return new HttpUrl.Builder() 50 | .scheme("http") 51 | .host(server.getHostName()) 52 | .port(server.getPort()) 53 | .encodedPath("/shrink") 54 | .build(); 55 | } 56 | } 57 | }; 58 | } 59 | 60 | @After 61 | public void tearDown() throws IOException { 62 | server.shutdown(); 63 | } 64 | 65 | public void assertJsonEquals(String expected, String actual) 66 | { 67 | Gson gson = new Gson(); 68 | @SuppressWarnings("unchecked") 69 | Map expectedMap = gson.fromJson(expected, Map.class); 70 | @SuppressWarnings("unchecked") 71 | Map actualMap = gson.fromJson(actual, Map.class); 72 | 73 | assertEquals(expectedMap, actualMap); 74 | } 75 | 76 | @Test(expected = AccountException.class) 77 | public void withInvalidApiKeyFromFileShouldThrowAccountException() throws Exception, IOException, URISyntaxException { 78 | Tinify.setKey("invalid"); 79 | 80 | server.enqueue(new MockResponse() 81 | .setResponseCode(401) 82 | .setBody("{'error':'Unauthorized','message':'Credentials are invalid'}")); 83 | 84 | String filePath = Paths.get(getClass().getResource("/dummy.png").toURI()).toAbsolutePath().toString(); 85 | Source.fromFile(filePath); 86 | } 87 | 88 | @Test(expected = AccountException.class) 89 | public void withInvalidApiKeyFromBufferShouldThrowAccountException() throws Exception, IOException { 90 | Tinify.setKey("invalid"); 91 | 92 | server.enqueue(new MockResponse() 93 | .setResponseCode(401) 94 | .setBody("{'error':'Unauthorized','message':'Credentials are invalid'}")); 95 | 96 | Source.fromBuffer("png file".getBytes()); 97 | } 98 | 99 | @Test(expected = AccountException.class) 100 | public void withInvalidApiKeyFromUrlShouldThrowAccountException() throws Exception, IOException { 101 | Tinify.setKey("invalid"); 102 | 103 | server.enqueue(new MockResponse() 104 | .setResponseCode(401) 105 | .setBody("{'error':'Unauthorized','message':'Credentials are invalid'}")); 106 | 107 | Source.fromUrl("http://example.com/test.jpg"); 108 | } 109 | 110 | @Test 111 | public void withValidApiKeyFromFileShouldReturnSource() throws IOException, Exception, URISyntaxException { 112 | Tinify.setKey("valid"); 113 | 114 | server.enqueue(new MockResponse() 115 | .setResponseCode(201) 116 | .addHeader("Location", "https://api.tinify.com/some/location") 117 | .addHeader("Compression-Count", 12)); 118 | 119 | String filePath = Paths.get(getClass().getResource("/dummy.png").toURI()).toAbsolutePath().toString(); 120 | assertThat(Source.fromFile(filePath), isA(Source.class)); 121 | } 122 | 123 | @Test 124 | public void withValidApiKeyFromFileShouldReturnSourceWithData() throws IOException, Exception, URISyntaxException { 125 | Tinify.setKey("valid"); 126 | 127 | server.enqueue(new MockResponse() 128 | .setResponseCode(201) 129 | .addHeader("Location", "https://api.tinify.com/some/location")); 130 | 131 | server.enqueue(new MockResponse() 132 | .setResponseCode(200) 133 | .setBody("compressed file")); 134 | 135 | String filePath = Paths.get(getClass().getResource("/dummy.png").toURI()).toAbsolutePath().toString(); 136 | assertThat(Source.fromFile(filePath).toBuffer(), is(equalTo("compressed file".getBytes()))); 137 | } 138 | 139 | @Test 140 | public void withValidApiKeyFromBufferShouldReturnSource() throws IOException, Exception { 141 | Tinify.setKey("valid"); 142 | 143 | server.enqueue(new MockResponse() 144 | .setResponseCode(201) 145 | .addHeader("Location", "https://api.tinify.com/some/location") 146 | .addHeader("Compression-Count", 12)); 147 | 148 | assertThat(Source.fromBuffer("png file".getBytes()), isA(Source.class)); 149 | } 150 | 151 | @Test 152 | public void withValidApiKeyFromBufferShouldReturnSourceWithData() throws IOException, Exception { 153 | Tinify.setKey("valid"); 154 | 155 | server.enqueue(new MockResponse() 156 | .setResponseCode(201) 157 | .addHeader("Location", "https://api.tinify.com/some/location")); 158 | 159 | server.enqueue(new MockResponse() 160 | .setResponseCode(200) 161 | .setBody("compressed file")); 162 | 163 | assertThat(Source.fromBuffer("png file".getBytes()).toBuffer(), 164 | is(equalTo("compressed file".getBytes()))); 165 | } 166 | 167 | @Test 168 | public void withValidApiKeyFromUrlShouldReturnSource() throws IOException, Exception { 169 | Tinify.setKey("valid"); 170 | 171 | server.enqueue(new MockResponse() 172 | .setResponseCode(201) 173 | .addHeader("Location", "https://api.tinify.com/some/location") 174 | .addHeader("Compression-Count", 12)); 175 | 176 | assertThat(Source.fromUrl("http://example.com/test.jpg"), isA(Source.class)); 177 | } 178 | 179 | @Test 180 | public void withValidApiKeyFromUrlShouldReturnSourceWithData() throws IOException, Exception, InterruptedException { 181 | Tinify.setKey("valid"); 182 | 183 | server.enqueue(new MockResponse() 184 | .setResponseCode(201) 185 | .addHeader("Location", "https://api.tinify.com/some/location")); 186 | 187 | server.enqueue(new MockResponse() 188 | .setResponseCode(200) 189 | .setBody("compressed file")); 190 | 191 | assertThat(Source.fromUrl("http://example.com/test.jpg").toBuffer(), 192 | is(equalTo("compressed file".getBytes()))); 193 | 194 | RecordedRequest request1 = server.takeRequest(3, TimeUnit.SECONDS); 195 | assertJsonEquals("{\"source\":{\"url\":\"http://example.com/test.jpg\"}}", request1.getBody().readUtf8()); 196 | } 197 | 198 | @Test(expected = ClientException.class) 199 | public void withValidApiKeyFromUrlShouldThrowExceptionIfRequestIsNotOK() throws IOException, Exception { 200 | Tinify.setKey("valid"); 201 | 202 | server.enqueue(new MockResponse() 203 | .setResponseCode(400) 204 | .setBody("{'error':'Source not found','message':'Cannot parse URL'}")); 205 | 206 | Source.fromUrl("file://wrong"); 207 | } 208 | 209 | @Test 210 | public void withValidApiKeyResultShouldReturnResult() throws Exception, IOException { 211 | Tinify.setKey("valid"); 212 | 213 | server.enqueue(new MockResponse() 214 | .setResponseCode(201) 215 | .addHeader("Location", "https://api.tinify.com/some/location") 216 | .addHeader("Compression-Count", 12)); 217 | 218 | server.enqueue(new MockResponse() 219 | .setResponseCode(200) 220 | .setBody("compressed file")); 221 | 222 | assertThat(Source.fromBuffer("png file".getBytes()).result(), 223 | isA(Result.class)); 224 | } 225 | 226 | @Test 227 | public void withValidApiKeyPreserveShouldReturnSource() throws Exception, InterruptedException { 228 | Tinify.setKey("valid"); 229 | 230 | server.enqueue(new MockResponse() 231 | .setResponseCode(201) 232 | .addHeader("Location", "https://api.tinify.com/some/location")); 233 | 234 | server.enqueue(new MockResponse() 235 | .setResponseCode(200) 236 | .setBody("copyrighted file")); 237 | 238 | assertThat(Source.fromBuffer("png file".getBytes()).preserve("copyright", "location"), 239 | isA(Source.class)); 240 | 241 | RecordedRequest request1 = server.takeRequest(3, TimeUnit.SECONDS); 242 | assertEquals("png file", request1.getBody().readUtf8()); 243 | } 244 | 245 | @Test 246 | public void withValidApiKeyConvertShouldReturnSource() throws Exception, InterruptedException { 247 | Tinify.setKey("valid"); 248 | 249 | server.enqueue(new MockResponse() 250 | .setResponseCode(201) 251 | .addHeader("Location", "https://api.tinify.com/some/location")); 252 | 253 | server.enqueue(new MockResponse() 254 | .setResponseCode(200) 255 | .setBody("copyrighted file")); 256 | 257 | assertThat(Source.fromBuffer("png file".getBytes()).convert(new Options().with("type", "image/webp")), 258 | isA(Source.class)); 259 | 260 | RecordedRequest request1 = server.takeRequest(3, TimeUnit.SECONDS); 261 | assertEquals("png file", request1.getBody().readUtf8()); 262 | } 263 | 264 | @Test 265 | public void withValidApiKeyPreserveShouldReturnSourceWithData() throws Exception, IOException, InterruptedException { 266 | Tinify.setKey("valid"); 267 | 268 | server.enqueue(new MockResponse() 269 | .setResponseCode(201) 270 | .addHeader("Location", "https://api.tinify.com/some/location")); 271 | 272 | server.enqueue(new MockResponse() 273 | .setResponseCode(200) 274 | .setBody("copyrighted file")); 275 | 276 | assertThat(Source.fromBuffer("png file".getBytes()).preserve("copyright", "location").toBuffer(), 277 | is(equalTo("copyrighted file".getBytes()))); 278 | 279 | RecordedRequest request1 = server.takeRequest(3, TimeUnit.SECONDS); 280 | assertEquals("png file", request1.getBody().readUtf8()); 281 | 282 | RecordedRequest request2 = server.takeRequest(3, TimeUnit.SECONDS); 283 | assertJsonEquals("{\"preserve\":[\"copyright\",\"location\"]}", request2.getBody().readUtf8()); 284 | } 285 | 286 | @Test 287 | public void withValidApiKeyPreserveShouldReturnSourceWithDataForArray() throws Exception, IOException, InterruptedException { 288 | Tinify.setKey("valid"); 289 | 290 | server.enqueue(new MockResponse() 291 | .setResponseCode(201) 292 | .addHeader("Location", "https://api.tinify.com/some/location")); 293 | 294 | server.enqueue(new MockResponse() 295 | .setResponseCode(200) 296 | .setBody("copyrighted file")); 297 | 298 | String[] options = new String [] {"copyright", "location"}; 299 | assertThat(Source.fromBuffer("png file".getBytes()).preserve(options).toBuffer(), 300 | is(equalTo("copyrighted file".getBytes()))); 301 | 302 | RecordedRequest request1 = server.takeRequest(3, TimeUnit.SECONDS); 303 | assertEquals("png file", request1.getBody().readUtf8()); 304 | 305 | RecordedRequest request2 = server.takeRequest(3, TimeUnit.SECONDS); 306 | assertJsonEquals("{\"preserve\":[\"copyright\",\"location\"]}", request2.getBody().readUtf8()); 307 | } 308 | 309 | @Test 310 | public void withValidApiKeyPreserveShouldIncludeOtherOptionsIfSet() throws Exception, IOException, InterruptedException { 311 | Tinify.setKey("valid"); 312 | 313 | server.enqueue(new MockResponse() 314 | .setResponseCode(201) 315 | .addHeader("Location", "https://api.tinify.com/some/location")); 316 | 317 | server.enqueue(new MockResponse() 318 | .setResponseCode(200) 319 | .setBody("copyrighted resized file")); 320 | 321 | Options resizeOptions = new Options().with("width", 100).with("height", 60); 322 | String[] preserveOptions = new String [] {"copyright", "location"}; 323 | assertThat(Source.fromBuffer("png file".getBytes()).resize(resizeOptions).preserve(preserveOptions).toBuffer(), 324 | is(equalTo("copyrighted resized file".getBytes()))); 325 | 326 | RecordedRequest request1 = server.takeRequest(3, TimeUnit.SECONDS); 327 | assertEquals("png file", request1.getBody().readUtf8()); 328 | 329 | RecordedRequest request2 = server.takeRequest(3, TimeUnit.SECONDS); 330 | assertJsonEquals("{\"resize\":{\"width\":100,\"height\":60},\"preserve\":[\"copyright\",\"location\"]}", request2.getBody().readUtf8()); 331 | } 332 | 333 | @Test 334 | public void withValidApiKeyResizeShouldReturnSource() throws Exception, InterruptedException { 335 | Tinify.setKey("valid"); 336 | 337 | server.enqueue(new MockResponse() 338 | .setResponseCode(201) 339 | .addHeader("Location", "https://api.tinify.com/some/location")); 340 | 341 | server.enqueue(new MockResponse() 342 | .setResponseCode(200) 343 | .setBody("small file")); 344 | 345 | Options options = new Options().with("width", 100).with("height", 60); 346 | 347 | assertThat(Source.fromBuffer("png file".getBytes()).resize(options), 348 | isA(Source.class)); 349 | 350 | RecordedRequest request1 = server.takeRequest(3, TimeUnit.SECONDS); 351 | assertEquals("png file", request1.getBody().readUtf8()); 352 | } 353 | 354 | @Test 355 | public void withValidApiKeyResizeShouldReturnSourceWithData() throws Exception, IOException, InterruptedException { 356 | Tinify.setKey("valid"); 357 | 358 | server.enqueue(new MockResponse() 359 | .setResponseCode(201) 360 | .addHeader("Location", "https://api.tinify.com/some/location")); 361 | 362 | server.enqueue(new MockResponse() 363 | .setResponseCode(200) 364 | .setBody("small file")); 365 | 366 | Options options = new Options().with("width", 100).with("height", 60); 367 | 368 | assertThat(Source.fromBuffer("png file".getBytes()).resize(options).toBuffer(), 369 | is(equalTo("small file".getBytes()))); 370 | 371 | RecordedRequest request1 = server.takeRequest(3, TimeUnit.SECONDS); 372 | assertEquals("png file", request1.getBody().readUtf8()); 373 | 374 | RecordedRequest request2 = server.takeRequest(3, TimeUnit.SECONDS); 375 | assertJsonEquals("{\"resize\":{\"width\":100,\"height\":60}}", request2.getBody().readUtf8()); 376 | } 377 | 378 | @Test 379 | public void withValidApiKeyTransformShouldReturnSourceWithData() throws Exception, IOException, InterruptedException { 380 | Tinify.setKey("valid"); 381 | 382 | server.enqueue(new MockResponse() 383 | .setResponseCode(201) 384 | .addHeader("Location", "https://api.tinify.com/some/location")); 385 | 386 | server.enqueue(new MockResponse() 387 | .setResponseCode(200) 388 | .setBody("small file")); 389 | 390 | Options options = new Options().with("background", "black"); 391 | 392 | assertThat(Source.fromBuffer("png file".getBytes()).transform(options).toBuffer(), 393 | is(equalTo("small file".getBytes()))); 394 | 395 | RecordedRequest request1 = server.takeRequest(3, TimeUnit.SECONDS); 396 | assertEquals("png file", request1.getBody().readUtf8()); 397 | 398 | RecordedRequest request2 = server.takeRequest(3, TimeUnit.SECONDS); 399 | assertJsonEquals("{\"transform\":{\"background\":\"black\"}}", request2.getBody().readUtf8()); 400 | } 401 | 402 | @Test 403 | public void withValidApiKeyStoreShouldReturnResultMeta() throws Exception, InterruptedException { 404 | Tinify.setKey("valid"); 405 | 406 | server.enqueue(new MockResponse() 407 | .setResponseCode(201) 408 | .addHeader("Location", "https://api.tinify.com/some/location") 409 | .addHeader("Compression-Count", 12)); 410 | 411 | server.enqueue(new MockResponse() 412 | .setResponseCode(200) 413 | .addHeader("Location", "https://bucket.s3.amazonaws.com/example")); 414 | 415 | Options options = new Options().with("service", "s3"); 416 | 417 | assertThat(Source.fromBuffer("png file".getBytes()).store(options), 418 | isA(ResultMeta.class)); 419 | 420 | RecordedRequest request1 = server.takeRequest(3, TimeUnit.SECONDS); 421 | assertEquals("png file", request1.getBody().readUtf8()); 422 | 423 | RecordedRequest request2 = server.takeRequest(3, TimeUnit.SECONDS); 424 | assertJsonEquals("{\"store\":{\"service\":\"s3\"}}", request2.getBody().readUtf8()); 425 | } 426 | 427 | @Test 428 | public void withValidApiKeyStoreShouldReturnResultMetaWithLocation() throws Exception, InterruptedException { 429 | Tinify.setKey("valid"); 430 | 431 | server.enqueue(new MockResponse() 432 | .setResponseCode(201) 433 | .addHeader("Location", "https://api.tinify.com/some/location") 434 | .addHeader("Compression-Count", 12)); 435 | 436 | server.enqueue(new MockResponse() 437 | .setResponseCode(200) 438 | .addHeader("Location", "https://bucket.s3.amazonaws.com/example")); 439 | 440 | Options options = new Options().with("service", "s3"); 441 | 442 | assertEquals("https://bucket.s3.amazonaws.com/example", 443 | Source.fromBuffer("png file".getBytes()).store(options).location()); 444 | 445 | RecordedRequest request1 = server.takeRequest(3, TimeUnit.SECONDS); 446 | assertEquals("png file", request1.getBody().readUtf8()); 447 | 448 | RecordedRequest request2 = server.takeRequest(3, TimeUnit.SECONDS); 449 | assertJsonEquals("{\"store\":{\"service\":\"s3\"}}", request2.getBody().readUtf8()); 450 | } 451 | 452 | @Test 453 | public void withValidApiKeyStoreShouldIncludeOtherOptionsIfSet() throws Exception, IOException, InterruptedException { 454 | Tinify.setKey("valid"); 455 | 456 | server.enqueue(new MockResponse() 457 | .setResponseCode(201) 458 | .addHeader("Location", "https://api.tinify.com/some/location") 459 | .addHeader("Compression-Count", 12)); 460 | 461 | server.enqueue(new MockResponse() 462 | .setResponseCode(200) 463 | .setBody("compressed file")); 464 | 465 | server.enqueue(new MockResponse() 466 | .setResponseCode(200) 467 | .addHeader("Location", "https://bucket.s3.amazonaws.com/example")); 468 | 469 | Options resizeOptions = new Options().with("width", 100); 470 | Options storeOptions = new Options().with("service", "s3"); 471 | 472 | Source.fromBuffer("png file".getBytes()).resize(resizeOptions).store(storeOptions); 473 | 474 | RecordedRequest request1 = server.takeRequest(3, TimeUnit.SECONDS); 475 | assertEquals("png file", request1.getBody().readUtf8()); 476 | 477 | RecordedRequest request2 = server.takeRequest(3, TimeUnit.SECONDS); 478 | assertJsonEquals("{\"resize\":{\"width\":100},\"store\":{\"service\":\"s3\"}}", request2.getBody().readUtf8()); 479 | } 480 | 481 | @Test 482 | public void withValidApiKeyToBufferShouldReturnImageData() throws Exception, IOException { 483 | Tinify.setKey("valid"); 484 | 485 | server.enqueue(new MockResponse() 486 | .setResponseCode(201) 487 | .addHeader("Location", "https://api.tinify.com/some/location") 488 | .addHeader("Compression-Count", 12)); 489 | 490 | server.enqueue(new MockResponse() 491 | .setResponseCode(200) 492 | .setBody("compressed file")); 493 | 494 | assertThat(Source.fromBuffer("png file".getBytes()).toBuffer(), 495 | is(equalTo("compressed file".getBytes()))); 496 | } 497 | 498 | @Test 499 | public void withValidApiKeyToFileShouldStoreImageData() throws Exception, IOException { 500 | Tinify.setKey("valid"); 501 | 502 | server.enqueue(new MockResponse() 503 | .setResponseCode(201) 504 | .addHeader("Location", "https://api.tinify.com/some/location") 505 | .addHeader("Compression-Count", 12)); 506 | 507 | server.enqueue(new MockResponse() 508 | .setResponseCode(200) 509 | .setBody("compressed file")); 510 | 511 | Path tempFile = Files.createTempFile("tinify_", null); 512 | tempFile.toFile().deleteOnExit(); 513 | 514 | Source.fromBuffer("png file".getBytes()).toFile(tempFile.toString()); 515 | 516 | assertThat(Files.readAllBytes(tempFile), 517 | is(equalTo("compressed file".getBytes()))); 518 | } 519 | 520 | 521 | /* 522 | * The following tests should probably be done with parametrized tests 523 | */ 524 | @Test 525 | public void withOptionsNotEmptyResultDoesAPOST() throws Exception, IOException, InterruptedException { 526 | Tinify.setKey("valid"); 527 | 528 | server.enqueue(new MockResponse() 529 | .setResponseCode(200) 530 | .setBody("compressed file")); 531 | Result result = new Source("https://api.tinify.com/some/location", new Options().with("I am not", "empty")).result(); 532 | RecordedRequest outputRequest = server.takeRequest(1, TimeUnit.SECONDS); 533 | assertEquals("POST", outputRequest.getMethod()); 534 | } 535 | 536 | @Test 537 | public void withOptionsNULLResultDoesAGET() throws Exception, IOException, InterruptedException { 538 | Tinify.setKey("valid"); 539 | 540 | server.enqueue(new MockResponse() 541 | .setResponseCode(200) 542 | .setBody("compressed file")); 543 | Result result = new Source("https://api.tinify.com/some/location", null).result(); 544 | RecordedRequest outputRequest = server.takeRequest(1, TimeUnit.SECONDS); 545 | assertEquals("GET", outputRequest.getMethod()); 546 | } 547 | 548 | @Test 549 | public void withOptionsEmptyResultDoesAGET() throws Exception, IOException, InterruptedException { 550 | Tinify.setKey("valid"); 551 | 552 | server.enqueue(new MockResponse() 553 | .setResponseCode(200) 554 | .setBody("compressed file")); 555 | Result result = new Source("https://api.tinify.com/some/location", new Options()).result(); 556 | RecordedRequest outputRequest = server.takeRequest(1, TimeUnit.SECONDS); 557 | assertEquals("GET", outputRequest.getMethod()); 558 | } 559 | } -------------------------------------------------------------------------------- /src/test/java/com/tinify/TinifyTest.java: -------------------------------------------------------------------------------- 1 | package com.tinify; 2 | 3 | import mockit.Invocation; 4 | import mockit.Mock; 5 | import mockit.MockUp; 6 | import okhttp3.HttpUrl; 7 | import okhttp3.mockwebserver.MockResponse; 8 | import okhttp3.mockwebserver.MockWebServer; 9 | import okhttp3.mockwebserver.RecordedRequest; 10 | import org.apache.commons.codec.binary.Base64; 11 | import org.junit.After; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | 15 | import java.io.IOException; 16 | import java.net.URISyntaxException; 17 | import java.nio.file.Paths; 18 | import java.util.concurrent.TimeUnit; 19 | import java.util.logging.Level; 20 | import java.util.logging.Logger; 21 | 22 | import static org.hamcrest.CoreMatchers.is; 23 | import static org.hamcrest.CoreMatchers.isA; 24 | import static org.hamcrest.MatcherAssert.assertThat; 25 | import static org.junit.Assert.assertEquals; 26 | 27 | public class TinifyTest { 28 | MockWebServer server; 29 | 30 | @Before 31 | public void setup() throws IOException { 32 | Logger.getLogger(MockWebServer.class.getName()).setLevel(Level.WARNING); 33 | 34 | server = new MockWebServer(); 35 | server.start(); 36 | new MockUp() 37 | { 38 | @Mock 39 | @SuppressWarnings("unused") 40 | HttpUrl parse(Invocation inv, String url) 41 | { 42 | if (url.contains("localhost")) { 43 | return inv.proceed(); 44 | } else { 45 | return new HttpUrl.Builder() 46 | .scheme("http") 47 | .host(server.getHostName()) 48 | .port(server.getPort()) 49 | .encodedPath(url.replaceFirst(".*(/.*)", "$1")) 50 | .build(); 51 | } 52 | } 53 | }; 54 | } 55 | 56 | @After 57 | public void tearDown() throws IOException { 58 | Tinify.setKey(null); 59 | Tinify.setProxy(null); 60 | server.shutdown(); 61 | } 62 | 63 | @Test 64 | public void keyShouldResetClientWithNewKey() throws Exception, InterruptedException { 65 | server.enqueue(new MockResponse().setResponseCode(200)); 66 | 67 | Tinify.setKey("abcde"); 68 | Tinify.client(); 69 | Tinify.setKey("fghij"); 70 | Tinify.client().request(Client.Method.GET, "/"); 71 | 72 | RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS); 73 | 74 | String credentials = new String(Base64.encodeBase64(("api:fghij").getBytes())); 75 | assertEquals("Basic " + credentials, request.getHeader("Authorization")); 76 | } 77 | 78 | @Test 79 | public void appIdentifierShouldResetClientWithNewAppIdentifier() throws InterruptedException { 80 | server.enqueue(new MockResponse().setResponseCode(200)); 81 | 82 | Tinify.setKey("abcde"); 83 | Tinify.setAppIdentifier("MyApp/1.0"); 84 | Tinify.client(); 85 | Tinify.setAppIdentifier("MyApp/2.0"); 86 | Tinify.client().request(Client.Method.GET, "/"); 87 | 88 | RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS); 89 | assertEquals(Client.USER_AGENT + " MyApp/2.0", request.getHeader("User-Agent")); 90 | } 91 | 92 | @Test 93 | public void proxyShouldResetClientWithNewProxy() throws InterruptedException { 94 | server.enqueue(new MockResponse().setResponseCode(407)); 95 | server.enqueue(new MockResponse().setResponseCode(200)); 96 | 97 | Tinify.setKey("abcde"); 98 | Tinify.setProxy("http://localhost"); 99 | Tinify.client(); 100 | Tinify.setProxy("http://user:pass@" + server.getHostName() + ":" + server.getPort()); 101 | Tinify.client().request(Client.Method.GET, "/"); 102 | 103 | RecordedRequest request1 = server.takeRequest(5, TimeUnit.SECONDS); 104 | RecordedRequest request2 = server.takeRequest(5, TimeUnit.SECONDS); 105 | assertEquals("Basic dXNlcjpwYXNz", request2.getHeader("Proxy-Authorization")); 106 | } 107 | 108 | @Test 109 | public void clientWithKeyShouldReturnClient() { 110 | Tinify.setKey("abcde"); 111 | assertThat(Tinify.client(), isA(Client.class)); 112 | } 113 | 114 | @Test(expected = AccountException.class) 115 | public void clientWithoutKeyShouldThrowException() { 116 | Tinify.client(); 117 | } 118 | 119 | @Test(expected = ConnectionException.class) 120 | public void clientWithInvalidProxyShouldThrowException() { 121 | Tinify.setKey("abcde"); 122 | Tinify.setProxy("http-bad-url"); 123 | Tinify.client(); 124 | } 125 | 126 | @Test 127 | public void validateWithValidKeyShouldReturnTrue() throws InterruptedException { 128 | server.enqueue(new MockResponse() 129 | .setResponseCode(400) 130 | .setBody("{'error':'Input missing','message':'No input'}")); 131 | 132 | Tinify.setKey("valid"); 133 | assertThat(Tinify.validate(), is(true)); 134 | 135 | RecordedRequest request = server.takeRequest(); 136 | assertEquals("POST /shrink HTTP/1.1", request.getRequestLine()); 137 | assertEquals(0, request.getBody().size()); 138 | } 139 | 140 | @Test 141 | public void validateWithLimitedKeyShouldReturnTrue() throws InterruptedException { 142 | server.enqueue(new MockResponse() 143 | .setResponseCode(429) 144 | .setBody("{'error':'Too many requests','message':'Your monthly limit has been exceeded'}")); 145 | 146 | Tinify.setKey("valid"); 147 | assertThat(Tinify.validate(), is(true)); 148 | 149 | RecordedRequest request = server.takeRequest(); 150 | assertEquals("POST /shrink HTTP/1.1", request.getRequestLine()); 151 | assertEquals(0, request.getBody().size()); 152 | } 153 | 154 | @Test(expected = AccountException.class) 155 | public void validateWithErrorShouldThrowException() throws InterruptedException { 156 | server.enqueue(new MockResponse() 157 | .setResponseCode(401) 158 | .setBody("{'error':'Unauthorized','message':'Credentials are invalid'}")); 159 | 160 | Tinify.setKey("invalid"); 161 | Tinify.validate(); 162 | 163 | RecordedRequest request = server.takeRequest(); 164 | assertEquals("POST /shrink HTTP/1.1", request.getRequestLine()); 165 | assertEquals(0, request.getBody().size()); 166 | } 167 | 168 | @Test 169 | public void fromBufferShouldReturnSource() { 170 | Tinify.setKey("valid"); 171 | 172 | server.enqueue(new MockResponse() 173 | .setResponseCode(201) 174 | .addHeader("Location", "https://api.tinify.com/some/location") 175 | .addHeader("Compression-Count", 12)); 176 | 177 | assertThat(Tinify.fromBuffer("png file".getBytes()), isA(Source.class)); 178 | } 179 | 180 | @Test 181 | public void fromFileShouldReturnSource() throws IOException, URISyntaxException { 182 | Tinify.setKey("valid"); 183 | 184 | server.enqueue(new MockResponse() 185 | .setResponseCode(201) 186 | .addHeader("Location", "https://api.tinify.com/some/location") 187 | .addHeader("Compression-Count", 12)); 188 | 189 | String filePath = Paths.get(getClass().getResource("/dummy.png").toURI()).toAbsolutePath().toString(); 190 | assertThat(Tinify.fromFile(filePath), isA(Source.class)); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/test/resources/dummy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinify/tinify-java/a18df055003a2312f463986d58e880df0329bf10/src/test/resources/dummy.png -------------------------------------------------------------------------------- /src/test/resources/voormedia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinify/tinify-java/a18df055003a2312f463986d58e880df0329bf10/src/test/resources/voormedia.png -------------------------------------------------------------------------------- /update-cacert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | dir=src/main/resources 3 | 4 | cert=0 5 | curl --silent --fail https://curl.se/ca/cacert.pem | while read -r line; do 6 | if [ "-----BEGIN CERTIFICATE-----" == "$line" ]; then 7 | cert=1 8 | echo "$line" 9 | elif [ "-----END CERTIFICATE-----" == "$line" ]; then 10 | cert=0 11 | echo "$line" 12 | else 13 | if [ $cert == 1 ]; then 14 | echo "$line" 15 | fi 16 | fi 17 | done > "$dir/cacert.pem" 18 | --------------------------------------------------------------------------------