SEBSERV-62 testing and fixes
This commit is contained in:
		
							parent
							
								
									b314ca651f
								
							
						
					
					
						commit
						3b6e3d88e0
					
				
					 29 changed files with 1201 additions and 191 deletions
				
			
		|  | @ -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) { | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -8,13 +8,25 @@ | |||
| 
 | ||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.dao; | ||||
| 
 | ||||
| import java.util.Collection; | ||||
| 
 | ||||
| import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; | ||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | ||||
| 
 | ||||
| public interface ClientConnectionDAO extends EntityDAO<ClientConnection, ClientConnection> { | ||||
| 
 | ||||
|     Result<ClientConnection> byConnectionToken(Long institutionId, String connectionToken); | ||||
|     /** Get a list of all connection tokens of all connections (no matter what state) | ||||
|      * of an exam. | ||||
|      * | ||||
|      * @param examId The exam identifier | ||||
|      * @return list of all connection tokens of all connections (no matter what state) | ||||
|      *         of an exam */ | ||||
|     Result<Collection<String>> getConnectionTokens(Long examId); | ||||
| 
 | ||||
|     /** Get a ClientConnection for a specified token. | ||||
|      * | ||||
|      * @param connectionToken the connection token | ||||
|      * @return Result refer to ClientConnection or refer to a error if happened */ | ||||
|     Result<ClientConnection> byConnectionToken(String connectionToken); | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -16,8 +16,21 @@ public interface ExamConfigurationMapDAO extends | |||
|         EntityDAO<ExamConfigurationMap, ExamConfigurationMap>, | ||||
|         BulkActionSupportDAO<ExamConfigurationMap> { | ||||
| 
 | ||||
|     /** Get the ConfigurationNode identifier of the default Exam Configuration of | ||||
|      * the Exam with specified identifier. | ||||
|      * | ||||
|      * @param examId The Exam identifier | ||||
|      * @return ConfigurationNode identifier of the default Exam Configuration of | ||||
|      *         the Exam with specified identifier */ | ||||
|     public Result<Long> getDefaultConfigurationForExam(Long examId); | ||||
| 
 | ||||
|     /** Get the ConfigurationNode identifier of the Exam Configuration of | ||||
|      * the Exam for a specified user identifier. | ||||
|      * | ||||
|      * @param examId The Exam identifier | ||||
|      * @param userId the user identifier | ||||
|      * @return ConfigurationNode identifier of the Exam Configuration of | ||||
|      *         the Exam for a specified user identifier */ | ||||
|     public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId); | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -110,6 +110,24 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { | |||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     @Transactional(readOnly = true) | ||||
|     public Result<Collection<String>> getConnectionTokens(final Long examId) { | ||||
|         return Result.tryCatch(() -> { | ||||
|             return this.clientConnectionRecordMapper | ||||
|                     .selectByExample() | ||||
|                     .where( | ||||
|                             ClientConnectionRecordDynamicSqlSupport.examId, | ||||
|                             SqlBuilder.isEqualTo(examId)) | ||||
|                     .build() | ||||
|                     .execute() | ||||
|                     .stream() | ||||
|                     .map(ClientConnectionRecord::getConnectionToken) | ||||
|                     .filter(StringUtils::isNotBlank) | ||||
|                     .collect(Collectors.toList()); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     @Transactional | ||||
|     public Result<ClientConnection> createNew(final ClientConnection data) { | ||||
|  | @ -140,10 +158,10 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { | |||
|             final ClientConnectionRecord updateRecord = new ClientConnectionRecord( | ||||
|                     data.id, | ||||
|                     null, | ||||
|                     null, | ||||
|                     data.examId, | ||||
|                     data.status != null ? data.status.name() : null, | ||||
|                     data.connectionToken, | ||||
|                     null, | ||||
|                     data.userSessionId, | ||||
|                     data.clientAddress, | ||||
|                     data.virtualClientAddress); | ||||
| 
 | ||||
|  | @ -183,37 +201,6 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { | |||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     @Transactional(readOnly = true) | ||||
|     public Result<ClientConnection> byConnectionToken( | ||||
|             final Long institutionId, | ||||
|             final String connectionToken) { | ||||
| 
 | ||||
|         return Result.tryCatch(() -> { | ||||
|             final List<ClientConnectionRecord> list = this.clientConnectionRecordMapper | ||||
|                     .selectByExample() | ||||
|                     .where( | ||||
|                             ClientConnectionRecordDynamicSqlSupport.institutionId, | ||||
|                             SqlBuilder.isEqualTo(institutionId)) | ||||
|                     .and( | ||||
|                             ClientConnectionRecordDynamicSqlSupport.connectionToken, | ||||
|                             SqlBuilder.isEqualTo(connectionToken)) | ||||
|                     .build() | ||||
|                     .execute(); | ||||
| 
 | ||||
|             if (list.isEmpty()) { | ||||
|                 throw new ResourceNotFoundException(EntityType.CLIENT_CONNECTION, "connectionToken"); | ||||
|             } | ||||
| 
 | ||||
|             if (list.size() > 1) { | ||||
|                 throw new IllegalStateException("Only one ClientConnection expected but there are: " + list.size()); | ||||
|             } | ||||
| 
 | ||||
|             return list.get(0); | ||||
|         }) | ||||
|                 .flatMap(ClientConnectionDAOImpl::toDomainModel); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Result<ClientConnection> byConnectionToken(final String connectionToken) { | ||||
|         return Result.tryCatch(() -> { | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -130,7 +130,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { | |||
|     @Transactional(readOnly = true) | ||||
|     public Result<Long> getDefaultConfigurationForExam(final Long examId) { | ||||
|         return Result.tryCatch(() -> this.examConfigurationMapRecordMapper | ||||
|                 .selectIdsByExample() | ||||
|                 .selectByExample() | ||||
|                 .where( | ||||
|                         ExamConfigurationMapRecordDynamicSqlSupport.examId, | ||||
|                         SqlBuilder.isEqualTo(examId)) | ||||
|  | @ -140,13 +140,14 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { | |||
|                 .build() | ||||
|                 .execute() | ||||
|                 .stream() | ||||
|                 .map(mapping -> mapping.getConfigurationNodeId()) | ||||
|                 .collect(Utils.toSingleton())); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) { | ||||
|         return Result.tryCatch(() -> this.examConfigurationMapRecordMapper | ||||
|                 .selectIdsByExample() | ||||
|                 .selectByExample() | ||||
|                 .where( | ||||
|                         ExamConfigurationMapRecordDynamicSqlSupport.examId, | ||||
|                         SqlBuilder.isEqualTo(examId)) | ||||
|  | @ -156,6 +157,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { | |||
|                 .build() | ||||
|                 .execute() | ||||
|                 .stream() | ||||
|                 .map(mapping -> mapping.getConfigurationNodeId()) | ||||
|                 .collect(Utils.toSingleton())); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -13,7 +13,6 @@ import java.io.OutputStream; | |||
| import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException; | ||||
| import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues; | ||||
| import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; | ||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | ||||
| 
 | ||||
| /** The base interface and service for all SEB Exam Configuration related functionality. */ | ||||
| public interface SebExamConfigService { | ||||
|  | @ -32,10 +31,6 @@ public interface SebExamConfigService { | |||
|      * @throws FieldValidationException on validation exception */ | ||||
|     void validate(ConfigurationTableValues tableValue) throws FieldValidationException; | ||||
| 
 | ||||
|     Result<Long> getDefaultConfigurationIdForExam(Long examId); | ||||
| 
 | ||||
|     Result<Long> getUserConfigurationIdForExam(Long examId, String userId); | ||||
| 
 | ||||
|     /** Used to export a specified SEB Exam Configuration as plain XML | ||||
|      * This exports the values of the follow-up configuration defined by a given | ||||
|      * ConfigurationNode (configurationNodeId) | ||||
|  | @ -45,21 +40,26 @@ public interface SebExamConfigService { | |||
|      * @param configurationNodeId the identifier of the ConfigurationNode to export */ | ||||
|     void exportPlainXML(OutputStream out, Long institutionId, Long configurationNodeId); | ||||
| 
 | ||||
|     /** Used to export a SEB Exam Configuration within its defined Configuration Exam Mapping. | ||||
|     /** Used to export the default SEB Exam Configuration for a given exam identifier. | ||||
|      * either with encryption if defined or as plain text within the SEB Configuration format | ||||
|      * as described here: https://www.safeexambrowser.org/developer/seb-file-format.html | ||||
|      * | ||||
|      * @param out The output stream to write the export data to | ||||
|      * @param configExamMappingId The identifier of the Exam Configuration Mapping */ | ||||
|     void exportForExam(OutputStream out, Long configExamMappingId); | ||||
|      * @param institutionId The identifier of the institution of the requesting user | ||||
|      * @param examId the exam identifier */ | ||||
|     default Long exportForExam(final OutputStream out, final Long institutionId, final Long examId) { | ||||
|         return exportForExam(out, institutionId, examId, null); | ||||
|     } | ||||
| 
 | ||||
|     /** Used to export the default SEB Exam Configuration for a given exam identifier. | ||||
|      * either with encryption if defined or as plain text within the SEB Configuration format | ||||
|      * as described here: https://www.safeexambrowser.org/developer/seb-file-format.html | ||||
|      * | ||||
|      * @param out The output stream to write the export data to | ||||
|      * @param examId the exam identifier */ | ||||
|     void exportDefaultForExam(OutputStream out, Long examId); | ||||
|      * @param institutionId The identifier of the institution of the requesting user | ||||
|      * @param examId the exam identifier | ||||
|      * @param userId the user identifier if a specific user based configuration shall be exported */ | ||||
|     Long exportForExam(OutputStream out, Long institutionId, Long examId, String userId); | ||||
| 
 | ||||
|     /** TODO */ | ||||
|     String generateConfigKey(Long configurationNodeId); | ||||
|  |  | |||
|  | @ -58,7 +58,11 @@ public class ExamConfigIO { | |||
|     } | ||||
| 
 | ||||
|     @Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) | ||||
|     void exportPlainXML(final OutputStream out, final Long institutionId, final Long configurationNodeId) { | ||||
|     void exportPlainXML( | ||||
|             final OutputStream out, | ||||
|             final Long institutionId, | ||||
|             final Long configurationNodeId) { | ||||
| 
 | ||||
|         // get all defined root configuration attributes | ||||
|         final Map<Long, ConfigurationAttribute> attributes = this.configurationAttributeDAO.getAllRootAttributes() | ||||
|                 .getOrThrow() | ||||
|  | @ -112,7 +116,7 @@ public class ExamConfigIO { | |||
|             out.write(Constants.XML_PLIST_END_UTF_8); | ||||
|             out.flush(); | ||||
| 
 | ||||
|         } catch (final IOException e) { | ||||
|         } catch (final Exception e) { | ||||
|             log.error("Unexpected error while trying to write SEB Exam Configuration XML to output stream: ", e); | ||||
|             try { | ||||
|                 out.flush(); | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ import java.io.PipedOutputStream; | |||
| import java.util.Collection; | ||||
| 
 | ||||
| import org.apache.commons.io.IOUtils; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.springframework.context.annotation.Lazy; | ||||
|  | @ -96,13 +97,16 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { | |||
|             pout = new PipedOutputStream(); | ||||
|             pin = new PipedInputStream(pout); | ||||
| 
 | ||||
|             this.examConfigIO.exportPlainXML(pout, institutionId, configurationNodeId); | ||||
|             this.examConfigIO.exportPlainXML( | ||||
|                     pout, | ||||
|                     institutionId, | ||||
|                     configurationNodeId); | ||||
| 
 | ||||
|             IOUtils.copyLarge(pin, out); | ||||
| 
 | ||||
|             pin.close(); | ||||
|             pout.flush(); | ||||
|             pout.close(); | ||||
|             pin.close(); | ||||
| 
 | ||||
|         } catch (final IOException e) { | ||||
|             log.error("Error while stream plain text SEB clonfiguration data: ", e); | ||||
|  | @ -127,26 +131,32 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { | |||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Result<Long> getDefaultConfigurationIdForExam(final Long examId) { | ||||
|         return this.examConfigurationMapDAO.getDefaultConfigurationForExam(examId); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) { | ||||
|         return this.examConfigurationMapDAO.getUserConfigurationIdForExam(examId, userId); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void exportForExam(final OutputStream out, final Long configExamMappingId) { | ||||
|         // TODO Auto-generated method stub | ||||
|     public Long exportForExam( | ||||
|             final OutputStream out, | ||||
|             final Long institutionId, | ||||
|             final Long examId, | ||||
|             final String userId) { | ||||
| 
 | ||||
|     } | ||||
|         final Long configurationNodeId = (StringUtils.isBlank(userId)) | ||||
|                 ? getDefaultConfigurationIdForExam(examId) | ||||
|                         .getOrThrow() | ||||
|                 : getUserConfigurationIdForExam(examId, userId) | ||||
|                         .getOrThrow(); | ||||
| 
 | ||||
|     @Override | ||||
|     public void exportDefaultForExam(final OutputStream out, final Long examId) { | ||||
|         // TODO Auto-generated method stub | ||||
|         // TODO add header, zip and encrypt if needed | ||||
| 
 | ||||
|         this.exportPlainXML(out, institutionId, configurationNodeId); | ||||
| 
 | ||||
|         return configurationNodeId; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  |  | |||
|  | @ -65,5 +65,4 @@ public class IntegerConverter implements XMLValueConverter { | |||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -20,4 +20,8 @@ public interface EventHandlingStrategy extends Consumer<ClientEvent> { | |||
|     String EVENT_CONSUMER_STRATEGY_SINGLE_EVENT_STORE = "SINGLE_EVENT_STORE_STRATEGY"; | ||||
|     String EVENT_CONSUMER_STRATEGY_ASYNC_BATCH_STORE = "ASYNC_BATCH_STORE_STRATEGY"; | ||||
| 
 | ||||
|     void enable(); | ||||
| 
 | ||||
|     void disable(); | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -39,6 +39,10 @@ public interface ExamSessionService { | |||
|      *         happened. */ | ||||
|     Result<Collection<Exam>> getRunningExamsForInstitution(Long institutionId); | ||||
| 
 | ||||
|     void streamDefaultExamConfig(Long institutionId, String connectionToken, OutputStream out); | ||||
|     /** Streams the default SEB Exam Configuration to a ClientConnection with given connectionToken. | ||||
|      * | ||||
|      * @param connectionToken The connection token that identifiers the ClientConnection | ||||
|      * @param out The OutputStream to stream the data to */ | ||||
|     void streamDefaultExamConfig(String connectionToken, OutputStream out); | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -55,8 +55,8 @@ public interface SebClientConnectionService { | |||
|     Result<ClientConnection> updateClientConnection( | ||||
|             String connectionToken, | ||||
|             Long institutionId, | ||||
|             String clientAddress, | ||||
|             Long examId, | ||||
|             String clientAddress, | ||||
|             String userSessionId); | ||||
| 
 | ||||
|     /** This is used to establish a already created ClientConnection and set it to sate: ESTABLISHED | ||||
|  |  | |||
|  | @ -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()); | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -62,6 +62,7 @@ public class AsyncBatchEventSaveStrategy implements EventHandlingStrategy { | |||
| 
 | ||||
|     private final BlockingDeque<ClientEvent> eventQueue = new LinkedBlockingDeque<>(); | ||||
|     private boolean workersRunning = false; | ||||
|     private boolean enabled = false; | ||||
| 
 | ||||
|     public AsyncBatchEventSaveStrategy( | ||||
|             final SqlSessionFactory sqlSessionFactory, | ||||
|  | @ -75,9 +76,22 @@ public class AsyncBatchEventSaveStrategy implements EventHandlingStrategy { | |||
|         this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void enable() { | ||||
|         this.enabled = true; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void disable() { | ||||
|         this.enabled = false; | ||||
|     } | ||||
| 
 | ||||
|     @EventListener(ApplicationReadyEvent.class) | ||||
|     protected void recover() { | ||||
|         runWorkers(); | ||||
|         if (this.enabled) { | ||||
|             runWorkers(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  |  | |||
|  | @ -52,6 +52,10 @@ public class ClientIndicatorFactory { | |||
|     public Collection<ClientIndicator> createFor(final ClientConnection clientConnection) { | ||||
|         final List<ClientIndicator> result = new ArrayList<>(); | ||||
| 
 | ||||
|         if (clientConnection.examId == null) { | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
| 
 | ||||
|             final Collection<Indicator> examIndicators = this.indicatorDAO | ||||
|  |  | |||
|  | @ -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); | ||||
|         } | ||||
|  |  | |||
|  | @ -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)); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -62,6 +62,7 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | |||
|         this.eventHandlingStrategy = applicationContext.getBean( | ||||
|                 eventHandlingStrategyProperty, | ||||
|                 EventHandlingStrategy.class); | ||||
|         this.eventHandlingStrategy.enable(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -90,15 +91,23 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | |||
|                     null, | ||||
|                     institutionId, | ||||
|                     examId, | ||||
|                     ClientConnection.ConnectionStatus.CONNECTION_REQUESTED, | ||||
|                     ConnectionStatus.CONNECTION_REQUESTED, | ||||
|                     connectionToken, | ||||
|                     null, | ||||
|                     clientAddress, | ||||
|                     null)) | ||||
|                     .getOrThrow(); | ||||
| 
 | ||||
|             if (log.isDebugEnabled()) { | ||||
|                 log.debug("New ClientConnection created: {}", clientConnection); | ||||
|             // load client connection data into cache | ||||
|             final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService | ||||
|                     .getActiveClientConnection(connectionToken); | ||||
| 
 | ||||
|             if (activeClientConnection == null) { | ||||
|                 log.warn("Failed to load ClientConnectionDataInternal into cache on update"); | ||||
|             } else { | ||||
|                 if (log.isDebugEnabled()) { | ||||
|                     log.debug("New ClientConnection created: {}", clientConnection); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return clientConnection; | ||||
|  | @ -109,8 +118,8 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | |||
|     public Result<ClientConnection> updateClientConnection( | ||||
|             final String connectionToken, | ||||
|             final Long institutionId, | ||||
|             final String clientAddress, | ||||
|             final Long examId, | ||||
|             final String clientAddress, | ||||
|             final String userSessionId) { | ||||
| 
 | ||||
|         return Result.tryCatch(() -> { | ||||
|  | @ -131,9 +140,7 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | |||
| 
 | ||||
|             checkExamRunning(examId); | ||||
| 
 | ||||
|             final ClientConnection clientConnection = getClientConnection( | ||||
|                     connectionToken, | ||||
|                     institutionId); | ||||
|             final ClientConnection clientConnection = getClientConnection(connectionToken); | ||||
| 
 | ||||
|             checkInstitutionalIntegrity( | ||||
|                     institutionId, | ||||
|  | @ -156,7 +163,15 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | |||
|                             virtualClientAddress)) | ||||
|                     .getOrThrow(); | ||||
| 
 | ||||
|             if (log.isDebugEnabled()) { | ||||
|             // evict cached ClientConnection | ||||
|             this.examSessionCacheService.evictClientConnection(connectionToken); | ||||
|             // and load updated ClientConnection into cache | ||||
|             final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService | ||||
|                     .getActiveClientConnection(connectionToken); | ||||
| 
 | ||||
|             if (activeClientConnection == null) { | ||||
|                 log.warn("Failed to load ClientConnectionDataInternal into cache on update"); | ||||
|             } else if (log.isDebugEnabled()) { | ||||
|                 log.debug("SEB client connection, successfully updated ClientConnection: {}", | ||||
|                         updatedClientConnection); | ||||
|             } | ||||
|  | @ -192,9 +207,7 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | |||
| 
 | ||||
|             checkExamRunning(examId); | ||||
| 
 | ||||
|             final ClientConnection clientConnection = getClientConnection( | ||||
|                     connectionToken, | ||||
|                     institutionId); | ||||
|             final ClientConnection clientConnection = getClientConnection(connectionToken); | ||||
| 
 | ||||
|             checkInstitutionalIntegrity( | ||||
|                     institutionId, | ||||
|  | @ -217,42 +230,40 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | |||
|                     clientConnection.id, | ||||
|                     null, | ||||
|                     examId, | ||||
|                     ClientConnection.ConnectionStatus.ESTABLISHED, | ||||
|                     ConnectionStatus.ESTABLISHED, | ||||
|                     null, | ||||
|                     userSessionId, | ||||
|                     null, | ||||
|                     virtualClientAddress); | ||||
| 
 | ||||
|             // ClientConnection integrity | ||||
|             if (establishedClientConnection.institutionId == null || | ||||
|             if (clientConnection.institutionId == null || | ||||
|                     clientConnection.connectionToken == null || | ||||
|                     establishedClientConnection.examId == null || | ||||
|                     establishedClientConnection.clientAddress == null || | ||||
|                     establishedClientConnection.connectionToken == null) { | ||||
|                     clientConnection.clientAddress == null || | ||||
|                     establishedClientConnection.status != ConnectionStatus.ESTABLISHED) { | ||||
| 
 | ||||
|                 log.error("ClientConnection integrity violation: {}", establishedClientConnection); | ||||
|                 throw new IllegalStateException("ClientConnection integrity violation: " + establishedClientConnection); | ||||
|                 log.error("ClientConnection integrity violation, clientConnection: {}, establishedClientConnection: {}", | ||||
|                         clientConnection, | ||||
|                         establishedClientConnection); | ||||
|                 throw new IllegalStateException("ClientConnection integrity violation"); | ||||
|             } | ||||
| 
 | ||||
|             final ClientConnection updatedClientConnection = this.clientConnectionDAO | ||||
|                     .save(establishedClientConnection) | ||||
|                     .getOrThrow(); | ||||
| 
 | ||||
|             if (updatedClientConnection.status == ConnectionStatus.ESTABLISHED) { | ||||
|                 // load into cache... | ||||
|                 final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService | ||||
|                         .getActiveClientConnection(updatedClientConnection.connectionToken); | ||||
|             // evict cached ClientConnection | ||||
|             this.examSessionCacheService.evictClientConnection(connectionToken); | ||||
|             // and load updated ClientConnection into cache | ||||
|             final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService | ||||
|                     .getActiveClientConnection(connectionToken); | ||||
| 
 | ||||
|                 if (activeClientConnection == null) { | ||||
|                     log.warn("Unable to access and cache ClientConnection"); | ||||
|                 } | ||||
| 
 | ||||
|                 if (log.isDebugEnabled()) { | ||||
|                     log.debug("ClientConnection: {} successfully established", clientConnection); | ||||
|                 } | ||||
|             } else { | ||||
|                 if (log.isDebugEnabled()) { | ||||
|                     log.debug("ClientConnection: {} updated", clientConnection); | ||||
|                 } | ||||
|             if (activeClientConnection == null) { | ||||
|                 log.warn("Failed to load ClientConnectionDataInternal into cache on update"); | ||||
|             } else if (log.isDebugEnabled()) { | ||||
|                 log.debug("SEB client connection, successfully established ClientConnection: {}", | ||||
|                         updatedClientConnection); | ||||
|             } | ||||
| 
 | ||||
|             return updatedClientConnection; | ||||
|  | @ -278,18 +289,14 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | |||
|             } | ||||
| 
 | ||||
|             final ClientConnection clientConnection = this.clientConnectionDAO | ||||
|                     .byConnectionToken(institutionId, connectionToken) | ||||
|                     .byConnectionToken(connectionToken) | ||||
|                     .getOrThrow(); | ||||
| 
 | ||||
|             // evict ClientConnection from cache | ||||
|             this.examSessionCacheService | ||||
|                     .evictClientConnection(clientConnection.connectionToken); | ||||
| 
 | ||||
|             final ClientConnection updatedClientConnection = this.clientConnectionDAO.save(new ClientConnection( | ||||
|                     clientConnection.id, | ||||
|                     null, | ||||
|                     null, | ||||
|                     ClientConnection.ConnectionStatus.CLOSED, | ||||
|                     ConnectionStatus.CLOSED, | ||||
|                     null, | ||||
|                     null, | ||||
|                     null, | ||||
|  | @ -300,6 +307,11 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | |||
|                         clientConnection); | ||||
|             } | ||||
| 
 | ||||
|             // evict cached ClientConnection | ||||
|             this.examSessionCacheService.evictClientConnection(connectionToken); | ||||
|             // and load updated ClientConnection into cache | ||||
|             this.examSessionCacheService.getActiveClientConnection(connectionToken); | ||||
| 
 | ||||
|             return updatedClientConnection; | ||||
|         }); | ||||
| 
 | ||||
|  | @ -326,12 +338,27 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | |||
|             final String connectionToken, | ||||
|             final ClientEvent event) { | ||||
| 
 | ||||
|         this.eventHandlingStrategy.accept(event); | ||||
| 
 | ||||
|         final ClientConnectionDataInternal activeClientConnection = | ||||
|                 this.examSessionCacheService.getActiveClientConnection(connectionToken); | ||||
| 
 | ||||
|         if (activeClientConnection != null) { | ||||
|             if (activeClientConnection.clientConnection.status != ConnectionStatus.ESTABLISHED) { | ||||
|                 throw new IllegalStateException("ClientConnection is not fully established or closed"); | ||||
|             } | ||||
| 
 | ||||
|             // store event | ||||
|             this.eventHandlingStrategy.accept( | ||||
|                     (event.connectionId != null) | ||||
|                             ? event | ||||
|                             : new ClientEvent( | ||||
|                                     null, | ||||
|                                     activeClientConnection.getConnectionId(), | ||||
|                                     event.eventType, | ||||
|                                     event.timestamp, | ||||
|                                     event.numValue, | ||||
|                                     event.text)); | ||||
| 
 | ||||
|             // update indicators | ||||
|             activeClientConnection.getindicatorMapping(event.eventType) | ||||
|                     .stream() | ||||
|                     .forEach(indicator -> indicator.notifyValueChange(event)); | ||||
|  | @ -344,9 +371,9 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private ClientConnection getClientConnection(final String connectionToken, final Long institutionId) { | ||||
|     private ClientConnection getClientConnection(final String connectionToken) { | ||||
|         final ClientConnection clientConnection = this.clientConnectionDAO | ||||
|                 .byConnectionToken(institutionId, connectionToken) | ||||
|                 .byConnectionToken(connectionToken) | ||||
|                 .getOrThrow(); | ||||
|         return clientConnection; | ||||
|     } | ||||
|  |  | |||
|  | @ -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; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ import org.springframework.http.MediaType; | |||
| import org.springframework.http.ResponseEntity; | ||||
| import org.springframework.util.MultiValueMap; | ||||
| import org.springframework.web.bind.annotation.RequestBody; | ||||
| import org.springframework.web.bind.annotation.RequestHeader; | ||||
| import org.springframework.web.bind.annotation.RequestMapping; | ||||
| import org.springframework.web.bind.annotation.RequestMethod; | ||||
| import org.springframework.web.bind.annotation.RequestParam; | ||||
|  | @ -31,6 +32,8 @@ import org.springframework.web.bind.annotation.RestController; | |||
| import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; | ||||
| 
 | ||||
| import ch.ethz.seb.sebserver.gbl.api.API; | ||||
| import ch.ethz.seb.sebserver.gbl.api.APIMessage; | ||||
| import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | ||||
| import ch.ethz.seb.sebserver.gbl.api.POSTMapper; | ||||
| import ch.ethz.seb.sebserver.gbl.model.exam.Exam; | ||||
| import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; | ||||
|  | @ -38,6 +41,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; | |||
| import ch.ethz.seb.sebserver.gbl.model.session.PingResponse; | ||||
| import ch.ethz.seb.sebserver.gbl.model.session.RunningExam; | ||||
| import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||
| import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; | ||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SebClientConfigDAO; | ||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; | ||||
|  | @ -54,17 +58,20 @@ public class ExamAPI_V1_Controller { | |||
|     private final ExamSessionService examSessionService; | ||||
|     private final SebClientConnectionService sebClientConnectionService; | ||||
|     private final SebClientConfigDAO sebClientConfigDAO; | ||||
|     private final JSONMapper jsonMapper; | ||||
| 
 | ||||
|     protected ExamAPI_V1_Controller( | ||||
|             final ExamDAO examDAO, | ||||
|             final ExamSessionService examSessionService, | ||||
|             final SebClientConnectionService sebClientConnectionService, | ||||
|             final SebClientConfigDAO sebClientConfigDAO) { | ||||
|             final SebClientConfigDAO sebClientConfigDAO, | ||||
|             final JSONMapper jsonMapper) { | ||||
| 
 | ||||
|         this.examDAO = examDAO; | ||||
|         this.examSessionService = examSessionService; | ||||
|         this.sebClientConnectionService = sebClientConnectionService; | ||||
|         this.sebClientConfigDAO = sebClientConfigDAO; | ||||
|         this.jsonMapper = jsonMapper; | ||||
|     } | ||||
| 
 | ||||
|     @RequestMapping( | ||||
|  | @ -143,8 +150,8 @@ public class ExamAPI_V1_Controller { | |||
|             method = RequestMethod.PATCH, | ||||
|             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) | ||||
|     public void handshakeUpdate( | ||||
|             @RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||
|             @RequestParam(name = API.EXAM_API_PARAM_EXAM_ID, required = false) final Long examId, | ||||
|             @RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||
|             @RequestParam(name = API.EXAM_API_USER_SESSION_ID, required = false) final String userSessionId, | ||||
|             final Principal principal, | ||||
|             final HttpServletRequest request) { | ||||
|  | @ -164,7 +171,7 @@ public class ExamAPI_V1_Controller { | |||
|                     remoteAddr); | ||||
|         } | ||||
| 
 | ||||
|         this.sebClientConnectionService.establishClientConnection( | ||||
|         this.sebClientConnectionService.updateClientConnection( | ||||
|                 connectionToken, | ||||
|                 institutionId, | ||||
|                 examId, | ||||
|  | @ -178,8 +185,8 @@ public class ExamAPI_V1_Controller { | |||
|             method = RequestMethod.PUT, | ||||
|             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) | ||||
|     public void handshakeEstablish( | ||||
|             @RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||
|             @RequestParam(name = API.EXAM_API_PARAM_EXAM_ID, required = false) final Long examId, | ||||
|             @RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||
|             @RequestParam(name = API.EXAM_API_USER_SESSION_ID, required = false) final String userSessionId, | ||||
|             final Principal principal, | ||||
|             final HttpServletRequest request) { | ||||
|  | @ -210,8 +217,8 @@ public class ExamAPI_V1_Controller { | |||
|             path = API.EXAM_API_HANDSHAKE_ENDPOINT, | ||||
|             method = RequestMethod.DELETE, | ||||
|             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) | ||||
|     public void handshakeEstablish( | ||||
|             @RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||
|     public void handshakeDelete( | ||||
|             @RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||
|             final Principal principal, | ||||
|             final HttpServletRequest request) { | ||||
| 
 | ||||
|  | @ -239,25 +246,30 @@ public class ExamAPI_V1_Controller { | |||
|             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, | ||||
|             produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) | ||||
|     public ResponseEntity<StreamingResponseBody> getConfig( | ||||
|             @RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||
|             final Principal principal) { | ||||
|             @RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken) { | ||||
| 
 | ||||
|         final Long institutionId = getInstitutionId(principal); | ||||
|         final StreamingResponseBody stream = out -> this.examSessionService.streamDefaultExamConfig( | ||||
|                 institutionId, | ||||
|                 connectionToken, | ||||
|                 out); | ||||
|         final StreamingResponseBody stream = out -> { | ||||
|             try { | ||||
|                 this.examSessionService | ||||
|                         .streamDefaultExamConfig( | ||||
|                                 connectionToken, | ||||
|                                 out); | ||||
|             } catch (final Exception e) { | ||||
|                 final APIMessage errorMessage = APIMessage.ErrorMessage.GENERIC.of(e.getMessage()); | ||||
|                 out.write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessage))); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         return new ResponseEntity<>(stream, HttpStatus.OK); | ||||
|     } | ||||
| 
 | ||||
|     @RequestMapping( | ||||
|             path = API.EXAM_API_PING_ENDPOINT, | ||||
|             method = RequestMethod.PUT, | ||||
|             method = RequestMethod.POST, | ||||
|             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, | ||||
|             produces = MediaType.APPLICATION_JSON_UTF8_VALUE) | ||||
|     public PingResponse ping( | ||||
|             @RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||
|             @RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||
|             @RequestParam(name = API.EXAM_API_PING_TIMESTAMP, required = true) final long timestamp, | ||||
|             @RequestParam(name = API.EXAM_API_PING_NUMBER, required = false) final int pingNumber) { | ||||
| 
 | ||||
|  | @ -272,9 +284,9 @@ public class ExamAPI_V1_Controller { | |||
|     @RequestMapping( | ||||
|             path = API.EXAM_API_EVENT_ENDPOINT, | ||||
|             method = RequestMethod.POST, | ||||
|             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) | ||||
|             consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) | ||||
|     public void event( | ||||
|             @RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||
|             @RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||
|             @RequestBody(required = true) final ClientEvent event) { | ||||
| 
 | ||||
|         this.sebClientConnectionService.notifyClientEvent(connectionToken, event); | ||||
|  |  | |||
|  | @ -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); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -0,0 +1,624 @@ | |||
| /* | ||||
|  * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) | ||||
|  * | ||||
|  * This Source Code Form is subject to the terms of the Mozilla Public | ||||
|  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||
|  */ | ||||
| 
 | ||||
| package ch.ethz.seb.sebserver.webservice.integration.api.exam; | ||||
| 
 | ||||
| import static org.junit.Assert.*; | ||||
| 
 | ||||
| import java.util.Collection; | ||||
| import java.util.List; | ||||
| 
 | ||||
| import org.junit.Test; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.cache.Cache; | ||||
| import org.springframework.cache.Cache.ValueWrapper; | ||||
| import org.springframework.http.HttpStatus; | ||||
| import org.springframework.mock.web.MockHttpServletResponse; | ||||
| import org.springframework.test.context.jdbc.Sql; | ||||
| 
 | ||||
| import com.fasterxml.jackson.core.type.TypeReference; | ||||
| 
 | ||||
| import ch.ethz.seb.sebserver.gbl.api.API; | ||||
| import ch.ethz.seb.sebserver.gbl.api.APIMessage; | ||||
| import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage; | ||||
| import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | ||||
| import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType; | ||||
| import ch.ethz.seb.sebserver.gbl.model.session.IndicatorValue; | ||||
| import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordMapper; | ||||
| import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper; | ||||
| import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord; | ||||
| import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; | ||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.AbstractPingIndicator; | ||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ClientConnectionDataInternal; | ||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCacheService; | ||||
| 
 | ||||
| @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) | ||||
| public class SebConnectionTest extends ExamAPIIntegrationTester { | ||||
| 
 | ||||
|     @Autowired | ||||
|     private ClientConnectionRecordMapper clientConnectionRecordMapper; | ||||
|     @Autowired | ||||
|     private ClientEventRecordMapper clientEventRecordMapper; | ||||
|     @Autowired | ||||
|     private JSONMapper jsonMapper; | ||||
| 
 | ||||
|     @Test | ||||
|     @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) | ||||
|     public void testCreateConnection() throws Exception { | ||||
|         final String accessToken = super.obtainAccessToken("test", "test", "SEBClient"); | ||||
|         assertNotNull(accessToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null); | ||||
|         assertNotNull(createConnection); | ||||
| 
 | ||||
|         // check correct response | ||||
|         assertTrue(HttpStatus.OK.value() == createConnection.getStatus()); | ||||
|         final String contentAsString = createConnection.getContentAsString(); | ||||
|         assertEquals("[{\"examId\":\"2\",\"name\":\"Demo Quiz 6\",\"url\":\"http://lms.mockup.com/api/\"}]", | ||||
|                 contentAsString); | ||||
| 
 | ||||
|         // check connection token | ||||
|         final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN); | ||||
|         assertNotNull(connectionToken); | ||||
| 
 | ||||
|         // check correct stored | ||||
|         final List<ClientConnectionRecord> records = this.clientConnectionRecordMapper | ||||
|                 .selectByExample() | ||||
|                 .build() | ||||
|                 .execute(); | ||||
| 
 | ||||
|         assertTrue(records.size() == 1); | ||||
|         final ClientConnectionRecord clientConnectionRecord = records.get(0); | ||||
|         assertEquals("1", String.valueOf(clientConnectionRecord.getInstitutionId())); | ||||
|         assertNull(clientConnectionRecord.getExamId()); | ||||
|         assertEquals("CONNECTION_REQUESTED", String.valueOf(clientConnectionRecord.getStatus())); | ||||
|         assertEquals(connectionToken, clientConnectionRecord.getConnectionToken()); | ||||
|         assertNotNull(clientConnectionRecord.getClientAddress()); | ||||
|         assertNull(clientConnectionRecord.getExamUserSessionIdentifer()); | ||||
|         assertNull(clientConnectionRecord.getVirtualClientAddress()); | ||||
| 
 | ||||
|         // check caching | ||||
|         final Cache examCache = this.cacheManager | ||||
|                 .getCache(ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM); | ||||
|         final ValueWrapper exam = examCache.get(2L); | ||||
|         assertNotNull(exam); | ||||
| 
 | ||||
|         final Cache connectionCache = this.cacheManager | ||||
|                 .getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION); | ||||
|         final ValueWrapper connection = connectionCache.get(connectionToken); | ||||
|         assertNotNull(connection); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) | ||||
|     public void testCreateConnectionWithExamId() throws Exception { | ||||
|         final String accessToken = super.obtainAccessToken("test", "test", "SEBClient"); | ||||
|         assertNotNull(accessToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, 2L); | ||||
|         assertNotNull(createConnection); | ||||
| 
 | ||||
|         // check correct response | ||||
|         assertTrue(HttpStatus.OK.value() == createConnection.getStatus()); | ||||
|         final String contentAsString = createConnection.getContentAsString(); | ||||
|         assertEquals("[{\"examId\":\"2\",\"name\":\"Demo Quiz 6\",\"url\":\"http://lms.mockup.com/api/\"}]", | ||||
|                 contentAsString); | ||||
| 
 | ||||
|         // check connection token | ||||
|         final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN); | ||||
|         assertNotNull(connectionToken); | ||||
| 
 | ||||
|         // check correct stored | ||||
|         final List<ClientConnectionRecord> records = this.clientConnectionRecordMapper | ||||
|                 .selectByExample() | ||||
|                 .build() | ||||
|                 .execute(); | ||||
| 
 | ||||
|         assertTrue(records.size() == 1); | ||||
|         final ClientConnectionRecord clientConnectionRecord = records.get(0); | ||||
|         assertEquals("1", String.valueOf(clientConnectionRecord.getInstitutionId())); | ||||
|         assertEquals("2", String.valueOf(clientConnectionRecord.getExamId())); | ||||
|         assertEquals("CONNECTION_REQUESTED", String.valueOf(clientConnectionRecord.getStatus())); | ||||
|         assertEquals(connectionToken, clientConnectionRecord.getConnectionToken()); | ||||
|         assertNotNull(clientConnectionRecord.getClientAddress()); | ||||
|         assertNull(clientConnectionRecord.getExamUserSessionIdentifer()); | ||||
|         assertNull(clientConnectionRecord.getVirtualClientAddress()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) | ||||
|     public void testCreateConnectionWithWrongExamId() throws Exception { | ||||
|         final String accessToken = super.obtainAccessToken("test", "test", "SEBClient"); | ||||
|         assertNotNull(accessToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, 1L); | ||||
|         assertNotNull(createConnection); | ||||
| 
 | ||||
|         // expecting error status | ||||
|         assertTrue(createConnection.getStatus() != HttpStatus.OK.value()); | ||||
|         final String contentAsString = createConnection.getContentAsString(); | ||||
|         final Collection<APIMessage> errorMessage = this.jsonMapper.readValue( | ||||
|                 contentAsString, | ||||
|                 new TypeReference<Collection<APIMessage>>() { | ||||
|                 }); | ||||
|         final APIMessage error = errorMessage.iterator().next(); | ||||
|         assertEquals(ErrorMessage.UNEXPECTED.messageCode, error.messageCode); | ||||
|         assertEquals("The exam 1 is not running", error.details); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) | ||||
|     public void testCreateConnectionNoInstitutionId() throws Exception { | ||||
|         final String accessToken = super.obtainAccessToken("test", "test", "SEBClient"); | ||||
|         assertNotNull(accessToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse createConnection = super.createConnection(accessToken, null, null); | ||||
|         assertNotNull(createConnection); | ||||
| 
 | ||||
|         // expecting error status | ||||
|         assertTrue(createConnection.getStatus() != HttpStatus.OK.value()); | ||||
|         final String contentAsString = createConnection.getContentAsString(); | ||||
|         final Collection<APIMessage> errorMessage = this.jsonMapper.readValue( | ||||
|                 contentAsString, | ||||
|                 new TypeReference<Collection<APIMessage>>() { | ||||
|                 }); | ||||
|         final APIMessage error = errorMessage.iterator().next(); | ||||
|         assertEquals(ErrorMessage.GENERIC.messageCode, error.messageCode); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) | ||||
|     public void testUpdateConnection() throws Exception { | ||||
|         final String accessToken = super.obtainAccessToken("test", "test", "SEBClient"); | ||||
|         assertNotNull(accessToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null); | ||||
|         assertNotNull(createConnection); | ||||
| 
 | ||||
|         final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN); | ||||
|         assertNotNull(connectionToken); | ||||
| 
 | ||||
|         // check cache after creation | ||||
|         Cache connectionCache = this.cacheManager | ||||
|                 .getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION); | ||||
|         ClientConnectionDataInternal ccdi = | ||||
|                 (ClientConnectionDataInternal) connectionCache.get(connectionToken).get(); | ||||
|         assertNotNull(ccdi); | ||||
|         assertNull(ccdi.clientConnection.examId); | ||||
|         assertTrue(ccdi.indicatorValues.isEmpty()); | ||||
| 
 | ||||
|         final MockHttpServletResponse updatedConnection = super.updateConnection( | ||||
|                 accessToken, | ||||
|                 connectionToken, | ||||
|                 2L, | ||||
|                 "userSessionId"); | ||||
| 
 | ||||
|         // check correct response | ||||
|         assertTrue(HttpStatus.OK.value() == updatedConnection.getStatus()); | ||||
| 
 | ||||
|         // check correct stored | ||||
|         final List<ClientConnectionRecord> records = this.clientConnectionRecordMapper | ||||
|                 .selectByExample() | ||||
|                 .build() | ||||
|                 .execute(); | ||||
| 
 | ||||
|         assertTrue(records.size() == 1); | ||||
|         final ClientConnectionRecord clientConnectionRecord = records.get(0); | ||||
|         assertEquals("1", String.valueOf(clientConnectionRecord.getInstitutionId())); | ||||
|         assertEquals("2", String.valueOf(clientConnectionRecord.getExamId())); | ||||
|         assertEquals("CONNECTION_REQUESTED", String.valueOf(clientConnectionRecord.getStatus())); | ||||
|         assertNotNull(clientConnectionRecord.getConnectionToken()); | ||||
|         assertNotNull(clientConnectionRecord.getClientAddress()); | ||||
|         assertEquals("userSessionId", clientConnectionRecord.getExamUserSessionIdentifer()); | ||||
|         assertNull(clientConnectionRecord.getVirtualClientAddress()); | ||||
| 
 | ||||
|         // check cache after update | ||||
|         connectionCache = this.cacheManager | ||||
|                 .getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION); | ||||
|         ccdi = | ||||
|                 (ClientConnectionDataInternal) connectionCache.get(connectionToken).get(); | ||||
|         assertNotNull(ccdi); | ||||
|         assertNotNull(ccdi.clientConnection.examId); | ||||
|         assertFalse(ccdi.indicatorValues.isEmpty()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) | ||||
|     public void testUpdateConnectionWithWrongConnectionToken() throws Exception { | ||||
|         final String accessToken = super.obtainAccessToken("test", "test", "SEBClient"); | ||||
|         assertNotNull(accessToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse updatedConnection = super.updateConnection( | ||||
|                 accessToken, | ||||
|                 "", | ||||
|                 2L, | ||||
|                 "userSessionId"); | ||||
| 
 | ||||
|         // expecting error status | ||||
|         assertTrue(HttpStatus.OK.value() != updatedConnection.getStatus()); | ||||
|         final String contentAsString = updatedConnection.getContentAsString(); | ||||
|         final Collection<APIMessage> errorMessage = this.jsonMapper.readValue( | ||||
|                 contentAsString, | ||||
|                 new TypeReference<Collection<APIMessage>>() { | ||||
|                 }); | ||||
|         final APIMessage error = errorMessage.iterator().next(); | ||||
|         assertEquals(ErrorMessage.RESOURCE_NOT_FOUND.messageCode, error.messageCode); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) | ||||
|     public void testEstablishConnection() throws Exception { | ||||
|         final String accessToken = super.obtainAccessToken("test", "test", "SEBClient"); | ||||
|         assertNotNull(accessToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null); | ||||
|         assertNotNull(createConnection); | ||||
| 
 | ||||
|         final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN); | ||||
|         assertNotNull(connectionToken); | ||||
| 
 | ||||
|         // check cache after creation | ||||
|         final Cache connectionCache = this.cacheManager | ||||
|                 .getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION); | ||||
|         ClientConnectionDataInternal ccdi = | ||||
|                 (ClientConnectionDataInternal) connectionCache.get(connectionToken).get(); | ||||
|         assertNotNull(ccdi); | ||||
|         assertNull(ccdi.clientConnection.examId); | ||||
|         assertTrue(ccdi.indicatorValues.isEmpty()); | ||||
| 
 | ||||
|         final MockHttpServletResponse updatedConnection = super.establishConnection( | ||||
|                 accessToken, | ||||
|                 connectionToken, | ||||
|                 2L, | ||||
|                 "userSessionId"); | ||||
| 
 | ||||
|         // check correct response | ||||
|         assertTrue(HttpStatus.OK.value() == updatedConnection.getStatus()); | ||||
| 
 | ||||
|         // check correct stored | ||||
|         final List<ClientConnectionRecord> records = this.clientConnectionRecordMapper | ||||
|                 .selectByExample() | ||||
|                 .build() | ||||
|                 .execute(); | ||||
| 
 | ||||
|         assertTrue(records.size() == 1); | ||||
|         final ClientConnectionRecord clientConnectionRecord = records.get(0); | ||||
|         assertEquals("1", String.valueOf(clientConnectionRecord.getInstitutionId())); | ||||
|         assertEquals("2", String.valueOf(clientConnectionRecord.getExamId())); | ||||
|         assertEquals("ESTABLISHED", String.valueOf(clientConnectionRecord.getStatus())); | ||||
|         assertNotNull(clientConnectionRecord.getConnectionToken()); | ||||
|         assertNotNull(clientConnectionRecord.getClientAddress()); | ||||
|         assertEquals("userSessionId", clientConnectionRecord.getExamUserSessionIdentifer()); | ||||
|         assertNull(clientConnectionRecord.getVirtualClientAddress()); | ||||
| 
 | ||||
|         // check cache after update | ||||
|         ccdi = (ClientConnectionDataInternal) connectionCache.get(connectionToken).get(); | ||||
|         assertNotNull(ccdi); | ||||
|         assertNotNull(ccdi.clientConnection.examId); | ||||
|         assertFalse(ccdi.indicatorValues.isEmpty()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) | ||||
|     public void testEstablishConnectionNoExamLeadsToError() throws Exception { | ||||
|         final String accessToken = super.obtainAccessToken("test", "test", "SEBClient"); | ||||
|         assertNotNull(accessToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null); | ||||
|         assertNotNull(createConnection); | ||||
| 
 | ||||
|         final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN); | ||||
|         assertNotNull(connectionToken); | ||||
| 
 | ||||
|         // check cache after creation | ||||
|         final Cache connectionCache = this.cacheManager | ||||
|                 .getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION); | ||||
|         ClientConnectionDataInternal ccdi = | ||||
|                 (ClientConnectionDataInternal) connectionCache.get(connectionToken).get(); | ||||
|         assertNotNull(ccdi); | ||||
|         assertNull(ccdi.clientConnection.examId); | ||||
|         assertTrue(ccdi.indicatorValues.isEmpty()); | ||||
| 
 | ||||
|         final MockHttpServletResponse updatedConnection = super.establishConnection( | ||||
|                 accessToken, | ||||
|                 connectionToken, | ||||
|                 null, | ||||
|                 null); | ||||
| 
 | ||||
|         // check correct response | ||||
|         assertTrue(HttpStatus.OK.value() != updatedConnection.getStatus()); | ||||
|         final String contentAsString = updatedConnection.getContentAsString(); | ||||
|         final Collection<APIMessage> errorMessage = this.jsonMapper.readValue( | ||||
|                 contentAsString, | ||||
|                 new TypeReference<Collection<APIMessage>>() { | ||||
|                 }); | ||||
|         final APIMessage error = errorMessage.iterator().next(); | ||||
|         assertEquals(ErrorMessage.UNEXPECTED.messageCode, error.messageCode); | ||||
|         assertEquals("ClientConnection integrity violation", error.details); | ||||
| 
 | ||||
|         // check correct stored (no changes) | ||||
|         final List<ClientConnectionRecord> records = this.clientConnectionRecordMapper | ||||
|                 .selectByExample() | ||||
|                 .build() | ||||
|                 .execute(); | ||||
| 
 | ||||
|         assertTrue(records.size() == 1); | ||||
|         final ClientConnectionRecord clientConnectionRecord = records.get(0); | ||||
|         assertEquals("1", String.valueOf(clientConnectionRecord.getInstitutionId())); | ||||
|         assertNull(clientConnectionRecord.getExamId()); | ||||
|         assertEquals("CONNECTION_REQUESTED", String.valueOf(clientConnectionRecord.getStatus())); | ||||
|         assertNotNull(clientConnectionRecord.getConnectionToken()); | ||||
|         assertNotNull(clientConnectionRecord.getClientAddress()); | ||||
|         assertNull(clientConnectionRecord.getExamUserSessionIdentifer()); | ||||
|         assertNull(clientConnectionRecord.getVirtualClientAddress()); | ||||
| 
 | ||||
|         // check cache fail remains the same | ||||
|         ccdi = (ClientConnectionDataInternal) connectionCache.get(connectionToken).get(); | ||||
|         assertNotNull(ccdi); | ||||
|         assertNull(ccdi.clientConnection.examId); | ||||
|         assertTrue(ccdi.indicatorValues.isEmpty()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) | ||||
|     public void testCloseConnection() throws Exception { | ||||
|         final String accessToken = super.obtainAccessToken("test", "test", "SEBClient"); | ||||
|         assertNotNull(accessToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null); | ||||
|         assertNotNull(createConnection); | ||||
| 
 | ||||
|         final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN); | ||||
|         assertNotNull(connectionToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse establishConnection = super.establishConnection( | ||||
|                 accessToken, | ||||
|                 connectionToken, | ||||
|                 2L, | ||||
|                 null); | ||||
| 
 | ||||
|         // check correct response | ||||
|         assertTrue(HttpStatus.OK.value() == establishConnection.getStatus()); | ||||
| 
 | ||||
|         // check cache after creation | ||||
|         final Cache connectionCache = this.cacheManager | ||||
|                 .getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION); | ||||
|         ClientConnectionDataInternal ccdi = | ||||
|                 (ClientConnectionDataInternal) connectionCache.get(connectionToken).get(); | ||||
|         assertNotNull(ccdi); | ||||
|         assertNotNull(ccdi.clientConnection.examId); | ||||
|         assertFalse(ccdi.indicatorValues.isEmpty()); | ||||
| 
 | ||||
|         // close connection | ||||
|         final MockHttpServletResponse closedConnection = super.closeConnection( | ||||
|                 accessToken, | ||||
|                 connectionToken); | ||||
| 
 | ||||
|         // check correct response | ||||
|         assertTrue(HttpStatus.OK.value() == closedConnection.getStatus()); | ||||
| 
 | ||||
|         // check correct stored (no changes) | ||||
|         final List<ClientConnectionRecord> records = this.clientConnectionRecordMapper | ||||
|                 .selectByExample() | ||||
|                 .build() | ||||
|                 .execute(); | ||||
| 
 | ||||
|         assertTrue(records.size() == 1); | ||||
|         final ClientConnectionRecord clientConnectionRecord = records.get(0); | ||||
|         assertEquals("1", String.valueOf(clientConnectionRecord.getInstitutionId())); | ||||
|         assertEquals("2", String.valueOf(clientConnectionRecord.getExamId())); | ||||
|         assertEquals("CLOSED", String.valueOf(clientConnectionRecord.getStatus())); | ||||
|         assertNotNull(clientConnectionRecord.getConnectionToken()); | ||||
|         assertNotNull(clientConnectionRecord.getClientAddress()); | ||||
|         assertNull(clientConnectionRecord.getExamUserSessionIdentifer()); | ||||
|         assertNull(clientConnectionRecord.getVirtualClientAddress()); | ||||
| 
 | ||||
|         // check cache after update | ||||
|         ccdi = (ClientConnectionDataInternal) connectionCache.get(connectionToken).get(); | ||||
|         assertNotNull(ccdi); | ||||
|         assertNotNull(ccdi.clientConnection.examId); | ||||
|         assertFalse(ccdi.indicatorValues.isEmpty()); | ||||
|         assertEquals("CLOSED", ccdi.clientConnection.status.toString()); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) | ||||
|     public void testPing() throws Exception { | ||||
|         final String accessToken = super.obtainAccessToken("test", "test", "SEBClient"); | ||||
|         assertNotNull(accessToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null); | ||||
|         assertNotNull(createConnection); | ||||
| 
 | ||||
|         final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN); | ||||
|         assertNotNull(connectionToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse establishConnection = super.establishConnection( | ||||
|                 accessToken, | ||||
|                 connectionToken, | ||||
|                 2L, | ||||
|                 null); | ||||
| 
 | ||||
|         // check correct response | ||||
|         assertTrue(HttpStatus.OK.value() == establishConnection.getStatus()); | ||||
| 
 | ||||
|         final Cache connectionCache = this.cacheManager | ||||
|                 .getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION); | ||||
|         final ClientConnectionDataInternal ccdi = | ||||
|                 (ClientConnectionDataInternal) connectionCache.get(connectionToken).get(); | ||||
|         assertNotNull(ccdi); | ||||
|         assertNotNull(ccdi.clientConnection.examId); | ||||
|         assertFalse(ccdi.indicatorValues.isEmpty()); | ||||
|         final IndicatorValue pingIndicator = ccdi.indicatorValues.iterator().next(); | ||||
|         assertTrue(pingIndicator.getType() == IndicatorType.LAST_PING); | ||||
|         assertEquals("0", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber())); | ||||
| 
 | ||||
|         super.sendPing(accessToken, connectionToken, 1); | ||||
|         assertEquals("1", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber())); | ||||
|         super.sendPing(accessToken, connectionToken, 2); | ||||
|         assertEquals("2", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber())); | ||||
|         super.sendPing(accessToken, connectionToken, 3); | ||||
|         assertEquals("3", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber())); | ||||
|         super.sendPing(accessToken, connectionToken, 5); | ||||
|         assertEquals("5", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber())); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) | ||||
|     public void testSendPingToNoneEstablishedConnectionIsIgnored() throws Exception { | ||||
|         final String accessToken = super.obtainAccessToken("test", "test", "SEBClient"); | ||||
|         assertNotNull(accessToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null); | ||||
|         assertNotNull(createConnection); | ||||
| 
 | ||||
|         final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN); | ||||
|         assertNotNull(connectionToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse sendPing = super.sendPing(accessToken, connectionToken, 1); | ||||
|         // check correct response | ||||
|         assertTrue(HttpStatus.OK.value() == sendPing.getStatus()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) | ||||
|     public void testEvent() throws Exception { | ||||
|         final String accessToken = super.obtainAccessToken("test", "test", "SEBClient"); | ||||
|         assertNotNull(accessToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null); | ||||
|         assertNotNull(createConnection); | ||||
| 
 | ||||
|         final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN); | ||||
|         assertNotNull(connectionToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse establishConnection = super.establishConnection( | ||||
|                 accessToken, | ||||
|                 connectionToken, | ||||
|                 2L, | ||||
|                 null); | ||||
| 
 | ||||
|         // check correct response | ||||
|         assertTrue(HttpStatus.OK.value() == establishConnection.getStatus()); | ||||
| 
 | ||||
|         final Cache connectionCache = this.cacheManager | ||||
|                 .getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION); | ||||
|         final ClientConnectionDataInternal ccdi = | ||||
|                 (ClientConnectionDataInternal) connectionCache.get(connectionToken).get(); | ||||
|         assertNotNull(ccdi); | ||||
|         assertNotNull(ccdi.clientConnection.examId); | ||||
|         assertFalse(ccdi.indicatorValues.isEmpty()); | ||||
|         final IndicatorValue pingIndicator = ccdi.indicatorValues.iterator().next(); | ||||
|         assertTrue(pingIndicator.getType() == IndicatorType.LAST_PING); | ||||
|         assertEquals("0", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber())); | ||||
| 
 | ||||
|         MockHttpServletResponse sendEvent = super.sendEvent( | ||||
|                 accessToken, | ||||
|                 connectionToken, | ||||
|                 "INFO_LOG", | ||||
|                 1l, | ||||
|                 100.0, | ||||
|                 "testEvent1"); | ||||
| 
 | ||||
|         // check correct response | ||||
|         assertTrue(HttpStatus.OK.value() == sendEvent.getStatus()); | ||||
| 
 | ||||
|         // check event stored on db | ||||
|         List<ClientEventRecord> events = this.clientEventRecordMapper | ||||
|                 .selectByExample() | ||||
|                 .build() | ||||
|                 .execute(); | ||||
| 
 | ||||
|         assertFalse(events.isEmpty()); | ||||
|         final ClientEventRecord clientEventRecord = events.get(0); | ||||
|         assertEquals( | ||||
|                 "ClientEventRecord [" | ||||
|                         + "Hash = -1088444763, " | ||||
|                         + "id=1, " | ||||
|                         + "connectionId=1, " | ||||
|                         + "type=2, " | ||||
|                         + "timestamp=1, " | ||||
|                         + "numericValue=100.0000, " | ||||
|                         + "text=testEvent1]", | ||||
|                 clientEventRecord.toString()); | ||||
| 
 | ||||
|         // send another event | ||||
|         sendEvent = super.sendEvent( | ||||
|                 accessToken, | ||||
|                 connectionToken, | ||||
|                 "ERROR_LOG", | ||||
|                 2l, | ||||
|                 10000.0, | ||||
|                 "testEvent2"); | ||||
| 
 | ||||
|         // check correct response | ||||
|         assertTrue(HttpStatus.OK.value() == sendEvent.getStatus()); | ||||
| 
 | ||||
|         // check event stored on db | ||||
|         events = this.clientEventRecordMapper | ||||
|                 .selectByExample() | ||||
|                 .build() | ||||
|                 .execute(); | ||||
| 
 | ||||
|         assertFalse(events.isEmpty()); | ||||
|         assertTrue(events.stream().filter(ev -> ev.getTimestamp().equals(2l)).findFirst().isPresent()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) | ||||
|     public void testSendEventToNoneEstablishedConnectionLeadsToError() throws Exception { | ||||
|         final String accessToken = super.obtainAccessToken("test", "test", "SEBClient"); | ||||
|         assertNotNull(accessToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse createConnection = super.createConnection(accessToken, 1L, null); | ||||
|         assertNotNull(createConnection); | ||||
| 
 | ||||
|         final String connectionToken = createConnection.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN); | ||||
|         assertNotNull(connectionToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse sendEvent = super.sendEvent( | ||||
|                 accessToken, | ||||
|                 connectionToken, | ||||
|                 "INFO_LOG", | ||||
|                 1l, | ||||
|                 100.0, | ||||
|                 "testEvent1"); | ||||
|         // check correct response | ||||
|         assertTrue(HttpStatus.OK.value() != sendEvent.getStatus()); | ||||
| 
 | ||||
|         final List<ClientEventRecord> events = this.clientEventRecordMapper | ||||
|                 .selectByExample() | ||||
|                 .build() | ||||
|                 .execute(); | ||||
|         assertTrue(events.isEmpty()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) | ||||
|     public void testSendEventToNoneExistingConnectionIsIgnored() throws Exception { | ||||
|         final String accessToken = super.obtainAccessToken("test", "test", "SEBClient"); | ||||
|         assertNotNull(accessToken); | ||||
| 
 | ||||
|         final MockHttpServletResponse sendEvent = super.sendEvent( | ||||
|                 accessToken, | ||||
|                 "someInvalidConnectionToken", | ||||
|                 "INFO_LOG", | ||||
|                 1l, | ||||
|                 100.0, | ||||
|                 "testEvent1"); | ||||
|         // check correct response | ||||
|         assertTrue(HttpStatus.OK.value() == sendEvent.getStatus()); | ||||
| 
 | ||||
|         final List<ClientEventRecord> events = this.clientEventRecordMapper | ||||
|                 .selectByExample() | ||||
|                 .build() | ||||
|                 .execute(); | ||||
|         assertTrue(events.isEmpty()); | ||||
|     } | ||||
| } | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -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 | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 anhefti
						anhefti