Skip to content

Commit

Permalink
[DT-824] Return 400 for invalid byte sequence (#2451)
Browse files Browse the repository at this point in the history
  • Loading branch information
fboulnois authored Jan 27, 2025
1 parent 5ec63c2 commit 0e4b8e6
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
import java.io.InputStream;
import java.sql.SQLException;
import java.sql.SQLSyntaxErrorException;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.broadinstitute.consent.http.enumeration.UserRoles;
import org.broadinstitute.consent.http.exceptions.ConsentConflictException;
import org.broadinstitute.consent.http.exceptions.UnknownIdentifierException;
Expand Down Expand Up @@ -57,9 +57,14 @@ abstract public class Resource implements ConsentLogger {
public static final String ITDIRECTOR = "ITDirector";

// NOTE: implement more Postgres vendor codes as we encounter them
private static final Map<String, Integer> vendorCodeStatusMap = Map.ofEntries(
new AbstractMap.SimpleEntry<>(PSQLState.UNIQUE_VIOLATION.getState(),
Response.Status.CONFLICT.getStatusCode())
private static final Map<String, ImmutablePair<Integer, String>> vendorCodeStatusMap = Map.ofEntries(
Map.entry(PSQLState.UNKNOWN_STATE.getState(),
ImmutablePair.of(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
"Database error")),
Map.entry(PSQLState.UNIQUE_VIOLATION.getState(),
ImmutablePair.of(Response.Status.CONFLICT.getStatusCode(), "Database conflict")),
Map.entry("22021",
ImmutablePair.of(Response.Status.BAD_REQUEST.getStatusCode(), "Invalid byte sequence"))
);

protected Response createExceptionResponse(Exception e) {
Expand Down Expand Up @@ -179,12 +184,13 @@ private static Response errorLoggedExceptionHandler(Exception e, Error error) {
}

//Helper method to process generic JDBI Postgres exceptions for responses
private static Response unableToExecuteExceptionHandler(Exception e) {
protected static Response unableToExecuteExceptionHandler(Exception e) {
//default status definition
LoggerFactory.getLogger(Resource.class.getName()).error(e.getMessage());
// static makes using the interface less flexible
Sentry.captureEvent(new SentryEvent(e));
Integer status = Response.Status.INTERNAL_SERVER_ERROR.getStatusCode();

var status = vendorCodeStatusMap.get(PSQLState.UNKNOWN_STATE.getState());

try {
if (e.getCause() instanceof PSQLException) {
Expand All @@ -197,9 +203,12 @@ private static Response unableToExecuteExceptionHandler(Exception e) {
//no need to handle, default status already assigned
}

return Response.status(status)
int statusCode = status.getLeft();
String message = status.getRight();

return Response.status(statusCode)
.type(MediaType.APPLICATION_JSON)
.entity(new Error("Database Error", status))
.entity(new Error(message, statusCode))
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
import java.util.Random;
import java.util.UUID;
import org.apache.commons.lang3.RandomStringUtils;
import org.broadinstitute.consent.http.ConsentApplication;
import org.broadinstitute.consent.http.AbstractTestHelper;
import org.broadinstitute.consent.http.ConsentApplication;
import org.broadinstitute.consent.http.configurations.ConsentConfiguration;
import org.broadinstitute.consent.http.enumeration.OrganizationType;
import org.broadinstitute.consent.http.enumeration.UserFields;
Expand All @@ -33,6 +33,7 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.wait.strategy.Wait;

public class DAOTestHelper extends AbstractTestHelper {

Expand Down Expand Up @@ -74,7 +75,8 @@ public class DAOTestHelper extends AbstractTestHelper {
public static void startUp() throws Exception {
// Start the database
postgresContainer = new PostgreSQLContainer<>(POSTGRES_IMAGE).
withCommand("postgres -c max_connections=" + maxConnections);
withCommand("postgres -c max_connections=" + maxConnections).
waitingFor(Wait.forListeningPorts());
postgresContainer.start();
ConfigOverride driverOverride = ConfigOverride.config("database.driverClass",
postgresContainer.getDriverClassName());
Expand Down
19 changes: 16 additions & 3 deletions src/test/java/org/broadinstitute/consent/http/db/UserDAOTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.broadinstitute.consent.http.db;

import static org.junit.Assert.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
Expand Down Expand Up @@ -30,6 +31,7 @@
import org.broadinstitute.consent.http.models.LibraryCard;
import org.broadinstitute.consent.http.models.User;
import org.broadinstitute.consent.http.models.UserRole;
import org.jdbi.v3.core.statement.UnableToExecuteStatementException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
Expand Down Expand Up @@ -206,7 +208,8 @@ void testDeleteUserById() {
@Test
void testFindUsersWithLCsAndInstitution() {
User user = createUserWithInstitution();
int dacId = dacDAO.createDac(RandomStringUtils.randomAlphabetic(5), RandomStringUtils.randomAlphabetic(5), new Date());
int dacId = dacDAO.createDac(RandomStringUtils.randomAlphabetic(5),
RandomStringUtils.randomAlphabetic(5), new Date());
Instant now = Instant.now();
int daaId = daaDAO.createDaa(user.getUserId(), now, user.getUserId(), now, dacId);
int lcId1 = libraryCardDAO.insertLibraryCard(user.getUserId(), user.getInstitutionId(), "asdf",
Expand Down Expand Up @@ -270,6 +273,14 @@ void testUpdateDisplayName() {
assertEquals(newName, u1.getDisplayName());
}

@Test
void testUpdateDisplayNameInvalidChars() {
User researcher = createUserWithRole(UserRoles.RESEARCHER.getRoleId());
String newName = "invalid\0name";
assertThrows(UnableToExecuteStatementException.class,
() -> userDAO.updateDisplayName(researcher.getUserId(), newName));
}

@Test
void testFindUserByEmailAndRoleId() {
User chair = createUserWithRole(UserRoles.CHAIRPERSON.getRoleId());
Expand Down Expand Up @@ -341,7 +352,8 @@ void testGetSOsByInstitution() {

@Test
void testGetUsersFromInstitutionWithCards() {
int dacId = dacDAO.createDac(RandomStringUtils.randomAlphabetic(5), RandomStringUtils.randomAlphabetic(5), new Date());
int dacId = dacDAO.createDac(RandomStringUtils.randomAlphabetic(5),
RandomStringUtils.randomAlphabetic(5), new Date());
Instant now = Instant.now();
LibraryCard card = createLibraryCard();
int daaId = daaDAO.createDaa(card.getUserId(), now, card.getUserId(), now, dacId);
Expand All @@ -360,7 +372,8 @@ void testGetUsersFromInstitutionWithCards() {

@Test
void testGetUsersWithCardsByDaaId() {
int dacId = dacDAO.createDac(RandomStringUtils.randomAlphabetic(5), RandomStringUtils.randomAlphabetic(5), new Date());
int dacId = dacDAO.createDac(RandomStringUtils.randomAlphabetic(5),
RandomStringUtils.randomAlphabetic(5), new Date());
Instant now = Instant.now();
LibraryCard card = createLibraryCard();
int daaId = daaDAO.createDaa(card.getUserId(), now, card.getUserId(), now, dacId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,72 @@
package org.broadinstitute.consent.http.resources;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import org.broadinstitute.consent.http.models.Error;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.jdbi.v3.core.statement.StatementContext;
import org.jdbi.v3.core.statement.StatementExceptions;
import org.jdbi.v3.core.statement.UnableToExecuteStatementException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.owasp.fileio.FileValidator;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;

@ExtendWith(MockitoExtension.class)
class ResourceTest {

@Test
void testUnableToExecuteExceptionGeneric() {
var error = new UnableToExecuteStatementException("generic error");
var result = Resource.unableToExecuteExceptionHandler(error);
var entity = (Error) result.getEntity();
assertThat(result.getStatus(), is(500));
assertThat(entity.message(), is("Database error"));
}

@Test
void testUnableToExecuteExceptionDatabaseConflict() {
PSQLException psqlException = new PSQLException(
"duplicate key value violates unique constraint", PSQLState.UNIQUE_VIOLATION);
StatementContext ctx = mock(StatementContext.class);
StatementExceptions exceptions = mock(StatementExceptions.class);
when(ctx.getConfig(StatementExceptions.class)).thenReturn(exceptions);
UnableToExecuteStatementException exception = new UnableToExecuteStatementException(
"Failed to execute statement", psqlException, ctx);

var result = Resource.unableToExecuteExceptionHandler(exception);
var entity = (Error) result.getEntity();
assertThat(result.getStatus(), is(409));
assertThat(entity.message(), is("Database conflict"));
}

@Test
void testUnableToExecuteExceptionInvalidByteSequence() {
PSQLState psqlState = mock(PSQLState.class);
// PSQLState is missing the enum constant 22021 for invalid byte sequence but returns it so we mock it
when(psqlState.getState()).thenReturn("22021");
PSQLException psqlException = new PSQLException(
"invalid byte sequence for encoding \"UTF8\": 0x00", psqlState);
StatementContext ctx = mock(StatementContext.class);
StatementExceptions exceptions = mock(StatementExceptions.class);
when(ctx.getConfig(StatementExceptions.class)).thenReturn(exceptions);
UnableToExecuteStatementException exception = new UnableToExecuteStatementException(
"Failed to execute statement", psqlException, ctx);

var result = Resource.unableToExecuteExceptionHandler(exception);
var entity = (Error) result.getEntity();
assertThat(result.getStatus(), is(400));
assertThat(entity.message(), is("Invalid byte sequence"));
}

@Test
void testValidateFileDetails() {
Long maxSize = new FileValidator().getMaxFileUploadSize();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.google.api.client.http.HttpStatusCodes;
Expand Down Expand Up @@ -47,10 +48,15 @@
import org.broadinstitute.consent.http.service.UserService;
import org.broadinstitute.consent.http.service.sam.SamService;
import org.broadinstitute.consent.http.util.gson.GsonUtil;
import org.jdbi.v3.core.statement.StatementContext;
import org.jdbi.v3.core.statement.StatementExceptions;
import org.jdbi.v3.core.statement.UnableToExecuteStatementException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;

@ExtendWith(MockitoExtension.class)
class UserResourceTest {
Expand Down Expand Up @@ -456,6 +462,31 @@ void testUpdateSelf() {
assertEquals(HttpStatusCodes.STATUS_CODE_OK, response.getStatus());
}

@Test
void testUpdateSelfInvalidName() {
PSQLState psqlState = mock(PSQLState.class);
// PSQLState is missing the enum constant 22021 for invalid byte sequence but returns it so we mock it
when(psqlState.getState()).thenReturn("22021");
PSQLException psqlException = new PSQLException(
"invalid byte sequence for encoding \"UTF8\": 0x00", psqlState);
StatementContext ctx = mock(StatementContext.class);
StatementExceptions exceptions = mock(StatementExceptions.class);
when(ctx.getConfig(StatementExceptions.class)).thenReturn(exceptions);
UnableToExecuteStatementException exception = new UnableToExecuteStatementException(
"Failed to execute statement", psqlException, ctx);

User user = createUserWithRole();
String invalidName = "invalid\0name";
UserUpdateFields userUpdateFields = new UserUpdateFields();
userUpdateFields.setDisplayName(invalidName);
when(userService.findUserByEmail(any())).thenReturn(user);
when(userService.updateUserFieldsById(any(), any())).thenThrow(exception);
initResource();

Response response = userResource.updateSelf(authUser, uriInfo, gson.toJson(userUpdateFields));
assertEquals(HttpStatusCodes.STATUS_CODE_BAD_REQUEST, response.getStatus());
}

@Test
void testUpdateSelfRolesNotAdmin() {
User user = createUserWithRole();
Expand Down

0 comments on commit 0e4b8e6

Please sign in to comment.