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

@ -55,12 +55,6 @@ public interface ClientCredentialService {
return encryptClientCredentials(clientIdPlaintext, secretPlaintext, null); 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 /** Use this to get a decrypted plain text secret form given ClientCredentials
* *
* @param credentials ClientCredentials containing the secret to decrypt * @param credentials ClientCredentials containing the secret to decrypt

View file

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

View file

@ -8,13 +8,25 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao; 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.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
public interface ClientConnectionDAO extends EntityDAO<ClientConnection, ClientConnection> { 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); Result<ClientConnection> byConnectionToken(String connectionToken);
} }

View file

@ -16,8 +16,21 @@ public interface ExamConfigurationMapDAO extends
EntityDAO<ExamConfigurationMap, ExamConfigurationMap>, EntityDAO<ExamConfigurationMap, ExamConfigurationMap>,
BulkActionSupportDAO<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); 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); 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 @Override
@Transactional @Transactional
public Result<ClientConnection> createNew(final ClientConnection data) { public Result<ClientConnection> createNew(final ClientConnection data) {
@ -140,10 +158,10 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
final ClientConnectionRecord updateRecord = new ClientConnectionRecord( final ClientConnectionRecord updateRecord = new ClientConnectionRecord(
data.id, data.id,
null, null,
null, data.examId,
data.status != null ? data.status.name() : null, data.status != null ? data.status.name() : null,
data.connectionToken,
null, null,
data.userSessionId,
data.clientAddress, data.clientAddress,
data.virtualClientAddress); 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 @Override
public Result<ClientConnection> byConnectionToken(final String connectionToken) { public Result<ClientConnection> byConnectionToken(final String connectionToken) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {

View file

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

View file

@ -130,7 +130,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Long> getDefaultConfigurationForExam(final Long examId) { public Result<Long> getDefaultConfigurationForExam(final Long examId) {
return Result.tryCatch(() -> this.examConfigurationMapRecordMapper return Result.tryCatch(() -> this.examConfigurationMapRecordMapper
.selectIdsByExample() .selectByExample()
.where( .where(
ExamConfigurationMapRecordDynamicSqlSupport.examId, ExamConfigurationMapRecordDynamicSqlSupport.examId,
SqlBuilder.isEqualTo(examId)) SqlBuilder.isEqualTo(examId))
@ -140,13 +140,14 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
.build() .build()
.execute() .execute()
.stream() .stream()
.map(mapping -> mapping.getConfigurationNodeId())
.collect(Utils.toSingleton())); .collect(Utils.toSingleton()));
} }
@Override @Override
public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) { public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) {
return Result.tryCatch(() -> this.examConfigurationMapRecordMapper return Result.tryCatch(() -> this.examConfigurationMapRecordMapper
.selectIdsByExample() .selectByExample()
.where( .where(
ExamConfigurationMapRecordDynamicSqlSupport.examId, ExamConfigurationMapRecordDynamicSqlSupport.examId,
SqlBuilder.isEqualTo(examId)) SqlBuilder.isEqualTo(examId))
@ -156,6 +157,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
.build() .build()
.execute() .execute()
.stream() .stream()
.map(mapping -> mapping.getConfigurationNodeId())
.collect(Utils.toSingleton())); .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.api.APIMessage.FieldValidationException;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; 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. */ /** The base interface and service for all SEB Exam Configuration related functionality. */
public interface SebExamConfigService { public interface SebExamConfigService {
@ -32,10 +31,6 @@ public interface SebExamConfigService {
* @throws FieldValidationException on validation exception */ * @throws FieldValidationException on validation exception */
void validate(ConfigurationTableValues tableValue) throws FieldValidationException; 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 /** Used to export a specified SEB Exam Configuration as plain XML
* This exports the values of the follow-up configuration defined by a given * This exports the values of the follow-up configuration defined by a given
* ConfigurationNode (configurationNodeId) * ConfigurationNode (configurationNodeId)
@ -45,21 +40,26 @@ public interface SebExamConfigService {
* @param configurationNodeId the identifier of the ConfigurationNode to export */ * @param configurationNodeId the identifier of the ConfigurationNode to export */
void exportPlainXML(OutputStream out, Long institutionId, Long configurationNodeId); 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 * 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 * as described here: https://www.safeexambrowser.org/developer/seb-file-format.html
* *
* @param out The output stream to write the export data to * @param out The output stream to write the export data to
* @param configExamMappingId The identifier of the Exam Configuration Mapping */ * @param institutionId The identifier of the institution of the requesting user
void exportForExam(OutputStream out, Long configExamMappingId); * @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. /** 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 * 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 * as described here: https://www.safeexambrowser.org/developer/seb-file-format.html
* *
* @param out The output stream to write the export data to * @param out The output stream to write the export data to
* @param examId the exam identifier */ * @param institutionId The identifier of the institution of the requesting user
void exportDefaultForExam(OutputStream out, Long examId); * @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 */ /** TODO */
String generateConfigKey(Long configurationNodeId); String generateConfigKey(Long configurationNodeId);

View file

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

View file

@ -15,6 +15,7 @@ import java.io.PipedOutputStream;
import java.util.Collection; import java.util.Collection;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
@ -96,13 +97,16 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
pout = new PipedOutputStream(); pout = new PipedOutputStream();
pin = new PipedInputStream(pout); pin = new PipedInputStream(pout);
this.examConfigIO.exportPlainXML(pout, institutionId, configurationNodeId); this.examConfigIO.exportPlainXML(
pout,
institutionId,
configurationNodeId);
IOUtils.copyLarge(pin, out); IOUtils.copyLarge(pin, out);
pin.close();
pout.flush(); pout.flush();
pout.close(); pout.close();
pin.close();
} catch (final IOException e) { } catch (final IOException e) {
log.error("Error while stream plain text SEB clonfiguration data: ", 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) { public Result<Long> getDefaultConfigurationIdForExam(final Long examId) {
return this.examConfigurationMapDAO.getDefaultConfigurationForExam(examId); return this.examConfigurationMapDAO.getDefaultConfigurationForExam(examId);
} }
@Override
public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) { public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) {
return this.examConfigurationMapDAO.getUserConfigurationIdForExam(examId, userId); return this.examConfigurationMapDAO.getUserConfigurationIdForExam(examId, userId);
} }
@Override @Override
public void exportForExam(final OutputStream out, final Long configExamMappingId) { public Long exportForExam(
// TODO Auto-generated method stub 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 // TODO add header, zip and encrypt if needed
public void exportDefaultForExam(final OutputStream out, final Long examId) {
// TODO Auto-generated method stub
this.exportPlainXML(out, institutionId, configurationNodeId);
return configurationNodeId;
} }
@Override @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_SINGLE_EVENT_STORE = "SINGLE_EVENT_STORE_STRATEGY";
String EVENT_CONSUMER_STRATEGY_ASYNC_BATCH_STORE = "ASYNC_BATCH_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. */ * happened. */
Result<Collection<Exam>> getRunningExamsForInstitution(Long institutionId); 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( Result<ClientConnection> updateClientConnection(
String connectionToken, String connectionToken,
Long institutionId, Long institutionId,
String clientAddress,
Long examId, Long examId,
String clientAddress,
String userSessionId); String userSessionId);
/** This is used to establish a already created ClientConnection and set it to sate: ESTABLISHED /** 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 @Override
public double getValue() { 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()); 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.EnumSet;
import java.util.Set; import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnore;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
public abstract class AbstractPingIndicator extends AbstractClientIndicator { public abstract class AbstractPingIndicator extends AbstractClientIndicator {
@ -37,4 +39,14 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator {
return this.EMPTY_SET; 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 final BlockingDeque<ClientEvent> eventQueue = new LinkedBlockingDeque<>();
private boolean workersRunning = false; private boolean workersRunning = false;
private boolean enabled = false;
public AsyncBatchEventSaveStrategy( public AsyncBatchEventSaveStrategy(
final SqlSessionFactory sqlSessionFactory, final SqlSessionFactory sqlSessionFactory,
@ -75,10 +76,23 @@ public class AsyncBatchEventSaveStrategy implements EventHandlingStrategy {
this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
} }
@Override
public void enable() {
this.enabled = true;
}
@Override
public void disable() {
this.enabled = false;
}
@EventListener(ApplicationReadyEvent.class) @EventListener(ApplicationReadyEvent.class)
protected void recover() { protected void recover() {
if (this.enabled) {
runWorkers(); runWorkers();
} }
}
@Override @Override
public void accept(final ClientEvent event) { public void accept(final ClientEvent event) {

View file

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

View file

@ -8,13 +8,8 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict; 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.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; 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.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
@ -63,7 +57,7 @@ public class ExamSessionCacheService {
cacheNames = CACHE_NAME_RUNNING_EXAM, cacheNames = CACHE_NAME_RUNNING_EXAM,
key = "#examId", key = "#examId",
unless = "#result == null") unless = "#result == null")
Exam getRunningExam(final Long examId) { public Exam getRunningExam(final Long examId) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Verify running exam for id: {}" + examId); log.debug("Verify running exam for id: {}" + examId);
@ -87,7 +81,7 @@ public class ExamSessionCacheService {
cacheNames = CACHE_NAME_RUNNING_EXAM, cacheNames = CACHE_NAME_RUNNING_EXAM,
key = "#exam.id", key = "#exam.id",
condition = "#target.isRunning(#result)") condition = "#target.isRunning(#result)")
Exam evict(final Exam exam) { public Exam evict(final Exam exam) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Conditional eviction of running Exam from cache: {}", isRunning(exam)); log.debug("Conditional eviction of running Exam from cache: {}", isRunning(exam));
@ -108,7 +102,7 @@ public class ExamSessionCacheService {
cacheNames = CACHE_NAME_ACTIVE_CLIENT_CONNECTION, cacheNames = CACHE_NAME_ACTIVE_CLIENT_CONNECTION,
key = "#connectionToken", key = "#connectionToken",
unless = "#result == null") unless = "#result == null")
ClientConnectionDataInternal getActiveClientConnection(final String connectionToken) { public ClientConnectionDataInternal getActiveClientConnection(final String connectionToken) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Verify ClientConnection for running exam for caching by connectionToken: ", connectionToken); log.debug("Verify ClientConnection for running exam for caching by connectionToken: ", connectionToken);
@ -123,20 +117,6 @@ public class ExamSessionCacheService {
} }
final ClientConnection clientConnection = byPK.get(); 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( return new ClientConnectionDataInternal(
clientConnection, clientConnection,
this.clientIndicatorFactory.createFor(clientConnection)); this.clientIndicatorFactory.createFor(clientConnection));
@ -145,7 +125,7 @@ public class ExamSessionCacheService {
@CacheEvict( @CacheEvict(
cacheNames = CACHE_NAME_ACTIVE_CLIENT_CONNECTION, cacheNames = CACHE_NAME_ACTIVE_CLIENT_CONNECTION,
key = "#connectionToken") key = "#connectionToken")
void evictClientConnection(final String connectionToken) { public void evictClientConnection(final String connectionToken) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Eviction of ClientConnectionData from cache: {}", connectionToken); log.debug("Eviction of ClientConnectionData from cache: {}", connectionToken);
} }
@ -155,25 +135,20 @@ public class ExamSessionCacheService {
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM, cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
key = "#examId", key = "#examId",
unless = "#result == null") unless = "#result == null")
InMemorySebConfig getDefaultSebConfigForExam(final Long examId) { public InMemorySebConfig getDefaultSebConfigForExam(final Long examId) {
final Exam runningExam = this.getRunningExam(examId); final Exam runningExam = this.getRunningExam(examId);
final PipedOutputStream pipOut = new PipedOutputStream();
try { 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(); 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()); 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); log.error("Unexpected error while getting default exam configuration for running exam; {}", runningExam, e);
return null; return null;
} }
@ -182,7 +157,7 @@ public class ExamSessionCacheService {
@CacheEvict( @CacheEvict(
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM, cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
key = "#examId") key = "#examId")
void evictDefaultSebConfig(final Long examId) { public void evictDefaultSebConfig(final Long examId) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Eviction of default SEB Configuration from cache for exam: {}", examId); 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.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -71,7 +72,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
return Result.of(exam); return Result.of(exam);
} else { } else {
if (exam != null) { if (exam != null) {
this.examSessionCacheService.evict(exam); flushCache(exam);
} }
log.warn("Exam {} is not currently running", examId); log.warn("Exam {} is not currently running", examId);
@ -92,7 +93,6 @@ public class ExamSessionServiceImpl implements ExamSessionService {
@Override @Override
public void streamDefaultExamConfig( public void streamDefaultExamConfig(
final Long institutionId,
final String connectionToken, final String connectionToken,
final OutputStream out) { final OutputStream out) {
@ -101,7 +101,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
} }
final ClientConnection connection = this.clientConnectionDAO final ClientConnection connection = this.clientConnectionDAO
.byConnectionToken(institutionId, connectionToken) .byConnectionToken(connectionToken)
.getOrThrow(); .getOrThrow();
if (connection == null || connection.status != ConnectionStatus.ESTABLISHED) { if (connection == null || connection.status != ConnectionStatus.ESTABLISHED) {
@ -111,17 +111,15 @@ public class ExamSessionServiceImpl implements ExamSessionService {
} }
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("SEB exam configuration download request: {}", connection); log.debug("Trying to get exam from InMemorySebConfig");
log.debug("Trying to get exam form InMemorySebConfig");
} }
final InMemorySebConfig sebConfigForExam = this.examSessionCacheService final InMemorySebConfig sebConfigForExam = this.examSessionCacheService
.getDefaultSebConfigForExam(connection.examId); .getDefaultSebConfigForExam(connection.examId);
if (log.isDebugEnabled()) {
if (sebConfigForExam == null) { if (sebConfigForExam == null) {
log.debug("Failed to get and cache InMemorySebConfig for connection: {}", connection); log.error("Failed to get and cache InMemorySebConfig for connection: {}", connection);
} return;
} }
try { 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( this.eventHandlingStrategy = applicationContext.getBean(
eventHandlingStrategyProperty, eventHandlingStrategyProperty,
EventHandlingStrategy.class); EventHandlingStrategy.class);
this.eventHandlingStrategy.enable();
} }
@Override @Override
@ -90,16 +91,24 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
null, null,
institutionId, institutionId,
examId, examId,
ClientConnection.ConnectionStatus.CONNECTION_REQUESTED, ConnectionStatus.CONNECTION_REQUESTED,
connectionToken, connectionToken,
null, null,
clientAddress, clientAddress,
null)) null))
.getOrThrow(); .getOrThrow();
// 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()) { if (log.isDebugEnabled()) {
log.debug("New ClientConnection created: {}", clientConnection); log.debug("New ClientConnection created: {}", clientConnection);
} }
}
return clientConnection; return clientConnection;
}); });
@ -109,8 +118,8 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
public Result<ClientConnection> updateClientConnection( public Result<ClientConnection> updateClientConnection(
final String connectionToken, final String connectionToken,
final Long institutionId, final Long institutionId,
final String clientAddress,
final Long examId, final Long examId,
final String clientAddress,
final String userSessionId) { final String userSessionId) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
@ -131,9 +140,7 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
checkExamRunning(examId); checkExamRunning(examId);
final ClientConnection clientConnection = getClientConnection( final ClientConnection clientConnection = getClientConnection(connectionToken);
connectionToken,
institutionId);
checkInstitutionalIntegrity( checkInstitutionalIntegrity(
institutionId, institutionId,
@ -156,7 +163,15 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
virtualClientAddress)) virtualClientAddress))
.getOrThrow(); .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: {}", log.debug("SEB client connection, successfully updated ClientConnection: {}",
updatedClientConnection); updatedClientConnection);
} }
@ -192,9 +207,7 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
checkExamRunning(examId); checkExamRunning(examId);
final ClientConnection clientConnection = getClientConnection( final ClientConnection clientConnection = getClientConnection(connectionToken);
connectionToken,
institutionId);
checkInstitutionalIntegrity( checkInstitutionalIntegrity(
institutionId, institutionId,
@ -217,42 +230,40 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
clientConnection.id, clientConnection.id,
null, null,
examId, examId,
ClientConnection.ConnectionStatus.ESTABLISHED, ConnectionStatus.ESTABLISHED,
null, null,
userSessionId, userSessionId,
null, null,
virtualClientAddress); virtualClientAddress);
// ClientConnection integrity // ClientConnection integrity
if (establishedClientConnection.institutionId == null || if (clientConnection.institutionId == null ||
clientConnection.connectionToken == null ||
establishedClientConnection.examId == null || establishedClientConnection.examId == null ||
establishedClientConnection.clientAddress == null || clientConnection.clientAddress == null ||
establishedClientConnection.connectionToken == null) { establishedClientConnection.status != ConnectionStatus.ESTABLISHED) {
log.error("ClientConnection integrity violation: {}", establishedClientConnection); log.error("ClientConnection integrity violation, clientConnection: {}, establishedClientConnection: {}",
throw new IllegalStateException("ClientConnection integrity violation: " + establishedClientConnection); clientConnection,
establishedClientConnection);
throw new IllegalStateException("ClientConnection integrity violation");
} }
final ClientConnection updatedClientConnection = this.clientConnectionDAO final ClientConnection updatedClientConnection = this.clientConnectionDAO
.save(establishedClientConnection) .save(establishedClientConnection)
.getOrThrow(); .getOrThrow();
if (updatedClientConnection.status == ConnectionStatus.ESTABLISHED) { // evict cached ClientConnection
// load into cache... this.examSessionCacheService.evictClientConnection(connectionToken);
// and load updated ClientConnection into cache
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
.getActiveClientConnection(updatedClientConnection.connectionToken); .getActiveClientConnection(connectionToken);
if (activeClientConnection == null) { if (activeClientConnection == null) {
log.warn("Unable to access and cache ClientConnection"); log.warn("Failed to load ClientConnectionDataInternal into cache on update");
} } else if (log.isDebugEnabled()) {
log.debug("SEB client connection, successfully established ClientConnection: {}",
if (log.isDebugEnabled()) { updatedClientConnection);
log.debug("ClientConnection: {} successfully established", clientConnection);
}
} else {
if (log.isDebugEnabled()) {
log.debug("ClientConnection: {} updated", clientConnection);
}
} }
return updatedClientConnection; return updatedClientConnection;
@ -278,18 +289,14 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
} }
final ClientConnection clientConnection = this.clientConnectionDAO final ClientConnection clientConnection = this.clientConnectionDAO
.byConnectionToken(institutionId, connectionToken) .byConnectionToken(connectionToken)
.getOrThrow(); .getOrThrow();
// evict ClientConnection from cache
this.examSessionCacheService
.evictClientConnection(clientConnection.connectionToken);
final ClientConnection updatedClientConnection = this.clientConnectionDAO.save(new ClientConnection( final ClientConnection updatedClientConnection = this.clientConnectionDAO.save(new ClientConnection(
clientConnection.id, clientConnection.id,
null, null,
null, null,
ClientConnection.ConnectionStatus.CLOSED, ConnectionStatus.CLOSED,
null, null,
null, null,
null, null,
@ -300,6 +307,11 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
clientConnection); clientConnection);
} }
// evict cached ClientConnection
this.examSessionCacheService.evictClientConnection(connectionToken);
// and load updated ClientConnection into cache
this.examSessionCacheService.getActiveClientConnection(connectionToken);
return updatedClientConnection; return updatedClientConnection;
}); });
@ -326,12 +338,27 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
final String connectionToken, final String connectionToken,
final ClientEvent event) { final ClientEvent event) {
this.eventHandlingStrategy.accept(event);
final ClientConnectionDataInternal activeClientConnection = final ClientConnectionDataInternal activeClientConnection =
this.examSessionCacheService.getActiveClientConnection(connectionToken); this.examSessionCacheService.getActiveClientConnection(connectionToken);
if (activeClientConnection != null) { 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) activeClientConnection.getindicatorMapping(event.eventType)
.stream() .stream()
.forEach(indicator -> indicator.notifyValueChange(event)); .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 final ClientConnection clientConnection = this.clientConnectionDAO
.byConnectionToken(institutionId, connectionToken) .byConnectionToken(connectionToken)
.getOrThrow(); .getOrThrow();
return clientConnection; return clientConnection;
} }

View file

@ -30,6 +30,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.EventHandlingStrate
public class SingleEventSaveStrategy implements EventHandlingStrategy { public class SingleEventSaveStrategy implements EventHandlingStrategy {
private final ClientEventDAO clientEventDAO; private final ClientEventDAO clientEventDAO;
private boolean enabled = false;
public SingleEventSaveStrategy(final ClientEventDAO clientEventDAO) { public SingleEventSaveStrategy(final ClientEventDAO clientEventDAO) {
this.clientEventDAO = clientEventDAO; this.clientEventDAO = clientEventDAO;
@ -37,7 +38,24 @@ public class SingleEventSaveStrategy implements EventHandlingStrategy {
@Override @Override
public void accept(final ClientEvent event) { 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.http.ResponseEntity;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestBody; 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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; 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 org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import ch.ethz.seb.sebserver.gbl.api.API; 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.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; 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;
@ -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.PingResponse;
import ch.ethz.seb.sebserver.gbl.model.session.RunningExam; import ch.ethz.seb.sebserver.gbl.model.session.RunningExam;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; 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.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SebClientConfigDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SebClientConfigDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@ -54,17 +58,20 @@ public class ExamAPI_V1_Controller {
private final ExamSessionService examSessionService; private final ExamSessionService examSessionService;
private final SebClientConnectionService sebClientConnectionService; private final SebClientConnectionService sebClientConnectionService;
private final SebClientConfigDAO sebClientConfigDAO; private final SebClientConfigDAO sebClientConfigDAO;
private final JSONMapper jsonMapper;
protected ExamAPI_V1_Controller( protected ExamAPI_V1_Controller(
final ExamDAO examDAO, final ExamDAO examDAO,
final ExamSessionService examSessionService, final ExamSessionService examSessionService,
final SebClientConnectionService sebClientConnectionService, final SebClientConnectionService sebClientConnectionService,
final SebClientConfigDAO sebClientConfigDAO) { final SebClientConfigDAO sebClientConfigDAO,
final JSONMapper jsonMapper) {
this.examDAO = examDAO; this.examDAO = examDAO;
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
this.sebClientConnectionService = sebClientConnectionService; this.sebClientConnectionService = sebClientConnectionService;
this.sebClientConfigDAO = sebClientConfigDAO; this.sebClientConfigDAO = sebClientConfigDAO;
this.jsonMapper = jsonMapper;
} }
@RequestMapping( @RequestMapping(
@ -143,8 +150,8 @@ public class ExamAPI_V1_Controller {
method = RequestMethod.PATCH, method = RequestMethod.PATCH,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void handshakeUpdate( 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_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, @RequestParam(name = API.EXAM_API_USER_SESSION_ID, required = false) final String userSessionId,
final Principal principal, final Principal principal,
final HttpServletRequest request) { final HttpServletRequest request) {
@ -164,7 +171,7 @@ public class ExamAPI_V1_Controller {
remoteAddr); remoteAddr);
} }
this.sebClientConnectionService.establishClientConnection( this.sebClientConnectionService.updateClientConnection(
connectionToken, connectionToken,
institutionId, institutionId,
examId, examId,
@ -178,8 +185,8 @@ public class ExamAPI_V1_Controller {
method = RequestMethod.PUT, method = RequestMethod.PUT,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void handshakeEstablish( 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_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, @RequestParam(name = API.EXAM_API_USER_SESSION_ID, required = false) final String userSessionId,
final Principal principal, final Principal principal,
final HttpServletRequest request) { final HttpServletRequest request) {
@ -210,8 +217,8 @@ public class ExamAPI_V1_Controller {
path = API.EXAM_API_HANDSHAKE_ENDPOINT, path = API.EXAM_API_HANDSHAKE_ENDPOINT,
method = RequestMethod.DELETE, method = RequestMethod.DELETE,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void handshakeEstablish( public void handshakeDelete(
@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,
final Principal principal, final Principal principal,
final HttpServletRequest request) { final HttpServletRequest request) {
@ -239,25 +246,30 @@ public class ExamAPI_V1_Controller {
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<StreamingResponseBody> getConfig( public ResponseEntity<StreamingResponseBody> getConfig(
@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) {
final Principal principal) {
final Long institutionId = getInstitutionId(principal); final StreamingResponseBody stream = out -> {
final StreamingResponseBody stream = out -> this.examSessionService.streamDefaultExamConfig( try {
institutionId, this.examSessionService
.streamDefaultExamConfig(
connectionToken, connectionToken,
out); 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); return new ResponseEntity<>(stream, HttpStatus.OK);
} }
@RequestMapping( @RequestMapping(
path = API.EXAM_API_PING_ENDPOINT, path = API.EXAM_API_PING_ENDPOINT,
method = RequestMethod.PUT, method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE) produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public PingResponse ping( 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_TIMESTAMP, required = true) final long timestamp,
@RequestParam(name = API.EXAM_API_PING_NUMBER, required = false) final int pingNumber) { @RequestParam(name = API.EXAM_API_PING_NUMBER, required = false) final int pingNumber) {
@ -272,9 +284,9 @@ public class ExamAPI_V1_Controller {
@RequestMapping( @RequestMapping(
path = API.EXAM_API_EVENT_ENDPOINT, path = API.EXAM_API_EVENT_ENDPOINT,
method = RequestMethod.POST, method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public void event( 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) { @RequestBody(required = true) final ClientEvent event) {
this.sebClientConnectionService.notifyClientEvent(connectionToken, 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; package ch.ethz.seb.sebserver.webservice.integration.api.exam;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; 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.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Collections; 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.Before;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito; 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.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean; 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.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.client.BaseClientDetails; 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.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc; 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.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; 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.SEBServer;
import ch.ethz.seb.sebserver.WebSecurityConfig; 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.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebClientConfigService; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebClientConfigService;
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.AdminAPIClientDetails; import ch.ethz.seb.sebserver.webservice.weblayer.oauth.AdminAPIClientDetails;
@ -66,6 +75,9 @@ public abstract class ExamAPIIntegrationTester {
protected MockMvc mockMvc; protected MockMvc mockMvc;
@Autowired
protected CacheManager cacheManager;
@MockBean @MockBean
public WebClientDetailsService webClientDetailsService; public WebClientDetailsService webClientDetailsService;
@ -75,6 +87,12 @@ public abstract class ExamAPIIntegrationTester {
.addFilter(this.springSecurityFilterChain).build(); .addFilter(this.springSecurityFilterChain).build();
Mockito.when(this.webClientDetailsService.loadClientByClientId(Mockito.anyString())).thenReturn( Mockito.when(this.webClientDetailsService.loadClientByClientId(Mockito.anyString())).thenReturn(
getForExamClientAPI()); getForExamClientAPI());
// clear all caches before a test
this.cacheManager.getCacheNames()
.stream()
.map(name -> this.cacheManager.getCache(name))
.forEach(cache -> cache.clear());
} }
protected ClientDetails getForExamClientAPI() { protected ClientDetails getForExamClientAPI() {
@ -103,9 +121,9 @@ public abstract class ExamAPIIntegrationTester {
final ResultActions result = this.mockMvc.perform(post("/oauth/token") final ResultActions result = this.mockMvc.perform(post("/oauth/token")
.params(params) .params(params)
.with(httpBasic(clientId, clientSecret)) .with(httpBasic(clientId, clientSecret))
.accept("application/json;charset=UTF-8")) .accept(MediaType.APPLICATION_JSON_UTF8_VALUE))
.andExpect(status().isOk()) .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(); final String resultString = result.andReturn().getResponse().getContentAsString();
@ -113,6 +131,152 @@ public abstract class ExamAPIIntegrationTester {
return jsonParser.parseMap(resultString).get("access_token").toString(); 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 @Autowired
AdminAPIClientDetails adminClientDetails; AdminAPIClientDetails adminClientDetails;
@Autowired @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.endpoint.v1=${sebserver.webservice.api.exam.endpoint}/v1
sebserver.webservice.api.exam.accessTokenValiditySeconds=1800 sebserver.webservice.api.exam.accessTokenValiditySeconds=1800
sebserver.webservice.api.exam.refreshTokenValiditySeconds=-1 sebserver.webservice.api.exam.refreshTokenValiditySeconds=-1
sebserver.webservice.internalSecret=TO_SET
sebserver.webservice.api.redirect.unauthorized=none sebserver.webservice.api.redirect.unauthorized=none
# comma separated list of known possible OpenEdX API access token request endpoints # comma separated list of known possible OpenEdX API access token request endpoints
sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token