From 412ed6fd7ba10a07b4e6d7c05ee0d6e38a1a0f3c Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 4 Jul 2019 10:19:12 +0200 Subject: [PATCH] SEBSERV-62 configuration download and testing --- .../session/SebClientConnectionService.java | 16 ++ .../session/impl/ExamSessionServiceImpl.java | 9 +- .../impl/SebClientConnectionServiceImpl.java | 39 +++- .../weblayer/api/APIExceptionHandler.java | 1 - .../weblayer/api/ExamAPI_V1_Controller.java | 34 +++- .../api/exam/ExamAPIIntegrationTester.java | 11 +- .../exam/SebExamConfigurationRequestTest.java | 168 +++++++++++++++++- 7 files changed, 266 insertions(+), 12 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SebClientConnectionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SebClientConnectionService.java index 22ed640b..24b96588 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SebClientConnectionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SebClientConnectionService.java @@ -9,6 +9,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -99,8 +100,23 @@ public interface SebClientConnectionService { Long institutionId, String clientAddress); + /** Get ClientConnectionData for an active connection (connection on running exam) + * + * @param connectionToken The connection token of the connection to get the ClientConnectionData from + * @return ClientConnectionData for an active connection (connection on running exam) */ + Result getActiveConnectionData(String connectionToken); + + /** Notify a ping for a certain client connection. + * + * @param connectionToken the connection token + * @param timestamp the ping time-stamp + * @param pingNumber the ping number */ void notifyPing(String connectionToken, long timestamp, int pingNumber); + /** Notify a SEB client event for live indication and storing to database. + * + * @param connectionToken the connection token + * @param event The SEB client event data */ void notifyClientEvent(String connectionToken, final ClientEvent event); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java index 1fdd9508..3b6d3b5b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java @@ -23,7 +23,6 @@ import org.springframework.stereotype.Service; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; -import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; @@ -104,12 +103,18 @@ public class ExamSessionServiceImpl implements ExamSessionService { .byConnectionToken(connectionToken) .getOrThrow(); - if (connection == null || connection.status != ConnectionStatus.ESTABLISHED) { + if (connection == null) { log.warn("SEB exam configuration download request, no active ClientConnection found for token: {}", connectionToken); throw new AccessDeniedException("Illegal connection token. No active ClientConnection found for token"); } + // exam integrity check + if (connection.examId == null || !isExamRunning(connection.examId)) { + log.error("Missing exam identifer or requested exam is not running for connection: {}", connection); + throw new IllegalStateException("Missing exam identider or requested exam is not running"); + } + if (log.isDebugEnabled()) { log.debug("Trying to get exam from InMemorySebConfig"); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SebClientConnectionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SebClientConnectionServiceImpl.java index dc79dfab..41b2c53e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SebClientConnectionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SebClientConnectionServiceImpl.java @@ -20,6 +20,7 @@ import org.springframework.stereotype.Service; import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -138,14 +139,36 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic userSessionId); } - checkExamRunning(examId); - final ClientConnection clientConnection = getClientConnection(connectionToken); checkInstitutionalIntegrity( institutionId, clientConnection); + // examId integrity check + if (examId != null && + clientConnection.examId != null && + !examId.equals(clientConnection.examId)) { + + log.error("Exam integrity violation: another examId is already set for the connection: {}", + clientConnection); + throw new IllegalArgumentException( + "Exam integrity violation: another examId is already set for the connection"); + } + checkExamRunning(examId); + + // userSessionId integrity check + if (userSessionId != null && + clientConnection.userSessionId != null && + !userSessionId.equals(clientConnection.userSessionId)) { + + log.error( + "User session identifer integrity violation: another User session identifer is already set for the connection: {}", + clientConnection); + throw new IllegalArgumentException( + "User session identifer integrity violation: another User session identifer is already set for the connection"); + } + final String virtualClientAddress = getVirtualClientAddress( (examId != null) ? examId : clientConnection.examId, clientAddress, @@ -314,7 +337,19 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic return updatedClientConnection; }); + } + @Override + public Result getActiveConnectionData(final String connectionToken) { + final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService + .getActiveClientConnection(connectionToken); + + if (activeClientConnection == null) { + return Result + .ofError(new IllegalArgumentException("No active client connection found for connectionToken")); + } else { + return Result.of(activeClientConnection); + } } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/APIExceptionHandler.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/APIExceptionHandler.java index b0553168..8bb4dd38 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/APIExceptionHandler.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/APIExceptionHandler.java @@ -126,7 +126,6 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler { final Exception ex, final WebRequest request) { - log.error("Unexpected internal error catched at the API endpoint: ", ex); return APIMessage.ErrorMessage.UNEXPECTED .createErrorResponse(ex.getMessage()); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java index a95760c5..0ee88e01 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java @@ -37,6 +37,7 @@ import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.PingResponse; import ch.ethz.seb.sebserver.gbl.model.session.RunningExam; @@ -246,10 +247,41 @@ public class ExamAPI_V1_Controller { consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) public ResponseEntity getConfig( - @RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken) { + @RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, + @RequestBody(required = false) final MultiValueMap formParams, + final Principal principal, + final HttpServletRequest request) { + + try { + // if an examId is provided with the request, update the connection first + if (formParams != null && formParams.containsKey(API.EXAM_API_PARAM_EXAM_ID)) { + final String examId = formParams.getFirst(API.EXAM_API_PARAM_EXAM_ID); + handshakeUpdate(connectionToken, Long.valueOf(examId), null, principal, request); + } + + final ClientConnectionData connection = this.sebClientConnectionService + .getActiveConnectionData(connectionToken) + .getOrThrow(); + + // exam integrity check + if (connection.clientConnection.examId == null || + !this.examSessionService.isExamRunning(connection.clientConnection.examId)) { + + log.error("Missing exam identifer or requested exam is not running for connection: {}", connection); + throw new IllegalStateException("Missing exam identider or requested exam is not running"); + } + } catch (final Exception e) { + log.error("Unexpected error: ", e); + final StreamingResponseBody stream = out -> { + final APIMessage errorMessage = APIMessage.ErrorMessage.GENERIC.of(e.getMessage()); + out.write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessage))); + }; + return new ResponseEntity<>(stream, HttpStatus.BAD_REQUEST); + } final StreamingResponseBody stream = out -> { try { + this.examSessionService .streamDefaultExamConfig( connectionToken, diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java index 2b2d2ca8..97a33f38 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java @@ -262,14 +262,21 @@ public abstract class ExamAPIIntegrationTester { return result.andReturn().getResponse(); } - protected MockHttpServletResponse getExamConfig(final String accessToken, final String connectionToken) - throws Exception { + protected MockHttpServletResponse getExamConfig( + final String accessToken, + final String connectionToken, + final Long examId) throws Exception { + final MockHttpServletRequestBuilder builder = get(this.endpoint + API.EXAM_API_CONFIGURATION_REQUEST_ENDPOINT) .header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE) .header("Authorization", "Bearer " + accessToken) .header(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken) .accept(MediaType.APPLICATION_OCTET_STREAM_VALUE); + if (examId != null) { + builder.content("examId=" + examId); + } + final ResultActions result = this.mockMvc .perform(builder) .andDo(MvcResult::getAsyncResult); diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/SebExamConfigurationRequestTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/SebExamConfigurationRequestTest.java index 600983fc..50a941b3 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/SebExamConfigurationRequestTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/SebExamConfigurationRequestTest.java @@ -18,6 +18,7 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.context.jdbc.Sql; import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ClientConnectionDataInternal; import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCacheService; @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) @@ -49,7 +50,8 @@ public class SebExamConfigurationRequestTest extends ExamAPIIntegrationTester { // try to download Exam Configuration final MockHttpServletResponse configResponse = super.getExamConfig( accessToken, - connectionToken); + connectionToken, + null); // check correct response assertTrue(HttpStatus.OK.value() == configResponse.getStatus()); @@ -66,6 +68,163 @@ public class SebExamConfigurationRequestTest extends ExamAPIIntegrationTester { assertNotNull(config); } + @Test + @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) + public void testGetExamConfigOnNoneEstablishedConnectionButExamIdExists() throws Exception { + + // If an connection was created but is not yet established, the download of a configuration should + // work correctly as long as a examId is already defined for the connection or an examId is provided + // by the configuration request. This tests the first case + + final String accessToken = super.obtainAccessToken("test", "test", "SEBClient"); + assertNotNull(accessToken); + + final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, EXAM_ID); + assertNotNull(createConnection); + + // check correct response + assertTrue(HttpStatus.OK.value() == createConnection.getStatus()); + + final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN); + assertNotNull(connectionToken); + + // in this state the connection is created but no examId is set + + // try to download Exam Configuration + final MockHttpServletResponse configResponse = super.getExamConfig( + accessToken, + connectionToken, + null); + + // check correct response + assertTrue(HttpStatus.OK.value() == configResponse.getStatus()); + + // check error + final String contentAsString = configResponse.getContentAsString(); + assertNotNull(contentAsString); + assertTrue(contentAsString.startsWith("