diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java index e7d5964c..aaee9efd 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java @@ -172,7 +172,7 @@ public class Order extends AcmeResource { URL certUrl = json.get("certificate").asURL(); 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() .map(JSON.Value::asURL) diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Problem.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Problem.java index 66eb15cd..a46a250c 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Problem.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Problem.java @@ -14,6 +14,7 @@ package org.shredzone.acme4j; import java.io.Serializable; +import java.net.URI; import org.shredzone.acme4j.util.JSON; @@ -25,6 +26,7 @@ import org.shredzone.acme4j.util.JSON; public class Problem implements Serializable { private static final long serialVersionUID = -8418248862966754214L; + private final URI baseUri; private final JSON problem; /** @@ -32,16 +34,20 @@ public class Problem implements Serializable { * * @param problem * 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.baseUri = baseUri; } /** - * Returns the problem type. + * Returns the problem type. It is always an absolute URI. */ - public String getType() { - return problem.get("type").asString(); + public URI getType() { + 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(); } + /** + * 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. */ diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java index 6534b769..a9b5ca50 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java @@ -43,6 +43,7 @@ import org.jose4j.base64url.Base64Url; import org.jose4j.jwk.PublicJsonWebKey; import org.jose4j.jws.JsonWebSignature; import org.jose4j.lang.JoseException; +import org.shredzone.acme4j.Problem; import org.shredzone.acme4j.Session; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeNetworkException; @@ -237,7 +238,10 @@ public class DefaultConnection implements Connection { 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) { throw new AcmeNetworkException(ex); } @@ -411,32 +415,29 @@ public class DefaultConnection implements Connection { * {@link AcmeServerException} or subtype will be thrown. Otherwise a generic * {@link AcmeException} is thrown. */ - private AcmeException createAcmeException(JSON json) { - String type = json.get("type").asString(); - String detail = json.get("detail").asString(); - String error = AcmeUtils.stripErrorPrefix(type); - - if (type == null) { - return new AcmeException(detail); + private AcmeException createAcmeException(Problem problem) { + if (problem.getType() == null) { + return new AcmeException(problem.getDetail()); } + String error = AcmeUtils.stripErrorPrefix(problem.getType().toString()); + if ("unauthorized".equals(error)) { - return new AcmeUnauthorizedException(type, detail); + return new AcmeUnauthorizedException(problem); } if ("userActionRequired".equals(error)) { - URI instance = resolveRelative(json.get("instance").asString()); 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)) { Optional retryAfter = getRetryAfterHeader(); Collection 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); } /** diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededException.java b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededException.java index 66792440..d29256fe 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededException.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededException.java @@ -18,6 +18,8 @@ import java.time.Instant; import java.util.Collection; import java.util.Collections; +import org.shredzone.acme4j.Problem; + /** * 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}. * - * @param type - * System readable error type (here - * {@code "urn:ietf:params:acme:error:rateLimited"}) - * @param detail - * Human readable error message + * @param problem + * {@link Problem} that caused the exception * @param retryAfter * The moment the request is expected to succeed again, may be {@code null} * if not known * @param documents * URIs pointing to documents about the rate limit that was hit */ - public AcmeRateLimitExceededException(String type, String detail, Instant retryAfter, Collection documents) { - super(type, detail); + public AcmeRateLimitExceededException(Problem problem, Instant retryAfter, Collection documents) { + super(problem); this.retryAfter = retryAfter; this.documents = documents != null ? Collections.unmodifiableCollection(documents) : null; diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeServerException.java b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeServerException.java index 7c0010eb..c36c227f 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeServerException.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeServerException.java @@ -13,8 +13,11 @@ */ package org.shredzone.acme4j.exception; +import java.net.URI; import java.util.Objects; +import org.shredzone.acme4j.Problem; + /** * An exception that is thrown when the ACME server returned an error. It contains * further details of the cause. @@ -22,27 +25,31 @@ import java.util.Objects; public class AcmeServerException extends AcmeException { private static final long serialVersionUID = 5971622508467042792L; - private final String type; + private final Problem problem; /** * Creates a new {@link AcmeServerException}. * - * @param type - * System readable error type (e.g. - * {@code "urn:ietf:params:acme:error:malformed"}) - * @param detail - * Human readable error message + * @param problem + * {@link Problem} that caused the exception */ - public AcmeServerException(String type, String detail) { - super(detail); - this.type = Objects.requireNonNull(type, "type"); + public AcmeServerException(Problem problem) { + super(Objects.requireNonNull(problem).getDetail()); + this.problem = problem; } /** * Returns the error type. */ - public String getType() { - return type; + public URI getType() { + return problem.getType(); + } + + /** + * Returns the {@link Problem} that caused the exception + */ + public Problem getProblem() { + return problem; } } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeUnauthorizedException.java b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeUnauthorizedException.java index 651d807e..11d8fec8 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeUnauthorizedException.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeUnauthorizedException.java @@ -13,6 +13,8 @@ */ 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 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}. * - * @param type - * System readable error type (here - * {@code "urn:ietf:params:acme:error:unauthorized"}) - * @param detail - * Human readable error message + * @param problem + * {@link Problem} that caused the exception */ - public AcmeUnauthorizedException(String type, String detail) { - super(type, detail); + public AcmeUnauthorizedException(Problem problem) { + super(problem); } } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeUserActionRequiredException.java b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeUserActionRequiredException.java index 6a5dfee6..7f6ea698 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeUserActionRequiredException.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeUserActionRequiredException.java @@ -13,9 +13,12 @@ */ package org.shredzone.acme4j.exception; +import java.net.MalformedURLException; import java.net.URI; import java.net.URL; +import org.shredzone.acme4j.Problem; + /** * 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 final URI tosUri; - private final URL instance; /** * Creates a new {@link AcmeUserActionRequiredException}. * - * @param type - * System readable error type (here - * {@code "urn:ietf:params:acme:error:userActionRequired"}) - * @param detail - * Human readable error message + * @param problem + * {@link Problem} that caused the exception * @param tosUri * {@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) { - super(type, detail); + public AcmeUserActionRequiredException(Problem problem, URI tosUri) { + super(problem); 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. */ 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); + } } } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/util/JSON.java b/acme4j-client/src/main/java/org/shredzone/acme4j/util/JSON.java index 9df71ade..a2a6b6cf 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/util/JSON.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/util/JSON.java @@ -306,14 +306,20 @@ public final class JSON implements Serializable { /** * 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. */ - public Problem asProblem() { + public Problem asProblem(URL baseUrl) { if (val == null) { return null; } - return new Problem(asObject()); + try { + return new Problem(asObject(), baseUrl.toURI()); + } catch (URISyntaxException ex) { + throw new IllegalArgumentException("Bad base URL", ex); + } } /** diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java index 0abf4a78..2353a81f 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java @@ -19,6 +19,7 @@ import static org.shredzone.acme4j.util.AcmeUtils.parseTimestamp; import static org.shredzone.acme4j.util.TestUtils.*; import java.net.HttpURLConnection; +import java.net.URI; import java.net.URL; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -76,7 +77,7 @@ public class OrderTest { assertThat(order.getCsr(), is(csr)); 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")); List auths = order.getAuthorizations(); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/ProblemTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/ProblemTest.java index edad3c9a..29cd4126 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/ProblemTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/ProblemTest.java @@ -17,6 +17,8 @@ import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; +import java.net.URI; + import org.junit.Test; import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.TestUtils; @@ -28,12 +30,14 @@ public class ProblemTest { @Test public void testProblem() { + URI baseUri = URI.create("https://example.com/acme/1"); 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.getInstance(), is(URI.create("https://example.com/documents/error.html"))); assertThat(problem.asJSON().toString(), is(sameJSONAs(original.toString()))); assertThat(problem.toString(), is(sameJSONAs(original.toString()))); } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java index fd2a8ed2..17d508db 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java @@ -16,6 +16,7 @@ package org.shredzone.acme4j.connector; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; +import static org.shredzone.acme4j.util.TestUtils.url; import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; import java.io.ByteArrayInputStream; @@ -425,13 +426,14 @@ public class DefaultConnectionTest { 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"))); + when(mockUrlConnection.getURL()).thenReturn(url("https://example.com/acme/1")); try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) { conn.conn = mockUrlConnection; conn.accept(HttpURLConnection.HTTP_OK); fail("Expected to fail"); } 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")); } catch (AcmeException ex) { fail("Expected an AcmeServerException"); @@ -440,6 +442,7 @@ public class DefaultConnectionTest { verify(mockUrlConnection, atLeastOnce()).getHeaderField("Content-Type"); verify(mockUrlConnection, atLeastOnce()).getResponseCode(); verify(mockUrlConnection).getErrorStream(); + verify(mockUrlConnection).getURL(); verifyNoMoreInteractions(mockUrlConnection); } @@ -452,6 +455,8 @@ public class DefaultConnectionTest { .thenReturn("application/problem+json"); when(mockUrlConnection.getResponseCode()) .thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR); + when(mockUrlConnection.getURL()) + .thenReturn(url("https://example.com/acme/1")); try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) { @Override @@ -466,7 +471,7 @@ public class DefaultConnectionTest { conn.accept(HttpURLConnection.HTTP_OK); fail("Expected to fail"); } 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")); } catch (AcmeException ex) { fail("Expected an AcmeServerException"); @@ -474,6 +479,7 @@ public class DefaultConnectionTest { verify(mockUrlConnection).getHeaderField("Content-Type"); verify(mockUrlConnection, atLeastOnce()).getResponseCode(); + verify(mockUrlConnection).getURL(); verifyNoMoreInteractions(mockUrlConnection); } @@ -486,6 +492,8 @@ public class DefaultConnectionTest { .thenReturn("application/problem+json"); when(mockUrlConnection.getResponseCode()) .thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR); + when(mockUrlConnection.getURL()) + .thenReturn(url("https://example.com/acme/1")); try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) { @Override @@ -504,6 +512,7 @@ public class DefaultConnectionTest { verify(mockUrlConnection).getHeaderField("Content-Type"); verify(mockUrlConnection, atLeastOnce()).getResponseCode(); + verify(mockUrlConnection).getURL(); verifyNoMoreInteractions(mockUrlConnection); } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededExceptionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededExceptionTest.java index 8450d6cf..b587d412 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededExceptionTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededExceptionTest.java @@ -15,6 +15,7 @@ package org.shredzone.acme4j.exception; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; +import static org.shredzone.acme4j.util.TestUtils.createProblem; import java.net.URI; import java.time.Duration; @@ -23,6 +24,7 @@ import java.util.Arrays; import java.util.Collection; import org.junit.Test; +import org.shredzone.acme4j.Problem; /** * Unit tests for {@link AcmeRateLimitExceededException}. @@ -34,15 +36,17 @@ public class AcmeRateLimitExceededExceptionTest { */ @Test 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"; Instant retryAfter = Instant.now().plus(Duration.ofMinutes(1)); Collection documents = Arrays.asList( URI.create("http://example.com/doc1.html"), URI.create("http://example.com/doc2.html")); + Problem problem = createProblem(type, detail, null); + AcmeRateLimitExceededException ex - = new AcmeRateLimitExceededException(type, detail, retryAfter, documents); + = new AcmeRateLimitExceededException(problem, retryAfter, documents); assertThat(ex.getType(), is(type)); assertThat(ex.getMessage(), is(detail)); @@ -55,11 +59,13 @@ public class AcmeRateLimitExceededExceptionTest { */ @Test 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"; + Problem problem = createProblem(type, detail, null); + AcmeRateLimitExceededException ex - = new AcmeRateLimitExceededException(type, detail, null, null); + = new AcmeRateLimitExceededException(problem, null, null); assertThat(ex.getType(), is(type)); assertThat(ex.getMessage(), is(detail)); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeUserActionRequiredExceptionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeUserActionRequiredExceptionTest.java index bc37791e..db615314 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeUserActionRequiredExceptionTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeUserActionRequiredExceptionTest.java @@ -15,12 +15,14 @@ package org.shredzone.acme4j.exception; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; +import static org.shredzone.acme4j.util.TestUtils.createProblem; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import org.junit.Test; +import org.shredzone.acme4j.Problem; /** * Unit tests for {@link AcmeUserActionRequiredException}. @@ -32,13 +34,15 @@ public class AcmeUserActionRequiredExceptionTest { */ @Test 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"; URI tosUri = URI.create("http://example.com/agreement.pdf"); URL instanceUrl = new URL("http://example.com/howToAgree.html"); + Problem problem = createProblem(type, detail, instanceUrl); + AcmeUserActionRequiredException ex - = new AcmeUserActionRequiredException(type, detail, tosUri, instanceUrl); + = new AcmeUserActionRequiredException(problem, tosUri); assertThat(ex.getType(), is(type)); assertThat(ex.getMessage(), is(detail)); @@ -51,11 +55,13 @@ public class AcmeUserActionRequiredExceptionTest { */ @Test 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"; + Problem problem = createProblem(type, detail, null); + AcmeUserActionRequiredException ex - = new AcmeUserActionRequiredException(type, detail, null, null); + = new AcmeUserActionRequiredException(problem, null); assertThat(ex.getType(), is(type)); assertThat(ex.getMessage(), is(detail)); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/util/JSONTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/util/JSONTest.java index 13838d62..0dfeaf28 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/util/JSONTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/util/JSONTest.java @@ -26,6 +26,7 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.MalformedURLException; import java.net.URI; +import java.net.URL; import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; @@ -45,6 +46,8 @@ import org.shredzone.acme4j.exception.AcmeProtocolException; */ public class JSONTest { + private static final URL BASE_URL = url("https://example.com/acme/1"); + /** * Test that an empty {@link JSON} is empty. */ @@ -210,10 +213,11 @@ public class JSONTest { JSON sub = array.get(3).asObject(); 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.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.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").asStatusOrElse(Status.INVALID), is(Status.INVALID)); assertThat(json.get("none").asBinary(), is(nullValue())); - assertThat(json.get("none").asProblem(), is(nullValue())); + assertThat(json.get("none").asProblem(BASE_URL), is(nullValue())); try { json.get("none").asInt(); @@ -308,7 +312,7 @@ public class JSONTest { } try { - json.get("text").asProblem(); + json.get("text").asProblem(BASE_URL); fail("no exception was thrown"); } catch (AcmeProtocolException ex) { // expected diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/util/TestUtils.java b/acme4j-client/src/test/java/org/shredzone/acme4j/util/TestUtils.java index 83f39183..3e1b8e94 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/util/TestUtils.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/util/TestUtils.java @@ -52,6 +52,7 @@ import org.jose4j.base64url.Base64Url; import org.jose4j.json.JsonUtil; import org.jose4j.jwk.JsonWebKey; import org.jose4j.jwk.JsonWebKey.OutputControlLevel; +import org.shredzone.acme4j.Problem; import org.shredzone.acme4j.Session; 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 * parameters to be set in the {@link TestUtils} class. diff --git a/acme4j-client/src/test/resources/json/datatypes.json b/acme4j-client/src/test/resources/json/datatypes.json index 72374e6d..90997f90 100644 --- a/acme4j-client/src/test/resources/json/datatypes.json +++ b/acme4j-client/src/test/resources/json/datatypes.json @@ -11,6 +11,7 @@ "binary": "Q2hhaW5zYXc", "problem": { "type": "urn:ietf:params:acme:error:rateLimited", - "detail": "too many requests" + "detail": "too many requests", + "instance": "/documents/errors.html" } } diff --git a/acme4j-client/src/test/resources/json/problem.json b/acme4j-client/src/test/resources/json/problem.json index 41e78281..f43a53fd 100644 --- a/acme4j-client/src/test/resources/json/problem.json +++ b/acme4j-client/src/test/resources/json/problem.json @@ -1,4 +1,5 @@ { "type": "urn:ietf:params:acme:error:connection", - "detail": "connection refused" + "detail": "connection refused", + "instance": "/documents/error.html" }