diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessage.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessage.java index 1a47c15a..010252a5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessage.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessage.java @@ -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) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialService.java index 164641dd..40b224bf 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialService.java @@ -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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java index c29b0644..c77bc503 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java @@ -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(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java index ddad7435..123f8260 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java @@ -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 { - Result 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> 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 byConnectionToken(String connectionToken); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java index 93730344..1be1d520 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java @@ -16,8 +16,21 @@ public interface ExamConfigurationMapDAO extends EntityDAO, BulkActionSupportDAO { + /** 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 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 getUserConfigurationIdForExam(final Long examId, final String userId); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java index c174ac22..5ece1948 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java @@ -110,6 +110,24 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { }); } + @Override + @Transactional(readOnly = true) + public Result> 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 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 byConnectionToken( - final Long institutionId, - final String connectionToken) { - - return Result.tryCatch(() -> { - final List 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 byConnectionToken(final String connectionToken) { return Result.tryCatch(() -> { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java index 317c9835..1051f31b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java @@ -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) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java index 03397173..06ae0e94 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java @@ -130,7 +130,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { @Transactional(readOnly = true) public Result 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 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())); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebExamConfigService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebExamConfigService.java index 3380c300..906569a3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebExamConfigService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebExamConfigService.java @@ -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 getDefaultConfigurationIdForExam(Long examId); - - Result 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); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java index a53ec8c5..9eabda55 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java @@ -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 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(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java index afd65fb7..09f21728 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java @@ -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 getDefaultConfigurationIdForExam(final Long examId) { return this.examConfigurationMapDAO.getDefaultConfigurationForExam(examId); } - @Override public Result 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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/converter/IntegerConverter.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/converter/IntegerConverter.java index 40d0ad88..255ddcc6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/converter/IntegerConverter.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/converter/IntegerConverter.java @@ -65,5 +65,4 @@ public class IntegerConverter implements XMLValueConverter { } } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/EventHandlingStrategy.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/EventHandlingStrategy.java index 0facec23..b4fd59b0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/EventHandlingStrategy.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/EventHandlingStrategy.java @@ -20,4 +20,8 @@ public interface EventHandlingStrategy extends Consumer { 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(); + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java index ab379b9e..6f60f046 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java @@ -39,6 +39,10 @@ public interface ExamSessionService { * happened. */ Result> 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); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SebClientConnectionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SebClientConnectionService.java index 292bcaec..22ed640b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SebClientConnectionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SebClientConnectionService.java @@ -55,8 +55,8 @@ public interface SebClientConnectionService { Result 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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractClientIndicator.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractClientIndicator.java index 6913f891..3b451c50 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractClientIndicator.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractClientIndicator.java @@ -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()); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractPingIndicator.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractPingIndicator.java index 10d9132f..23cc691d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractPingIndicator.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractPingIndicator.java @@ -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; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AsyncBatchEventSaveStrategy.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AsyncBatchEventSaveStrategy.java index 2a0f821e..2e2b0cb6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AsyncBatchEventSaveStrategy.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AsyncBatchEventSaveStrategy.java @@ -62,6 +62,7 @@ public class AsyncBatchEventSaveStrategy implements EventHandlingStrategy { private final BlockingDeque 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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientIndicatorFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientIndicatorFactory.java index fd183323..9a004755 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientIndicatorFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientIndicatorFactory.java @@ -52,6 +52,10 @@ public class ClientIndicatorFactory { public Collection createFor(final ClientConnection clientConnection) { final List result = new ArrayList<>(); + if (clientConnection.examId == null) { + return result; + } + try { final Collection examIndicators = this.indicatorDAO diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java index 92450035..8ff4b79c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java @@ -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); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java index cc0110a7..1fdd9508 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java @@ -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)); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SebClientConnectionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SebClientConnectionServiceImpl.java index ef217631..dc79dfab 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SebClientConnectionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SebClientConnectionServiceImpl.java @@ -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 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; } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SingleEventSaveStrategy.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SingleEventSaveStrategy.java index b11f6e5a..4833ce56 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SingleEventSaveStrategy.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SingleEventSaveStrategy.java @@ -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; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java index bfe0f5fb..a95760c5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java @@ -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 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); diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIAccessTokenRequestTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIAccessTokenRequestTest.java new file mode 100644 index 00000000..f3f25d4d --- /dev/null +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIAccessTokenRequestTest.java @@ -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); + } + +} diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java index 8290ea19..2b2d2ca8 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java @@ -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 diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/SebConnectionTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/SebConnectionTest.java new file mode 100644 index 00000000..a76bd021 --- /dev/null +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/SebConnectionTest.java @@ -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 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 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 errorMessage = this.jsonMapper.readValue( + contentAsString, + new TypeReference>() { + }); + 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 errorMessage = this.jsonMapper.readValue( + contentAsString, + new TypeReference>() { + }); + 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 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 errorMessage = this.jsonMapper.readValue( + contentAsString, + new TypeReference>() { + }); + 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 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 errorMessage = this.jsonMapper.readValue( + contentAsString, + new TypeReference>() { + }); + 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 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 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 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 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 events = this.clientEventRecordMapper + .selectByExample() + .build() + .execute(); + assertTrue(events.isEmpty()); + } +} diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/SebExamConfigurationRequestTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/SebExamConfigurationRequestTest.java new file mode 100644 index 00000000..600983fc --- /dev/null +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/SebExamConfigurationRequestTest.java @@ -0,0 +1,95 @@ +/* + * 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 org.junit.Test; +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 ch.ethz.seb.sebserver.gbl.api.API; +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 SebExamConfigurationRequestTest extends ExamAPIIntegrationTester { + + private static final long EXAM_ID = 2L; + + @Test + @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) + public void testGetExamConfigOnAFullyEstablishedConnection() 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 establishedConnection = super.establishConnection( + accessToken, + connectionToken, + EXAM_ID, + "userSessionId"); + + // check correct response + assertTrue(HttpStatus.OK.value() == establishedConnection.getStatus()); + + // try to download Exam Configuration + final MockHttpServletResponse configResponse = super.getExamConfig( + accessToken, + connectionToken); + + // check correct response + assertTrue(HttpStatus.OK.value() == configResponse.getStatus()); + + final String contentAsString = configResponse.getContentAsString(); + assertEquals( + "allowAudioCaptureallowBrowsingBackForwardallowDictionaryLookupallowDisplayMirroringallowDownUploadsallowedDisplayBuiltinallowedDisplaysMaxNumber1allowFlashFullscreenallowPDFPlugInallowQuitallowScreenSharingallowSiriallowSpellCheckallowSpellCheckDictionaryda-DKen-AUen-GBen-USes-ESfr-FRpt-PTsv-SEsv-FIallowSwitchToApplicationsallowUserAppFolderInstallallowVideoCaptureallowVirtualMachineaudioControlEnabledaudioMuteaudioSetVolumeLevelaudioVolumeLevel25blockPopUpWindowsbrowserUserAgentbrowserUserAgentMac0browserUserAgentMacCustombrowserUserAgentWinDesktopMode0browserUserAgentWinDesktopModeCustombrowserUserAgentWinTouchMode0browserUserAgentWinTouchModeCustombrowserViewMode0browserWindowAllowReloadbrowserWindowTitleSuffixchooseFileToUploadPolicy0detectStoppedProcessdownloadAndOpenSebConfigdownloadDirectoryOSXdownloadDirectoryWindownloadPDFFilesenableAltEscenableAltF4enableAltMouseWheelenableAltTabenableAppSwitcherCheckenableBrowserWindowToolbarenableCtrlEscenableEscenableF1enableF10enableF11enableF12enableF2enableF3enableF4enableF5enableF6enableF7enableF8enableF9enableJavaenableJavaScriptenableLoggingenablePlugInsenablePrintScreenenablePrivateClipboardenableRightMouseenableSebBrowserenableStartMenuenableTouchExitenableZoomPageenableZoomTextexitKey12exitKey210exitKey35forceAppFolderInstallhashedAdminPasswordhashedQuitPasswordhideBrowserWindowToolbarignoreExitKeysinsideSebEnableChangeAPasswordinsideSebEnableEaseOfAccessinsideSebEnableLockThisComputerinsideSebEnableLogOffinsideSebEnableNetworkConnectionSelectorinsideSebEnableShutDowninsideSebEnableStartTaskManagerinsideSebEnableSwitchUserinsideSebEnableVmWareClientShadecreateNewDesktopkillExplorerShelllogDirectoryOSX~/DocumentslogDirectoryWinmainBrowserWindowHeight100%mainBrowserWindowPositioning1mainBrowserWindowWidth100%minMacOSVersion0newBrowserWindowAllowReloadnewBrowserWindowByLinkBlockForeignnewBrowserWindowByLinkHeight100%newBrowserWindowByLinkPolicy2newBrowserWindowByLinkPositioning2newBrowserWindowByLinkWidth100%newBrowserWindowNavigationnewBrowserWindowShowReloadWarningopenDownloadsoriginatorVersionSEB_Server_0.3.0permittedProcessesprohibitedProcessesproxiesAutoConfigurationEnabledAutoConfigurationJavaScriptAutoConfigurationURLAutoDiscoveryEnabledExceptionsListExcludeSimpleHostnamesFTPEnableFTPPassiveFTPPasswordFTPPort21FTPProxyFTPRequiresPasswordFTPUsernameHTTPEnableHTTPPasswordHTTPPort80HTTPProxyHTTPRequiresPasswordHTTPSEnableHTTPSPasswordHTTPSPort443HTTPSProxyHTTPSRequiresPasswordHTTPSUsernameHTTPUsernameRTSPEnableRTSPPasswordRTSPPort1080RTSPProxyRTSPRequiresPasswordRTSPUsernameSOCKSEnableSOCKSPasswordSOCKSPort1080SOCKSProxySOCKSRequiresPasswordSOCKSUsernameproxySettingsPolicy0quitURLquitURLConfirmremoveBrowserProfileremoveLocalStoragerestartExamPasswordProtectedrestartExamTextrestartExamURLrestartExamUseStartURLsebConfigPurpose0sebServicePolicy2showInputLanguageshowMenuBarshowReloadButtonshowReloadWarningshowTaskBarshowTimetaskBarHeight40URLFilterEnableURLFilterEnableContentFilterURLFilterRuleszoomMode0", + contentAsString); + + // check cache + final Cache cache = this.cacheManager + .getCache(ExamSessionCacheService.CACHE_NAME_SEB_CONFIG_EXAM); + final ValueWrapper config = cache.get(EXAM_ID); + assertNotNull(config); + } + + @Test + @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) + public void testGetExamConfigOnConnectionNoExamIdSouldFail() 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); + + // try to download Exam Configuration + final MockHttpServletResponse configResponse = super.getExamConfig( + accessToken, + connectionToken); + + // check correct response + assertTrue(HttpStatus.OK.value() == configResponse.getStatus()); + final String contentAsString = configResponse.getContentAsString(); + assertEquals( + "{\"messageCode\":\"0\",\"systemMessage\":\"Generic error message\",\"details\":\"Illegal connection token. No active ClientConnection found for token\",\"attributes\":[]}", + contentAsString); + + } + +} diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index e3012f51..18b0c671 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -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