From 0a288fa2901dfaf527a78b99f3d6e97567c3a32d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Fri, 16 Dec 2016 01:47:26 +0100 Subject: [PATCH] Increase unit test coverage --- .../shredzone/acme4j/challenge/Challenge.java | 20 ++++- .../challenge/OutOfBand01Challenge.java | 8 +- .../acme4j/connector/HttpConnector.java | 22 ++++- .../shredzone/acme4j/AcmeResourceTest.java | 13 +++ .../org/shredzone/acme4j/CertificateTest.java | 8 ++ .../acme4j/challenge/ChallengeTest.java | 74 ++++++++++++++- .../acme4j/challenge/HttpChallengeTest.java | 11 +++ .../acme4j/connector/HttpConnectorTest.java | 30 +++++++ .../AcmeAgreementRequiredExceptionTest.java | 64 +++++++++++++ .../exception/AcmeConflictExceptionTest.java | 40 +++++++++ .../acme4j/exception/AcmeExceptionTest.java | 53 +++++++++++ .../exception/AcmeNetworkExceptionTest.java | 38 ++++++++ .../exception/AcmeProtocolExceptionTest.java | 44 +++++++++ .../AcmeRateLimitExceededExceptionTest.java | 69 ++++++++++++++ .../acme4j/util/TimestampParserTest.java | 14 +++ .../src/test/resources/json.properties | 6 ++ .../acme4j/util/CertificateUtilsTest.java | 90 +++++++++++++++++-- .../acme4j/util/KeyPairUtilsTest.java | 13 +++ 18 files changed, 601 insertions(+), 16 deletions(-) create mode 100644 acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeAgreementRequiredExceptionTest.java create mode 100644 acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeConflictExceptionTest.java create mode 100644 acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeExceptionTest.java create mode 100644 acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeNetworkExceptionTest.java create mode 100644 acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeProtocolExceptionTest.java create mode 100644 acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededExceptionTest.java diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java index dc477adc..cea5e042 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java @@ -17,7 +17,9 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.HttpURLConnection; +import java.net.MalformedURLException; import java.net.URI; +import java.net.URL; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -162,7 +164,7 @@ public class Challenge extends AcmeResource { * JSON map containing the challenge data */ public void unmarshall(Map map) { - String type = map.get(KEY_TYPE).toString(); + String type = (String) map.get(KEY_TYPE); if (type == null) { throw new IllegalArgumentException("map does not contain a type"); } @@ -187,6 +189,22 @@ public class Challenge extends AcmeResource { return (T) data.get(key); } + /** + * Gets an {@link URL} value from the challenge state. + * + * @param key + * Key + * @return Value, or {@code null} if not set + */ + protected URL getUrl(String key) { + try { + String value = get(key); + return value != null ? new URL(value) : null; + } catch (MalformedURLException ex) { + throw new AcmeProtocolException(key + ": invalid URL", ex); + } + } + /** * Callback that is invoked when the challenge is supposed to compute its * authorization data. diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/OutOfBand01Challenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/OutOfBand01Challenge.java index 6a033852..be721515 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/OutOfBand01Challenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/OutOfBand01Challenge.java @@ -13,11 +13,9 @@ */ package org.shredzone.acme4j.challenge; -import java.net.MalformedURLException; import java.net.URL; import org.shredzone.acme4j.Session; -import org.shredzone.acme4j.exception.AcmeProtocolException; /** * Implements the {@value TYPE} challenge. @@ -45,11 +43,7 @@ public class OutOfBand01Challenge extends Challenge { * challenge. */ public URL getValidationUrl() { - try { - return new URL((String) get("href")); - } catch (MalformedURLException ex) { - throw new AcmeProtocolException("Invalid validation URL", ex); - } + return getUrl("href"); } } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/HttpConnector.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/HttpConnector.java index 433b6efb..ceab5b69 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/HttpConnector.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/HttpConnector.java @@ -49,6 +49,13 @@ public class HttpConnector { USER_AGENT = agent.toString(); } + /** + * Returns the default User-Agent to be used. + */ + public static String defaultUserAgent() { + return USER_AGENT; + } + /** * Opens a {@link HttpURLConnection} to the given {@link URI}. * @@ -58,11 +65,24 @@ public class HttpConnector { */ public HttpURLConnection openConnection(URI uri) throws IOException { HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection(); + configure(conn); + return conn; + } + + /** + * Configures the new {@link HttpURLConnection}. + *

+ * This implementation sets reasonable timeouts, forbids caching, and sets an user + * agent. + * + * @param conn + * {@link HttpURLConnection} to configure. + */ + protected void configure(HttpURLConnection conn) { conn.setConnectTimeout(TIMEOUT); conn.setReadTimeout(TIMEOUT); conn.setUseCaches(false); conn.setRequestProperty("User-Agent", USER_AGENT); - return conn; } } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/AcmeResourceTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/AcmeResourceTest.java index 50fca806..fa4155a9 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/AcmeResourceTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/AcmeResourceTest.java @@ -122,6 +122,19 @@ public class AcmeResourceTest { assertThat(restored.getSession(), is(session)); } + /** + * Test if a rebind attempt fails. + */ + @Test(expected = IllegalStateException.class) + public void testRebind() throws Exception { + Session session = TestUtils.session(); + AcmeResource resource = new DummyResource(session); + assertThat(resource.getSession(), is(session)); + + Session session2 = TestUtils.session(); + resource.rebind(session2); // fails to rebind to another session + } + /** * Minimum implementation of {@link AcmeResource}. */ diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java index b39a9c7d..69a54b93 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java @@ -195,4 +195,12 @@ public class CertificateTest { provider.close(); } + /** + * Test that numeric revocation reasons are correctly translated. + */ + @Test + public void testRevocationReason() { + assertThat(RevocationReason.code(1), is(RevocationReason.KEY_COMPROMISE)); + } + } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/ChallengeTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/ChallengeTest.java index 478f6ba1..de1fcfa1 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/ChallengeTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/ChallengeTest.java @@ -24,8 +24,10 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.HttpURLConnection; +import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; import java.util.Date; import java.util.Map; @@ -98,7 +100,7 @@ public class ChallengeTest { * Test that after unmarshalling, the challenge properties are set correctly. */ @Test - public void testUnmarshall() throws URISyntaxException { + public void testUnmarshall() throws URISyntaxException, MalformedURLException { Challenge challenge = new Challenge(session); // Test default values @@ -115,6 +117,17 @@ public class ChallengeTest { assertThat(challenge.getStatus(), is(Status.VALID)); assertThat(challenge.getLocation(), is(new URI("http://example.com/challenge/123"))); assertThat(challenge.getValidated(), is(TimestampParser.parse("2015-12-12T17:19:36.336785823Z"))); + assertThat((String) challenge.get("type"), is("generic-01")); + assertThat(challenge.getUrl("uri"), is(new URL("http://example.com/challenge/123"))); + assertThat(challenge.get("not-present"), is(nullValue())); + assertThat(challenge.getUrl("not-present-url"), is(nullValue())); + + try { + challenge.getUrl("type"); + fail("bad URL is not detected"); + } catch (AcmeProtocolException ex) { + // expected + } } /** @@ -267,6 +280,65 @@ public class ChallengeTest { provider.close(); } + /** + * Test that null is handled properly. + */ + @Test + public void testNullChallenge() throws Exception { + try { + Challenge.bind(session, null); + fail("locationUri accepts null"); + } catch (NullPointerException ex) { + // expected + } + + try { + Challenge.bind(null, locationUri); + fail("session accepts null"); + } catch (NullPointerException ex) { + // expected + } + } + + /** + * Test that an exception is thrown on a bad location URI. + */ + @Test(expected = IllegalArgumentException.class) + public void testBadBind() throws Exception { + TestableConnectionProvider provider = new TestableConnectionProvider() { + @Override + public void sendRequest(URI uri, Session session) { + assertThat(uri, is(locationUri)); + } + + @Override + public int accept(int... httpStatus) throws AcmeException { + assertThat(httpStatus, isIntArrayContainingInAnyOrder( + HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_ACCEPTED)); + return HttpURLConnection.HTTP_ACCEPTED; + } + + @Override + public Map readJsonResponse() { + return getJsonAsMap("updateRegistrationResponse"); + } + }; + + Session session = provider.createSession(); + Challenge.bind(session, locationUri); + + provider.close(); + } + + /** + * Test that unmarshalling something different like a challenge fails. + */ + @Test(expected = IllegalArgumentException.class) + public void testBadUnmarshall() { + Challenge challenge = new Challenge(session); + challenge.unmarshall(TestUtils.getJsonAsMap("updateRegistrationResponse")); + } + /** * Test that challenge serialization works correctly. */ diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/HttpChallengeTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/HttpChallengeTest.java index 720fc1f2..94dd6ebf 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/HttpChallengeTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/HttpChallengeTest.java @@ -23,6 +23,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.shredzone.acme4j.Session; import org.shredzone.acme4j.Status; +import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.util.ClaimBuilder; import org.shredzone.acme4j.util.TestUtils; @@ -62,4 +63,14 @@ public class HttpChallengeTest { + KEY_AUTHORIZATION + "\"}").allowingExtraUnexpectedFields()); } + /** + * Test that an exception is thrown if there is no token. + */ + @Test(expected = AcmeProtocolException.class) + public void testNoTokenSet() { + Http01Challenge challenge = new Http01Challenge(session); + challenge.unmarshall(TestUtils.getJsonAsMap("httpNoTokenChallenge")); + challenge.getToken(); + } + } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/HttpConnectorTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/HttpConnectorTest.java index 58028a90..990a8fdc 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/HttpConnectorTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/HttpConnectorTest.java @@ -15,6 +15,8 @@ package org.shredzone.acme4j.connector; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.*; import java.io.IOException; import java.net.HttpURLConnection; @@ -29,6 +31,24 @@ import org.junit.experimental.categories.Category; */ public class HttpConnectorTest { + /** + * Test if a HTTP connection can be opened. + *

+ * This is just a mock to check that the parameters are properly set. + */ + @Test + public void testMockOpenConnection() throws IOException, URISyntaxException { + HttpURLConnection conn = mock(HttpURLConnection.class); + + HttpConnector connector = new HttpConnector(); + connector.configure(conn); + + verify(conn).setConnectTimeout(anyInt()); + verify(conn).setReadTimeout(anyInt()); + verify(conn).setUseCaches(false); + verify(conn).setRequestProperty("User-Agent", HttpConnector.defaultUserAgent()); + } + /** * Test if a HTTP connection can be opened. *

@@ -45,4 +65,14 @@ public class HttpConnectorTest { assertThat(conn.getResponseCode(), is(HttpURLConnection.HTTP_OK)); } + /** + * Tests that the user agent is correct. + */ + @Test + public void testUserAgent() { + String userAgent = HttpConnector.defaultUserAgent(); + assertThat(userAgent.contains("acme4j/"), is(true)); + assertThat(userAgent.contains("Java/"), is(true)); + } + } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeAgreementRequiredExceptionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeAgreementRequiredExceptionTest.java new file mode 100644 index 00000000..b9216de7 --- /dev/null +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeAgreementRequiredExceptionTest.java @@ -0,0 +1,64 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2016 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.exception; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +import java.net.URI; + +import org.junit.Test; + +/** + * Unit tests for {@link AcmeAgreementRequiredException}. + */ +public class AcmeAgreementRequiredExceptionTest { + + /** + * Test that parameters are correctly returned. + */ + @Test + public void testAcmeAgreementRequiredException() { + String type = "urn:ietf:params:acme:error:agreementRequired"; + String detail = "Agreement is required"; + URI agreementUri = URI.create("http://example.com/agreement.pdf"); + URI instanceUri = URI.create("http://example.com/howToAgree.html"); + + AcmeAgreementRequiredException ex + = new AcmeAgreementRequiredException(type, detail, agreementUri, instanceUri); + + assertThat(ex.getType(), is(type)); + assertThat(ex.getMessage(), is(detail)); + assertThat(ex.getAgreementUri(), is(agreementUri)); + assertThat(ex.getInstance(), is(instanceUri)); + } + + /** + * Test that optional parameters are null-safe. + */ + @Test + public void testNullAcmeAgreementRequiredException() { + String type = "urn:ietf:params:acme:error:agreementRequired"; + String detail = "Agreement is required"; + + AcmeAgreementRequiredException ex + = new AcmeAgreementRequiredException(type, detail, null, null); + + assertThat(ex.getType(), is(type)); + assertThat(ex.getMessage(), is(detail)); + assertThat(ex.getAgreementUri(), nullValue()); + assertThat(ex.getInstance(), nullValue()); + } + +} diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeConflictExceptionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeConflictExceptionTest.java new file mode 100644 index 00000000..dd8ec815 --- /dev/null +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeConflictExceptionTest.java @@ -0,0 +1,40 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2016 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.exception; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import java.net.URI; + +import org.junit.Test; + +/** + * Unit tests for {@link AcmeConflictException}. + */ +public class AcmeConflictExceptionTest { + + @Test + public void testAcmeConflictException() { + String msg = "Account already exists"; + URI locationUri = URI.create("http://example.com/location/123"); + + AcmeConflictException ex + = new AcmeConflictException(msg, locationUri); + + assertThat(ex.getMessage(), is(msg)); + assertThat(ex.getLocation(), is(locationUri)); + } + +} diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeExceptionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeExceptionTest.java new file mode 100644 index 00000000..477a88c8 --- /dev/null +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeExceptionTest.java @@ -0,0 +1,53 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2016 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.exception; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +import java.io.IOException; + +import org.junit.Test; + +/** + * Unit tests for {@link AcmeException}. + */ +public class AcmeExceptionTest { + + @Test + public void testAcmeException() { + AcmeException ex = new AcmeException(); + assertThat(ex.getMessage(), nullValue()); + assertThat(ex.getCause(), nullValue()); + } + + @Test + public void testMessageAcmeException() { + String message = "Failure"; + AcmeException ex = new AcmeException(message); + assertThat(ex.getMessage(), is(message)); + assertThat(ex.getCause(), nullValue()); + } + + @Test + public void testCausedAcmeException() { + String message = "Failure"; + IOException cause = new IOException("No network"); + + AcmeException ex = new AcmeException(message, cause); + assertThat(ex.getMessage(), is(message)); + assertThat(ex.getCause(), is((Throwable) cause)); + } + +} diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeNetworkExceptionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeNetworkExceptionTest.java new file mode 100644 index 00000000..da168c07 --- /dev/null +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeNetworkExceptionTest.java @@ -0,0 +1,38 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2016 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.exception; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +import java.io.IOException; + +import org.junit.Test; + +/** + * Unit tests for {@link AcmeNetworkException}. + */ +public class AcmeNetworkExceptionTest { + + @Test + public void testAcmeNetworkException() { + IOException cause = new IOException("Network not reachable"); + + AcmeNetworkException ex = new AcmeNetworkException(cause); + + assertThat(ex.getMessage(), notNullValue()); + assertThat(ex.getCause(), is((Throwable) cause)); + } + +} diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeProtocolExceptionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeProtocolExceptionTest.java new file mode 100644 index 00000000..c29aa0eb --- /dev/null +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeProtocolExceptionTest.java @@ -0,0 +1,44 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2016 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.exception; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +import org.junit.Test; + +/** + * Unit tests for {@link AcmeProtocolException}. + */ +public class AcmeProtocolExceptionTest { + + @Test + public void testAcmeProtocolException() { + String msg = "Bad content"; + AcmeProtocolException ex = new AcmeProtocolException(msg); + assertThat(ex, is(instanceOf(RuntimeException.class))); + assertThat(ex.getMessage(), is(msg)); + assertThat(ex.getCause(), nullValue()); + } + + @Test + public void testCausedAcmeProtocolException() { + String message = "Bad content"; + NumberFormatException cause = new NumberFormatException("Not a number: abc"); + AcmeProtocolException ex = new AcmeProtocolException(message, cause); + assertThat(ex.getMessage(), is(message)); + assertThat(ex.getCause(), is((Throwable) cause)); + } + +} diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededExceptionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededExceptionTest.java new file mode 100644 index 00000000..79beba36 --- /dev/null +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededExceptionTest.java @@ -0,0 +1,69 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2016 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.exception; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; + +import org.junit.Test; + +/** + * Unit tests for {@link AcmeRateLimitExceededException}. + */ +public class AcmeRateLimitExceededExceptionTest { + + /** + * Test that parameters are correctly returned. + */ + @Test + public void testAcmeRateLimitExceededException() { + String type = "urn:ietf:params:acme:error:rateLimited"; + String detail = "Too many requests per minute"; + Date retryAfter = new Date(System.currentTimeMillis() + 60 * 1000L); + Collection documents = Arrays.asList( + URI.create("http://example.com/doc1.html"), + URI.create("http://example.com/doc2.html")); + + AcmeRateLimitExceededException ex + = new AcmeRateLimitExceededException(type, detail, retryAfter, documents); + + assertThat(ex.getType(), is(type)); + assertThat(ex.getMessage(), is(detail)); + assertThat(ex.getRetryAfter(), is(retryAfter)); + assertThat(ex.getDocuments(), containsInAnyOrder(documents.toArray())); + } + + /** + * Test that optional parameters are null-safe. + */ + @Test + public void testNullAcmeRateLimitExceededException() { + String type = "urn:ietf:params:acme:error:rateLimited"; + String detail = "Too many requests per minute"; + + AcmeRateLimitExceededException ex + = new AcmeRateLimitExceededException(type, detail, null, null); + + assertThat(ex.getType(), is(type)); + assertThat(ex.getMessage(), is(detail)); + assertThat(ex.getRetryAfter(), nullValue()); + assertThat(ex.getDocuments(), nullValue()); + } + +} diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/util/TimestampParserTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/util/TimestampParserTest.java index 74527792..2639268d 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/util/TimestampParserTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/util/TimestampParserTest.java @@ -13,9 +13,12 @@ */ package org.shredzone.acme4j.util; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.*; import static org.shredzone.acme4j.util.TimestampParser.parse; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; @@ -99,6 +102,17 @@ public class TimestampParserTest { } } + /** + * Test that constructor is private. + */ + @Test + public void testPrivateConstructor() throws Exception { + Constructor constructor = TimestampParser.class.getDeclaredConstructor(); + assertThat(Modifier.isPrivate(constructor.getModifiers()), is(true)); + constructor.setAccessible(true); + constructor.newInstance(); + } + /** * Matches the given time. */ diff --git a/acme4j-client/src/test/resources/json.properties b/acme4j-client/src/test/resources/json.properties index d9aae368..78b5fb2b 100644 --- a/acme4j-client/src/test/resources/json.properties +++ b/acme4j-client/src/test/resources/json.properties @@ -218,6 +218,12 @@ httpChallenge = \ "token": "rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ" \ } +httpNoTokenChallenge = \ + { \ + "type":"http-01", \ + "status":"pending" \ + } + tlsSniChallenge = \ { \ "type":"tls-sni-01", \ diff --git a/acme4j-utils/src/test/java/org/shredzone/acme4j/util/CertificateUtilsTest.java b/acme4j-utils/src/test/java/org/shredzone/acme4j/util/CertificateUtilsTest.java index 9923ebd1..0547712a 100644 --- a/acme4j-utils/src/test/java/org/shredzone/acme4j/util/CertificateUtilsTest.java +++ b/acme4j-utils/src/test/java/org/shredzone/acme4j/util/CertificateUtilsTest.java @@ -20,6 +20,9 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.StringWriter; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; import java.security.KeyPair; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -57,13 +60,9 @@ public class CertificateUtilsTest { @Test public void testReadWriteX509Certificate() throws IOException, CertificateException { // Read a demonstration certificate - X509Certificate original; - try (InputStream cert = getClass().getResourceAsStream("/cert.pem")) { - original = (X509Certificate) certificateFactory.generateCertificate(cert); - } - assertThat(original, is(notNullValue())); + X509Certificate original = createCertificate(); - // Write to StringWriter + // Write to Byte Array byte[] pem; try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { CertificateUtils.writeX509Certificate(original, out); @@ -83,6 +82,43 @@ public class CertificateUtilsTest { assertThat(original.getEncoded(), is(equalTo(written.getEncoded()))); } + /** + * Test if + * {@link CertificateUtils#writeX509CertificateChain(java.io.Writer, X509Certificate, X509Certificate...)} + * writes a correct chain. + */ + @Test + public void testWriteX509CertificateChain() throws IOException, CertificateException { + X509Certificate leaf = createCertificate(); + X509Certificate chain1 = createCertificate(); + X509Certificate chain2 = createCertificate(); + + String out; + try (StringWriter w = new StringWriter()) { + CertificateUtils.writeX509CertificateChain(w, leaf); + out = w.toString(); + } + assertThat(countCertificates(out), is(1)); + + try (StringWriter w = new StringWriter()) { + CertificateUtils.writeX509CertificateChain(w, leaf, chain1); + out = w.toString(); + } + assertThat(countCertificates(out), is(2)); + + try (StringWriter w = new StringWriter()) { + CertificateUtils.writeX509CertificateChain(w, leaf, chain1, chain2); + out = w.toString(); + } + assertThat(countCertificates(out), is(3)); + + try (StringWriter w = new StringWriter()) { + CertificateUtils.writeX509CertificateChain(w, leaf, chain1, null, chain2); + out = w.toString(); + } + assertThat(countCertificates(out), is(3)); + } + /** * Test if {@link CertificateUtils#createTlsSniCertificate(KeyPair, String)} creates a * good certificate. @@ -155,6 +191,48 @@ public class CertificateUtilsTest { } } + /** + * Returns a test certificates. + */ + private X509Certificate createCertificate() throws IOException, CertificateException { + X509Certificate original; + try (InputStream cert = getClass().getResourceAsStream("/cert.pem")) { + original = (X509Certificate) certificateFactory.generateCertificate(cert); + } + assertThat(original, is(notNullValue())); + return original; + } + + /** + * Test that constructor is private. + */ + @Test + public void testPrivateConstructor() throws Exception { + Constructor constructor = CertificateUtils.class.getDeclaredConstructor(); + assertThat(Modifier.isPrivate(constructor.getModifiers()), is(true)); + constructor.setAccessible(true); + constructor.newInstance(); + } + + /** + * Counts number of certificates in a PEM string. + * + * @param str + * String containing certificates in PEM format + * @return Number of certificates found + */ + private int countCertificates(String str) { + int count = 0; + int pos = 0; + while (true) { + pos = str.indexOf("-----BEGIN CERTIFICATE-----", pos); + if (pos < 0) break; + count++; + pos++; + } + return count; + } + /** * Extracts all DNSName SANs from a certificate. * diff --git a/acme4j-utils/src/test/java/org/shredzone/acme4j/util/KeyPairUtilsTest.java b/acme4j-utils/src/test/java/org/shredzone/acme4j/util/KeyPairUtilsTest.java index dc143f3b..66415927 100644 --- a/acme4j-utils/src/test/java/org/shredzone/acme4j/util/KeyPairUtilsTest.java +++ b/acme4j-utils/src/test/java/org/shredzone/acme4j/util/KeyPairUtilsTest.java @@ -19,6 +19,8 @@ import static org.junit.Assert.assertThat; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; import java.security.KeyPair; import java.security.Security; import java.security.interfaces.ECPublicKey; @@ -131,4 +133,15 @@ public class KeyPairUtilsTest { assertThat(pair.getPrivate().getEncoded(), is(equalTo(readPair.getPrivate().getEncoded()))); } + /** + * Test that constructor is private. + */ + @Test + public void testPrivateConstructor() throws Exception { + Constructor constructor = KeyPairUtils.class.getDeclaredConstructor(); + assertThat(Modifier.isPrivate(constructor.getModifiers()), is(true)); + constructor.setAccessible(true); + constructor.newInstance(); + } + }