Add unit tests for acme4j-client

pull/17/merge
Richard Körber 2015-12-13 19:34:58 +01:00
parent 1267684614
commit 7097f23a2d
18 changed files with 1172 additions and 8 deletions

View File

@ -0,0 +1,52 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 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;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import java.io.IOException;
import java.security.KeyPair;
import org.junit.Test;
import org.shredzone.acme4j.util.TestUtils;
/**
* Unit tests for {@link Account}.
*
* @author Richard "Shred" Körber
*/
public class AccountTest {
/**
* Test getters and setters.
*/
@Test
public void testGetterAndSetter() throws IOException {
KeyPair keypair = TestUtils.createKeyPair();
Account account = new Account(keypair);
assertThat(account.getKeyPair(), is(sameInstance(keypair)));
}
/**
* Test null values.
*/
@Test(expected = NullPointerException.class)
public void testNull() {
new Account(null);
}
}

View File

@ -64,6 +64,32 @@ public class AuthorizationTest {
authorization.setCombinations(Collections.unmodifiableList(combinations));
}
/**
* Test getters and setters.
*/
@Test
public void testGetterAndSetter() {
Authorization auth = new Authorization();
assertThat(auth.getDomain(), is(nullValue()));
assertThat(auth.getStatus(), is(nullValue()));
assertThat(auth.getExpires(), is(nullValue()));
assertThat(auth.getChallenges(), is(nullValue()));
assertThat(auth.getCombinations(), is(nullValue()));
auth.setDomain("example.com");
auth.setStatus("invalid");
auth.setExpires("2015-12-12T17:19:36.336785823Z");
auth.setChallenges(authorization.getChallenges());
auth.setCombinations(authorization.getCombinations());
assertThat(auth.getDomain(), is("example.com"));
assertThat(auth.getStatus(), is("invalid"));
assertThat(auth.getExpires(), is("2015-12-12T17:19:36.336785823Z"));
assertThat(auth.getChallenges(), is(sameInstance(authorization.getChallenges())));
assertThat(auth.getCombinations(), is(sameInstance(authorization.getCombinations())));
}
/**
* Test that {@link Authorization#findChallenge(String)} does only find standalone
* challenges, and nothing else.

View File

@ -0,0 +1,52 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 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;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import java.net.URI;
import java.net.URISyntaxException;
import org.junit.Test;
/**
* Unit tests for {@link Registration}.
*
* @author Richard "Shred" Körber
*/
public class RegistrationTest {
/**
* Test getters and setters.
*/
@Test
public void testGetterAndSetter() throws URISyntaxException {
Registration registration = new Registration();
assertThat(registration.getAgreementUrl(), is(nullValue()));
assertThat(registration.getLocation(), is(nullValue()));
assertThat(registration.getContacts(), is(empty()));
registration.setAgreementUrl("http://example.com/agreement.pdf");
registration.setLocation(new URI("http://example.com/acme/12345"));
registration.getContacts().add("mailto:foo@example.com");
registration.getContacts().add("mailto:bar@example.com");
assertThat(registration.getAgreementUrl(), is("http://example.com/agreement.pdf"));
assertThat(registration.getLocation(), is(new URI("http://example.com/acme/12345")));
assertThat(registration.getContacts(), contains("mailto:foo@example.com", "mailto:bar@example.com"));
}
}

View File

@ -0,0 +1,74 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 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.challenge;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.*;
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
import java.io.IOException;
import java.security.KeyPair;
import org.junit.Test;
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.challenge.Challenge.Status;
import org.shredzone.acme4j.util.ClaimBuilder;
import org.shredzone.acme4j.util.TestUtils;
/**
* Unit tests for {@link DnsChallenge}.
*
* @author Richard "Shred" Körber
*/
public class DnsChallengeTest {
private static final String TOKEN =
"pNvmJivs0WCko2suV7fhe-59oFqyYx_yB7tx6kIMAyE";
private static final String KEY_AUTHORIZATION =
"pNvmJivs0WCko2suV7fhe-59oFqyYx_yB7tx6kIMAyE.HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0";
/**
* Test that {@link DnsChallenge} generates a correct authorization key.
*/
@Test
public void testHttpChallenge() throws IOException {
KeyPair keypair = TestUtils.createKeyPair();
Account account = new Account(keypair);
DnsChallenge challenge = new DnsChallenge();
challenge.unmarshall(TestUtils.getResourceAsJsonMap("/dnsChallenge.json"));
assertThat(challenge.getType(), is(DnsChallenge.TYPE));
assertThat(challenge.getStatus(), is(Status.PENDING));
try {
challenge.getAuthorization();
fail("getAuthorization() without previous authorize()");
} catch (IllegalStateException ex) {
// expected
}
challenge.authorize(account);
assertThat(challenge.getToken(), is(TOKEN));
assertThat(challenge.getAuthorization(), is(KEY_AUTHORIZATION));
ClaimBuilder cb = new ClaimBuilder();
challenge.marshall(cb);
assertThat(cb.toString(), sameJSONAs("{\"keyAuthorization\"=\""
+ KEY_AUTHORIZATION + "\"}").allowingExtraUnexpectedFields());
}
}

View File

@ -0,0 +1,126 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 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.challenge;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyPair;
import org.jose4j.base64url.Base64Url;
import org.jose4j.json.JsonUtil;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.JsonWebKey.OutputControlLevel;
import org.jose4j.lang.JoseException;
import org.junit.Test;
import org.shredzone.acme4j.challenge.Challenge.Status;
import org.shredzone.acme4j.util.ClaimBuilder;
import org.shredzone.acme4j.util.TestUtils;
/**
* Unit tests for {@link GenericChallenge}.
*
* @author Richard "Shred" Körber
*/
public class GenericChallengeTest {
/**
* Test that after unmarshalling, the challenge properties are set correctly.
*/
@Test
public void testUnmarshall() throws IOException, URISyntaxException {
GenericChallenge challenge = new GenericChallenge();
// Test default values
assertThat(challenge.getType(), is(nullValue()));
assertThat(challenge.getStatus(), is(Status.PENDING));
assertThat(challenge.getUri(), is(nullValue()));
assertThat(challenge.getValidated(), is(nullValue()));
// Unmarshall a challenge JSON
challenge.unmarshall(TestUtils.getResourceAsJsonMap("/genericChallenge.json"));
// Test unmarshalled values
assertThat(challenge.getType(), is("generic-01"));
assertThat(challenge.getStatus(), is(Status.VALID));
assertThat(challenge.getUri(), is(new URI("http://example.com/challenge/123")));
assertThat(challenge.getValidated(), is("2015-12-12T17:19:36.336785823Z"));
}
/**
* Test get and put methods.
*/
public void testGetPut() {
GenericChallenge challenge = new GenericChallenge();
challenge.put("a-string", "foo");
challenge.put("a-number", 1234);
assertThat((String) challenge.get("a-string"), is("foo"));
assertThat((Integer) challenge.get("a-number"), is(1234));
ClaimBuilder cb = new ClaimBuilder();
challenge.marshall(cb);
assertThat(cb.toString(), sameJSONAs("{\"a-string\":\"foo\",\"a-number\":1234}")
.allowingExtraUnexpectedFields());
}
/**
* Test that marshalling results in an identical JSON like the one that was
* unmarshalled.
*/
@Test
public void testMarshall() throws IOException, JoseException {
String json = TestUtils.getResourceAsString("/genericChallenge.json");
GenericChallenge challenge = new GenericChallenge();
challenge.unmarshall(JsonUtil.parseJson(json));
ClaimBuilder cb = new ClaimBuilder();
challenge.marshall(cb);
assertThat(cb.toString(), sameJSONAs(json));
}
/**
* Test that the test keypair's thumbprint is correct.
*/
@Test
public void testJwkThumbprint() throws IOException, JoseException {
StringBuilder json = new StringBuilder();
json.append('{');
json.append("\"e\":\"").append(TestUtils.E).append("\",");
json.append("\"kty\":\"").append(TestUtils.KTY).append("\",");
json.append("\"n\":\"").append(TestUtils.N).append("\"");
json.append('}');
KeyPair keypair = TestUtils.createKeyPair();
// Test the JWK raw output. The JSON string must match the assert string
// exactly, as the thumbprint is a digest of that string.
final JsonWebKey jwk = JsonWebKey.Factory.newJwk(keypair.getPublic());
ClaimBuilder cb = new ClaimBuilder();
cb.putAll(jwk.toParams(OutputControlLevel.PUBLIC_ONLY));
assertThat(cb.toString(), is(json.toString()));
// Make sure the returned thumbprint is correct
byte[] thumbprint = GenericChallenge.jwkThumbprint(keypair.getPublic());
assertThat(thumbprint, is(Base64Url.decode(TestUtils.THUMBPRINT)));
}
}

View File

@ -0,0 +1,74 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 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.challenge;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.*;
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
import java.io.IOException;
import java.security.KeyPair;
import org.junit.Test;
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.challenge.Challenge.Status;
import org.shredzone.acme4j.util.ClaimBuilder;
import org.shredzone.acme4j.util.TestUtils;
/**
* Unit tests for {@link HttpChallenge}.
*
* @author Richard "Shred" Körber
*/
public class HttpChallengeTest {
private static final String TOKEN =
"rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ";
private static final String KEY_AUTHORIZATION =
"rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ.HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0";
/**
* Test that {@link HttpChallenge} generates a correct authorization key.
*/
@Test
public void testHttpChallenge() throws IOException {
KeyPair keypair = TestUtils.createKeyPair();
Account account = new Account(keypair);
HttpChallenge challenge = new HttpChallenge();
challenge.unmarshall(TestUtils.getResourceAsJsonMap("/httpChallenge.json"));
assertThat(challenge.getType(), is(HttpChallenge.TYPE));
assertThat(challenge.getStatus(), is(Status.PENDING));
try {
challenge.getAuthorization();
fail("getAuthorization() without previous authorize()");
} catch (IllegalStateException ex) {
// expected
}
challenge.authorize(account);
assertThat(challenge.getToken(), is(TOKEN));
assertThat(challenge.getAuthorization(), is(KEY_AUTHORIZATION));
ClaimBuilder cb = new ClaimBuilder();
challenge.marshall(cb);
assertThat(cb.toString(), sameJSONAs("{\"keyAuthorization\"=\""
+ KEY_AUTHORIZATION + "\"}").allowingExtraUnexpectedFields());
}
}

View File

