Use Problem in AcmeServerException

pull/55/head
Richard Körber 2017-05-01 15:24:44 +02:00
parent c623d72426
commit 02cedf9935
17 changed files with 162 additions and 77 deletions

View File

@ -172,7 +172,7 @@ public class Order extends AcmeResource {
URL certUrl = json.get("certificate").asURL(); URL certUrl = json.get("certificate").asURL();
certificate = certUrl != null ? Certificate.bind(getSession(), certUrl) : null; certificate = certUrl != null ? Certificate.bind(getSession(), certUrl) : null;
this.error = json.get("error").asProblem(); this.error = json.get("error").asProblem(getLocation());
this.authorizations = json.get("authorizations").asArray().stream() this.authorizations = json.get("authorizations").asArray().stream()
.map(JSON.Value::asURL) .map(JSON.Value::asURL)

View File

@ -14,6 +14,7 @@
package org.shredzone.acme4j; package org.shredzone.acme4j;
import java.io.Serializable; import java.io.Serializable;
import java.net.URI;
import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.JSON;
@ -25,6 +26,7 @@ import org.shredzone.acme4j.util.JSON;
public class Problem implements Serializable { public class Problem implements Serializable {
private static final long serialVersionUID = -8418248862966754214L; private static final long serialVersionUID = -8418248862966754214L;
private final URI baseUri;
private final JSON problem; private final JSON problem;
/** /**
@ -32,16 +34,20 @@ public class Problem implements Serializable {
* *
* @param problem * @param problem
* Problem as JSON structure * Problem as JSON structure
* @param baseUri
* Document's base {@link URI} to resolve relative URIs against
*/ */
public Problem(JSON problem) { public Problem(JSON problem, URI baseUri) {
this.problem = problem; this.problem = problem;
this.baseUri = baseUri;
} }
/** /**
* Returns the problem type. * Returns the problem type. It is always an absolute URI.
*/ */
public String getType() { public URI getType() {
return problem.get("type").asString(); String type = problem.get("type").asString();
return type != null ? baseUri.resolve(type) : null;
} }
/** /**
@ -51,6 +57,15 @@ public class Problem implements Serializable {
return problem.get("detail").asString(); return problem.get("detail").asString();
} }
/**
* Returns an URI that identifies the specific occurence of the problem. It is always
* an absolute URI.
*/
public URI getInstance() {
String instance = problem.get("instance").asString();
return instance != null ? baseUri.resolve(instance) : null;
}
/** /**
* Returns the problem as {@link JSON} object, to access other fields. * Returns the problem as {@link JSON} object, to access other fields.
*/ */

View File

@ -43,6 +43,7 @@ import org.jose4j.base64url.Base64Url;
import org.jose4j.jwk.PublicJsonWebKey; import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jws.JsonWebSignature; import org.jose4j.jws.JsonWebSignature;
import org.jose4j.lang.JoseException; import org.jose4j.lang.JoseException;
import org.shredzone.acme4j.Problem;
import org.shredzone.acme4j.Session; import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeNetworkException; import org.shredzone.acme4j.exception.AcmeNetworkException;
@ -237,7 +238,10 @@ public class DefaultConnection implements Connection {
throw new AcmeException("HTTP " + rc + ": " + conn.getResponseMessage()); throw new AcmeException("HTTP " + rc + ": " + conn.getResponseMessage());
} }
throw createAcmeException(readJsonResponse()); Problem problem = new Problem(readJsonResponse(), conn.getURL().toURI());
throw createAcmeException(problem);
} catch (URISyntaxException ex) {
throw new AcmeProtocolException("Bad request URI: " + conn.getURL(), ex);
} catch (IOException ex) { } catch (IOException ex) {
throw new AcmeNetworkException(ex); throw new AcmeNetworkException(ex);
} }
@ -411,32 +415,29 @@ public class DefaultConnection implements Connection {
* {@link AcmeServerException} or subtype will be thrown. Otherwise a generic * {@link AcmeServerException} or subtype will be thrown. Otherwise a generic
* {@link AcmeException} is thrown. * {@link AcmeException} is thrown.
*/ */
private AcmeException createAcmeException(JSON json) { private AcmeException createAcmeException(Problem problem) {
String type = json.get("type").asString(); if (problem.getType() == null) {
String detail = json.get("detail").asString(); return new AcmeException(problem.getDetail());
String error = AcmeUtils.stripErrorPrefix(type);
if (type == null) {
return new AcmeException(detail);
} }
String error = AcmeUtils.stripErrorPrefix(problem.getType().toString());
if ("unauthorized".equals(error)) { if ("unauthorized".equals(error)) {
return new AcmeUnauthorizedException(type, detail); return new AcmeUnauthorizedException(problem);
} }
if ("userActionRequired".equals(error)) { if ("userActionRequired".equals(error)) {
URI instance = resolveRelative(json.get("instance").asString());
URI tos = getLinks("terms-of-service").stream().findFirst().orElse(null); URI tos = getLinks("terms-of-service").stream().findFirst().orElse(null);
return new AcmeUserActionRequiredException(type, detail, tos, toURL(instance)); return new AcmeUserActionRequiredException(problem, tos);
} }
if ("rateLimited".equals(error)) { if ("rateLimited".equals(error)) {
Optional<Instant> retryAfter = getRetryAfterHeader(); Optional<Instant> retryAfter = getRetryAfterHeader();
Collection<URI> rateLimits = getLinks("urn:ietf:params:acme:documentation"); Collection<URI> rateLimits = getLinks("urn:ietf:params:acme:documentation");
return new AcmeRateLimitExceededException(type, detail, retryAfter.orElse(null), rateLimits); return new AcmeRateLimitExceededException(problem, retryAfter.orElse(null), rateLimits);
} }
return new AcmeServerException(type, detail); return new AcmeServerException(problem);
} }
/** /**

View File

@ -18,6 +18,8 @@ import java.time.Instant;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import org.shredzone.acme4j.Problem;
/** /**
* An exception that is thrown when a rate limit was exceeded. * An exception that is thrown when a rate limit was exceeded.
*/ */
@ -30,19 +32,16 @@ public class AcmeRateLimitExceededException extends AcmeServerException {
/** /**
* Creates a new {@link AcmeRateLimitExceededException}. * Creates a new {@link AcmeRateLimitExceededException}.
* *
* @param type * @param problem
* System readable error type (here * {@link Problem} that caused the exception
* {@code "urn:ietf:params:acme:error:rateLimited"})
* @param detail
* Human readable error message
* @param retryAfter * @param retryAfter
* The moment the request is expected to succeed again, may be {@code null} * The moment the request is expected to succeed again, may be {@code null}
* if not known * if not known
* @param documents * @param documents
* URIs pointing to documents about the rate limit that was hit * URIs pointing to documents about the rate limit that was hit
*/ */
public AcmeRateLimitExceededException(String type, String detail, Instant retryAfter, Collection<URI> documents) { public AcmeRateLimitExceededException(Problem problem, Instant retryAfter, Collection<URI> documents) {
super(type, detail); super(problem);
this.retryAfter = retryAfter; this.retryAfter = retryAfter;
this.documents = this.documents =
documents != null ? Collections.unmodifiableCollection(documents) : null; documents != null ? Collections.unmodifiableCollection(documents) : null;

View File

@ -13,8 +13,11 @@
*/ */
package org.shredzone.acme4j.exception; package org.shredzone.acme4j.exception;
import java.net.URI;
import java.util.Objects; import java.util.Objects;
import org.shredzone.acme4j.Problem;
/** /**
* An exception that is thrown when the ACME server returned an error. It contains * An exception that is thrown when the ACME server returned an error. It contains
* further details of the cause. * further details of the cause.
@ -22,27 +25,31 @@ import java.util.Objects;
public class AcmeServerException extends AcmeException { public class AcmeServerException extends AcmeException {
private static final long serialVersionUID = 5971622508467042792L; private static final long serialVersionUID = 5971622508467042792L;
private final String type; private final Problem problem;
/** /**
* Creates a new {@link AcmeServerException}. * Creates a new {@link AcmeServerException}.
* *
* @param type * @param problem
* System readable error type (e.g. * {@link Problem} that caused the exception
* {@code "urn:ietf:params:acme:error:malformed"})
* @param detail
* Human readable error message
*/ */
public AcmeServerException(String type, String detail) { public AcmeServerException(Problem problem) {
super(detail); super(Objects.requireNonNull(problem).getDetail());
this.type = Objects.requireNonNull(type, "type"); this.problem = problem;
} }
/** /**
* Returns the error type. * Returns the error type.
*/ */
public String getType() { public URI getType() {
return type; return problem.getType();
}
/**
* Returns the {@link Problem} that caused the exception
*/
public Problem getProblem() {
return problem;
} }
} }

View File

@ -13,6 +13,8 @@
*/ */
package org.shredzone.acme4j.exception; package org.shredzone.acme4j.exception;
import org.shredzone.acme4j.Problem;
/** /**
* An exception that is thrown when the client is not authorized. The details will give * An exception that is thrown when the client is not authorized. The details will give
* an explanation for the reasons (e.g. "client not on a whitelist"). * an explanation for the reasons (e.g. "client not on a whitelist").
@ -23,14 +25,11 @@ public class AcmeUnauthorizedException extends AcmeServerException {
/** /**
* Creates a new {@link AcmeUnauthorizedException}. * Creates a new {@link AcmeUnauthorizedException}.
* *
* @param type * @param problem
* System readable error type (here * {@link Problem} that caused the exception
* {@code "urn:ietf:params:acme:error:unauthorized"})
* @param detail
* Human readable error message
*/ */
public AcmeUnauthorizedException(String type, String detail) { public AcmeUnauthorizedException(Problem problem) {
super(type, detail); super(problem);
} }
} }

