Increase unit test coverage

pull/30/head
Richard Körber 2016-12-16 01:47:26 +01:00
parent b3fc9a732c
commit 0a288fa290
18 changed files with 601 additions and 16 deletions

View File

@ -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<String, Object> 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.

View File

@ -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");
}
}

View File

@ -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}.
* <p>
* 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;
}
}

View File

@ -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}.
*/

View File

@ -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));
}
}

View File

@ -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<String, Object> 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.
*/

View File

@ -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();
}
}

View File

@ -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.
* <p>
* 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.
* <p>
@ -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));
}
}

View File

@ -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());
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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<URI> 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());
}
}

View File

@ -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<TimestampParser> constructor = TimestampParser.class.getDeclaredConstructor();
assertThat(Modifier.isPrivate(constructor.getModifiers()), is(true));
constructor.setAccessible(true);
constructor.newInstance();
}
/**
* Matches the given time.
*/

View File

@ -218,6 +218,12 @@ httpChallenge = \
"token": "rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ" \
}
httpNoTokenChallenge = \
{ \
"type":"http-01", \
"status":"pending" \
}
tlsSniChallenge = \
{ \
"type":"tls-sni-01", \

View File

@ -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<CertificateUtils> 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.
*

View File

@ -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<KeyPairUtils> constructor = KeyPairUtils.class.getDeclaredConstructor();
assertThat(Modifier.isPrivate(constructor.getModifiers()), is(true));
constructor.setAccessible(true);
constructor.newInstance();
}
}