SEBSERV-62 testing and fixes

This commit is contained in:
anhefti 2019-07-03 16:34:52 +02:00
parent b314ca651f
commit 3b6e3d88e0
29 changed files with 1201 additions and 191 deletions

View file

@ -152,7 +152,7 @@ public class APIMessage implements Serializable {
/** Use this as a conversion from a given FieldError of Spring to a APIMessage
* of type field validation.
*
*
* @param error FieldError instance
* @return converted APIMessage of type field validation */
public static final APIMessage fieldValidationError(final FieldError error) {

View file

@ -55,12 +55,6 @@ public interface ClientCredentialService {
return encryptClientCredentials(clientIdPlaintext, secretPlaintext, null);
}
// /** Use this to get a decrypted plain text clientId form given ClientCredentials
// *
// * @param credentials ClientCredentials containing the clientId to decrypt
// * @return decrypted plain text clientId */
// CharSequence getPlainClientId(ClientCredentials credentials);
/** Use this to get a decrypted plain text secret form given ClientCredentials
*
* @param credentials ClientCredentials containing the secret to decrypt

View file

@ -61,7 +61,7 @@ public class ClientCredentialServiceImpl implements ClientCredentialService {
final CharSequence accessTokenPlaintext) {
final CharSequence secret = this.environment
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
.getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
return new ClientCredentials(
clientIdPlaintext,
@ -73,11 +73,6 @@ public class ClientCredentialServiceImpl implements ClientCredentialService {
: null);
}
// @Override
// public CharSequence getPlainClientId(final ClientCredentials credentials) {
// return credentials.clientId;
// }
@Override
public CharSequence getPlainClientSecret(final ClientCredentials credentials) {
if (credentials == null || !credentials.hasSecret()) {
@ -85,7 +80,7 @@ public class ClientCredentialServiceImpl implements ClientCredentialService {
}
final CharSequence secret = this.environment
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
.getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
return this.decrypt(credentials.secret, secret);
}
@ -96,7 +91,7 @@ public class ClientCredentialServiceImpl implements ClientCredentialService {
}
final CharSequence secret = this.environment
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
.getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
return this.decrypt(credentials.accessToken, secret);
}
@ -105,7 +100,7 @@ public class ClientCredentialServiceImpl implements ClientCredentialService {
public CharSequence encrypt(final CharSequence text) {
final CharSequence secret = this.environment
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
.getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
return encrypt(text, secret);
}
@ -114,7 +109,7 @@ public class ClientCredentialServiceImpl implements ClientCredentialService {
public CharSequence decrypt(final CharSequence text) {
final CharSequence secret = this.environment
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
.getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
return decrypt(text, secret);
}
@ -124,6 +119,11 @@ public class ClientCredentialServiceImpl implements ClientCredentialService {
throw new IllegalArgumentException("Text has null reference");
}
if (secret == null) {
log.warn("No internal secret supplied: skip encryption");
return text;
}
try {
final CharSequence salt = KeyGenerators.string().generateKey();
@ -145,6 +145,11 @@ public class ClientCredentialServiceImpl implements ClientCredentialService {
throw new IllegalArgumentException("Cipher has null reference");
}
if (secret == null) {
log.warn("No internal secret supplied: skip decryption");
return cipher;
}
try {
final int length = cipher.length();

View file

@ -8,13 +8,25 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import java.util.Collection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.util.Result;
public interface ClientConnectionDAO extends EntityDAO<ClientConnection, ClientConnection> {
Result<ClientConnection> byConnectionToken(Long institutionId, String connectionToken);
/** Get a list of all connection tokens of all connections (no matter what state)
* of an exam.
*
* @param examId The exam identifier
* @return list of all connection tokens of all connections (no matter what state)
* of an exam */
Result<Collection<String>> getConnectionTokens(Long examId);
/** Get a ClientConnection for a specified token.
*
* @param connectionToken the connection token
* @return Result refer to ClientConnection or refer to a error if happened */
Result<ClientConnection> byConnectionToken(String connectionToken);
}

View file

@ -16,8 +16,21 @@ public interface ExamConfigurationMapDAO extends
EntityDAO<ExamConfigurationMap, ExamConfigurationMap>,
BulkActionSupportDAO<ExamConfigurationMap> {
/** Get the ConfigurationNode identifier of the default Exam Configuration of
* the Exam with specified identifier.
*
* @param examId The Exam identifier
* @return ConfigurationNode identifier of the default Exam Configuration of
* the Exam with specified identifier */
public Result<Long> getDefaultConfigurationForExam(Long examId);
/** Get the ConfigurationNode identifier of the Exam Configuration of
* the Exam for a specified user identifier.
*
* @param examId The Exam identifier
* @param userId the user identifier
* @return ConfigurationNode identifier of the Exam Configuration of
* the Exam for a specified user identifier */
public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId);
}

View file

@ -110,6 +110,24 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
});
}
@Override
@Transactional(readOnly = true)
public Result<Collection<String>> getConnectionTokens(final Long examId) {
return Result.tryCatch(() -> {
return this.clientConnectionRecordMapper
.selectByExample()
.where(
ClientConnectionRecordDynamicSqlSupport.examId,
SqlBuilder.isEqualTo(examId))
.build()
.execute()
.stream()
.map(ClientConnectionRecord::getConnectionToken)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
});
}
@Override
@Transactional
public Result<ClientConnection> createNew(final ClientConnection data) {
@ -140,10 +158,10 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
final ClientConnectionRecord updateRecord = new ClientConnectionRecord(
data.id,
null,
null,
data.examId,
data.status != null ? data.status.name() : null,
data.connectionToken,
null,
data.userSessionId,
data.clientAddress,
data.virtualClientAddress);
@ -183,37 +201,6 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
});
}
@Override
@Transactional(readOnly = true)
public Result<ClientConnection> byConnectionToken(
final Long institutionId,
final String connectionToken) {
return Result.tryCatch(() -> {
final List<ClientConnectionRecord> list = this.clientConnectionRecordMapper
.selectByExample()
.where(
ClientConnectionRecordDynamicSqlSupport.institutionId,
SqlBuilder.isEqualTo(institutionId))
.and(
ClientConnectionRecordDynamicSqlSupport.connectionToken,
SqlBuilder.isEqualTo(connectionToken))
.build()
.execute();
if (list.isEmpty()) {
throw new ResourceNotFoundException(EntityType.CLIENT_CONNECTION, "connectionToken");
}
if (list.size() > 1) {
throw new IllegalStateException("Only one ClientConnection expected but there are: " + list.size());
}
return list.get(0);
})
.flatMap(ClientConnectionDAOImpl::toDomainModel);
}
@Override
public Result<ClientConnection> byConnectionToken(final String connectionToken) {
return Result.tryCatch(() -> {

View file

@ -118,7 +118,7 @@ public class ClientEventDAOImpl implements ClientEventDAO {
(data.numValue != null) ? new BigDecimal(data.numValue) : null,
data.text);
this.clientEventRecordMapper.insert(newRecord);
this.clientEventRecordMapper.insertSelective(newRecord);
return newRecord;
})
.flatMap(ClientEventDAOImpl::toDomainModel)

View file

@ -130,7 +130,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
@Transactional(readOnly = true)
public Result<Long> getDefaultConfigurationForExam(final Long examId) {
return Result.tryCatch(() -> this.examConfigurationMapRecordMapper
.selectIdsByExample()
.selectByExample()
.where(
ExamConfigurationMapRecordDynamicSqlSupport.examId,
SqlBuilder.isEqualTo(examId))
@ -140,13 +140,14 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
.build()
.execute()
.stream()
.map(mapping -> mapping.getConfigurationNodeId())
.collect(Utils.toSingleton()));
}
@Override
public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) {
return Result.tryCatch(() -> this.examConfigurationMapRecordMapper
.selectIdsByExample()
.selectByExample()
.where(
ExamConfigurationMapRecordDynamicSqlSupport.examId,
SqlBuilder.isEqualTo(examId))
@ -156,6 +157,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
.build()
.execute()
.stream()
.map(mapping -> mapping.getConfigurationNodeId())
.collect(Utils.toSingleton()));
}

