mirror of https://github.com/shred/acme4j
Use Problem in AcmeServerException
parent
c623d72426
commit
02cedf9935
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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())));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue