SEBSERV-62 configuration download and testing

This commit is contained in:
anhefti 2019-07-04 10:19:12 +02:00
parent 3b6e3d88e0
commit 412ed6fd7b
7 changed files with 266 additions and 12 deletions

View file

@ -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<ClientConnectionData> 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);
}

View file

@ -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");
}

View file

@ -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<ClientConnectionData> 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

View file

@ -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());
}

View file

@ -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<StreamingResponseBody> 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<String, String> 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,

View file

@ -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);

View file

@ -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("<?xml version=\"1.0\""));
// check connection cache
final Cache connectionCache = this.cacheManager
.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
final ValueWrapper connection = connectionCache.get(connectionToken);
assertNotNull(connection);
final ClientConnectionDataInternal ccdi =
(ClientConnectionDataInternal) connectionCache.get(connectionToken).get();
assertNotNull(ccdi);
assertNotNull(ccdi.clientConnection.examId);
// check config cache
final Cache cache = this.cacheManager
.getCache(ExamSessionCacheService.CACHE_NAME_SEB_CONFIG_EXAM);
final ValueWrapper config = cache.get(EXAM_ID);
assertNotNull(config);
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testGetExamConfigOnNoneEstablishedConnectionProvidingExamId() 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 second case
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null);
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,
EXAM_ID);
// check correct response
assertTrue(HttpStatus.OK.value() == configResponse.getStatus());
// check error
final String contentAsString = configResponse.getContentAsString();
assertNotNull(contentAsString);
assertTrue(contentAsString.startsWith("<?xml version=\"1.0\""));
// check connection cache
final Cache connectionCache = this.cacheManager
.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
final ValueWrapper connection = connectionCache.get(connectionToken);
assertNotNull(connection);
final ClientConnectionDataInternal ccdi =
(ClientConnectionDataInternal) connectionCache.get(connectionToken).get();
assertNotNull(ccdi);
assertNotNull(ccdi.clientConnection.examId);
// check config cache
final Cache cache = this.cacheManager
.getCache(ExamSessionCacheService.CACHE_NAME_SEB_CONFIG_EXAM);
final ValueWrapper config = cache.get(EXAM_ID);
assertNotNull(config);
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testGetExamConfigOnNoneEstablishedConnectionNoneExamId() throws Exception {
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null);
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();
assertEquals(
"{\"messageCode\":\"0\",\"systemMessage\":\"Generic error message\",\"details\":\"Missing exam identider or requested exam is not running\",\"attributes\":[]}",
contentAsString);
// check connection cache
final Cache connectionCache = this.cacheManager
.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
final ValueWrapper connection = connectionCache.get(connectionToken);
assertNotNull(connection);
final ClientConnectionDataInternal ccdi =
(ClientConnectionDataInternal) connectionCache.get(connectionToken).get();
assertNotNull(ccdi);
assertNull(ccdi.clientConnection.examId);
assertTrue(ccdi.indicatorValues.isEmpty());
// check config cache
final Cache cache = this.cacheManager
.getCache(ExamSessionCacheService.CACHE_NAME_SEB_CONFIG_EXAM);
final ValueWrapper config = cache.get(EXAM_ID);
assertNull(config);
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testGetExamConfigOnConnectionNoExamIdSouldFail() throws Exception {
@ -81,13 +240,14 @@ 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());
assertTrue(HttpStatus.OK.value() != configResponse.getStatus());
final String contentAsString = configResponse.getContentAsString();
assertEquals(
"{\"messageCode\":\"0\",\"systemMessage\":\"Generic error message\",\"details\":\"Illegal connection token. No active ClientConnection found for token\",\"attributes\":[]}",
"{\"messageCode\":\"0\",\"systemMessage\":\"Generic error message\",\"details\":\"Missing exam identider or requested exam is not running\",\"attributes\":[]}",
contentAsString);
}