View File

@ -13,9 +13,12 @@
*/ */
package org.shredzone.acme4j.exception; package org.shredzone.acme4j.exception;
import java.net.MalformedURLException;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;
import org.shredzone.acme4j.Problem;
/** /**
* An exception that is thrown when the user is required to take action as indicated. * An exception that is thrown when the user is required to take action as indicated.
*/ */
@ -23,26 +26,18 @@ public class AcmeUserActionRequiredException extends AcmeServerException {
private static final long serialVersionUID = 7719055447283858352L; private static final long serialVersionUID = 7719055447283858352L;
private final URI tosUri; private final URI tosUri;
private final URL instance;
/** /**
* Creates a new {@link AcmeUserActionRequiredException}. * Creates a new {@link AcmeUserActionRequiredException}.
* *
* @param type * @param problem
* System readable error type (here * {@link Problem} that caused the exception
* {@code "urn:ietf:params:acme:error:userActionRequired"})
* @param detail
* Human readable error message
* @param tosUri * @param tosUri
* {@link URI} of the terms-of-service document to accept * {@link URI} of the terms-of-service document to accept
* @param instance
* {@link URL} to be visited by a human, showing instructions for how to
* agree to the terms and conditions.
*/ */
public AcmeUserActionRequiredException(String type, String detail, URI tosUri, URL instance) { public AcmeUserActionRequiredException(Problem problem, URI tosUri) {
super(type, detail); super(problem);
this.tosUri = tosUri; this.tosUri = tosUri;
this.instance = instance;
} }
/** /**
@ -58,7 +53,13 @@ public class AcmeUserActionRequiredException extends AcmeServerException {
* or {@code null} if the server did not provide such a link. * or {@code null} if the server did not provide such a link.
*/ */
public URL getInstance() { public URL getInstance() {
return instance; try {
URI instance = getProblem().getInstance();
return instance != null ? instance.toURL() : null;
} catch (MalformedURLException ex) {
throw new AcmeProtocolException(
"Bad instance URL: " + getProblem().getInstance().toString(), ex);
}
} }
} }