@ -0,0 +1,456 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 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.connector;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*;
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyPair;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.jose4j.base64url.Base64Url;
import org.jose4j.jwx.CompactSerializer;
import org.junit.Before;
import org.junit.Test;
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeServerException;
import org.shredzone.acme4j.provider.AcmeClientProvider;
import org.shredzone.acme4j.util.ClaimBuilder;
import org.shredzone.acme4j.util.TestUtils;
/**
* Unit tests for {@link Connection}.
*
* @author Richard "Shred" Körber
*/
public class ConnectionTest {
private URI requestUri;
private AcmeClientProvider mockProvider;
private HttpURLConnection mockUrlConnection;
@Before
public void setup() throws IOException, URISyntaxException {
requestUri = new URI("http://example.com/acme/");
mockUrlConnection = mock(HttpURLConnection.class);
mockProvider = mock(AcmeClientProvider.class);
when(mockProvider.openConnection(requestUri)).thenReturn(mockUrlConnection);
}
/**
* Test if {@link Connection#getNonceFromHeader(HttpURLConnection)} throws an
* exception if there is no {@code Replay-Nonce} header.
*/
@Test
public void testNoNonceFromHeader() throws AcmeException {
when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(null);
try (Connection conn = new Connection(mockProvider)) {
conn.getNonceFromHeader(mockUrlConnection);
fail("Expected to fail");
} catch (AcmeException ex) {
assertThat(ex.getMessage(), is("No replay nonce"));
}
verify(mockUrlConnection).getHeaderField("Replay-Nonce");
verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
}
/**
* Test that {@link Connection#getNonceFromHeader(HttpURLConnection)} extracts a
* {@code Replay-Nonce} header correctly.
*/
@Test
public void testGetNonceFromHeader() throws AcmeException {
byte[] nonce = "foo-nonce-foo".getBytes();
when(mockUrlConnection.getHeaderField("Replay-Nonce"))
.thenReturn(Base64Url.encode(nonce));
try (Connection conn = new Connection(mockProvider)) {
byte[] nonceFromHeader = conn.getNonceFromHeader(mockUrlConnection);
assertThat(nonceFromHeader, is(nonce));
}
verify(mockUrlConnection).getHeaderField("Replay-Nonce");
verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
}
/**
* Test that {@link Connection#getNonceFromHeader(HttpURLConnection)} fails on an
* invalid {@code Replay-Nonce} header.
*/
@Test
public void testInvalidNonceFromHeader() throws AcmeException {
String badNonce = "#$%&/*+*#'";
when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(badNonce);
try (Connection conn = new Connection(mockProvider)) {
conn.getNonceFromHeader(mockUrlConnection);
fail("Expected to fail");
} catch (AcmeException ex) {
assertThat(ex.getMessage(), org.hamcrest.Matchers.startsWith("Invalid replay nonce"));
}
verify(mockUrlConnection).getHeaderField("Replay-Nonce");
verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
}
/**
* Test that a Location header is evaluated.
*/
@Test
public void testGetLocation() throws Exception {
when(mockUrlConnection.getHeaderField("Location")).thenReturn("http://example.com/otherlocation");
try (Connection conn = new Connection(mockProvider)) {
conn.conn = mockUrlConnection;
URI location = conn.getLocation();
assertThat(location, is(new URI("http://example.com/otherlocation")));
}
verify(mockUrlConnection).getHeaderField("Location");
verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
}
/**
* Test that no Location header returns {@code null}.
*/
@Test
public void testNoLocation() throws Exception {
try (Connection conn = new Connection(mockProvider)) {
conn.conn = mockUrlConnection;
URI location = conn.getLocation();
assertThat(location, is(nullValue()));
}
verify(mockUrlConnection).getHeaderField("Location");
verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
}
/**
* Test that no exception is thrown if there is no problem.
*/
@Test
public void testNoThrowException() throws AcmeException {
when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/json");
try (Connection conn = new Connection(mockProvider)) {
conn.conn = mockUrlConnection;
conn.throwException();
}
verify(mockUrlConnection).getHeaderField("Content-Type");
verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
}
/**
* Test if an {@link AcmeServerException} is thrown on an acme problem.
*/
@Test
public void testThrowException() throws Exception {
String jsonData = "{\"type\":\"urn:acme:error:unauthorized\",\"detail\":\"Invalid response: 404\"}";
when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/problem+json");
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN);
when(mockUrlConnection.getErrorStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8")));
try (Connection conn = new Connection(mockProvider)) {
conn.conn = mockUrlConnection;
conn.throwException();
fail("Expected to fail");
} catch (AcmeServerException ex) {
assertThat(ex.getType(), is("urn:acme:error:unauthorized"));
assertThat(ex.getMessage(), is("Invalid response: 404"));
assertThat(ex.getAcmeErrorType(), is("unauthorized"));
} catch (AcmeException ex) {
fail("Expected an AcmeServerException");
}
verify(mockUrlConnection, atLeastOnce()).getHeaderField("Content-Type");
verify(mockUrlConnection).getResponseCode();
verify(mockUrlConnection).getErrorStream();
verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
}
/**
* Test if an {@link AcmeServerException} is thrown on another problem.
*/
@Test
public void testOtherThrowException() {
when(mockUrlConnection.getHeaderField("Content-Type"))
.thenReturn("application/problem+json");
try (Connection conn = new Connection(mockProvider) {
@Override
public Map<String,Object> readJsonResponse() throws AcmeException {
Map<String, Object> result = new HashMap<String, Object>();
result.put("type", "urn:zombie:error:apocalypse");
result.put("detail", "Zombie apocalypse in progress");
return result;
};
}) {
conn.conn = mockUrlConnection;
conn.throwException();
fail("Expected to fail");
} catch (AcmeServerException ex) {
assertThat(ex.getType(), is("urn:zombie:error:apocalypse"));
assertThat(ex.getMessage(), is("Zombie apocalypse in progress"));
assertThat(ex.getAcmeErrorType(), is(nullValue()));
} catch (AcmeException ex) {
fail("Expected an AcmeServerException");
}
verify(mockUrlConnection).getHeaderField("Content-Type");
verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
}
/**
* Test that a session is properly started.
*/
@Test
public void testStartSession() throws Exception {
byte[] nonce = "foo-nonce-foo".getBytes();
when(mockUrlConnection.getHeaderField("Replay-Nonce"))
.thenReturn(Base64Url.encode(nonce));
Session session = new Session();
try (Connection conn = new Connection(mockProvider)) {
conn.startSession(requestUri, session);
}
assertThat(session.getNonce(), is(nonce));
verify(mockUrlConnection).setRequestMethod("HEAD");
verify(mockUrlConnection).connect();
verify(mockUrlConnection).getHeaderField("Replay-Nonce");
verify(mockProvider).openConnection(requestUri);
verifyNoMoreInteractions(mockUrlConnection);
verifyNoMoreInteractions(mockProvider);
}
/**
* Test GET requests.
*/
@Test
public void testSendRequest() throws Exception {
final Set<String> invoked = new HashSet<>();
try (Connection conn = new Connection(mockProvider) {
@Override
protected void throwException() throws AcmeException {
invoked.add("throwException");
};
}) {
conn.sendRequest(requestUri);
}
verify(mockUrlConnection).setRequestMethod("GET");
verify(mockUrlConnection).setRequestProperty("Accept-Charset", "utf-8");
verify(mockUrlConnection).setDoOutput(false);
verify(mockUrlConnection).connect();
verify(mockUrlConnection).getResponseCode();
verify(mockProvider).openConnection(requestUri);
verifyNoMoreInteractions(mockUrlConnection);
verifyNoMoreInteractions(mockProvider);
assertThat(invoked, hasItem("throwException"));
}
/**
* Test signed POST requests.
*/
@Test
public void testSendSignedRequest() throws Exception {
final byte[] nonce1 = "foo-nonce-1-foo".getBytes();
final byte[] nonce2 = "foo-nonce-2-foo".getBytes();
final Session testSession = new Session();
final Set<String> invoked = new HashSet<>();
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
when(mockUrlConnection.getOutputStream()).thenReturn(outputStream);
when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(Base64Url.encode(nonce2));
try (Connection conn = new Connection(mockProvider) {
@Override
protected void throwException() throws AcmeException {
invoked.add("throwException");
};
@Override
public void startSession(URI uri, Session session) throws AcmeException {
assertThat(uri, is(requestUri));
assertThat(session, is(sameInstance(testSession)));
assertThat(testSession.getNonce(), is(nullValue()));
invoked.add("startSession");
session.setNonce(nonce1);
};
}) {
ClaimBuilder cb = new ClaimBuilder();
cb.put("foo", 123).put("bar", "a-string");
KeyPair keypair = TestUtils.createKeyPair();
Account account = new Account(keypair);
conn.sendSignedRequest(requestUri, cb, testSession, account);
}
verify(mockUrlConnection).setRequestMethod("POST");
verify(mockUrlConnection).setRequestProperty("Accept", "application/json");
verify(mockUrlConnection).setRequestProperty("Accept-Charset", "utf-8");
verify(mockUrlConnection).setRequestProperty("Content-Type", "application/json");
verify(mockUrlConnection).setDoOutput(true);
verify(mockUrlConnection).connect();
verify(mockUrlConnection).setFixedLengthStreamingMode(outputStream.toByteArray().length);
verify(mockUrlConnection, atLeastOnce()).getHeaderField(anyString());
verify(mockUrlConnection).getOutputStream();
verify(mockUrlConnection).getResponseCode();
verify(mockProvider).openConnection(requestUri);
verifyNoMoreInteractions(mockUrlConnection);
verifyNoMoreInteractions(mockProvider);
assertThat(invoked, hasItems("throwException", "startSession"));
String[] written = CompactSerializer.deserialize(new String(outputStream.toByteArray(), "utf-8"));
String header = Base64Url.decodeToUtf8String(written[0]);
String claims = Base64Url.decodeToUtf8String(written[1]);
String signature = written[2];
StringBuilder expectedHeader = new StringBuilder();
expectedHeader.append('{');
expectedHeader.append("\"nonce\":\"").append(Base64Url.encode(nonce1)).append("\",");
expectedHeader.append("\"alg\":\"RS256\",");
expectedHeader.append("\"jwk\":{");
expectedHeader.append("\"kty\":\"").append(TestUtils.KTY).append("\",");
expectedHeader.append("\"e\":\"").append(TestUtils.E).append("\",");
expectedHeader.append("\"n\":\"").append(TestUtils.N).append("\"");
expectedHeader.append("}}");
assertThat(header, sameJSONAs(expectedHeader.toString()).allowingExtraUnexpectedFields());
assertThat(claims, sameJSONAs("{\"foo\":123,\"bar\":\"a-string\"}"));
assertThat(signature, not(isEmptyOrNullString()));
}
/**
* Test getting a JSON response.
*/
@Test
public void testReadJsonResponse() throws Exception {
String jsonData = "{\"foo\":123,\"bar\":\"a-string\"}";
when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/json");
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8")));
try (Connection conn = new Connection(mockProvider)) {
conn.conn = mockUrlConnection;
Map<String, Object> result = conn.readJsonResponse();
assertThat(result.keySet(), hasSize(2));
assertThat(result, hasEntry("foo", (Object) 123L));
assertThat(result, hasEntry("bar", (Object) "a-string"));
}
verify(mockUrlConnection).getHeaderField("Content-Type");
verify(mockUrlConnection).getResponseCode();
verify(mockUrlConnection).getInputStream();
verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
}
/**
* Test that a certificate is downloaded correctly.
*/
@Test
public void testReadCertificate() throws Exception {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate original;
try (InputStream cert = getClass().getResourceAsStream("/cert.pem")) {
original = (X509Certificate) certificateFactory.generateCertificate(cert);
}
when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/pkix-cert");
when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(original.getEncoded()));
X509Certificate downloaded;
try (Connection conn = new Connection(mockProvider)) {
conn.conn = mockUrlConnection;
downloaded = conn.readCertificate();
}
assertThat(original, not(nullValue()));
assertThat(downloaded, not(nullValue()));
assertThat(original.getEncoded(), is(equalTo(downloaded.getEncoded())));
verify(mockUrlConnection).getHeaderField("Content-Type");
verify(mockUrlConnection).getInputStream();
verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
}
/**
* Test if a resource directory is read correctly.
*/
@Test
public void testReadDirectory() throws Exception {
StringBuilder jsonData = new StringBuilder();
jsonData.append('{');
jsonData.append("\"new-reg\":\"http://example.com/acme/newreg\",");
jsonData.append("\"new-authz\":\"http://example.com/acme/newauthz\",");
jsonData.append("\"old-foo\":\"http://example.com/acme/oldfoo\"");
jsonData.append('}');
when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/json");
when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(jsonData.toString().getBytes("utf-8")));
try (Connection conn = new Connection(mockProvider)) {
conn.conn = mockUrlConnection;
Map<Resource, URI> result = conn.readDirectory();
assertThat(result.keySet(), hasSize(2));
assertThat(result, hasEntry(Resource.NEW_REG, new URI("http://example.com/acme/newreg")));
assertThat(result, hasEntry(Resource.NEW_AUTHZ, new URI("http://example.com/acme/newauthz")));
// "old-foo" resource is unknown and thus not available in the map
}
verify(mockUrlConnection).getHeaderField("Content-Type");
verify(mockUrlConnection).getInputStream();
verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
}
}

View File

@ -0,0 +1,56 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 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.connector;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import org.junit.Test;
/**
* Unit test for {@link Resource}.
*
* @author Richard "Shred" Körber
*/
public class ResourceTest {
/**
* Test {@link Resource#path()}.
*/
@Test
public void testPath() {
assertThat(Resource.NEW_AUTHZ.path(), is("new-authz"));
assertThat(Resource.NEW_CERT.path(), is("new-cert"));
assertThat(Resource.NEW_REG.path(), is("new-reg"));
// fails if there are untested future Resource values
assertThat(Resource.values().length, is(3));
}
/**
* Test that invoking {@link Resource#parse(String)} with a {@link Resource#path()}
* gives the same {@link Resource}.
*/
@Test
public void testParse() {
for (Resource r : Resource.values()) {
Resource parsed = Resource.parse(r.path());
assertThat(parsed, is(r));
}
// unknown paths return null
assertThat(Resource.parse("foo"), is(nullValue()));
}
}

View File

@ -0,0 +1,43 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 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.connector;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import org.junit.Test;
/**
* Unit test for {@link Session}.
*
* @author Richard "Shred" Körber
*/
public class SessionTest {
/**
* Test getters and setters.
*/
@Test
public void testGettersAndSetters() {
Session session = new Session();
assertThat(session.getNonce(), is(nullValue()));
byte[] data = "foo-nonce-bar".getBytes();
session.setNonce(data);
assertThat(session.getNonce(), is(equalTo(data)));
}
}

View File

@ -16,8 +16,8 @@ package org.shredzone.acme4j.util;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import java.io.IOException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
@ -118,10 +118,8 @@ public class ClaimBuilderTest {
*/
@Test
@SuppressWarnings("unchecked")
public void testKey() throws NoSuchAlgorithmException, JoseException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(512);
KeyPair keyPair = keyGen.generateKeyPair();
public void testKey() throws IOException, NoSuchAlgorithmException, JoseException {
KeyPair keyPair = TestUtils.createKeyPair();
ClaimBuilder res;
@ -134,9 +132,9 @@ public class ClaimBuilderTest {
Map<String, String> jwk = (Map<String, String>) json.get("foo");
assertThat(jwk.keySet(), hasSize(3));
assertThat(jwk.get("n"), not(isEmptyOrNullString()));
assertThat(jwk, hasEntry("e", "AQAB"));
assertThat(jwk, hasEntry("kty", "RSA"));
assertThat(jwk, hasEntry("n", TestUtils.N));
assertThat(jwk, hasEntry("e", TestUtils.E));
assertThat(jwk, hasEntry("kty", TestUtils.KTY));
}
/**

View File

@ -0,0 +1,165 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 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.util;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Map;
import java.util.TreeMap;
import org.jose4j.base64url.Base64Url;
import org.jose4j.json.JsonUtil;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.JsonWebKey.OutputControlLevel;
import org.jose4j.lang.JoseException;
/**
* Some utility methods for unit tests.
*
* @author Richard "Shred" Körber
*/
public final class TestUtils {
public static final String N = "pZsTKY41y_CwgJ0VX7BmmGs_7UprmXQMGPcnSbBeJAjZHA9SyyJKaWv4fNUdBIAX3Y2QoZixj50nQLyLv2ng3pvEoRL0sx9ZHgp5ndAjpIiVQ_8V01TTYCEDUc9ii7bjVkgFAb4ValZGFJZ54PcCnAHvXi5g0ELORzGcTuRqHVAUckMV2otr0g0u_5bWMm6EMAbBrGQCgUGjbZQHjava1Y-5tHXZkPBahJ2LvKRqMmJUlr0anKuJJtJUG03DJYAxABv8YAaXFBnGw6kKJRpUFAC55ry4sp4kGy0NrK2TVWmZW9kStniRv4RaJGI9aZGYwQy2kUykibBNmWEQUlIwIw";
public static final String E = "AQAB";
public static final String KTY = "RSA";
public static final String THUMBPRINT = "HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0";
private TestUtils() {
// utility class without constructor
}
/**
* Reads a resource as byte array.
*
* @param name
* Resource name
* @return Resource content as byte array.
*/
public static byte[] getResourceAsByteArray(String name) throws IOException {
byte[] buffer = new byte[2048];
try (InputStream in = TestUtils.class.getResourceAsStream(name);
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
int len;
while ((len = in.read(buffer)) >= 0) {
out.write(buffer, 0, len);
}
return out.toByteArray();
}
}
/**
* Reads a resource as String.
*
* @param name
* Resource name. The content is expected to be utf-8 encoded.
* @return Resource contents as string
*/
public static String getResourceAsString(String name) throws IOException {
try (InputStreamReader in = new InputStreamReader(
TestUtils.class.getResourceAsStream(name), "utf-8");
StringWriter out = new StringWriter()) {
int ch;
while ((ch = in.read()) >= 0) {
out.write(ch);
}
return out.toString();
}
}
/**
* Reads a JSON resource and parses it.
*
* @param name
* Resource name of a utf-8 encoded JSON file.
* @return Parsed contents
*/
public static Map<String, Object> getResourceAsJsonMap(String name) throws IOException {
try {
return JsonUtil.parseJson(getResourceAsString(name));
} catch (JoseException ex) {
throw new IOException("JSON error", ex);
}
}
/**
* Creates a standard key pair for testing. This keypair is read from a test resource
* and is guaranteed not to change between test runs.
* <p>
* The constants {@link #N}, {@link #E}, {@link #KTY} and {@link #THUMBPRINT} are
* related to the returned key pair and can be used for asserting results.
*
* @return {@link KeyPair} for testing
*/
public static KeyPair createKeyPair() throws IOException {
try {
KeyFactory keyFactory = KeyFactory.getInstance(KTY);
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(
getResourceAsByteArray("/public.key"));
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(
getResourceAsByteArray("/private.key"));
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
return new KeyPair(publicKey, privateKey);
} catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
throw new IOException(ex);
}
}
/**
* Generates a new keypair for unit tests, and return its N, E, KTY and THUMBPRINT
* parameters to be set in the {@link TestUtils} class.
*/
public static void main(String... args) throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair keyPair = keyGen.generateKeyPair();
try (FileOutputStream out = new FileOutputStream("public.key")) {
out.write(keyPair.getPublic().getEncoded());
}
try (FileOutputStream out = new FileOutputStream("private.key")) {
out.write(keyPair.getPrivate().getEncoded());
}
final JsonWebKey jwk = JsonWebKey.Factory.newJwk(keyPair.getPublic());
Map<String, Object> params = new TreeMap<>(jwk.toParams(OutputControlLevel.PUBLIC_ONLY));
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(JsonUtil.toJson(params).getBytes("UTF-8"));
byte[] thumbprint = md.digest();
System.out.println("N = " + params.get("n"));
System.out.println("E = " + params.get("e"));
System.out.println("KTY = " + params.get("kty"));
System.out.println("THUMBPRINT = " + Base64Url.encode(thumbprint));
}
}

