Skip to content

Commit 5bfe852

Browse files
fix: handle content types other than application json and update test.p12 for test environment.
1 parent 30d6059 commit 5bfe852

File tree

5 files changed

+249
-4
lines changed

5 files changed

+249
-4
lines changed

src/main/java/dev/eidentification/bankid/exceptions/BankIdApiUnexpectedResponseException.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package dev.eidentification.bankid.exceptions;
22

3+
import org.jspecify.annotations.Nullable;
4+
35
import java.net.http.HttpResponse;
46

57
/**
@@ -18,8 +20,14 @@ public final class BankIdApiUnexpectedResponseException extends BankIdException
1820
private final HttpResponse.ResponseInfo responseInfo;
1921
private final String responseBody;
2022

21-
private BankIdApiUnexpectedResponseException(final HttpResponse.ResponseInfo responseInfo, final String responseBody, final Throwable cause) {
22-
super(cause);
23+
private BankIdApiUnexpectedResponseException(final HttpResponse.ResponseInfo responseInfo, final String responseBody) {
24+
super("BankId API returned an unexpected response. Body: " + responseBody);
25+
this.responseInfo = responseInfo;
26+
this.responseBody = responseBody;
27+
}
28+
29+
private BankIdApiUnexpectedResponseException(final HttpResponse.ResponseInfo responseInfo, final String responseBody, @Nullable final Throwable cause) {
30+
super("BankId API returned an unexpected response. Body: " + responseBody, cause);
2331
this.responseInfo = responseInfo;
2432
this.responseBody = responseBody;
2533
}
@@ -32,10 +40,22 @@ private BankIdApiUnexpectedResponseException(final HttpResponse.ResponseInfo res
3240
* @param cause The cause of the exception.
3341
* @return A new instance of BankIdApiUnexpectedResponseException.
3442
*/
35-
public static BankIdApiUnexpectedResponseException of(final HttpResponse.ResponseInfo responseInfo, final String responseBody, final Throwable cause) {
43+
public static BankIdApiUnexpectedResponseException of(final HttpResponse.ResponseInfo responseInfo, final String responseBody,
44+
@Nullable final Throwable cause) {
3645
return new BankIdApiUnexpectedResponseException(responseInfo, responseBody, cause);
3746
}
3847

48+
/**
49+
* Creates a new instance of BankIdApiUnexpectedResponseException.
50+
*
51+
* @param responseInfo The response information associated with the exception.
52+
* @param responseBody The response body associated with the exception.
53+
* @return A new instance of BankIdApiUnexpectedResponseException.
54+
*/
55+
public static BankIdApiUnexpectedResponseException of(final HttpResponse.ResponseInfo responseInfo, final String responseBody) {
56+
return new BankIdApiUnexpectedResponseException(responseInfo, responseBody);
57+
}
58+
3959
/**
4060
* Retrieves the response information associated with an exception thrown during interaction with the BankId system.
4161
*

src/main/java/dev/eidentification/bankid/internal/http/JsonBodyHandler.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
package dev.eidentification.bankid.internal.http;
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
4-
import dev.eidentification.bankid.internal.annotations.Internal;
54
import dev.eidentification.bankid.client.response.ErrorResponse;
65
import dev.eidentification.bankid.client.response.Response;
76
import dev.eidentification.bankid.exceptions.BankIdApiErrorException;
87
import dev.eidentification.bankid.exceptions.BankIdApiUnexpectedResponseException;
8+
import dev.eidentification.bankid.internal.annotations.Internal;
99

1010
import java.io.InputStream;
1111
import java.net.http.HttpResponse;
1212
import java.nio.charset.StandardCharsets;
13+
import java.util.Optional;
1314

1415
@Internal
1516
public class JsonBodyHandler<R extends Response, E extends ErrorResponse> implements HttpResponse.BodyHandler<R> {
@@ -41,6 +42,16 @@ public HttpResponse.BodySubscriber<R> apply(final HttpResponse.ResponseInfo resp
4142
return HttpResponse.BodySubscribers.mapping(upstream, inputStream -> inputStreamTo(responseClazz, responseInfo, inputStream));
4243
}
4344

45+
final Optional<String> optionalContentType = responseInfo.headers()
46+
.firstValue("Content-Type")
47+
.map(String::toLowerCase);
48+
49+
if (optionalContentType.isEmpty() || !optionalContentType.get().contains("application/json")) {
50+
return HttpResponse.BodySubscribers.mapping(upstream, inputStream -> {
51+
throw BankIdApiUnexpectedResponseException.of(responseInfo, inputStreamToString(responseInfo, inputStream));
52+
});
53+
}
54+
4455
return HttpResponse.BodySubscribers.mapping(upstream, inputStream -> {
4556
throw BankIdApiErrorException.of(inputStreamTo(errorResponseClazz, responseInfo, inputStream));
4657
});
@@ -58,4 +69,13 @@ private <T> T inputStreamTo(final Class<T> targetType, final HttpResponse.Respon
5869
}
5970
}
6071

72+
private static String inputStreamToString(final HttpResponse.ResponseInfo responseInfo, final InputStream inputStream) {
73+
String body = "";
74+
75+
try (final InputStream stream = inputStream) {
76+
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
77+
} catch (final Exception e) {
78+
throw BankIdApiUnexpectedResponseException.of(responseInfo, body, e);
79+
}
80+
}
6181
}

src/main/resources/ca.prod.crt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
-----BEGIN CERTIFICATE-----
2+
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
33+
-----END CERTIFICATE-----

src/main/resources/test.p12

150 Bytes
Binary file not shown.
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package dev.eidentification.bankid.internal.http;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import dev.eidentification.bankid.UnitTest;
5+
import dev.eidentification.bankid.client.response.ErrorResponse;
6+
import dev.eidentification.bankid.client.response.SignResponse;
7+
import dev.eidentification.bankid.exceptions.BankIdApiErrorException;
8+
import dev.eidentification.bankid.exceptions.BankIdApiUnexpectedResponseException;
9+
import org.junit.jupiter.api.Assertions;
10+
import org.junit.jupiter.api.Test;
11+
12+
import java.net.http.HttpClient;
13+
import java.net.http.HttpHeaders;
14+
import java.net.http.HttpResponse;
15+
import java.nio.ByteBuffer;
16+
import java.nio.charset.StandardCharsets;
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.concurrent.CompletableFuture;
20+
import java.util.concurrent.CompletionException;
21+
import java.util.concurrent.ExecutionException;
22+
import java.util.concurrent.TimeUnit;
23+
import java.util.concurrent.TimeoutException;
24+
25+
import static org.junit.jupiter.api.Assertions.fail;
26+
27+
class JsonBodyHandlerUnitTest extends UnitTest {
28+
private final ObjectMapper objectMapper = new ObjectMapper();
29+
30+
@Test
31+
void givenStatus200AndValidJson_whenApply_thenParsesResponse() {
32+
final JsonBodyHandler<SignResponse, ErrorResponse> handler =
33+
new JsonBodyHandler<>(SignResponse.class, ErrorResponse.class, objectMapper);
34+
35+
final HttpResponse.ResponseInfo info = responseInfo(200, "application/json; charset=utf-8");
36+
37+
final String json = """
38+
{
39+
"orderRef":"131daac9-16c6-4618-beb0-365768f37288",
40+
"autoStartToken":"7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6"
41+
}""";
42+
43+
final HttpResponse.BodySubscriber<SignResponse> subscriber = handler.apply(info);
44+
final SignResponse result = complete(feed(subscriber, json));
45+
46+
Assertions.assertNotNull(result);
47+
Assertions.assertEquals("131daac9-16c6-4618-beb0-365768f37288", result.getOrderRef());
48+
Assertions.assertEquals("7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6", result.getAutoStartToken());
49+
}
50+
51+
@Test
52+
void givenStatus200AndInvalidJson_whenApply_thenThrowsUnexpectedResponse() {
53+
final JsonBodyHandler<SignResponse, ErrorResponse> handler =
54+
new JsonBodyHandler<>(SignResponse.class, ErrorResponse.class, objectMapper);
55+
final HttpResponse.ResponseInfo info = responseInfo(200, "application/json");
56+
final String invalidJson = "not-a-json";
57+
58+
final HttpResponse.BodySubscriber<SignResponse> subscriber = handler.apply(info);
59+
60+
final BankIdApiUnexpectedResponseException ex = Assertions.assertThrows(
61+
BankIdApiUnexpectedResponseException.class,
62+
() -> complete(feed(subscriber, invalidJson))
63+
);
64+
Assertions.assertTrue(ex.getMessage() != null && !ex.getMessage().isBlank());
65+
}
66+
67+
@Test
68+
void givenNon200AndMissingContentType_whenApply_thenThrowsUnexpectedResponse() {
69+
final JsonBodyHandler<SignResponse, ErrorResponse> handler =
70+
new JsonBodyHandler<>(SignResponse.class, ErrorResponse.class, objectMapper);
71+
final HttpResponse.ResponseInfo info = responseInfo(500, null);
72+
final String body = "<html>Server error</html>";
73+
74+
final HttpResponse.BodySubscriber<SignResponse> subscriber = handler.apply(info);
75+
76+
Assertions.assertThrows(BankIdApiUnexpectedResponseException.class, () -> complete(feed(subscriber, body)));
77+
}
78+
79+
@Test
80+
void givenNon200AndNonJsonContentType_whenApply_thenThrowsUnexpectedResponse() {
81+
final JsonBodyHandler<SignResponse, ErrorResponse> handler =
82+
new JsonBodyHandler<>(SignResponse.class, ErrorResponse.class, objectMapper);
83+
final HttpResponse.ResponseInfo info = responseInfo(404, "text/plain");
84+
final String body = "Not found";
85+
86+
final HttpResponse.BodySubscriber<SignResponse> subscriber = handler.apply(info);
87+
88+
Assertions.assertThrows(BankIdApiUnexpectedResponseException.class, () -> complete(feed(subscriber, body)));
89+
}
90+
91+
@Test
92+
void givenNon200AndJsonContentTypeWithValidJson_whenApply_thenThrowsApiError() {
93+
final JsonBodyHandler<SignResponse, ErrorResponse> handler =
94+
new JsonBodyHandler<>(SignResponse.class, ErrorResponse.class, objectMapper);
95+
final HttpResponse.ResponseInfo info = responseInfo(400, "application/json");
96+
final String errorJson = "{\"errorCode\":\"BAD_REQUEST\",\"details\":\"Invalid input\"}";
97+
98+
final HttpResponse.BodySubscriber<SignResponse> subscriber = handler.apply(info);
99+
100+
Assertions.assertThrows(BankIdApiErrorException.class, () -> complete(feed(subscriber, errorJson)));
101+
}
102+
103+
@Test
104+
void givenNon200AndJsonContentTypeWithInvalidJson_whenApply_thenThrowsUnexpectedResponse() {
105+
final JsonBodyHandler<SignResponse, ErrorResponse> handler =
106+
new JsonBodyHandler<>(SignResponse.class, ErrorResponse.class, objectMapper);
107+
final HttpResponse.ResponseInfo info = responseInfo(500, "Application/JSON; charset=UTF-8"); // case-insensitive
108+
final String invalidJson = "{ this is broken }";
109+
110+
final HttpResponse.BodySubscriber<SignResponse> subscriber = handler.apply(info);
111+
112+
Assertions.assertThrows(BankIdApiUnexpectedResponseException.class, () -> complete(feed(subscriber, invalidJson)));
113+
}
114+
115+
private static HttpResponse.ResponseInfo responseInfo(final int status, final String contentType) {
116+
final Map<String, List<String>> headerMap =
117+
contentType == null
118+
? Map.of()
119+
: Map.of("Content-Type", List.of(contentType));
120+
121+
final HttpHeaders headers = HttpHeaders.of(headerMap, (k, v) -> true);
122+
123+
return new HttpResponse.ResponseInfo() {
124+
@Override
125+
public int statusCode() {
126+
return status;
127+
}
128+
129+
@Override
130+
public HttpHeaders headers() {
131+
return headers;
132+
}
133+
134+
@Override
135+
public HttpClient.Version version() {
136+
return HttpClient.Version.HTTP_1_1;
137+
}
138+
};
139+
}
140+
141+
private static <T> T complete(final CompletableFuture<T> future) {
142+
try {
143+
return future.get(2, TimeUnit.SECONDS);
144+
} catch (final TimeoutException e) {
145+
fail("Timed out waiting for body subscriber to complete");
146+
return null; // unreachable
147+
} catch (final ExecutionException e) {
148+
if (e.getCause() instanceof RuntimeException re) {
149+
throw re;
150+
}
151+
throw new CompletionException(e.getCause());
152+
} catch (final InterruptedException e) {
153+
Thread.currentThread().interrupt();
154+
throw new RuntimeException(e);
155+
}
156+
}
157+
158+
private static <T> CompletableFuture<T> feed(final HttpResponse.BodySubscriber<T> subscriber, final String body) {
159+
subscriber.onSubscribe(new NoopSubscription());
160+
subscriber.onNext(List.of(ByteBuffer.wrap(body.getBytes(StandardCharsets.UTF_8))));
161+
subscriber.onComplete();
162+
return subscriber.getBody().toCompletableFuture();
163+
}
164+
165+
private static class NoopSubscription implements java.util.concurrent.Flow.Subscription {
166+
@Override
167+
public void request(long n) { /* no-op */ }
168+
169+
@Override
170+
public void cancel() { /* no-op */ }
171+
}
172+
}

0 commit comments

Comments
 (0)