Skip to content

Commit 2544432

Browse files
authored
DT-1206: Bug fixes for support requests (#2455)
1 parent 3746860 commit 2544432

File tree

7 files changed

+116
-9
lines changed

7 files changed

+116
-9
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.broadinstitute.consent.http.exceptions;
2+
3+
import com.google.api.client.http.HttpStatusCodes;
4+
import jakarta.ws.rs.ClientErrorException;
5+
6+
public class UnprocessableEntityException extends ClientErrorException {
7+
8+
public UnprocessableEntityException(String message) {
9+
super(message, HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY);
10+
}
11+
12+
}

src/main/java/org/broadinstitute/consent/http/resources/Resource.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.broadinstitute.consent.http.resources;
22

3+
import com.google.api.client.http.HttpStatusCodes;
34
import com.google.gson.JsonSyntaxException;
45
import com.google.gson.stream.MalformedJsonException;
56
import io.sentry.Sentry;
@@ -24,6 +25,7 @@
2425
import org.broadinstitute.consent.http.enumeration.UserRoles;
2526
import org.broadinstitute.consent.http.exceptions.ConsentConflictException;
2627
import org.broadinstitute.consent.http.exceptions.UnknownIdentifierException;
28+
import org.broadinstitute.consent.http.exceptions.UnprocessableEntityException;
2729
import org.broadinstitute.consent.http.models.Error;
2830
import org.broadinstitute.consent.http.models.User;
2931
import org.broadinstitute.consent.http.util.ConsentLogger;
@@ -124,6 +126,9 @@ private interface ExceptionHandler {
124126
dispatch.put(ConsentConflictException.class, e ->
125127
Response.status(Response.Status.CONFLICT).type(MediaType.APPLICATION_JSON)
126128
.entity(new Error(e.getMessage(), Response.Status.CONFLICT.getStatusCode())).build());
129+
dispatch.put(UnprocessableEntityException.class, e ->
130+
Response.status(HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY).type(MediaType.APPLICATION_JSON)
131+
.entity(new Error(e.getMessage(), HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY)).build());
127132
dispatch.put(UnsupportedOperationException.class, e ->
128133
Response.status(Response.Status.CONFLICT).type(MediaType.APPLICATION_JSON)
129134
.entity(new Error(e.getMessage(), Response.Status.CONFLICT.getStatusCode())).build());

src/main/java/org/broadinstitute/consent/http/service/SupportRequestService.java

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.nio.charset.StandardCharsets;
1414
import org.apache.commons.io.IOUtils;
1515
import org.broadinstitute.consent.http.configurations.ServicesConfiguration;
16+
import org.broadinstitute.consent.http.exceptions.UnprocessableEntityException;
1617
import org.broadinstitute.consent.http.models.support.DuosTicket;
1718
import org.broadinstitute.consent.http.models.support.TicketFactory;
1819
import org.broadinstitute.consent.http.util.ConsentLogger;
@@ -48,18 +49,23 @@ public JsonObject postAttachmentToSupport(byte[] content) throws Exception {
4849

4950
if (!response.isSuccessStatusCode()) {
5051
String errorMessage = "Error sending attachment to support: " + response.getStatusMessage();
51-
var errorException = new ServerErrorException(response.getStatusMessage(),
52-
HttpStatusCodes.STATUS_CODE_SERVER_ERROR);
52+
var errorException =
53+
response.getStatusCode() == HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY ?
54+
new UnprocessableEntityException(errorMessage) :
55+
new ServerErrorException(response.getStatusMessage(), response.getStatusCode());
5356
logException(errorMessage, errorException);
5457
throw errorException;
5558
}
5659
String responseContent = IOUtils.toString(response.getContent(), Charset.defaultCharset());
5760
JsonObject obj = GsonUtil.getInstance().fromJson(responseContent, JsonObject.class);
5861
if (obj != null && obj.get("upload") != null) {
59-
JsonObject uploadObj = obj.get("upload").getAsJsonObject();
60-
if (uploadObj != null && uploadObj.get("token") != null) {
61-
return uploadObj.get("token").getAsJsonObject();
62-
}
62+
return obj.get("upload").getAsJsonObject();
63+
} else {
64+
var errorException = new ServerErrorException(response.getStatusMessage(),
65+
HttpStatusCodes.STATUS_CODE_SERVER_ERROR);
66+
String errorMessage = "Error reading attachment response content: " + responseContent;
67+
logException(errorMessage, errorException);
68+
throw errorException;
6369
}
6470
}
6571
throw new BadRequestException("Not configured to send support attachments");
@@ -82,8 +88,10 @@ public Request postTicketToSupport(DuosTicket ticket) throws Exception {
8288

8389
if (!response.isSuccessStatusCode()) {
8490
String errorMessage = "Error posting ticket to support: " + response.getStatusMessage();
85-
var errorException = new ServerErrorException(response.getStatusMessage(),
86-
HttpStatusCodes.STATUS_CODE_SERVER_ERROR);
91+
var errorException =
92+
response.getStatusCode() == HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY ?
93+
new UnprocessableEntityException(errorMessage) :
94+
new ServerErrorException(response.getStatusMessage(), response.getStatusCode());
8795
logException(errorMessage, errorException);
8896
throw errorException;
8997
}

src/main/resources/assets/paths/supportRequest.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,8 @@ post:
1818
400:
1919
description: |
2020
Bad Request: not configured for support requests
21+
422:
22+
description: |
23+
Unprocessable Entity: provided content is not processable
2124
500:
2225
description: Internal Server Error

src/main/resources/assets/paths/supportUpload.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,8 @@ post:
2323
400:
2424
description: |
2525
Bad Request: max file upload size is 5 MB or server not configured for uploads
26+
422:
27+
description: |
28+
Unprocessable Entity: provided content is not processable
2629
500:
2730
description: Internal Server Error

src/test/java/org/broadinstitute/consent/http/resources/SupportResourceTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
44
import static org.mockito.ArgumentMatchers.any;
5+
import static org.mockito.Mockito.doThrow;
56
import static org.mockito.Mockito.when;
67

78
import com.google.api.client.http.HttpStatusCodes;
@@ -10,6 +11,7 @@
1011
import jakarta.ws.rs.core.Response;
1112
import java.util.stream.Stream;
1213
import org.broadinstitute.consent.http.enumeration.SupportRequestType;
14+
import org.broadinstitute.consent.http.exceptions.UnprocessableEntityException;
1315
import org.broadinstitute.consent.http.service.SupportRequestService;
1416
import org.junit.jupiter.api.BeforeEach;
1517
import org.junit.jupiter.api.Test;
@@ -131,6 +133,25 @@ void testPostRequestInvalidFields(String body) {
131133
}
132134
}
133135

136+
@Test
137+
void testUnprocessableTicket() throws Exception {
138+
doThrow(new UnprocessableEntityException("Unprocessable")).when(supportRequestService)
139+
.postTicketToSupport(any());
140+
try (Response response = supportResource.postRequest("""
141+
{
142+
"name": "Test User",
143+
"email": "[email protected]",
144+
"subject": "Test Subject",
145+
"description": "Test Description",
146+
"type": "QUESTION",
147+
"url": "https://example.com",
148+
"uploads": ["token1", "token2"]
149+
}
150+
""")) {
151+
assertEquals(HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY, response.getStatus());
152+
}
153+
}
154+
134155
@Test
135156
void testPostUpload() throws Exception {
136157
JsonObject obj = new JsonObject();
@@ -141,4 +162,13 @@ void testPostUpload() throws Exception {
141162
}
142163
}
143164

165+
@Test
166+
void testUnprocessableUpload() throws Exception {
167+
doThrow(new UnprocessableEntityException("Unprocessable")).when(supportRequestService)
168+
.postAttachmentToSupport(any());
169+
try (Response response = supportResource.postUpload("test".getBytes())) {
170+
assertEquals(HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY, response.getStatus());
171+
}
172+
}
173+
144174
}

src/test/java/org/broadinstitute/consent/http/service/SupportRequestServiceTest.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
import com.google.api.client.http.HttpStatusCodes;
1111
import jakarta.ws.rs.BadRequestException;
1212
import jakarta.ws.rs.ServerErrorException;
13+
import jakarta.ws.rs.WebApplicationException;
1314
import java.util.ArrayList;
1415
import java.util.Collections;
1516
import java.util.EnumSet;
1617
import java.util.List;
1718
import org.broadinstitute.consent.http.AbstractTestHelper;
1819
import org.broadinstitute.consent.http.configurations.ServicesConfiguration;
1920
import org.broadinstitute.consent.http.enumeration.SupportRequestType;
21+
import org.broadinstitute.consent.http.exceptions.UnprocessableEntityException;
2022
import org.broadinstitute.consent.http.models.support.DuosTicket;
2123
import org.broadinstitute.consent.http.models.support.TicketFactory;
2224
import org.broadinstitute.consent.http.models.support.TicketFields;
@@ -93,6 +95,19 @@ void testPostTicketToSupportNotificationsNotActivated() {
9395
assertThrows(BadRequestException.class, () -> service.postTicketToSupport(ticket));
9496
}
9597

98+
@Test
99+
void testPostTicketToSupportNotificationsUnprocessableEntity() {
100+
DuosTicket ticket = generateTicket();
101+
when(config.isActivateSupportNotifications()).thenReturn(true);
102+
when(config.postSupportRequestUrl()).thenReturn(
103+
"http://" + container.getHost() + ":" + container.getServerPort() + "/");
104+
mockServerClient.when(request())
105+
.respond(response()
106+
.withHeader(Header.header("Content-Type", "application/json"))
107+
.withStatusCode(HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY));
108+
assertThrows(UnprocessableEntityException.class, () -> service.postTicketToSupport(ticket));
109+
}
110+
96111
@Test
97112
void testPostTicketToSupportServerError() {
98113
DuosTicket ticket = generateTicket();
@@ -109,7 +124,7 @@ void testPostTicketToSupportServerError() {
109124
@Test
110125
void testPostAttachmentToSupport() throws Exception {
111126
String expectedBody = """
112-
{ "upload": { "token": { "token": "token string" } } }
127+
{ "upload": { "token": "token string" } }
113128
""";
114129
when(config.isActivateSupportNotifications()).thenReturn(true);
115130
when(config.postSupportUploadUrl()).thenReturn(
@@ -126,6 +141,25 @@ void testPostAttachmentToSupport() throws Exception {
126141
assertEquals(1, requests.length);
127142
}
128143

144+
@Test
145+
void testPostTicketToSupportUnableToParseResponse() {
146+
// This case should never happen, but we do inspect the response for a valid "upload" object.
147+
// We need to ensure that the service handles invalid response formats correctly.
148+
String expectedBody = """
149+
{ "invalid": { "missing_token": "token string" } }
150+
""";
151+
when(config.isActivateSupportNotifications()).thenReturn(true);
152+
when(config.postSupportUploadUrl()).thenReturn(
153+
"http://" + container.getHost() + ":" + container.getServerPort() + "/");
154+
mockServerClient.when(request().withMethod("POST"))
155+
.respond(response()
156+
.withHeader(Header.header("Content-Type", "application/json"))
157+
.withStatusCode(HttpStatusCodes.STATUS_CODE_CREATED)
158+
.withBody(expectedBody)
159+
);
160+
assertThrows(ServerErrorException.class, () -> service.postAttachmentToSupport("Test".getBytes()));
161+
}
162+
129163
@Test
130164
void testPostAttachmentToSupportNotificationsNotActivated() {
131165
when(config.isActivateSupportNotifications()).thenReturn(false);
@@ -146,6 +180,18 @@ void testPostAttachmentToSupportServerError() {
146180
assertThrows(ServerErrorException.class, () -> service.postAttachmentToSupport("Test".getBytes()));
147181
}
148182

183+
@Test
184+
void testPostAttachmentToSupportUnprocessableEntity() {
185+
when(config.isActivateSupportNotifications()).thenReturn(true);
186+
when(config.postSupportUploadUrl()).thenReturn(
187+
"http://" + container.getHost() + ":" + container.getServerPort() + "/");
188+
mockServerClient.when(request())
189+
.respond(response()
190+
.withHeader(Header.header("Content-Type", "application/json"))
191+
.withStatusCode(HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY));
192+
assertThrows(UnprocessableEntityException.class, () -> service.postAttachmentToSupport("Test".getBytes()));
193+
}
194+
149195
// Creates support ticket with random values
150196
private DuosTicket generateTicket() {
151197
List<SupportRequestType> types = new ArrayList<>(EnumSet.allOf(SupportRequestType.class));

0 commit comments

Comments
 (0)