View File

@ -306,14 +306,20 @@ public final class JSON implements Serializable {
/** /**
* Returns the value as {@link Problem}. * Returns the value as {@link Problem}.
* *
* @param baseUrl
* Base {@link URL} to resolve relative links against
* @return {@link Problem}, or {@code null} if the value was not set. * @return {@link Problem}, or {@code null} if the value was not set.
*/ */
public Problem asProblem() { public Problem asProblem(URL baseUrl) {
if (val == null) { if (val == null) {
return null; return null;
} }
return new Problem(asObject()); try {
return new Problem(asObject(), baseUrl.toURI());
} catch (URISyntaxException ex) {
throw new IllegalArgumentException("Bad base URL", ex);
}
} }
/** /**

View File

@ -19,6 +19,7 @@ import static org.shredzone.acme4j.util.AcmeUtils.parseTimestamp;
import static org.shredzone.acme4j.util.TestUtils.*; import static org.shredzone.acme4j.util.TestUtils.*;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL; import java.net.URL;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -76,7 +77,7 @@ public class OrderTest {
assertThat(order.getCsr(), is(csr)); assertThat(order.getCsr(), is(csr));
assertThat(order.getError(), is(notNullValue())); assertThat(order.getError(), is(notNullValue()));
assertThat(order.getError().getType(), is("urn:ietf:params:acme:error:connection")); assertThat(order.getError().getType(), is(URI.create("urn:ietf:params:acme:error:connection")));
assertThat(order.getError().getDetail(), is("connection refused")); assertThat(order.getError().getDetail(), is("connection refused"));
List<Authorization> auths = order.getAuthorizations(); List<Authorization> auths = order.getAuthorizations();

View File

@ -17,6 +17,8 @@ import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
import java.net.URI;
import org.junit.Test; import org.junit.Test;
import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.JSON;
import org.shredzone.acme4j.util.TestUtils; import org.shredzone.acme4j.util.TestUtils;
@ -28,12 +30,14 @@ public class ProblemTest {
@Test @Test
public void testProblem() { public void testProblem() {
URI baseUri = URI.create("https://example.com/acme/1");
JSON original = TestUtils.getJSON("problem"); JSON original = TestUtils.getJSON("problem");
Problem problem = new Problem(original); Problem problem = new Problem(original, baseUri);
assertThat(problem.getType(), is("urn:ietf:params:acme:error:connection")); assertThat(problem.getType(), is(URI.create("urn:ietf:params:acme:error:connection")));
assertThat(problem.getDetail(), is("connection refused")); assertThat(problem.getDetail(), is("connection refused"));
assertThat(problem.getInstance(), is(URI.create("https://example.com/documents/error.html")));
assertThat(problem.asJSON().toString(), is(sameJSONAs(original.toString()))); assertThat(problem.asJSON().toString(), is(sameJSONAs(original.toString())));
assertThat(problem.toString(), is(sameJSONAs(original.toString()))); assertThat(problem.toString(), is(sameJSONAs(original.toString())));
} }

View File

@ -16,6 +16,7 @@ package org.shredzone.acme4j.connector;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import static org.shredzone.acme4j.util.TestUtils.url;
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@ -425,13 +426,14 @@ public class DefaultConnectionTest {
when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/problem+json"); when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/problem+json");
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN); when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN);
when(mockUrlConnection.getErrorStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8"))); when(mockUrlConnection.getErrorStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8")));
when(mockUrlConnection.getURL()).thenReturn(url("https://example.com/acme/1"));
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) { try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
conn.conn = mockUrlConnection; conn.conn = mockUrlConnection;
conn.accept(HttpURLConnection.HTTP_OK); conn.accept(HttpURLConnection.HTTP_OK);
fail("Expected to fail"); fail("Expected to fail");
} catch (AcmeServerException ex) { } catch (AcmeServerException ex) {
assertThat(ex.getType(), is("urn:ietf:params:acme:error:unauthorized")); assertThat(ex.getType(), is(URI.create("urn:ietf:params:acme:error:unauthorized")));
assertThat(ex.getMessage(), is("Invalid response: 404")); assertThat(ex.getMessage(), is("Invalid response: 404"));
} catch (AcmeException ex) { } catch (AcmeException ex) {
fail("Expected an AcmeServerException"); fail("Expected an AcmeServerException");
@ -440,6 +442,7 @@ public class DefaultConnectionTest {
verify(mockUrlConnection, atLeastOnce()).getHeaderField("Content-Type"); verify(mockUrlConnection, atLeastOnce()).getHeaderField("Content-Type");
verify(mockUrlConnection, atLeastOnce()).getResponseCode(); verify(mockUrlConnection, atLeastOnce()).getResponseCode();
verify(mockUrlConnection).getErrorStream(); verify(mockUrlConnection).getErrorStream();
verify(mockUrlConnection).getURL();
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
} }
@ -452,6 +455,8 @@ public class DefaultConnectionTest {
.thenReturn("application/problem+json"); .thenReturn("application/problem+json");
when(mockUrlConnection.getResponseCode()) when(mockUrlConnection.getResponseCode())
.thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR); .thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR);
when(mockUrlConnection.getURL())
.thenReturn(url("https://example.com/acme/1"));
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) { try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) {
@Override @Override
@ -466,7 +471,7 @@ public class DefaultConnectionTest {
conn.accept(HttpURLConnection.HTTP_OK); conn.accept(HttpURLConnection.HTTP_OK);
fail("Expected to fail"); fail("Expected to fail");
} catch (AcmeServerException ex) { } catch (AcmeServerException ex) {
assertThat(ex.getType(), is("urn:zombie:error:apocalypse")); assertThat(ex.getType(), is(URI.create("urn:zombie:error:apocalypse")));
assertThat(ex.getMessage(), is("Zombie apocalypse in progress")); assertThat(ex.getMessage(), is("Zombie apocalypse in progress"));
} catch (AcmeException ex) { } catch (AcmeException ex) {
fail("Expected an AcmeServerException"); fail("Expected an AcmeServerException");
@ -474,6 +479,7 @@ public class DefaultConnectionTest {
verify(mockUrlConnection).getHeaderField("Content-Type"); verify(mockUrlConnection).getHeaderField("Content-Type");
verify(mockUrlConnection, atLeastOnce()).getResponseCode(); verify(mockUrlConnection, atLeastOnce()).getResponseCode();
verify(mockUrlConnection).getURL();
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
} }
@ -486,6 +492,8 @@ public class DefaultConnectionTest {
.thenReturn("application/problem+json"); .thenReturn("application/problem+json");
when(mockUrlConnection.getResponseCode()) when(mockUrlConnection.getResponseCode())
.thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR); .thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR);
when(mockUrlConnection.getURL())
.thenReturn(url("https://example.com/acme/1"));
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) { try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) {
@Override @Override
@ -504,6 +512,7 @@ public class DefaultConnectionTest {
verify(mockUrlConnection).getHeaderField("Content-Type"); verify(mockUrlConnection).getHeaderField("Content-Type");
verify(mockUrlConnection, atLeastOnce()).getResponseCode(); verify(mockUrlConnection, atLeastOnce()).getResponseCode();
verify(mockUrlConnection).getURL();
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
} }

View File

@ -15,6 +15,7 @@ package org.shredzone.acme4j.exception;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.shredzone.acme4j.util.TestUtils.createProblem;
import java.net.URI; import java.net.URI;
import java.time.Duration; import java.time.Duration;
@ -23,6 +24,7 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import org.junit.Test; import org.junit.Test;
import org.shredzone.acme4j.Problem;
/** /**
* Unit tests for {@link AcmeRateLimitExceededException}. * Unit tests for {@link AcmeRateLimitExceededException}.
@ -34,15 +36,17 @@ public class AcmeRateLimitExceededExceptionTest {
*/ */
@Test @Test
public void testAcmeRateLimitExceededException() { public void testAcmeRateLimitExceededException() {
String type = "urn:ietf:params:acme:error:rateLimited"; URI type = URI.create("urn:ietf:params:acme:error:rateLimited");
String detail = "Too many requests per minute"; String detail = "Too many requests per minute";
Instant retryAfter = Instant.now().plus(Duration.ofMinutes(1)); Instant retryAfter = Instant.now().plus(Duration.ofMinutes(1));
Collection<URI> documents = Arrays.asList( Collection<URI> documents = Arrays.asList(
URI.create("http://example.com/doc1.html"), URI.create("http://example.com/doc1.html"),
URI.create("http://example.com/doc2.html")); URI.create("http://example.com/doc2.html"));
Problem problem = createProblem(type, detail, null);
AcmeRateLimitExceededException ex AcmeRateLimitExceededException ex
= new AcmeRateLimitExceededException(type, detail, retryAfter, documents); = new AcmeRateLimitExceededException(problem, retryAfter, documents);
assertThat(ex.getType(), is(type)); assertThat(ex.getType(), is(type));
assertThat(ex.getMessage(), is(detail)); assertThat(ex.getMessage(), is(detail));
@ -55,11 +59,13 @@ public class AcmeRateLimitExceededExceptionTest {
*/ */
@Test @Test
public void testNullAcmeRateLimitExceededException() { public void testNullAcmeRateLimitExceededException() {
String type = "urn:ietf:params:acme:error:rateLimited"; URI type = URI.create("urn:ietf:params:acme:error:rateLimited");
String detail = "Too many requests per minute"; String detail = "Too many requests per minute";
Problem problem = createProblem(type, detail, null);
AcmeRateLimitExceededException ex AcmeRateLimitExceededException ex
= new AcmeRateLimitExceededException(type, detail, null, null); = new AcmeRateLimitExceededException(problem, null, null);
assertThat(ex.getType(), is(type)); assertThat(ex.getType(), is(type));
assertThat(ex.getMessage(), is(detail)); assertThat(ex.getMessage(), is(detail));

View File

@ -15,12 +15,14 @@ package org.shredzone.acme4j.exception;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.shredzone.acme4j.util.TestUtils.createProblem;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;
import org.junit.Test; import org.junit.Test;
import org.shredzone.acme4j.Problem;
/** /**
* Unit tests for {@link AcmeUserActionRequiredException}. * Unit tests for {@link AcmeUserActionRequiredException}.
@ -32,13 +34,15 @@ public class AcmeUserActionRequiredExceptionTest {
*/ */
@Test @Test
public void testAcmeUserActionRequiredException() throws MalformedURLException { public void testAcmeUserActionRequiredException() throws MalformedURLException {
String type = "urn:ietf:params:acme:error:userActionRequired"; URI type = URI.create("urn:ietf:params:acme:error:userActionRequired");
String detail = "Accept new TOS"; String detail = "Accept new TOS";
URI tosUri = URI.create("http://example.com/agreement.pdf"); URI tosUri = URI.create("http://example.com/agreement.pdf");
URL instanceUrl = new URL("http://example.com/howToAgree.html"); URL instanceUrl = new URL("http://example.com/howToAgree.html");
Problem problem = createProblem(type, detail, instanceUrl);
AcmeUserActionRequiredException ex AcmeUserActionRequiredException ex
= new AcmeUserActionRequiredException(type, detail, tosUri, instanceUrl); = new AcmeUserActionRequiredException(problem, tosUri);
assertThat(ex.getType(), is(type)); assertThat(ex.getType(), is(type));
assertThat(ex.getMessage(), is(detail)); assertThat(ex.getMessage(), is(detail));
@ -51,11 +55,13 @@ public class AcmeUserActionRequiredExceptionTest {
*/ */
@Test @Test
public void testNullAcmeUserActionRequiredException() { public void testNullAcmeUserActionRequiredException() {
String type = "urn:ietf:params:acme:error:userActionRequired"; URI type = URI.create("urn:ietf:params:acme:error:userActionRequired");
String detail = "Call our service"; String detail = "Call our service";
Problem problem = createProblem(type, detail, null);
AcmeUserActionRequiredException ex AcmeUserActionRequiredException ex
= new AcmeUserActionRequiredException(type, detail, null, null); = new AcmeUserActionRequiredException(problem, null);
assertThat(ex.getType(), is(type)); assertThat(ex.getType(), is(type));
assertThat(ex.getMessage(), is(detail)); assertThat(ex.getMessage(), is(detail));

View File

@ -26,6 +26,7 @@ import java.io.ObjectInputStream;
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URI; import java.net.URI;
import java.net.URL;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.ZoneId; import java.time.ZoneId;
@ -45,6 +46,8 @@ import org.shredzone.acme4j.exception.AcmeProtocolException;
*/ */
public class JSONTest { public class JSONTest {
private static final URL BASE_URL = url("https://example.com/acme/1");
/** /**
* Test that an empty {@link JSON} is empty. * Test that an empty {@link JSON} is empty.
*/ */
@ -210,10 +213,11 @@ public class JSONTest {
JSON sub = array.get(3).asObject(); JSON sub = array.get(3).asObject();
assertThat(sub.get("test").asString(), is("ok")); assertThat(sub.get("test").asString(), is("ok"));
Problem problem = json.get("problem").asProblem(); Problem problem = json.get("problem").asProblem(BASE_URL);
assertThat(problem, is(notNullValue())); assertThat(problem, is(notNullValue()));
assertThat(problem.getType(), is("urn:ietf:params:acme:error:rateLimited")); assertThat(problem.getType(), is(URI.create("urn:ietf:params:acme:error:rateLimited")));
assertThat(problem.getDetail(), is("too many requests")); assertThat(problem.getDetail(), is("too many requests"));
assertThat(problem.getInstance(), is(URI.create("https://example.com/documents/errors.html")));
} }
/** /**
@ -231,7 +235,7 @@ public class JSONTest {
assertThat(json.get("none").asObject(), is(nullValue())); assertThat(json.get("none").asObject(), is(nullValue()));
assertThat(json.get("none").asStatusOrElse(Status.INVALID), is(Status.INVALID)); assertThat(json.get("none").asStatusOrElse(Status.INVALID), is(Status.INVALID));
assertThat(json.get("none").asBinary(), is(nullValue())); assertThat(json.get("none").asBinary(), is(nullValue()));
assertThat(json.get("none").asProblem(), is(nullValue())); assertThat(json.get("none").asProblem(BASE_URL), is(nullValue()));
try { try {
json.get("none").asInt(); json.get("none").asInt();
@ -308,7 +312,7 @@ public class JSONTest {
} }
try { try {
json.get("text").asProblem(); json.get("text").asProblem(BASE_URL);
fail("no exception was thrown"); fail("no exception was thrown");
} catch (AcmeProtocolException ex) { } catch (AcmeProtocolException ex) {
// expected // expected

View File

@ -52,6 +52,7 @@ import org.jose4j.base64url.Base64Url;
import org.jose4j.json.JsonUtil; import org.jose4j.json.JsonUtil;
import org.jose4j.jwk.JsonWebKey; import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.JsonWebKey.OutputControlLevel; import org.jose4j.jwk.JsonWebKey.OutputControlLevel;
import org.shredzone.acme4j.Problem;
import org.shredzone.acme4j.Session; import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.provider.AcmeProvider; import org.shredzone.acme4j.provider.AcmeProvider;
@ -268,6 +269,30 @@ public final class TestUtils {
}; };
} }
/**
* Creates a {@link Problem} with the given type and details.
*
* @param type
* Problem type
* @param detail
* Problem details
* @param instance
* Instance, or {@code null}
* @return Created {@link Problem} object
*/
public static Problem createProblem(URI type, String detail, URL instance) {
URI baseUri = URI.create("https://example.com/acme/1");
JSONBuilder jb = new JSONBuilder();
jb.put("type", type);
jb.put("detail", detail);
if (instance != null) {
jb.put("instance", instance);
}
return new Problem(jb.toJSON(), baseUri);
}
/** /**
* Generates a new keypair for unit tests, and return its N, E, KTY and THUMBPRINT * Generates a new keypair for unit tests, and return its N, E, KTY and THUMBPRINT
* parameters to be set in the {@link TestUtils} class. * parameters to be set in the {@link TestUtils} class.

View File

@ -11,6 +11,7 @@
"binary": "Q2hhaW5zYXc", "binary": "Q2hhaW5zYXc",
"problem": { "problem": {
"type": "urn:ietf:params:acme:error:rateLimited", "type": "urn:ietf:params:acme:error:rateLimited",
"detail": "too many requests" "detail": "too many requests",
"instance": "/documents/errors.html"
} }
} }

View File

@ -1,4 +1,5 @@
{ {
"type": "urn:ietf:params:acme:error:connection", "type": "urn:ietf:params:acme:error:connection",
"detail": "connection refused" "detail": "connection refused",
"instance": "/documents/error.html"
} }