View file

@ -13,7 +13,6 @@ import java.io.OutputStream;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
import ch.ethz.seb.sebserver.gbl.util.Result;
/** The base interface and service for all SEB Exam Configuration related functionality. */
public interface SebExamConfigService {
@ -32,10 +31,6 @@ public interface SebExamConfigService {
* @throws FieldValidationException on validation exception */
void validate(ConfigurationTableValues tableValue) throws FieldValidationException;
Result<Long> getDefaultConfigurationIdForExam(Long examId);
Result<Long> getUserConfigurationIdForExam(Long examId, String userId);
/** Used to export a specified SEB Exam Configuration as plain XML
* This exports the values of the follow-up configuration defined by a given
* ConfigurationNode (configurationNodeId)
@ -45,21 +40,26 @@ public interface SebExamConfigService {
* @param configurationNodeId the identifier of the ConfigurationNode to export */
void exportPlainXML(OutputStream out, Long institutionId, Long configurationNodeId);
/** Used to export a SEB Exam Configuration within its defined Configuration Exam Mapping.
/** Used to export the default SEB Exam Configuration for a given exam identifier.
* either with encryption if defined or as plain text within the SEB Configuration format
* as described here: https://www.safeexambrowser.org/developer/seb-file-format.html
*
* @param out The output stream to write the export data to
* @param configExamMappingId The identifier of the Exam Configuration Mapping */
void exportForExam(OutputStream out, Long configExamMappingId);
* @param institutionId The identifier of the institution of the requesting user
* @param examId the exam identifier */
default Long exportForExam(final OutputStream out, final Long institutionId, final Long examId) {
return exportForExam(out, institutionId, examId, null);
}
/** Used to export the default SEB Exam Configuration for a given exam identifier.
* either with encryption if defined or as plain text within the SEB Configuration format
* as described here: https://www.safeexambrowser.org/developer/seb-file-format.html
*
* @param out The output stream to write the export data to
* @param examId the exam identifier */
void exportDefaultForExam(OutputStream out, Long examId);
* @param institutionId The identifier of the institution of the requesting user
* @param examId the exam identifier
* @param userId the user identifier if a specific user based configuration shall be exported */
Long exportForExam(OutputStream out, Long institutionId, Long examId, String userId);
/** TODO */
String generateConfigKey(Long configurationNodeId);

View file

@ -58,7 +58,11 @@ public class ExamConfigIO {
}
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
void exportPlainXML(final OutputStream out, final Long institutionId, final Long configurationNodeId) {
void exportPlainXML(
final OutputStream out,
final Long institutionId,
final Long configurationNodeId) {
// get all defined root configuration attributes
final Map<Long, ConfigurationAttribute> attributes = this.configurationAttributeDAO.getAllRootAttributes()
.getOrThrow()
@ -112,7 +116,7 @@ public class ExamConfigIO {
out.write(Constants.XML_PLIST_END_UTF_8);
out.flush();
} catch (final IOException e) {
} catch (final Exception e) {
log.error("Unexpected error while trying to write SEB Exam Configuration XML to output stream: ", e);
try {
out.flush();

View file

@ -15,6 +15,7 @@ import java.io.PipedOutputStream;
import java.util.Collection;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
@ -96,13 +97,16 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
pout = new PipedOutputStream();
pin = new PipedInputStream(pout);
this.examConfigIO.exportPlainXML(pout, institutionId, configurationNodeId);
this.examConfigIO.exportPlainXML(
pout,
institutionId,
configurationNodeId);
IOUtils.copyLarge(pin, out);
pin.close();
pout.flush();
pout.close();
pin.close();
} catch (final IOException e) {
log.error("Error while stream plain text SEB clonfiguration data: ", e);
@ -127,26 +131,32 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
}
@Override
public Result<Long> getDefaultConfigurationIdForExam(final Long examId) {
return this.examConfigurationMapDAO.getDefaultConfigurationForExam(examId);
}
@Override
public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) {
return this.examConfigurationMapDAO.getUserConfigurationIdForExam(examId, userId);
}
@Override
public void exportForExam(final OutputStream out, final Long configExamMappingId) {
// TODO Auto-generated method stub
public Long exportForExam(
final OutputStream out,
final Long institutionId,
final Long examId,
final String userId) {
}
final Long configurationNodeId = (StringUtils.isBlank(userId))
? getDefaultConfigurationIdForExam(examId)
.getOrThrow()
: getUserConfigurationIdForExam(examId, userId)
.getOrThrow();
@Override
public void exportDefaultForExam(final OutputStream out, final Long examId) {
// TODO Auto-generated method stub
// TODO add header, zip and encrypt if needed
this.exportPlainXML(out, institutionId, configurationNodeId);
return configurationNodeId;
}
@Override

View file

@ -65,5 +65,4 @@ public class IntegerConverter implements XMLValueConverter {
}
}
}

View file

@ -20,4 +20,8 @@ public interface EventHandlingStrategy extends Consumer<ClientEvent> {
String EVENT_CONSUMER_STRATEGY_SINGLE_EVENT_STORE = "SINGLE_EVENT_STORE_STRATEGY";
String EVENT_CONSUMER_STRATEGY_ASYNC_BATCH_STORE = "ASYNC_BATCH_STORE_STRATEGY";
void enable();
void disable();
}

View file

@ -39,6 +39,10 @@ public interface ExamSessionService {
* happened. */
Result<Collection<Exam>> getRunningExamsForInstitution(Long institutionId);
void streamDefaultExamConfig(Long institutionId, String connectionToken, OutputStream out);
/** Streams the default SEB Exam Configuration to a ClientConnection with given connectionToken.
*
* @param connectionToken The connection token that identifiers the ClientConnection
* @param out The OutputStream to stream the data to */
void streamDefaultExamConfig(String connectionToken, OutputStream out);
}

View file

@ -55,8 +55,8 @@ public interface SebClientConnectionService {
Result<ClientConnection> updateClientConnection(
String connectionToken,
Long institutionId,
String clientAddress,
Long examId,
String clientAddress,
String userSessionId);
/** This is used to establish a already created ClientConnection and set it to sate: ESTABLISHED

View file

@ -51,7 +51,7 @@ public abstract class AbstractClientIndicator implements ClientIndicator {
@Override
public double getValue() {
if (this.currentValue == Double.NaN || !this.cachingEnabled) {
if (Double.isNaN(this.currentValue) || !this.cachingEnabled) {
this.currentValue = computeValueAt(DateTime.now(DateTimeZone.UTC).getMillis());
}

View file

@ -12,6 +12,8 @@ import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnore;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
public abstract class AbstractPingIndicator extends AbstractClientIndicator {
@ -37,4 +39,14 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator {
return this.EMPTY_SET;
}
@JsonIgnore
public int getPingCount() {
return this.pingCount;
}
@JsonIgnore
public int getPingNumber() {
return this.pingNumber;
}
}

View file

@ -62,6 +62,7 @@ public class AsyncBatchEventSaveStrategy implements EventHandlingStrategy {
private final BlockingDeque<ClientEvent> eventQueue = new LinkedBlockingDeque<>();
private boolean workersRunning = false;
private boolean enabled = false;
public AsyncBatchEventSaveStrategy(
final SqlSessionFactory sqlSessionFactory,
@ -75,9 +76,22 @@ public class AsyncBatchEventSaveStrategy implements EventHandlingStrategy {
this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
}
@Override
public void enable() {
this.enabled = true;
}
@Override
public void disable() {
this.enabled = false;
}
@EventListener(ApplicationReadyEvent.class)
protected void recover() {
runWorkers();
if (this.enabled) {
runWorkers();
}
}
@Override

View file

@ -52,6 +52,10 @@ public class ClientIndicatorFactory {
public Collection<ClientIndicator> createFor(final ClientConnection clientConnection) {
final List<ClientIndicator> result = new ArrayList<>();
if (clientConnection.examId == null) {
return result;
}
try {
final Collection<Indicator> examIndicators = this.indicatorDAO

View file

@ -8,13 +8,8 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
@ -24,7 +19,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;
@ -63,7 +57,7 @@ public class ExamSessionCacheService {
cacheNames = CACHE_NAME_RUNNING_EXAM,
key = "#examId",
unless = "#result == null")
Exam getRunningExam(final Long examId) {
public Exam getRunningExam(final Long examId) {
if (log.isDebugEnabled()) {
log.debug("Verify running exam for id: {}" + examId);
@ -87,7 +81,7 @@ public class ExamSessionCacheService {
cacheNames = CACHE_NAME_RUNNING_EXAM,
key = "#exam.id",
condition = "#target.isRunning(#result)")
Exam evict(final Exam exam) {
public Exam evict(final Exam exam) {
if (log.isDebugEnabled()) {
log.debug("Conditional eviction of running Exam from cache: {}", isRunning(exam));
@ -108,7 +102,7 @@ public class ExamSessionCacheService {
cacheNames = CACHE_NAME_ACTIVE_CLIENT_CONNECTION,
key = "#connectionToken",
unless = "#result == null")
ClientConnectionDataInternal getActiveClientConnection(final String connectionToken) {
public ClientConnectionDataInternal getActiveClientConnection(final String connectionToken) {
if (log.isDebugEnabled()) {
log.debug("Verify ClientConnection for running exam for caching by connectionToken: ", connectionToken);
@ -123,20 +117,6 @@ public class ExamSessionCacheService {
}
final ClientConnection clientConnection = byPK.get();
// verify connection is established
if (clientConnection.status != ConnectionStatus.ESTABLISHED) {
log.error("Illegal state: ClientConnection is not in expected state; ESTABLISHED. ClientConnection: ",
clientConnection);
return null;
}
// verify exam is running
if (getRunningExam(clientConnection.examId) == null) {
log.error("Exam for ClientConnection with id { is not currently running}", clientConnection.id);
return null;
}
return new ClientConnectionDataInternal(
clientConnection,
this.clientIndicatorFactory.createFor(clientConnection));
@ -145,7 +125,7 @@ public class ExamSessionCacheService {
@CacheEvict(
cacheNames = CACHE_NAME_ACTIVE_CLIENT_CONNECTION,
key = "#connectionToken")
void evictClientConnection(final String connectionToken) {
public void evictClientConnection(final String connectionToken) {
if (log.isDebugEnabled()) {
log.debug("Eviction of ClientConnectionData from cache: {}", connectionToken);
}
@ -155,25 +135,20 @@ public class ExamSessionCacheService {
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
key = "#examId",
unless = "#result == null")
InMemorySebConfig getDefaultSebConfigForExam(final Long examId) {
public InMemorySebConfig getDefaultSebConfigForExam(final Long examId) {
final Exam runningExam = this.getRunningExam(examId);
final PipedOutputStream pipOut = new PipedOutputStream();
try {
final Long configId = this.sebExamConfigService
.getDefaultConfigurationIdForExam(runningExam.id)
.getOrThrow();
// TODO add header, zip and encrypt if needed
final BufferedInputStream in = new BufferedInputStream(new PipedInputStream(pipOut));
this.sebExamConfigService.exportPlainXML(pipOut, runningExam.institutionId, configId);
final ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
IOUtils.copyLarge(in, byteOut);
final Long configId = this.sebExamConfigService.exportForExam(
byteOut,
runningExam.institutionId,
examId);
return new InMemorySebConfig(configId, runningExam.id, byteOut.toByteArray());
} catch (final IOException e) {
} catch (final Exception e) {
log.error("Unexpected error while getting default exam configuration for running exam; {}", runningExam, e);
return null;
}
@ -182,7 +157,7 @@ public class ExamSessionCacheService {
@CacheEvict(
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
key = "#examId")
void evictDefaultSebConfig(final Long examId) {
public void evictDefaultSebConfig(final Long examId) {
if (log.isDebugEnabled()) {
log.debug("Eviction of default SEB Configuration from cache for exam: {}", examId);
}

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;
@ -71,7 +72,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
return Result.of(exam);
} else {
if (exam != null) {
this.examSessionCacheService.evict(exam);
flushCache(exam);
}
log.warn("Exam {} is not currently running", examId);
@ -92,7 +93,6 @@ public class ExamSessionServiceImpl implements ExamSessionService {
@Override
public void streamDefaultExamConfig(
final Long institutionId,
final String connectionToken,
final OutputStream out) {
@ -101,7 +101,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
}
final ClientConnection connection = this.clientConnectionDAO
.byConnectionToken(institutionId, connectionToken)
.byConnectionToken(connectionToken)
.getOrThrow();
if (connection == null || connection.status != ConnectionStatus.ESTABLISHED) {
@ -111,17 +111,15 @@ public class ExamSessionServiceImpl implements ExamSessionService {
}
if (log.isDebugEnabled()) {
log.debug("SEB exam configuration download request: {}", connection);
log.debug("Trying to get exam form InMemorySebConfig");
log.debug("Trying to get exam from InMemorySebConfig");
}
final InMemorySebConfig sebConfigForExam = this.examSessionCacheService
.getDefaultSebConfigForExam(connection.examId);
if (log.isDebugEnabled()) {
if (sebConfigForExam == null) {
log.debug("Failed to get and cache InMemorySebConfig for connection: {}", connection);
}
if (sebConfigForExam == null) {
log.error("Failed to get and cache InMemorySebConfig for connection: {}", connection);
return;
}
try {
@ -141,4 +139,13 @@ public class ExamSessionServiceImpl implements ExamSessionService {
}
}
private void flushCache(final Exam exam) {
this.examSessionCacheService.evict(exam);
this.examSessionCacheService.evictDefaultSebConfig(exam.id);
this.clientConnectionDAO
.getConnectionTokens(exam.id)
.getOrElse(() -> Collections.emptyList())
.forEach(token -> this.examSessionCacheService.evictClientConnection(token));
}
}

View file

@ -62,6 +62,7 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
this.eventHandlingStrategy = applicationContext.getBean(
eventHandlingStrategyProperty,
EventHandlingStrategy.class);
this.eventHandlingStrategy.enable();
}
@Override
@ -90,15 +91,23 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
null,
institutionId,
examId,
ClientConnection.ConnectionStatus.CONNECTION_REQUESTED,
ConnectionStatus.CONNECTION_REQUESTED,
connectionToken,
null,
clientAddress,
null))
.getOrThrow();
if (log.isDebugEnabled()) {
log.debug("New ClientConnection created: {}", clientConnection);
// load client connection data into cache
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
.getActiveClientConnection(connectionToken);
if (activeClientConnection == null) {
log.warn("Failed to load ClientConnectionDataInternal into cache on update");
} else {
if (log.isDebugEnabled()) {
log.debug("New ClientConnection created: {}", clientConnection);
}
}
return clientConnection;
@ -109,8 +118,8 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
public Result<ClientConnection> updateClientConnection(
final String connectionToken,
final Long institutionId,
final String clientAddress,
final Long examId,
final String clientAddress,
final String userSessionId) {
return Result.tryCatch(() -> {
@ -131,9 +140,7 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
checkExamRunning(examId);
final ClientConnection clientConnection = getClientConnection(
connectionToken,
institutionId);
final ClientConnection clientConnection = getClientConnection(connectionToken);
checkInstitutionalIntegrity(
institutionId,
@ -156,7 +163,15 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
virtualClientAddress))
.getOrThrow();
if (log.isDebugEnabled()) {
// evict cached ClientConnection
this.examSessionCacheService.evictClientConnection(connectionToken);
// and load updated ClientConnection into cache
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
.getActiveClientConnection(connectionToken);
if (activeClientConnection == null) {
log.warn("Failed to load ClientConnectionDataInternal into cache on update");
} else if (log.isDebugEnabled()) {
log.debug("SEB client connection, successfully updated ClientConnection: {}",
updatedClientConnection);
}
@ -192,9 +207,7 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
checkExamRunning(examId);
final ClientConnection clientConnection = getClientConnection(
connectionToken,
institutionId);
final ClientConnection clientConnection = getClientConnection(connectionToken);
checkInstitutionalIntegrity(
institutionId,
@ -217,42 +230,40 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
clientConnection.id,
null,
examId,
ClientConnection.ConnectionStatus.ESTABLISHED,
ConnectionStatus.ESTABLISHED,
null,
userSessionId,
null,
virtualClientAddress);
// ClientConnection integrity
if (establishedClientConnection.institutionId == null ||
if (clientConnection.institutionId == null ||
clientConnection.connectionToken == null ||
establishedClientConnection.examId == null ||
establishedClientConnection.clientAddress == null ||
establishedClientConnection.connectionToken == null) {
clientConnection.clientAddress == null ||
establishedClientConnection.status != ConnectionStatus.ESTABLISHED) {
log.error("ClientConnection integrity violation: {}", establishedClientConnection);
throw new IllegalStateException("ClientConnection integrity violation: " + establishedClientConnection);
log.error("ClientConnection integrity violation, clientConnection: {}, establishedClientConnection: {}",
clientConnection,
establishedClientConnection);
throw new IllegalStateException("ClientConnection integrity violation");
}
final ClientConnection updatedClientConnection = this.clientConnectionDAO
.save(establishedClientConnection)
.getOrThrow();
if (updatedClientConnection.status == ConnectionStatus.ESTABLISHED) {
// load into cache...
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
.getActiveClientConnection(updatedClientConnection.connectionToken);
// evict cached ClientConnection
this.examSessionCacheService.evictClientConnection(connectionToken);
// and load updated ClientConnection into cache
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
.getActiveClientConnection(connectionToken);
if (activeClientConnection == null) {
log.warn("Unable to access and cache ClientConnection");
}
if (log.isDebugEnabled()) {
log.debug("ClientConnection: {} successfully established", clientConnection);
}
} else {
if (log.isDebugEnabled()) {
log.debug("ClientConnection: {} updated", clientConnection);
}
if (activeClientConnection == null) {
log.warn("Failed to load ClientConnectionDataInternal into cache on update");
} else if (log.isDebugEnabled()) {
log.debug("SEB client connection, successfully established ClientConnection: {}",
updatedClientConnection);
}
return updatedClientConnection;
@ -278,18 +289,14 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
}
final ClientConnection clientConnection = this.clientConnectionDAO
.byConnectionToken(institutionId, connectionToken)
.byConnectionToken(connectionToken)
.getOrThrow();
// evict ClientConnection from cache
this.examSessionCacheService
.evictClientConnection(clientConnection.connectionToken);
final ClientConnection updatedClientConnection = this.clientConnectionDAO.save(new ClientConnection(
clientConnection.id,
null,
null,
ClientConnection.ConnectionStatus.CLOSED,
ConnectionStatus.CLOSED,
null,
null,
null,
@ -300,6 +307,11 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
clientConnection);
}
// evict cached ClientConnection
this.examSessionCacheService.evictClientConnection(connectionToken);
// and load updated ClientConnection into cache
this.examSessionCacheService.getActiveClientConnection(connectionToken);
return updatedClientConnection;
});
@ -326,12 +338,27 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
final String connectionToken,
final ClientEvent event) {
this.eventHandlingStrategy.accept(event);
final ClientConnectionDataInternal activeClientConnection =
this.examSessionCacheService.getActiveClientConnection(connectionToken);
if (activeClientConnection != null) {
if (activeClientConnection.clientConnection.status != ConnectionStatus.ESTABLISHED) {
throw new IllegalStateException("ClientConnection is not fully established or closed");
}
// store event
this.eventHandlingStrategy.accept(
(event.connectionId != null)
? event
: new ClientEvent(
null,
activeClientConnection.getConnectionId(),
event.eventType,
event.timestamp,
event.numValue,
event.text));
// update indicators
activeClientConnection.getindicatorMapping(event.eventType)
.stream()
.forEach(indicator -> indicator.notifyValueChange(event));
@ -344,9 +371,9 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
}
}
private ClientConnection getClientConnection(final String connectionToken, final Long institutionId) {
private ClientConnection getClientConnection(final String connectionToken) {
final ClientConnection clientConnection = this.clientConnectionDAO
.byConnectionToken(institutionId, connectionToken)
.byConnectionToken(connectionToken)
.getOrThrow();
return clientConnection;
}

View file

@ -30,6 +30,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.EventHandlingStrate
public class SingleEventSaveStrategy implements EventHandlingStrategy {
private final ClientEventDAO clientEventDAO;
private boolean enabled = false;
public SingleEventSaveStrategy(final ClientEventDAO clientEventDAO) {
this.clientEventDAO = clientEventDAO;
@ -37,7 +38,24 @@ public class SingleEventSaveStrategy implements EventHandlingStrategy {
@Override
public void accept(final ClientEvent event) {
this.clientEventDAO.save(event);
this.clientEventDAO
.createNew(event)
.getOrThrow();
}
@Override
public void enable() {
this.enabled = true;
}
@Override
public void disable() {
this.enabled = false;
}
public boolean isEnabled() {
return this.enabled;
}
}

View file

@ -24,6 +24,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@ -31,6 +32,8 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
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;
@ -38,6 +41,7 @@ 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;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SebClientConfigDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@ -54,17 +58,20 @@ public class ExamAPI_V1_Controller {
private final ExamSessionService examSessionService;
private final SebClientConnectionService sebClientConnectionService;
private final SebClientConfigDAO sebClientConfigDAO;
private final JSONMapper jsonMapper;
protected ExamAPI_V1_Controller(
final ExamDAO examDAO,
final ExamSessionService examSessionService,
final SebClientConnectionService sebClientConnectionService,
final SebClientConfigDAO sebClientConfigDAO) {
final SebClientConfigDAO sebClientConfigDAO,
final JSONMapper jsonMapper) {
this.examDAO = examDAO;
this.examSessionService = examSessionService;
this.sebClientConnectionService = sebClientConnectionService;
this.sebClientConfigDAO = sebClientConfigDAO;
this.jsonMapper = jsonMapper;
}
@RequestMapping(
@ -143,8 +150,8 @@ public class ExamAPI_V1_Controller {
method = RequestMethod.PATCH,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void handshakeUpdate(
@RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
@RequestParam(name = API.EXAM_API_PARAM_EXAM_ID, required = false) final Long examId,
@RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
@RequestParam(name = API.EXAM_API_USER_SESSION_ID, required = false) final String userSessionId,
final Principal principal,
final HttpServletRequest request) {
@ -164,7 +171,7 @@ public class ExamAPI_V1_Controller {
remoteAddr);
}
this.sebClientConnectionService.establishClientConnection(
this.sebClientConnectionService.updateClientConnection(
connectionToken,
institutionId,
examId,
@ -178,8 +185,8 @@ public class ExamAPI_V1_Controller {
method = RequestMethod.PUT,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void handshakeEstablish(
@RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
@RequestParam(name = API.EXAM_API_PARAM_EXAM_ID, required = false) final Long examId,
@RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
@RequestParam(name = API.EXAM_API_USER_SESSION_ID, required = false) final String userSessionId,
final Principal principal,
final HttpServletRequest request) {
@ -210,8 +217,8 @@ public class ExamAPI_V1_Controller {
path = API.EXAM_API_HANDSHAKE_ENDPOINT,
method = RequestMethod.DELETE,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void handshakeEstablish(
@RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
public void handshakeDelete(
@RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
final Principal principal,
final HttpServletRequest request) {
@ -239,25 +246,30 @@ public class ExamAPI_V1_Controller {
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<StreamingResponseBody> getConfig(
@RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
final Principal principal) {
@RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken) {
final Long institutionId = getInstitutionId(principal);
final StreamingResponseBody stream = out -> this.examSessionService.streamDefaultExamConfig(
institutionId,
connectionToken,
out);
final StreamingResponseBody stream = out -> {
try {
this.examSessionService
.streamDefaultExamConfig(
connectionToken,
out);
} catch (final Exception e) {
final APIMessage errorMessage = APIMessage.ErrorMessage.GENERIC.of(e.getMessage());
out.write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessage)));
}
};
return new ResponseEntity<>(stream, HttpStatus.OK);
}
@RequestMapping(
path = API.EXAM_API_PING_ENDPOINT,
method = RequestMethod.PUT,
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public PingResponse ping(
@RequestParam(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,
@RequestParam(name = API.EXAM_API_PING_TIMESTAMP, required = true) final long timestamp,
@RequestParam(name = API.EXAM_API_PING_NUMBER, required = false) final int pingNumber) {
@ -272,9 +284,9 @@ public class ExamAPI_V1_Controller {
@RequestMapping(
path = API.EXAM_API_EVENT_ENDPOINT,
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public void event(
@RequestParam(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 = true) final ClientEvent event) {
this.sebClientConnectionService.notifyClientEvent(connectionToken, event);

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.integration.api.exam;
import static org.junit.Assert.assertNotNull;
import org.junit.Test;
import org.springframework.test.context.jdbc.Sql;
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public class ExamAPIAccessTokenRequestTest extends ExamAPIIntegrationTester {
@Test
public void testRequestAccessToken() throws Exception {
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
}
}

View file

@ -9,12 +9,15 @@
package ch.ethz.seb.sebserver.webservice.integration.api.exam;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Collections;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
@ -25,6 +28,9 @@ import org.springframework.boot.json.JacksonJsonParser;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.cache.CacheManager;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
@ -32,7 +38,9 @@ import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@ -40,6 +48,7 @@ import org.springframework.web.context.WebApplicationContext;
import ch.ethz.seb.sebserver.SEBServer;
import ch.ethz.seb.sebserver.WebSecurityConfig;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebClientConfigService;
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.AdminAPIClientDetails;
@ -66,6 +75,9 @@ public abstract class ExamAPIIntegrationTester {
protected MockMvc mockMvc;
@Autowired
protected CacheManager cacheManager;
@MockBean
public WebClientDetailsService webClientDetailsService;
@ -75,6 +87,12 @@ public abstract class ExamAPIIntegrationTester {
.addFilter(this.springSecurityFilterChain).build();
Mockito.when(this.webClientDetailsService.loadClientByClientId(Mockito.anyString())).thenReturn(
getForExamClientAPI());
// clear all caches before a test
this.cacheManager.getCacheNames()
.stream()
.map(name -> this.cacheManager.getCache(name))
.forEach(cache -> cache.clear());
}
protected ClientDetails getForExamClientAPI() {
@ -103,9 +121,9 @@ public abstract class ExamAPIIntegrationTester {
final ResultActions result = this.mockMvc.perform(post("/oauth/token")
.params(params)
.with(httpBasic(clientId, clientSecret))
.accept("application/json;charset=UTF-8"))
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE))
.andExpect(status().isOk())
.andExpect(content().contentType("application/json;charset=UTF-8"));
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE));
final String resultString = result.andReturn().getResponse().getContentAsString();
@ -113,6 +131,152 @@ public abstract class ExamAPIIntegrationTester {
return jsonParser.parseMap(resultString).get("access_token").toString();
}
protected MockHttpServletResponse createConnection(
final String accessToken,
final Long institutionId,
final Long examId) throws Exception {
final MockHttpServletRequestBuilder builder = get(this.endpoint + "/handshake")
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Bearer " + accessToken)
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE);
String body = "";
if (institutionId != null) {
body += "institutionId=" + institutionId;
}
if (examId != null) {
body += "&examId=" + examId;
}
builder.content(body);
final ResultActions result = this.mockMvc.perform(builder)
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE));
return result.andReturn().getResponse();
}
protected MockHttpServletResponse updateConnection(
final String accessToken,
final String connectionToken,
final Long examId,
final String userSessionId) throws Exception {
return updateConnection(accessToken, connectionToken, examId, userSessionId, false);
}
protected MockHttpServletResponse establishConnection(
final String accessToken,
final String connectionToken,
final Long examId,
final String userSessionId) throws Exception {
return updateConnection(accessToken, connectionToken, examId, userSessionId, true);
}
protected MockHttpServletResponse updateConnection(
final String accessToken,
final String connectionToken,
final Long examId,
final String userSessionId,
final boolean establish) throws Exception {
final MockHttpServletRequestBuilder builder = (establish)
? put(this.endpoint + "/handshake")
.header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.header("Authorization", "Bearer " + accessToken)
.header(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
: patch(this.endpoint + "/handshake")
.header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.header("Authorization", "Bearer " + accessToken)
.header(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE);
String body = "";
if (examId != null) {
body += "examId=" + examId;
}
if (userSessionId != null) {
if (!StringUtils.isBlank(body)) {
body += "&";
}
body += API.EXAM_API_USER_SESSION_ID + "=" + userSessionId;
}
builder.content(body);
final ResultActions result = this.mockMvc.perform(builder);
return result.andReturn().getResponse();
}
protected MockHttpServletResponse closeConnection(final String accessToken, final String connectionToken)
throws Exception {
final MockHttpServletRequestBuilder builder = delete(this.endpoint + "/handshake")
.header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.header("Authorization", "Bearer " + accessToken)
.header(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE);
final ResultActions result = this.mockMvc.perform(builder);
return result.andReturn().getResponse();
}
protected MockHttpServletResponse sendPing(
final String accessToken,
final String connectionToken,
final int num) throws Exception {
final MockHttpServletRequestBuilder builder = post(this.endpoint + API.EXAM_API_PING_ENDPOINT)
.header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.header("Authorization", "Bearer " + accessToken)
.header(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE);
final String body = API.EXAM_API_PING_TIMESTAMP + "=" + DateTime.now(DateTimeZone.UTC).getMillis()
+ "&" + API.EXAM_API_PING_NUMBER + "=" + num;
builder.content(body);
final ResultActions result = this.mockMvc.perform(builder);
return result.andReturn().getResponse();
}
protected MockHttpServletResponse sendEvent(
final String accessToken,
final String connectionToken,
final String type,
final long timestamp,
final double value,
final String text) throws Exception {
final MockHttpServletRequestBuilder builder = post(this.endpoint + API.EXAM_API_EVENT_ENDPOINT)
.header("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE)
.header("Authorization", "Bearer " + accessToken)
.header(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE);
final String body = "{ \"type\": \"%s\", \"timestamp\": %s, \"numericValue\": %s, \"text\": \"%s\" }";
builder.content(String.format(body, type, timestamp, value, text));
final ResultActions result = this.mockMvc.perform(builder);
return result.andReturn().getResponse();
}
protected MockHttpServletResponse getExamConfig(final String accessToken, final String connectionToken)
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);
final ResultActions result = this.mockMvc
.perform(builder)
.andDo(MvcResult::getAsyncResult);
return result.andReturn().getResponse();
}
@Autowired
AdminAPIClientDetails adminClientDetails;
@Autowired

View file

@ -0,0 +1,624 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.integration.api.exam;
import static org.junit.Assert.*;
import java.util.Collection;
import java.util.List;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.Cache.ValueWrapper;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.jdbc.Sql;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
import ch.ethz.seb.sebserver.gbl.model.session.IndicatorValue;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.AbstractPingIndicator;
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" })
public class SebConnectionTest extends ExamAPIIntegrationTester {
@Autowired
private ClientConnectionRecordMapper clientConnectionRecordMapper;
@Autowired
private ClientEventRecordMapper clientEventRecordMapper;
@Autowired
private JSONMapper jsonMapper;
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testCreateConnection() 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 contentAsString = createConnection.getContentAsString();
assertEquals("[{\"examId\":\"2\",\"name\":\"Demo Quiz 6\",\"url\":\"http://lms.mockup.com/api/\"}]",
contentAsString);
// check connection token
final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN);
assertNotNull(connectionToken);
// check correct stored
final List<ClientConnectionRecord> records = this.clientConnectionRecordMapper
.selectByExample()
.build()
.execute();
assertTrue(records.size() == 1);
final ClientConnectionRecord clientConnectionRecord = records.get(0);
assertEquals("1", String.valueOf(clientConnectionRecord.getInstitutionId()));
assertNull(clientConnectionRecord.getExamId());
assertEquals("CONNECTION_REQUESTED", String.valueOf(clientConnectionRecord.getStatus()));
assertEquals(connectionToken, clientConnectionRecord.getConnectionToken());
assertNotNull(clientConnectionRecord.getClientAddress());
assertNull(clientConnectionRecord.getExamUserSessionIdentifer());
assertNull(clientConnectionRecord.getVirtualClientAddress());
// check caching
final Cache examCache = this.cacheManager
.getCache(ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM);
final ValueWrapper exam = examCache.get(2L);
assertNotNull(exam);
final Cache connectionCache = this.cacheManager
.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
final ValueWrapper connection = connectionCache.get(connectionToken);
assertNotNull(connection);
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testCreateConnectionWithExamId() throws Exception {
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, 2L);
assertNotNull(createConnection);
// check correct response
assertTrue(HttpStatus.OK.value() == createConnection.getStatus());
final String contentAsString = createConnection.getContentAsString();
assertEquals("[{\"examId\":\"2\",\"name\":\"Demo Quiz 6\",\"url\":\"http://lms.mockup.com/api/\"}]",
contentAsString);
// check connection token
final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN);
assertNotNull(connectionToken);
// check correct stored
final List<ClientConnectionRecord> records = this.clientConnectionRecordMapper
.selectByExample()
.build()
.execute();
assertTrue(records.size() == 1);
final ClientConnectionRecord clientConnectionRecord = records.get(0);
assertEquals("1", String.valueOf(clientConnectionRecord.getInstitutionId()));
assertEquals("2", String.valueOf(clientConnectionRecord.getExamId()));
assertEquals("CONNECTION_REQUESTED", String.valueOf(clientConnectionRecord.getStatus()));
assertEquals(connectionToken, clientConnectionRecord.getConnectionToken());
assertNotNull(clientConnectionRecord.getClientAddress());
assertNull(clientConnectionRecord.getExamUserSessionIdentifer());
assertNull(clientConnectionRecord.getVirtualClientAddress());
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testCreateConnectionWithWrongExamId() throws Exception {
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, 1L);
assertNotNull(createConnection);
// expecting error status
assertTrue(createConnection.getStatus() != HttpStatus.OK.value());
final String contentAsString = createConnection.getContentAsString();
final Collection<APIMessage> errorMessage = this.jsonMapper.readValue(
contentAsString,
new TypeReference<Collection<APIMessage>>() {
});
final APIMessage error = errorMessage.iterator().next();
assertEquals(ErrorMessage.UNEXPECTED.messageCode, error.messageCode);
assertEquals("The exam 1 is not running", error.details);
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testCreateConnectionNoInstitutionId() throws Exception {
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
final MockHttpServletResponse createConnection = super.createConnection(accessToken, null, null);
assertNotNull(createConnection);
// expecting error status
assertTrue(createConnection.getStatus() != HttpStatus.OK.value());
final String contentAsString = createConnection.getContentAsString();
final Collection<APIMessage> errorMessage = this.jsonMapper.readValue(
contentAsString,
new TypeReference<Collection<APIMessage>>() {
});
final APIMessage error = errorMessage.iterator().next();
assertEquals(ErrorMessage.GENERIC.messageCode, error.messageCode);
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testUpdateConnection() throws Exception {
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null);
assertNotNull(createConnection);
final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN);
assertNotNull(connectionToken);
// check cache after creation
Cache connectionCache = this.cacheManager
.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
ClientConnectionDataInternal ccdi =
(ClientConnectionDataInternal) connectionCache.get(connectionToken).get();
assertNotNull(ccdi);
assertNull(ccdi.clientConnection.examId);
assertTrue(ccdi.indicatorValues.isEmpty());
final MockHttpServletResponse updatedConnection = super.updateConnection(
accessToken,
connectionToken,
2L,
"userSessionId");
// check correct response
assertTrue(HttpStatus.OK.value() == updatedConnection.getStatus());
// check correct stored
final List<ClientConnectionRecord> records = this.clientConnectionRecordMapper
.selectByExample()
.build()
.execute();
assertTrue(records.size() == 1);
final ClientConnectionRecord clientConnectionRecord = records.get(0);
assertEquals("1", String.valueOf(clientConnectionRecord.getInstitutionId()));
assertEquals("2", String.valueOf(clientConnectionRecord.getExamId()));
assertEquals("CONNECTION_REQUESTED", String.valueOf(clientConnectionRecord.getStatus()));
assertNotNull(clientConnectionRecord.getConnectionToken());
assertNotNull(clientConnectionRecord.getClientAddress());
assertEquals("userSessionId", clientConnectionRecord.getExamUserSessionIdentifer());
assertNull(clientConnectionRecord.getVirtualClientAddress());
// check cache after update
connectionCache = this.cacheManager
.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
ccdi =
(ClientConnectionDataInternal) connectionCache.get(connectionToken).get();
assertNotNull(ccdi);
assertNotNull(ccdi.clientConnection.examId);
assertFalse(ccdi.indicatorValues.isEmpty());
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testUpdateConnectionWithWrongConnectionToken() throws Exception {
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
final MockHttpServletResponse updatedConnection = super.updateConnection(
accessToken,
"",
2L,
"userSessionId");
// expecting error status
assertTrue(HttpStatus.OK.value() != updatedConnection.getStatus());
final String contentAsString = updatedConnection.getContentAsString();
final Collection<APIMessage> errorMessage = this.jsonMapper.readValue(
contentAsString,
new TypeReference<Collection<APIMessage>>() {
});
final APIMessage error = errorMessage.iterator().next();
assertEquals(ErrorMessage.RESOURCE_NOT_FOUND.messageCode, error.messageCode);
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testEstablishConnection() throws Exception {
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null);
assertNotNull(createConnection);
final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN);
assertNotNull(connectionToken);
// check cache after creation
final Cache connectionCache = this.cacheManager
.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
ClientConnectionDataInternal ccdi =
(ClientConnectionDataInternal) connectionCache.get(connectionToken).get();
assertNotNull(ccdi);
assertNull(ccdi.clientConnection.examId);
assertTrue(ccdi.indicatorValues.isEmpty());
final MockHttpServletResponse updatedConnection = super.establishConnection(
accessToken,
connectionToken,
2L,
"userSessionId");
// check correct response
assertTrue(HttpStatus.OK.value() == updatedConnection.getStatus());
// check correct stored
final List<ClientConnectionRecord> records = this.clientConnectionRecordMapper
.selectByExample()
.build()
.execute();
assertTrue(records.size() == 1);
final ClientConnectionRecord clientConnectionRecord = records.get(0);
assertEquals("1", String.valueOf(clientConnectionRecord.getInstitutionId()));
assertEquals("2", String.valueOf(clientConnectionRecord.getExamId()));
assertEquals("ESTABLISHED", String.valueOf(clientConnectionRecord.getStatus()));
assertNotNull(clientConnectionRecord.getConnectionToken());
assertNotNull(clientConnectionRecord.getClientAddress());
assertEquals("userSessionId", clientConnectionRecord.getExamUserSessionIdentifer());
assertNull(clientConnectionRecord.getVirtualClientAddress());
// check cache after update
ccdi = (ClientConnectionDataInternal) connectionCache.get(connectionToken).get();
assertNotNull(ccdi);
assertNotNull(ccdi.clientConnection.examId);
assertFalse(ccdi.indicatorValues.isEmpty());
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testEstablishConnectionNoExamLeadsToError() throws Exception {
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null);
assertNotNull(createConnection);
final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN);
assertNotNull(connectionToken);
// check cache after creation
final Cache connectionCache = this.cacheManager
.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
ClientConnectionDataInternal ccdi =
(ClientConnectionDataInternal) connectionCache.get(connectionToken).get();
assertNotNull(ccdi);
assertNull(ccdi.clientConnection.examId);
assertTrue(ccdi.indicatorValues.isEmpty());
final MockHttpServletResponse updatedConnection = super.establishConnection(
accessToken,
connectionToken,
null,
null);
// check correct response
assertTrue(HttpStatus.OK.value() != updatedConnection.getStatus());
final String contentAsString = updatedConnection.getContentAsString();
final Collection<APIMessage> errorMessage = this.jsonMapper.readValue(
contentAsString,
new TypeReference<Collection<APIMessage>>() {
});
final APIMessage error = errorMessage.iterator().next();
assertEquals(ErrorMessage.UNEXPECTED.messageCode, error.messageCode);
assertEquals("ClientConnection integrity violation", error.details);
// check correct stored (no changes)
final List<ClientConnectionRecord> records = this.clientConnectionRecordMapper
.selectByExample()
.build()
.execute();
assertTrue(records.size() == 1);
final ClientConnectionRecord clientConnectionRecord = records.get(0);
assertEquals("1", String.valueOf(clientConnectionRecord.getInstitutionId()));
assertNull(clientConnectionRecord.getExamId());
assertEquals("CONNECTION_REQUESTED", String.valueOf(clientConnectionRecord.getStatus()));
assertNotNull(clientConnectionRecord.getConnectionToken());
assertNotNull(clientConnectionRecord.getClientAddress());
assertNull(clientConnectionRecord.getExamUserSessionIdentifer());
assertNull(clientConnectionRecord.getVirtualClientAddress());
// check cache fail remains the same
ccdi = (ClientConnectionDataInternal) connectionCache.get(connectionToken).get();
assertNotNull(ccdi);
assertNull(ccdi.clientConnection.examId);
assertTrue(ccdi.indicatorValues.isEmpty());
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testCloseConnection() throws Exception {
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null);
assertNotNull(createConnection);
final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN);
assertNotNull(connectionToken);
final MockHttpServletResponse establishConnection = super.establishConnection(
accessToken,
connectionToken,
2L,
null);
// check correct response
assertTrue(HttpStatus.OK.value() == establishConnection.getStatus());
// check cache after creation
final Cache connectionCache = this.cacheManager
.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
ClientConnectionDataInternal ccdi =
(ClientConnectionDataInternal) connectionCache.get(connectionToken).get();
assertNotNull(ccdi);
assertNotNull(ccdi.clientConnection.examId);
assertFalse(ccdi.indicatorValues.isEmpty());
// close connection
final MockHttpServletResponse closedConnection = super.closeConnection(
accessToken,
connectionToken);
// check correct response
assertTrue(HttpStatus.OK.value() == closedConnection.getStatus());
// check correct stored (no changes)
final List<ClientConnectionRecord> records = this.clientConnectionRecordMapper
.selectByExample()
.build()
.execute();
assertTrue(records.size() == 1);
final ClientConnectionRecord clientConnectionRecord = records.get(0);
assertEquals("1", String.valueOf(clientConnectionRecord.getInstitutionId()));
assertEquals("2", String.valueOf(clientConnectionRecord.getExamId()));
assertEquals("CLOSED", String.valueOf(clientConnectionRecord.getStatus()));
assertNotNull(clientConnectionRecord.getConnectionToken());
assertNotNull(clientConnectionRecord.getClientAddress());
assertNull(clientConnectionRecord.getExamUserSessionIdentifer());
assertNull(clientConnectionRecord.getVirtualClientAddress());
// check cache after update
ccdi = (ClientConnectionDataInternal) connectionCache.get(connectionToken).get();
assertNotNull(ccdi);
assertNotNull(ccdi.clientConnection.examId);
assertFalse(ccdi.indicatorValues.isEmpty());
assertEquals("CLOSED", ccdi.clientConnection.status.toString());
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testPing() throws Exception {
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null);
assertNotNull(createConnection);
final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN);
assertNotNull(connectionToken);
final MockHttpServletResponse establishConnection = super.establishConnection(
accessToken,
connectionToken,
2L,
null);
// check correct response
assertTrue(HttpStatus.OK.value() == establishConnection.getStatus());
final Cache connectionCache = this.cacheManager
.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
final ClientConnectionDataInternal ccdi =
(ClientConnectionDataInternal) connectionCache.get(connectionToken).get();
assertNotNull(ccdi);
assertNotNull(ccdi.clientConnection.examId);
assertFalse(ccdi.indicatorValues.isEmpty());
final IndicatorValue pingIndicator = ccdi.indicatorValues.iterator().next();
assertTrue(pingIndicator.getType() == IndicatorType.LAST_PING);
assertEquals("0", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
super.sendPing(accessToken, connectionToken, 1);
assertEquals("1", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
super.sendPing(accessToken, connectionToken, 2);
assertEquals("2", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
super.sendPing(accessToken, connectionToken, 3);
assertEquals("3", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
super.sendPing(accessToken, connectionToken, 5);
assertEquals("5", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testSendPingToNoneEstablishedConnectionIsIgnored() throws Exception {
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null);
assertNotNull(createConnection);
final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN);
assertNotNull(connectionToken);
final MockHttpServletResponse sendPing = super.sendPing(accessToken, connectionToken, 1);
// check correct response
assertTrue(HttpStatus.OK.value() == sendPing.getStatus());
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testEvent() throws Exception {
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null);
assertNotNull(createConnection);
final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN);
assertNotNull(connectionToken);
final MockHttpServletResponse establishConnection = super.establishConnection(
accessToken,
connectionToken,
2L,
null);
// check correct response
assertTrue(HttpStatus.OK.value() == establishConnection.getStatus());
final Cache connectionCache = this.cacheManager
.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
final ClientConnectionDataInternal ccdi =
(ClientConnectionDataInternal) connectionCache.get(connectionToken).get();
assertNotNull(ccdi);
assertNotNull(ccdi.clientConnection.examId);
assertFalse(ccdi.indicatorValues.isEmpty());
final IndicatorValue pingIndicator = ccdi.indicatorValues.iterator().next();
assertTrue(pingIndicator.getType() == IndicatorType.LAST_PING);
assertEquals("0", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
MockHttpServletResponse sendEvent = super.sendEvent(
accessToken,
connectionToken,
"INFO_LOG",
1l,
100.0,
"testEvent1");
// check correct response
assertTrue(HttpStatus.OK.value() == sendEvent.getStatus());
// check event stored on db
List<ClientEventRecord> events = this.clientEventRecordMapper
.selectByExample()
.build()
.execute();
assertFalse(events.isEmpty());
final ClientEventRecord clientEventRecord = events.get(0);
assertEquals(
"ClientEventRecord ["
+ "Hash = -1088444763, "
+ "id=1, "
+ "connectionId=1, "
+ "type=2, "
+ "timestamp=1, "
+ "numericValue=100.0000, "
+ "text=testEvent1]",
clientEventRecord.toString());
// send another event
sendEvent = super.sendEvent(
accessToken,
connectionToken,
"ERROR_LOG",
2l,
10000.0,
"testEvent2");
// check correct response
assertTrue(HttpStatus.OK.value() == sendEvent.getStatus());
// check event stored on db
events = this.clientEventRecordMapper
.selectByExample()
.build()
.execute();
assertFalse(events.isEmpty());
assertTrue(events.stream().filter(ev -> ev.getTimestamp().equals(2l)).findFirst().isPresent());
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testSendEventToNoneEstablishedConnectionLeadsToError() throws Exception {
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null);
assertNotNull(createConnection);
final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN);
assertNotNull(connectionToken);
final MockHttpServletResponse sendEvent = super.sendEvent(
accessToken,
connectionToken,
"INFO_LOG",
1l,
100.0,
"testEvent1");
// check correct response
assertTrue(HttpStatus.OK.value() != sendEvent.getStatus());
final List<ClientEventRecord> events = this.clientEventRecordMapper
.selectByExample()
.build()
.execute();
assertTrue(events.isEmpty());
}
@Test
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public void testSendEventToNoneExistingConnectionIsIgnored() throws Exception {
final String accessToken = super.obtainAccessToken("test", "test", "SEBClient");
assertNotNull(accessToken);
final MockHttpServletResponse sendEvent = super.sendEvent(
accessToken,
"someInvalidConnectionToken",
"INFO_LOG",
1l,
100.0,
"testEvent1");
// check correct response
assertTrue(HttpStatus.OK.value() == sendEvent.getStatus());
final List<ClientEventRecord> events = this.clientEventRecordMapper
.selectByExample()
.build()
.execute();
assertTrue(events.isEmpty());
}
}

File diff suppressed because one or more lines are too long

View file

@ -21,7 +21,6 @@ sebserver.webservice.api.exam.endpoint.discovery=${sebserver.webservice.api.exam
sebserver.webservice.api.exam.endpoint.v1=${sebserver.webservice.api.exam.endpoint}/v1
sebserver.webservice.api.exam.accessTokenValiditySeconds=1800
sebserver.webservice.api.exam.refreshTokenValiditySeconds=-1
sebserver.webservice.internalSecret=TO_SET
sebserver.webservice.api.redirect.unauthorized=none
# comma separated list of known possible OpenEdX API access token request endpoints
sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token