View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDVzCCAj+gAwIBAgIJAM4KDTzb0Y7NMA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNV
BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg
Q29tcGFueSBMdGQwHhcNMTUxMjEwMDAxMTA4WhcNMjUxMjA3MDAxMTA4WjBCMQsw
CQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZh
dWx0IENvbXBhbnkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
r0g3w4C8xbj/5lzJiDxk0HkEJeZeyruq+0AzOPMigJZ7zxZtX/KUxOIHrQ4qjcFh
l0DmQImoM0wESU+kcsjAHCx8E1lgRVlVsMfLAQPHkg5UybqfadzKT3ALcSD+9F9m
VIP6liC/6KzLTASmx6zM7j92KTl1ArObZr5mh0jvSNORrMhEC4Byn3+NTxjuHON1
rWppCMwpeNNhFzaAig3O8PY8IyaLXNP2Ac5pXn0iW16S+Im9by7751UeW5a7Dznm
uMEM+WY640ffJDQ4+I64H403uAgvvSu+BGw8SEEZGuBCxoCnG1g6y6OvJyN5TgqF
dGosAfm1u+/MP1seoPdpBQIDAQABo1AwTjAdBgNVHQ4EFgQUrie5ZLOrA/HuhW1b
/CHjzEvj34swHwYDVR0jBBgwFoAUrie5ZLOrA/HuhW1b/CHjzEvj34swDAYDVR0T
BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAkSOP0FUgIIUeJTObgXrenHzZpLAk
qXi37dgdYuPhNveo3agueP51N7yIoh6YGShiJ73Rvr+lVYTwFXStrLih1Wh3tWvk
sMxnvocgd7l6USRb5/AgH7eHeFK4DoCAak2hUAcCLDRJN3XMhNLpyJhw7GJxowVI
GUlxcW5Asrmh9qflfyMyjripTP3CdHobmNcNHyScjNncKj37m8vomel9acekTtDl
2Ci7nLdE+3VqQCXMIfLiF3PO0gGpKei0RuVCSOG6W83zVInCPd/l3aluSR+f/VZl
k8KGQ4As4uTQi89j+J1YepzG0ASMZpjVbXeIg5QBAywVxBh5XVTz37KN8A==
-----END CERTIFICATE-----

View File

@ -0,0 +1,5 @@
{
"type":"dns-01",
"status":"pending",
"token": "pNvmJivs0WCko2suV7fhe-59oFqyYx_yB7tx6kIMAyE"
}

View File

@ -0,0 +1,6 @@
{
"type":"generic-01",
"status":"valid",
"uri":"http://example.com/challenge/123",
"validated":"2015-12-12T17:19:36.336785823Z"
}

View File

@ -0,0 +1,5 @@
{
"type":"http-01",
"status":"pending",
"token": "rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ"
}

Binary file not shown.

Binary file not shown.

View File

@ -164,6 +164,12 @@
<version>[1.3,)</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>uk.co.datumedge</groupId>
<artifactId>hamcrest-json</artifactId>
<version>[0.1,)</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>