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 |     /** Use this as a conversion from a given FieldError of Spring to a APIMessage | ||||||
|      * of type field validation. |      * of type field validation. | ||||||
|      *  |      * | ||||||
|      * @param error FieldError instance |      * @param error FieldError instance | ||||||
|      * @return converted APIMessage of type field validation */ |      * @return converted APIMessage of type field validation */ | ||||||
|     public static final APIMessage fieldValidationError(final FieldError error) { |     public static final APIMessage fieldValidationError(final FieldError error) { | ||||||
|  |  | ||||||
|  | @ -55,12 +55,6 @@ public interface ClientCredentialService { | ||||||
|         return encryptClientCredentials(clientIdPlaintext, secretPlaintext, null); |         return encryptClientCredentials(clientIdPlaintext, secretPlaintext, null); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| //    /** Use this to get a decrypted plain text clientId form given ClientCredentials |  | ||||||
| //     * |  | ||||||
| //     * @param credentials ClientCredentials containing the clientId to decrypt |  | ||||||
| //     * @return decrypted plain text clientId */ |  | ||||||
| //    CharSequence getPlainClientId(ClientCredentials credentials); |  | ||||||
| 
 |  | ||||||
|     /** Use this to get a decrypted plain text secret form given ClientCredentials |     /** Use this to get a decrypted plain text secret form given ClientCredentials | ||||||
|      * |      * | ||||||
|      * @param credentials ClientCredentials containing the secret to decrypt |      * @param credentials ClientCredentials containing the secret to decrypt | ||||||
|  |  | ||||||
|  | @ -61,7 +61,7 @@ public class ClientCredentialServiceImpl implements ClientCredentialService { | ||||||
|             final CharSequence accessTokenPlaintext) { |             final CharSequence accessTokenPlaintext) { | ||||||
| 
 | 
 | ||||||
|         final CharSequence secret = this.environment |         final CharSequence secret = this.environment | ||||||
|                 .getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); |                 .getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); | ||||||
| 
 | 
 | ||||||
|         return new ClientCredentials( |         return new ClientCredentials( | ||||||
|                 clientIdPlaintext, |                 clientIdPlaintext, | ||||||
|  | @ -73,11 +73,6 @@ public class ClientCredentialServiceImpl implements ClientCredentialService { | ||||||
|                         : null); |                         : null); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| //    @Override |  | ||||||
| //    public CharSequence getPlainClientId(final ClientCredentials credentials) { |  | ||||||
| //        return credentials.clientId; |  | ||||||
| //    } |  | ||||||
| 
 |  | ||||||
|     @Override |     @Override | ||||||
|     public CharSequence getPlainClientSecret(final ClientCredentials credentials) { |     public CharSequence getPlainClientSecret(final ClientCredentials credentials) { | ||||||
|         if (credentials == null || !credentials.hasSecret()) { |         if (credentials == null || !credentials.hasSecret()) { | ||||||
|  | @ -85,7 +80,7 @@ public class ClientCredentialServiceImpl implements ClientCredentialService { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         final CharSequence secret = this.environment |         final CharSequence secret = this.environment | ||||||
|                 .getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); |                 .getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); | ||||||
|         return this.decrypt(credentials.secret, secret); |         return this.decrypt(credentials.secret, secret); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -96,7 +91,7 @@ public class ClientCredentialServiceImpl implements ClientCredentialService { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         final CharSequence secret = this.environment |         final CharSequence secret = this.environment | ||||||
|                 .getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); |                 .getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); | ||||||
| 
 | 
 | ||||||
|         return this.decrypt(credentials.accessToken, secret); |         return this.decrypt(credentials.accessToken, secret); | ||||||
|     } |     } | ||||||
|  | @ -105,7 +100,7 @@ public class ClientCredentialServiceImpl implements ClientCredentialService { | ||||||
|     public CharSequence encrypt(final CharSequence text) { |     public CharSequence encrypt(final CharSequence text) { | ||||||
| 
 | 
 | ||||||
|         final CharSequence secret = this.environment |         final CharSequence secret = this.environment | ||||||
|                 .getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); |                 .getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); | ||||||
| 
 | 
 | ||||||
|         return encrypt(text, secret); |         return encrypt(text, secret); | ||||||
|     } |     } | ||||||
|  | @ -114,7 +109,7 @@ public class ClientCredentialServiceImpl implements ClientCredentialService { | ||||||
|     public CharSequence decrypt(final CharSequence text) { |     public CharSequence decrypt(final CharSequence text) { | ||||||
| 
 | 
 | ||||||
|         final CharSequence secret = this.environment |         final CharSequence secret = this.environment | ||||||
|                 .getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); |                 .getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); | ||||||
| 
 | 
 | ||||||
|         return decrypt(text, secret); |         return decrypt(text, secret); | ||||||
|     } |     } | ||||||
|  | @ -124,6 +119,11 @@ public class ClientCredentialServiceImpl implements ClientCredentialService { | ||||||
|             throw new IllegalArgumentException("Text has null reference"); |             throw new IllegalArgumentException("Text has null reference"); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         if (secret == null) { | ||||||
|  |             log.warn("No internal secret supplied: skip encryption"); | ||||||
|  |             return text; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         try { |         try { | ||||||
| 
 | 
 | ||||||
|             final CharSequence salt = KeyGenerators.string().generateKey(); |             final CharSequence salt = KeyGenerators.string().generateKey(); | ||||||
|  | @ -145,6 +145,11 @@ public class ClientCredentialServiceImpl implements ClientCredentialService { | ||||||
|             throw new IllegalArgumentException("Cipher has null reference"); |             throw new IllegalArgumentException("Cipher has null reference"); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         if (secret == null) { | ||||||
|  |             log.warn("No internal secret supplied: skip decryption"); | ||||||
|  |             return cipher; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         try { |         try { | ||||||
| 
 | 
 | ||||||
|             final int length = cipher.length(); |             final int length = cipher.length(); | ||||||
|  |  | ||||||
|  | @ -8,13 +8,25 @@ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.dao; | package ch.ethz.seb.sebserver.webservice.servicelayer.dao; | ||||||
| 
 | 
 | ||||||
|  | import java.util.Collection; | ||||||
|  | 
 | ||||||
| import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; | import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
| 
 | 
 | ||||||
| public interface ClientConnectionDAO extends EntityDAO<ClientConnection, ClientConnection> { | public interface ClientConnectionDAO extends EntityDAO<ClientConnection, ClientConnection> { | ||||||
| 
 | 
 | ||||||
|     Result<ClientConnection> byConnectionToken(Long institutionId, String connectionToken); |     /** Get a list of all connection tokens of all connections (no matter what state) | ||||||
|  |      * of an exam. | ||||||
|  |      * | ||||||
|  |      * @param examId The exam identifier | ||||||
|  |      * @return list of all connection tokens of all connections (no matter what state) | ||||||
|  |      *         of an exam */ | ||||||
|  |     Result<Collection<String>> getConnectionTokens(Long examId); | ||||||
| 
 | 
 | ||||||
|  |     /** Get a ClientConnection for a specified token. | ||||||
|  |      * | ||||||
|  |      * @param connectionToken the connection token | ||||||
|  |      * @return Result refer to ClientConnection or refer to a error if happened */ | ||||||
|     Result<ClientConnection> byConnectionToken(String connectionToken); |     Result<ClientConnection> byConnectionToken(String connectionToken); | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -16,8 +16,21 @@ public interface ExamConfigurationMapDAO extends | ||||||
|         EntityDAO<ExamConfigurationMap, ExamConfigurationMap>, |         EntityDAO<ExamConfigurationMap, ExamConfigurationMap>, | ||||||
|         BulkActionSupportDAO<ExamConfigurationMap> { |         BulkActionSupportDAO<ExamConfigurationMap> { | ||||||
| 
 | 
 | ||||||
|  |     /** Get the ConfigurationNode identifier of the default Exam Configuration of | ||||||
|  |      * the Exam with specified identifier. | ||||||
|  |      * | ||||||
|  |      * @param examId The Exam identifier | ||||||
|  |      * @return ConfigurationNode identifier of the default Exam Configuration of | ||||||
|  |      *         the Exam with specified identifier */ | ||||||
|     public Result<Long> getDefaultConfigurationForExam(Long examId); |     public Result<Long> getDefaultConfigurationForExam(Long examId); | ||||||
| 
 | 
 | ||||||
|  |     /** Get the ConfigurationNode identifier of the Exam Configuration of | ||||||
|  |      * the Exam for a specified user identifier. | ||||||
|  |      * | ||||||
|  |      * @param examId The Exam identifier | ||||||
|  |      * @param userId the user identifier | ||||||
|  |      * @return ConfigurationNode identifier of the Exam Configuration of | ||||||
|  |      *         the Exam for a specified user identifier */ | ||||||
|     public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId); |     public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId); | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -110,6 +110,24 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     @Transactional(readOnly = true) | ||||||
|  |     public Result<Collection<String>> getConnectionTokens(final Long examId) { | ||||||
|  |         return Result.tryCatch(() -> { | ||||||
|  |             return this.clientConnectionRecordMapper | ||||||
|  |                     .selectByExample() | ||||||
|  |                     .where( | ||||||
|  |                             ClientConnectionRecordDynamicSqlSupport.examId, | ||||||
|  |                             SqlBuilder.isEqualTo(examId)) | ||||||
|  |                     .build() | ||||||
|  |                     .execute() | ||||||
|  |                     .stream() | ||||||
|  |                     .map(ClientConnectionRecord::getConnectionToken) | ||||||
|  |                     .filter(StringUtils::isNotBlank) | ||||||
|  |                     .collect(Collectors.toList()); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     @Transactional |     @Transactional | ||||||
|     public Result<ClientConnection> createNew(final ClientConnection data) { |     public Result<ClientConnection> createNew(final ClientConnection data) { | ||||||
|  | @ -140,10 +158,10 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { | ||||||
|             final ClientConnectionRecord updateRecord = new ClientConnectionRecord( |             final ClientConnectionRecord updateRecord = new ClientConnectionRecord( | ||||||
|                     data.id, |                     data.id, | ||||||
|                     null, |                     null, | ||||||
|                     null, |                     data.examId, | ||||||
|                     data.status != null ? data.status.name() : null, |                     data.status != null ? data.status.name() : null, | ||||||
|                     data.connectionToken, |  | ||||||
|                     null, |                     null, | ||||||
|  |                     data.userSessionId, | ||||||
|                     data.clientAddress, |                     data.clientAddress, | ||||||
|                     data.virtualClientAddress); |                     data.virtualClientAddress); | ||||||
| 
 | 
 | ||||||
|  | @ -183,37 +201,6 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |  | ||||||
|     @Transactional(readOnly = true) |  | ||||||
|     public Result<ClientConnection> byConnectionToken( |  | ||||||
|             final Long institutionId, |  | ||||||
|             final String connectionToken) { |  | ||||||
| 
 |  | ||||||
|         return Result.tryCatch(() -> { |  | ||||||
|             final List<ClientConnectionRecord> list = this.clientConnectionRecordMapper |  | ||||||
|                     .selectByExample() |  | ||||||
|                     .where( |  | ||||||
|                             ClientConnectionRecordDynamicSqlSupport.institutionId, |  | ||||||
|                             SqlBuilder.isEqualTo(institutionId)) |  | ||||||
|                     .and( |  | ||||||
|                             ClientConnectionRecordDynamicSqlSupport.connectionToken, |  | ||||||
|                             SqlBuilder.isEqualTo(connectionToken)) |  | ||||||
|                     .build() |  | ||||||
|                     .execute(); |  | ||||||
| 
 |  | ||||||
|             if (list.isEmpty()) { |  | ||||||
|                 throw new ResourceNotFoundException(EntityType.CLIENT_CONNECTION, "connectionToken"); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (list.size() > 1) { |  | ||||||
|                 throw new IllegalStateException("Only one ClientConnection expected but there are: " + list.size()); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return list.get(0); |  | ||||||
|         }) |  | ||||||
|                 .flatMap(ClientConnectionDAOImpl::toDomainModel); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |     @Override | ||||||
|     public Result<ClientConnection> byConnectionToken(final String connectionToken) { |     public Result<ClientConnection> byConnectionToken(final String connectionToken) { | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
|  |  | ||||||
|  | @ -118,7 +118,7 @@ public class ClientEventDAOImpl implements ClientEventDAO { | ||||||
|                     (data.numValue != null) ? new BigDecimal(data.numValue) : null, |                     (data.numValue != null) ? new BigDecimal(data.numValue) : null, | ||||||
|                     data.text); |                     data.text); | ||||||
| 
 | 
 | ||||||
|             this.clientEventRecordMapper.insert(newRecord); |             this.clientEventRecordMapper.insertSelective(newRecord); | ||||||
|             return newRecord; |             return newRecord; | ||||||
|         }) |         }) | ||||||
|                 .flatMap(ClientEventDAOImpl::toDomainModel) |                 .flatMap(ClientEventDAOImpl::toDomainModel) | ||||||
|  |  | ||||||
|  | @ -130,7 +130,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { | ||||||
|     @Transactional(readOnly = true) |     @Transactional(readOnly = true) | ||||||
|     public Result<Long> getDefaultConfigurationForExam(final Long examId) { |     public Result<Long> getDefaultConfigurationForExam(final Long examId) { | ||||||
|         return Result.tryCatch(() -> this.examConfigurationMapRecordMapper |         return Result.tryCatch(() -> this.examConfigurationMapRecordMapper | ||||||
|                 .selectIdsByExample() |                 .selectByExample() | ||||||
|                 .where( |                 .where( | ||||||
|                         ExamConfigurationMapRecordDynamicSqlSupport.examId, |                         ExamConfigurationMapRecordDynamicSqlSupport.examId, | ||||||
|                         SqlBuilder.isEqualTo(examId)) |                         SqlBuilder.isEqualTo(examId)) | ||||||
|  | @ -140,13 +140,14 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { | ||||||
|                 .build() |                 .build() | ||||||
|                 .execute() |                 .execute() | ||||||
|                 .stream() |                 .stream() | ||||||
|  |                 .map(mapping -> mapping.getConfigurationNodeId()) | ||||||
|                 .collect(Utils.toSingleton())); |                 .collect(Utils.toSingleton())); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) { |     public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) { | ||||||
|         return Result.tryCatch(() -> this.examConfigurationMapRecordMapper |         return Result.tryCatch(() -> this.examConfigurationMapRecordMapper | ||||||
|                 .selectIdsByExample() |                 .selectByExample() | ||||||
|                 .where( |                 .where( | ||||||
|                         ExamConfigurationMapRecordDynamicSqlSupport.examId, |                         ExamConfigurationMapRecordDynamicSqlSupport.examId, | ||||||
|                         SqlBuilder.isEqualTo(examId)) |                         SqlBuilder.isEqualTo(examId)) | ||||||
|  | @ -156,6 +157,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { | ||||||
|                 .build() |                 .build() | ||||||
|                 .execute() |                 .execute() | ||||||
|                 .stream() |                 .stream() | ||||||
|  |                 .map(mapping -> mapping.getConfigurationNodeId()) | ||||||
|                 .collect(Utils.toSingleton())); |                 .collect(Utils.toSingleton())); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,7 +13,6 @@ import java.io.OutputStream; | ||||||
| import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException; | import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues; | import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; | import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; |  | ||||||
| 
 | 
 | ||||||
| /** The base interface and service for all SEB Exam Configuration related functionality. */ | /** The base interface and service for all SEB Exam Configuration related functionality. */ | ||||||
| public interface SebExamConfigService { | public interface SebExamConfigService { | ||||||
|  | @ -32,10 +31,6 @@ public interface SebExamConfigService { | ||||||
|      * @throws FieldValidationException on validation exception */ |      * @throws FieldValidationException on validation exception */ | ||||||
|     void validate(ConfigurationTableValues tableValue) throws FieldValidationException; |     void validate(ConfigurationTableValues tableValue) throws FieldValidationException; | ||||||
| 
 | 
 | ||||||
|     Result<Long> getDefaultConfigurationIdForExam(Long examId); |  | ||||||
| 
 |  | ||||||
|     Result<Long> getUserConfigurationIdForExam(Long examId, String userId); |  | ||||||
| 
 |  | ||||||
|     /** Used to export a specified SEB Exam Configuration as plain XML |     /** Used to export a specified SEB Exam Configuration as plain XML | ||||||
|      * This exports the values of the follow-up configuration defined by a given |      * This exports the values of the follow-up configuration defined by a given | ||||||
|      * ConfigurationNode (configurationNodeId) |      * ConfigurationNode (configurationNodeId) | ||||||
|  | @ -45,21 +40,26 @@ public interface SebExamConfigService { | ||||||
|      * @param configurationNodeId the identifier of the ConfigurationNode to export */ |      * @param configurationNodeId the identifier of the ConfigurationNode to export */ | ||||||
|     void exportPlainXML(OutputStream out, Long institutionId, Long configurationNodeId); |     void exportPlainXML(OutputStream out, Long institutionId, Long configurationNodeId); | ||||||
| 
 | 
 | ||||||
|     /** Used to export a SEB Exam Configuration within its defined Configuration Exam Mapping. |     /** Used to export the default SEB Exam Configuration for a given exam identifier. | ||||||
|      * either with encryption if defined or as plain text within the SEB Configuration format |      * either with encryption if defined or as plain text within the SEB Configuration format | ||||||
|      * as described here: https://www.safeexambrowser.org/developer/seb-file-format.html |      * as described here: https://www.safeexambrowser.org/developer/seb-file-format.html | ||||||
|      * |      * | ||||||
|      * @param out The output stream to write the export data to |      * @param out The output stream to write the export data to | ||||||
|      * @param configExamMappingId The identifier of the Exam Configuration Mapping */ |      * @param institutionId The identifier of the institution of the requesting user | ||||||
|     void exportForExam(OutputStream out, Long configExamMappingId); |      * @param examId the exam identifier */ | ||||||
|  |     default Long exportForExam(final OutputStream out, final Long institutionId, final Long examId) { | ||||||
|  |         return exportForExam(out, institutionId, examId, null); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     /** Used to export the default SEB Exam Configuration for a given exam identifier. |     /** Used to export the default SEB Exam Configuration for a given exam identifier. | ||||||
|      * either with encryption if defined or as plain text within the SEB Configuration format |      * either with encryption if defined or as plain text within the SEB Configuration format | ||||||
|      * as described here: https://www.safeexambrowser.org/developer/seb-file-format.html |      * as described here: https://www.safeexambrowser.org/developer/seb-file-format.html | ||||||
|      * |      * | ||||||
|      * @param out The output stream to write the export data to |      * @param out The output stream to write the export data to | ||||||
|      * @param examId the exam identifier */ |      * @param institutionId The identifier of the institution of the requesting user | ||||||
|     void exportDefaultForExam(OutputStream out, Long examId); |      * @param examId the exam identifier | ||||||
|  |      * @param userId the user identifier if a specific user based configuration shall be exported */ | ||||||
|  |     Long exportForExam(OutputStream out, Long institutionId, Long examId, String userId); | ||||||
| 
 | 
 | ||||||
|     /** TODO */ |     /** TODO */ | ||||||
|     String generateConfigKey(Long configurationNodeId); |     String generateConfigKey(Long configurationNodeId); | ||||||
|  |  | ||||||
|  | @ -58,7 +58,11 @@ public class ExamConfigIO { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) |     @Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) | ||||||
|     void exportPlainXML(final OutputStream out, final Long institutionId, final Long configurationNodeId) { |     void exportPlainXML( | ||||||
|  |             final OutputStream out, | ||||||
|  |             final Long institutionId, | ||||||
|  |             final Long configurationNodeId) { | ||||||
|  | 
 | ||||||
|         // get all defined root configuration attributes |         // get all defined root configuration attributes | ||||||
|         final Map<Long, ConfigurationAttribute> attributes = this.configurationAttributeDAO.getAllRootAttributes() |         final Map<Long, ConfigurationAttribute> attributes = this.configurationAttributeDAO.getAllRootAttributes() | ||||||
|                 .getOrThrow() |                 .getOrThrow() | ||||||
|  | @ -112,7 +116,7 @@ public class ExamConfigIO { | ||||||
|             out.write(Constants.XML_PLIST_END_UTF_8); |             out.write(Constants.XML_PLIST_END_UTF_8); | ||||||
|             out.flush(); |             out.flush(); | ||||||
| 
 | 
 | ||||||
|         } catch (final IOException e) { |         } catch (final Exception e) { | ||||||
|             log.error("Unexpected error while trying to write SEB Exam Configuration XML to output stream: ", e); |             log.error("Unexpected error while trying to write SEB Exam Configuration XML to output stream: ", e); | ||||||
|             try { |             try { | ||||||
|                 out.flush(); |                 out.flush(); | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ import java.io.PipedOutputStream; | ||||||
| import java.util.Collection; | import java.util.Collection; | ||||||
| 
 | 
 | ||||||
| import org.apache.commons.io.IOUtils; | import org.apache.commons.io.IOUtils; | ||||||
|  | import org.apache.commons.lang3.StringUtils; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
| import org.springframework.context.annotation.Lazy; | import org.springframework.context.annotation.Lazy; | ||||||
|  | @ -96,13 +97,16 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { | ||||||
|             pout = new PipedOutputStream(); |             pout = new PipedOutputStream(); | ||||||
|             pin = new PipedInputStream(pout); |             pin = new PipedInputStream(pout); | ||||||
| 
 | 
 | ||||||
|             this.examConfigIO.exportPlainXML(pout, institutionId, configurationNodeId); |             this.examConfigIO.exportPlainXML( | ||||||
|  |                     pout, | ||||||
|  |                     institutionId, | ||||||
|  |                     configurationNodeId); | ||||||
| 
 | 
 | ||||||
|             IOUtils.copyLarge(pin, out); |             IOUtils.copyLarge(pin, out); | ||||||
| 
 | 
 | ||||||
|             pin.close(); |  | ||||||
|             pout.flush(); |             pout.flush(); | ||||||
|             pout.close(); |             pout.close(); | ||||||
|  |             pin.close(); | ||||||
| 
 | 
 | ||||||
|         } catch (final IOException e) { |         } catch (final IOException e) { | ||||||
|             log.error("Error while stream plain text SEB clonfiguration data: ", e); |             log.error("Error while stream plain text SEB clonfiguration data: ", e); | ||||||
|  | @ -127,26 +131,32 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |  | ||||||
|     public Result<Long> getDefaultConfigurationIdForExam(final Long examId) { |     public Result<Long> getDefaultConfigurationIdForExam(final Long examId) { | ||||||
|         return this.examConfigurationMapDAO.getDefaultConfigurationForExam(examId); |         return this.examConfigurationMapDAO.getDefaultConfigurationForExam(examId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |  | ||||||
|     public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) { |     public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) { | ||||||
|         return this.examConfigurationMapDAO.getUserConfigurationIdForExam(examId, userId); |         return this.examConfigurationMapDAO.getUserConfigurationIdForExam(examId, userId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void exportForExam(final OutputStream out, final Long configExamMappingId) { |     public Long exportForExam( | ||||||
|         // TODO Auto-generated method stub |             final OutputStream out, | ||||||
|  |             final Long institutionId, | ||||||
|  |             final Long examId, | ||||||
|  |             final String userId) { | ||||||
| 
 | 
 | ||||||
|     } |         final Long configurationNodeId = (StringUtils.isBlank(userId)) | ||||||
|  |                 ? getDefaultConfigurationIdForExam(examId) | ||||||
|  |                         .getOrThrow() | ||||||
|  |                 : getUserConfigurationIdForExam(examId, userId) | ||||||
|  |                         .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|     @Override |         // TODO add header, zip and encrypt if needed | ||||||
|     public void exportDefaultForExam(final OutputStream out, final Long examId) { |  | ||||||
|         // TODO Auto-generated method stub |  | ||||||
| 
 | 
 | ||||||
|  |         this.exportPlainXML(out, institutionId, configurationNodeId); | ||||||
|  | 
 | ||||||
|  |         return configurationNodeId; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  |  | ||||||
|  | @ -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_SINGLE_EVENT_STORE = "SINGLE_EVENT_STORE_STRATEGY"; | ||||||
|     String EVENT_CONSUMER_STRATEGY_ASYNC_BATCH_STORE = "ASYNC_BATCH_STORE_STRATEGY"; |     String EVENT_CONSUMER_STRATEGY_ASYNC_BATCH_STORE = "ASYNC_BATCH_STORE_STRATEGY"; | ||||||
| 
 | 
 | ||||||
|  |     void enable(); | ||||||
|  | 
 | ||||||
|  |     void disable(); | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -39,6 +39,10 @@ public interface ExamSessionService { | ||||||
|      *         happened. */ |      *         happened. */ | ||||||
|     Result<Collection<Exam>> getRunningExamsForInstitution(Long institutionId); |     Result<Collection<Exam>> getRunningExamsForInstitution(Long institutionId); | ||||||
| 
 | 
 | ||||||
|     void streamDefaultExamConfig(Long institutionId, String connectionToken, OutputStream out); |     /** Streams the default SEB Exam Configuration to a ClientConnection with given connectionToken. | ||||||
|  |      * | ||||||
|  |      * @param connectionToken The connection token that identifiers the ClientConnection | ||||||
|  |      * @param out The OutputStream to stream the data to */ | ||||||
|  |     void streamDefaultExamConfig(String connectionToken, OutputStream out); | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -55,8 +55,8 @@ public interface SebClientConnectionService { | ||||||
|     Result<ClientConnection> updateClientConnection( |     Result<ClientConnection> updateClientConnection( | ||||||
|             String connectionToken, |             String connectionToken, | ||||||
|             Long institutionId, |             Long institutionId, | ||||||
|             String clientAddress, |  | ||||||
|             Long examId, |             Long examId, | ||||||
|  |             String clientAddress, | ||||||
|             String userSessionId); |             String userSessionId); | ||||||
| 
 | 
 | ||||||
|     /** This is used to establish a already created ClientConnection and set it to sate: ESTABLISHED |     /** This is used to establish a already created ClientConnection and set it to sate: ESTABLISHED | ||||||
|  |  | ||||||
|  | @ -51,7 +51,7 @@ public abstract class AbstractClientIndicator implements ClientIndicator { | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public double getValue() { |     public double getValue() { | ||||||
|         if (this.currentValue == Double.NaN || !this.cachingEnabled) { |         if (Double.isNaN(this.currentValue) || !this.cachingEnabled) { | ||||||
|             this.currentValue = computeValueAt(DateTime.now(DateTimeZone.UTC).getMillis()); |             this.currentValue = computeValueAt(DateTime.now(DateTimeZone.UTC).getMillis()); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -12,6 +12,8 @@ import java.util.Collections; | ||||||
| import java.util.EnumSet; | import java.util.EnumSet; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| 
 | 
 | ||||||
|  | import com.fasterxml.jackson.annotation.JsonIgnore; | ||||||
|  | 
 | ||||||
| import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; | import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; | ||||||
| 
 | 
 | ||||||
| public abstract class AbstractPingIndicator extends AbstractClientIndicator { | public abstract class AbstractPingIndicator extends AbstractClientIndicator { | ||||||
|  | @ -37,4 +39,14 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator { | ||||||
|         return this.EMPTY_SET; |         return this.EMPTY_SET; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @JsonIgnore | ||||||
|  |     public int getPingCount() { | ||||||
|  |         return this.pingCount; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JsonIgnore | ||||||
|  |     public int getPingNumber() { | ||||||
|  |         return this.pingNumber; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -62,6 +62,7 @@ public class AsyncBatchEventSaveStrategy implements EventHandlingStrategy { | ||||||
| 
 | 
 | ||||||
|     private final BlockingDeque<ClientEvent> eventQueue = new LinkedBlockingDeque<>(); |     private final BlockingDeque<ClientEvent> eventQueue = new LinkedBlockingDeque<>(); | ||||||
|     private boolean workersRunning = false; |     private boolean workersRunning = false; | ||||||
|  |     private boolean enabled = false; | ||||||
| 
 | 
 | ||||||
|     public AsyncBatchEventSaveStrategy( |     public AsyncBatchEventSaveStrategy( | ||||||
|             final SqlSessionFactory sqlSessionFactory, |             final SqlSessionFactory sqlSessionFactory, | ||||||
|  | @ -75,9 +76,22 @@ public class AsyncBatchEventSaveStrategy implements EventHandlingStrategy { | ||||||
|         this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); |         this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public void enable() { | ||||||
|  |         this.enabled = true; | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void disable() { | ||||||
|  |         this.enabled = false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @EventListener(ApplicationReadyEvent.class) |     @EventListener(ApplicationReadyEvent.class) | ||||||
|     protected void recover() { |     protected void recover() { | ||||||
|         runWorkers(); |         if (this.enabled) { | ||||||
|  |             runWorkers(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  |  | ||||||
|  | @ -52,6 +52,10 @@ public class ClientIndicatorFactory { | ||||||
|     public Collection<ClientIndicator> createFor(final ClientConnection clientConnection) { |     public Collection<ClientIndicator> createFor(final ClientConnection clientConnection) { | ||||||
|         final List<ClientIndicator> result = new ArrayList<>(); |         final List<ClientIndicator> result = new ArrayList<>(); | ||||||
| 
 | 
 | ||||||
|  |         if (clientConnection.examId == null) { | ||||||
|  |             return result; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         try { |         try { | ||||||
| 
 | 
 | ||||||
|             final Collection<Indicator> examIndicators = this.indicatorDAO |             final Collection<Indicator> examIndicators = this.indicatorDAO | ||||||
|  |  | ||||||
|  | @ -8,13 +8,8 @@ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; | package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; | ||||||
| 
 | 
 | ||||||
| import java.io.BufferedInputStream; |  | ||||||
| import java.io.ByteArrayOutputStream; | import java.io.ByteArrayOutputStream; | ||||||
| import java.io.IOException; |  | ||||||
| import java.io.PipedInputStream; |  | ||||||
| import java.io.PipedOutputStream; |  | ||||||
| 
 | 
 | ||||||
| import org.apache.tomcat.util.http.fileupload.IOUtils; |  | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
| import org.springframework.cache.annotation.CacheEvict; | import org.springframework.cache.annotation.CacheEvict; | ||||||
|  | @ -24,7 +19,6 @@ import org.springframework.stereotype.Service; | ||||||
| 
 | 
 | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.Exam; | import ch.ethz.seb.sebserver.gbl.model.exam.Exam; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; | import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; | ||||||
|  | @ -63,7 +57,7 @@ public class ExamSessionCacheService { | ||||||
|             cacheNames = CACHE_NAME_RUNNING_EXAM, |             cacheNames = CACHE_NAME_RUNNING_EXAM, | ||||||
|             key = "#examId", |             key = "#examId", | ||||||
|             unless = "#result == null") |             unless = "#result == null") | ||||||
|     Exam getRunningExam(final Long examId) { |     public Exam getRunningExam(final Long examId) { | ||||||
| 
 | 
 | ||||||
|         if (log.isDebugEnabled()) { |         if (log.isDebugEnabled()) { | ||||||
|             log.debug("Verify running exam for id: {}" + examId); |             log.debug("Verify running exam for id: {}" + examId); | ||||||
|  | @ -87,7 +81,7 @@ public class ExamSessionCacheService { | ||||||
|             cacheNames = CACHE_NAME_RUNNING_EXAM, |             cacheNames = CACHE_NAME_RUNNING_EXAM, | ||||||
|             key = "#exam.id", |             key = "#exam.id", | ||||||
|             condition = "#target.isRunning(#result)") |             condition = "#target.isRunning(#result)") | ||||||
|     Exam evict(final Exam exam) { |     public Exam evict(final Exam exam) { | ||||||
| 
 | 
 | ||||||
|         if (log.isDebugEnabled()) { |         if (log.isDebugEnabled()) { | ||||||
|             log.debug("Conditional eviction of running Exam from cache: {}", isRunning(exam)); |             log.debug("Conditional eviction of running Exam from cache: {}", isRunning(exam)); | ||||||
|  | @ -108,7 +102,7 @@ public class ExamSessionCacheService { | ||||||
|             cacheNames = CACHE_NAME_ACTIVE_CLIENT_CONNECTION, |             cacheNames = CACHE_NAME_ACTIVE_CLIENT_CONNECTION, | ||||||
|             key = "#connectionToken", |             key = "#connectionToken", | ||||||
|             unless = "#result == null") |             unless = "#result == null") | ||||||
|     ClientConnectionDataInternal getActiveClientConnection(final String connectionToken) { |     public ClientConnectionDataInternal getActiveClientConnection(final String connectionToken) { | ||||||
| 
 | 
 | ||||||
|         if (log.isDebugEnabled()) { |         if (log.isDebugEnabled()) { | ||||||
|             log.debug("Verify ClientConnection for running exam for caching by connectionToken: ", connectionToken); |             log.debug("Verify ClientConnection for running exam for caching by connectionToken: ", connectionToken); | ||||||
|  | @ -123,20 +117,6 @@ public class ExamSessionCacheService { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         final ClientConnection clientConnection = byPK.get(); |         final ClientConnection clientConnection = byPK.get(); | ||||||
| 
 |  | ||||||
|         // verify connection is established |  | ||||||
|         if (clientConnection.status != ConnectionStatus.ESTABLISHED) { |  | ||||||
|             log.error("Illegal state: ClientConnection is not in expected state; ESTABLISHED. ClientConnection: ", |  | ||||||
|                     clientConnection); |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // verify exam is running |  | ||||||
|         if (getRunningExam(clientConnection.examId) == null) { |  | ||||||
|             log.error("Exam for ClientConnection with id { is not currently running}", clientConnection.id); |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return new ClientConnectionDataInternal( |         return new ClientConnectionDataInternal( | ||||||
|                 clientConnection, |                 clientConnection, | ||||||
|                 this.clientIndicatorFactory.createFor(clientConnection)); |                 this.clientIndicatorFactory.createFor(clientConnection)); | ||||||
|  | @ -145,7 +125,7 @@ public class ExamSessionCacheService { | ||||||
|     @CacheEvict( |     @CacheEvict( | ||||||
|             cacheNames = CACHE_NAME_ACTIVE_CLIENT_CONNECTION, |             cacheNames = CACHE_NAME_ACTIVE_CLIENT_CONNECTION, | ||||||
|             key = "#connectionToken") |             key = "#connectionToken") | ||||||
|     void evictClientConnection(final String connectionToken) { |     public void evictClientConnection(final String connectionToken) { | ||||||
|         if (log.isDebugEnabled()) { |         if (log.isDebugEnabled()) { | ||||||
|             log.debug("Eviction of ClientConnectionData from cache: {}", connectionToken); |             log.debug("Eviction of ClientConnectionData from cache: {}", connectionToken); | ||||||
|         } |         } | ||||||
|  | @ -155,25 +135,20 @@ public class ExamSessionCacheService { | ||||||
|             cacheNames = CACHE_NAME_SEB_CONFIG_EXAM, |             cacheNames = CACHE_NAME_SEB_CONFIG_EXAM, | ||||||
|             key = "#examId", |             key = "#examId", | ||||||
|             unless = "#result == null") |             unless = "#result == null") | ||||||
|     InMemorySebConfig getDefaultSebConfigForExam(final Long examId) { |     public InMemorySebConfig getDefaultSebConfigForExam(final Long examId) { | ||||||
|         final Exam runningExam = this.getRunningExam(examId); |         final Exam runningExam = this.getRunningExam(examId); | ||||||
|         final PipedOutputStream pipOut = new PipedOutputStream(); | 
 | ||||||
|         try { |         try { | ||||||
|             final Long configId = this.sebExamConfigService |  | ||||||
|                     .getDefaultConfigurationIdForExam(runningExam.id) |  | ||||||
|                     .getOrThrow(); |  | ||||||
| 
 |  | ||||||
|             // TODO add header, zip and encrypt if needed |  | ||||||
| 
 |  | ||||||
|             final BufferedInputStream in = new BufferedInputStream(new PipedInputStream(pipOut)); |  | ||||||
|             this.sebExamConfigService.exportPlainXML(pipOut, runningExam.institutionId, configId); |  | ||||||
| 
 | 
 | ||||||
|             final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); |             final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); | ||||||
|             IOUtils.copyLarge(in, byteOut); |             final Long configId = this.sebExamConfigService.exportForExam( | ||||||
|  |                     byteOut, | ||||||
|  |                     runningExam.institutionId, | ||||||
|  |                     examId); | ||||||
| 
 | 
 | ||||||
|             return new InMemorySebConfig(configId, runningExam.id, byteOut.toByteArray()); |             return new InMemorySebConfig(configId, runningExam.id, byteOut.toByteArray()); | ||||||
| 
 | 
 | ||||||
|         } catch (final IOException e) { |         } catch (final Exception e) { | ||||||
|             log.error("Unexpected error while getting default exam configuration for running exam; {}", runningExam, e); |             log.error("Unexpected error while getting default exam configuration for running exam; {}", runningExam, e); | ||||||
|             return null; |             return null; | ||||||
|         } |         } | ||||||
|  | @ -182,7 +157,7 @@ public class ExamSessionCacheService { | ||||||
|     @CacheEvict( |     @CacheEvict( | ||||||
|             cacheNames = CACHE_NAME_SEB_CONFIG_EXAM, |             cacheNames = CACHE_NAME_SEB_CONFIG_EXAM, | ||||||
|             key = "#examId") |             key = "#examId") | ||||||
|     void evictDefaultSebConfig(final Long examId) { |     public void evictDefaultSebConfig(final Long examId) { | ||||||
|         if (log.isDebugEnabled()) { |         if (log.isDebugEnabled()) { | ||||||
|             log.debug("Eviction of default SEB Configuration from cache for exam: {}", examId); |             log.debug("Eviction of default SEB Configuration from cache for exam: {}", examId); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.OutputStream; | import java.io.OutputStream; | ||||||
| import java.util.Collection; | import java.util.Collection; | ||||||
|  | import java.util.Collections; | ||||||
| import java.util.NoSuchElementException; | import java.util.NoSuchElementException; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
| 
 | 
 | ||||||
|  | @ -71,7 +72,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { | ||||||
|             return Result.of(exam); |             return Result.of(exam); | ||||||
|         } else { |         } else { | ||||||
|             if (exam != null) { |             if (exam != null) { | ||||||
|                 this.examSessionCacheService.evict(exam); |                 flushCache(exam); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             log.warn("Exam {} is not currently running", examId); |             log.warn("Exam {} is not currently running", examId); | ||||||
|  | @ -92,7 +93,6 @@ public class ExamSessionServiceImpl implements ExamSessionService { | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void streamDefaultExamConfig( |     public void streamDefaultExamConfig( | ||||||
|             final Long institutionId, |  | ||||||
|             final String connectionToken, |             final String connectionToken, | ||||||
|             final OutputStream out) { |             final OutputStream out) { | ||||||
| 
 | 
 | ||||||
|  | @ -101,7 +101,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         final ClientConnection connection = this.clientConnectionDAO |         final ClientConnection connection = this.clientConnectionDAO | ||||||
|                 .byConnectionToken(institutionId, connectionToken) |                 .byConnectionToken(connectionToken) | ||||||
|                 .getOrThrow(); |                 .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|         if (connection == null || connection.status != ConnectionStatus.ESTABLISHED) { |         if (connection == null || connection.status != ConnectionStatus.ESTABLISHED) { | ||||||
|  | @ -111,17 +111,15 @@ public class ExamSessionServiceImpl implements ExamSessionService { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (log.isDebugEnabled()) { |         if (log.isDebugEnabled()) { | ||||||
|             log.debug("SEB exam configuration download request: {}", connection); |             log.debug("Trying to get exam from InMemorySebConfig"); | ||||||
|             log.debug("Trying to get exam form InMemorySebConfig"); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         final InMemorySebConfig sebConfigForExam = this.examSessionCacheService |         final InMemorySebConfig sebConfigForExam = this.examSessionCacheService | ||||||
|                 .getDefaultSebConfigForExam(connection.examId); |                 .getDefaultSebConfigForExam(connection.examId); | ||||||
| 
 | 
 | ||||||
|         if (log.isDebugEnabled()) { |         if (sebConfigForExam == null) { | ||||||
|             if (sebConfigForExam == null) { |             log.error("Failed to get and cache InMemorySebConfig for connection: {}", connection); | ||||||
|                 log.debug("Failed to get and cache InMemorySebConfig for connection: {}", connection); |             return; | ||||||
|             } |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|  | @ -141,4 +139,13 @@ public class ExamSessionServiceImpl implements ExamSessionService { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private void flushCache(final Exam exam) { | ||||||
|  |         this.examSessionCacheService.evict(exam); | ||||||
|  |         this.examSessionCacheService.evictDefaultSebConfig(exam.id); | ||||||
|  |         this.clientConnectionDAO | ||||||
|  |                 .getConnectionTokens(exam.id) | ||||||
|  |                 .getOrElse(() -> Collections.emptyList()) | ||||||
|  |                 .forEach(token -> this.examSessionCacheService.evictClientConnection(token)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -62,6 +62,7 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | ||||||
|         this.eventHandlingStrategy = applicationContext.getBean( |         this.eventHandlingStrategy = applicationContext.getBean( | ||||||
|                 eventHandlingStrategyProperty, |                 eventHandlingStrategyProperty, | ||||||
|                 EventHandlingStrategy.class); |                 EventHandlingStrategy.class); | ||||||
|  |         this.eventHandlingStrategy.enable(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -90,15 +91,23 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | ||||||
|                     null, |                     null, | ||||||
|                     institutionId, |                     institutionId, | ||||||
|                     examId, |                     examId, | ||||||
|                     ClientConnection.ConnectionStatus.CONNECTION_REQUESTED, |                     ConnectionStatus.CONNECTION_REQUESTED, | ||||||
|                     connectionToken, |                     connectionToken, | ||||||
|                     null, |                     null, | ||||||
|                     clientAddress, |                     clientAddress, | ||||||
|                     null)) |                     null)) | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|             if (log.isDebugEnabled()) { |             // load client connection data into cache | ||||||
|                 log.debug("New ClientConnection created: {}", clientConnection); |             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; |             return clientConnection; | ||||||
|  | @ -109,8 +118,8 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | ||||||
|     public Result<ClientConnection> updateClientConnection( |     public Result<ClientConnection> updateClientConnection( | ||||||
|             final String connectionToken, |             final String connectionToken, | ||||||
|             final Long institutionId, |             final Long institutionId, | ||||||
|             final String clientAddress, |  | ||||||
|             final Long examId, |             final Long examId, | ||||||
|  |             final String clientAddress, | ||||||
|             final String userSessionId) { |             final String userSessionId) { | ||||||
| 
 | 
 | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
|  | @ -131,9 +140,7 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | ||||||
| 
 | 
 | ||||||
|             checkExamRunning(examId); |             checkExamRunning(examId); | ||||||
| 
 | 
 | ||||||
|             final ClientConnection clientConnection = getClientConnection( |             final ClientConnection clientConnection = getClientConnection(connectionToken); | ||||||
|                     connectionToken, |  | ||||||
|                     institutionId); |  | ||||||
| 
 | 
 | ||||||
|             checkInstitutionalIntegrity( |             checkInstitutionalIntegrity( | ||||||
|                     institutionId, |                     institutionId, | ||||||
|  | @ -156,7 +163,15 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | ||||||
|                             virtualClientAddress)) |                             virtualClientAddress)) | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|             if (log.isDebugEnabled()) { |             // evict cached ClientConnection | ||||||
|  |             this.examSessionCacheService.evictClientConnection(connectionToken); | ||||||
|  |             // and load updated ClientConnection into cache | ||||||
|  |             final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService | ||||||
|  |                     .getActiveClientConnection(connectionToken); | ||||||
|  | 
 | ||||||
|  |             if (activeClientConnection == null) { | ||||||
|  |                 log.warn("Failed to load ClientConnectionDataInternal into cache on update"); | ||||||
|  |             } else if (log.isDebugEnabled()) { | ||||||
|                 log.debug("SEB client connection, successfully updated ClientConnection: {}", |                 log.debug("SEB client connection, successfully updated ClientConnection: {}", | ||||||
|                         updatedClientConnection); |                         updatedClientConnection); | ||||||
|             } |             } | ||||||
|  | @ -192,9 +207,7 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | ||||||
| 
 | 
 | ||||||
|             checkExamRunning(examId); |             checkExamRunning(examId); | ||||||
| 
 | 
 | ||||||
|             final ClientConnection clientConnection = getClientConnection( |             final ClientConnection clientConnection = getClientConnection(connectionToken); | ||||||
|                     connectionToken, |  | ||||||
|                     institutionId); |  | ||||||
| 
 | 
 | ||||||
|             checkInstitutionalIntegrity( |             checkInstitutionalIntegrity( | ||||||
|                     institutionId, |                     institutionId, | ||||||
|  | @ -217,42 +230,40 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | ||||||
|                     clientConnection.id, |                     clientConnection.id, | ||||||
|                     null, |                     null, | ||||||
|                     examId, |                     examId, | ||||||
|                     ClientConnection.ConnectionStatus.ESTABLISHED, |                     ConnectionStatus.ESTABLISHED, | ||||||
|                     null, |                     null, | ||||||
|                     userSessionId, |                     userSessionId, | ||||||
|                     null, |                     null, | ||||||
|                     virtualClientAddress); |                     virtualClientAddress); | ||||||
| 
 | 
 | ||||||
|             // ClientConnection integrity |             // ClientConnection integrity | ||||||
|             if (establishedClientConnection.institutionId == null || |             if (clientConnection.institutionId == null || | ||||||
|  |                     clientConnection.connectionToken == null || | ||||||
|                     establishedClientConnection.examId == null || |                     establishedClientConnection.examId == null || | ||||||
|                     establishedClientConnection.clientAddress == null || |                     clientConnection.clientAddress == null || | ||||||
|                     establishedClientConnection.connectionToken == null) { |                     establishedClientConnection.status != ConnectionStatus.ESTABLISHED) { | ||||||
| 
 | 
 | ||||||
|                 log.error("ClientConnection integrity violation: {}", establishedClientConnection); |                 log.error("ClientConnection integrity violation, clientConnection: {}, establishedClientConnection: {}", | ||||||
|                 throw new IllegalStateException("ClientConnection integrity violation: " + establishedClientConnection); |                         clientConnection, | ||||||
|  |                         establishedClientConnection); | ||||||
|  |                 throw new IllegalStateException("ClientConnection integrity violation"); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             final ClientConnection updatedClientConnection = this.clientConnectionDAO |             final ClientConnection updatedClientConnection = this.clientConnectionDAO | ||||||
|                     .save(establishedClientConnection) |                     .save(establishedClientConnection) | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|             if (updatedClientConnection.status == ConnectionStatus.ESTABLISHED) { |             // evict cached ClientConnection | ||||||
|                 // load into cache... |             this.examSessionCacheService.evictClientConnection(connectionToken); | ||||||
|                 final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService |             // and load updated ClientConnection into cache | ||||||
|                         .getActiveClientConnection(updatedClientConnection.connectionToken); |             final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService | ||||||
|  |                     .getActiveClientConnection(connectionToken); | ||||||
| 
 | 
 | ||||||
|                 if (activeClientConnection == null) { |             if (activeClientConnection == null) { | ||||||
|                     log.warn("Unable to access and cache ClientConnection"); |                 log.warn("Failed to load ClientConnectionDataInternal into cache on update"); | ||||||
|                 } |             } else if (log.isDebugEnabled()) { | ||||||
| 
 |                 log.debug("SEB client connection, successfully established ClientConnection: {}", | ||||||
|                 if (log.isDebugEnabled()) { |                         updatedClientConnection); | ||||||
|                     log.debug("ClientConnection: {} successfully established", clientConnection); |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 if (log.isDebugEnabled()) { |  | ||||||
|                     log.debug("ClientConnection: {} updated", clientConnection); |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return updatedClientConnection; |             return updatedClientConnection; | ||||||
|  | @ -278,18 +289,14 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             final ClientConnection clientConnection = this.clientConnectionDAO |             final ClientConnection clientConnection = this.clientConnectionDAO | ||||||
|                     .byConnectionToken(institutionId, connectionToken) |                     .byConnectionToken(connectionToken) | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|             // evict ClientConnection from cache |  | ||||||
|             this.examSessionCacheService |  | ||||||
|                     .evictClientConnection(clientConnection.connectionToken); |  | ||||||
| 
 |  | ||||||
|             final ClientConnection updatedClientConnection = this.clientConnectionDAO.save(new ClientConnection( |             final ClientConnection updatedClientConnection = this.clientConnectionDAO.save(new ClientConnection( | ||||||
|                     clientConnection.id, |                     clientConnection.id, | ||||||
|                     null, |                     null, | ||||||
|                     null, |                     null, | ||||||
|                     ClientConnection.ConnectionStatus.CLOSED, |                     ConnectionStatus.CLOSED, | ||||||
|                     null, |                     null, | ||||||
|                     null, |                     null, | ||||||
|                     null, |                     null, | ||||||
|  | @ -300,6 +307,11 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | ||||||
|                         clientConnection); |                         clientConnection); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             // evict cached ClientConnection | ||||||
|  |             this.examSessionCacheService.evictClientConnection(connectionToken); | ||||||
|  |             // and load updated ClientConnection into cache | ||||||
|  |             this.examSessionCacheService.getActiveClientConnection(connectionToken); | ||||||
|  | 
 | ||||||
|             return updatedClientConnection; |             return updatedClientConnection; | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  | @ -326,12 +338,27 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | ||||||
|             final String connectionToken, |             final String connectionToken, | ||||||
|             final ClientEvent event) { |             final ClientEvent event) { | ||||||
| 
 | 
 | ||||||
|         this.eventHandlingStrategy.accept(event); |  | ||||||
| 
 |  | ||||||
|         final ClientConnectionDataInternal activeClientConnection = |         final ClientConnectionDataInternal activeClientConnection = | ||||||
|                 this.examSessionCacheService.getActiveClientConnection(connectionToken); |                 this.examSessionCacheService.getActiveClientConnection(connectionToken); | ||||||
| 
 | 
 | ||||||
|         if (activeClientConnection != null) { |         if (activeClientConnection != null) { | ||||||
|  |             if (activeClientConnection.clientConnection.status != ConnectionStatus.ESTABLISHED) { | ||||||
|  |                 throw new IllegalStateException("ClientConnection is not fully established or closed"); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // store event | ||||||
|  |             this.eventHandlingStrategy.accept( | ||||||
|  |                     (event.connectionId != null) | ||||||
|  |                             ? event | ||||||
|  |                             : new ClientEvent( | ||||||
|  |                                     null, | ||||||
|  |                                     activeClientConnection.getConnectionId(), | ||||||
|  |                                     event.eventType, | ||||||
|  |                                     event.timestamp, | ||||||
|  |                                     event.numValue, | ||||||
|  |                                     event.text)); | ||||||
|  | 
 | ||||||
|  |             // update indicators | ||||||
|             activeClientConnection.getindicatorMapping(event.eventType) |             activeClientConnection.getindicatorMapping(event.eventType) | ||||||
|                     .stream() |                     .stream() | ||||||
|                     .forEach(indicator -> indicator.notifyValueChange(event)); |                     .forEach(indicator -> indicator.notifyValueChange(event)); | ||||||
|  | @ -344,9 +371,9 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private ClientConnection getClientConnection(final String connectionToken, final Long institutionId) { |     private ClientConnection getClientConnection(final String connectionToken) { | ||||||
|         final ClientConnection clientConnection = this.clientConnectionDAO |         final ClientConnection clientConnection = this.clientConnectionDAO | ||||||
|                 .byConnectionToken(institutionId, connectionToken) |                 .byConnectionToken(connectionToken) | ||||||
|                 .getOrThrow(); |                 .getOrThrow(); | ||||||
|         return clientConnection; |         return clientConnection; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -30,6 +30,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.EventHandlingStrate | ||||||
| public class SingleEventSaveStrategy implements EventHandlingStrategy { | public class SingleEventSaveStrategy implements EventHandlingStrategy { | ||||||
| 
 | 
 | ||||||
|     private final ClientEventDAO clientEventDAO; |     private final ClientEventDAO clientEventDAO; | ||||||
|  |     private boolean enabled = false; | ||||||
| 
 | 
 | ||||||
|     public SingleEventSaveStrategy(final ClientEventDAO clientEventDAO) { |     public SingleEventSaveStrategy(final ClientEventDAO clientEventDAO) { | ||||||
|         this.clientEventDAO = clientEventDAO; |         this.clientEventDAO = clientEventDAO; | ||||||
|  | @ -37,7 +38,24 @@ public class SingleEventSaveStrategy implements EventHandlingStrategy { | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void accept(final ClientEvent event) { |     public void accept(final ClientEvent event) { | ||||||
|         this.clientEventDAO.save(event); |         this.clientEventDAO | ||||||
|  |                 .createNew(event) | ||||||
|  |                 .getOrThrow(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void enable() { | ||||||
|  |         this.enabled = true; | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void disable() { | ||||||
|  |         this.enabled = false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean isEnabled() { | ||||||
|  |         return this.enabled; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ import org.springframework.http.MediaType; | ||||||
| import org.springframework.http.ResponseEntity; | import org.springframework.http.ResponseEntity; | ||||||
| import org.springframework.util.MultiValueMap; | import org.springframework.util.MultiValueMap; | ||||||
| import org.springframework.web.bind.annotation.RequestBody; | import org.springframework.web.bind.annotation.RequestBody; | ||||||
|  | import org.springframework.web.bind.annotation.RequestHeader; | ||||||
| import org.springframework.web.bind.annotation.RequestMapping; | import org.springframework.web.bind.annotation.RequestMapping; | ||||||
| import org.springframework.web.bind.annotation.RequestMethod; | import org.springframework.web.bind.annotation.RequestMethod; | ||||||
| import org.springframework.web.bind.annotation.RequestParam; | import org.springframework.web.bind.annotation.RequestParam; | ||||||
|  | @ -31,6 +32,8 @@ import org.springframework.web.bind.annotation.RestController; | ||||||
| import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; | import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; | ||||||
| 
 | 
 | ||||||
| import ch.ethz.seb.sebserver.gbl.api.API; | import ch.ethz.seb.sebserver.gbl.api.API; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.api.APIMessage; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | ||||||
| import ch.ethz.seb.sebserver.gbl.api.POSTMapper; | import ch.ethz.seb.sebserver.gbl.api.POSTMapper; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.Exam; | import ch.ethz.seb.sebserver.gbl.model.exam.Exam; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; | import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; | ||||||
|  | @ -38,6 +41,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.session.PingResponse; | import ch.ethz.seb.sebserver.gbl.model.session.PingResponse; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.session.RunningExam; | import ch.ethz.seb.sebserver.gbl.model.session.RunningExam; | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SebClientConfigDAO; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SebClientConfigDAO; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; | import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; | ||||||
|  | @ -54,17 +58,20 @@ public class ExamAPI_V1_Controller { | ||||||
|     private final ExamSessionService examSessionService; |     private final ExamSessionService examSessionService; | ||||||
|     private final SebClientConnectionService sebClientConnectionService; |     private final SebClientConnectionService sebClientConnectionService; | ||||||
|     private final SebClientConfigDAO sebClientConfigDAO; |     private final SebClientConfigDAO sebClientConfigDAO; | ||||||
|  |     private final JSONMapper jsonMapper; | ||||||
| 
 | 
 | ||||||
|     protected ExamAPI_V1_Controller( |     protected ExamAPI_V1_Controller( | ||||||
|             final ExamDAO examDAO, |             final ExamDAO examDAO, | ||||||
|             final ExamSessionService examSessionService, |             final ExamSessionService examSessionService, | ||||||
|             final SebClientConnectionService sebClientConnectionService, |             final SebClientConnectionService sebClientConnectionService, | ||||||
|             final SebClientConfigDAO sebClientConfigDAO) { |             final SebClientConfigDAO sebClientConfigDAO, | ||||||
|  |             final JSONMapper jsonMapper) { | ||||||
| 
 | 
 | ||||||
|         this.examDAO = examDAO; |         this.examDAO = examDAO; | ||||||
|         this.examSessionService = examSessionService; |         this.examSessionService = examSessionService; | ||||||
|         this.sebClientConnectionService = sebClientConnectionService; |         this.sebClientConnectionService = sebClientConnectionService; | ||||||
|         this.sebClientConfigDAO = sebClientConfigDAO; |         this.sebClientConfigDAO = sebClientConfigDAO; | ||||||
|  |         this.jsonMapper = jsonMapper; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @RequestMapping( |     @RequestMapping( | ||||||
|  | @ -143,8 +150,8 @@ public class ExamAPI_V1_Controller { | ||||||
|             method = RequestMethod.PATCH, |             method = RequestMethod.PATCH, | ||||||
|             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) |             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) | ||||||
|     public void handshakeUpdate( |     public void handshakeUpdate( | ||||||
|  |             @RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||||
|             @RequestParam(name = API.EXAM_API_PARAM_EXAM_ID, required = false) final Long examId, |             @RequestParam(name = API.EXAM_API_PARAM_EXAM_ID, required = false) final Long examId, | ||||||
|             @RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, |  | ||||||
|             @RequestParam(name = API.EXAM_API_USER_SESSION_ID, required = false) final String userSessionId, |             @RequestParam(name = API.EXAM_API_USER_SESSION_ID, required = false) final String userSessionId, | ||||||
|             final Principal principal, |             final Principal principal, | ||||||
|             final HttpServletRequest request) { |             final HttpServletRequest request) { | ||||||
|  | @ -164,7 +171,7 @@ public class ExamAPI_V1_Controller { | ||||||
|                     remoteAddr); |                     remoteAddr); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.sebClientConnectionService.establishClientConnection( |         this.sebClientConnectionService.updateClientConnection( | ||||||
|                 connectionToken, |                 connectionToken, | ||||||
|                 institutionId, |                 institutionId, | ||||||
|                 examId, |                 examId, | ||||||
|  | @ -178,8 +185,8 @@ public class ExamAPI_V1_Controller { | ||||||
|             method = RequestMethod.PUT, |             method = RequestMethod.PUT, | ||||||
|             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) |             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) | ||||||
|     public void handshakeEstablish( |     public void handshakeEstablish( | ||||||
|  |             @RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||||
|             @RequestParam(name = API.EXAM_API_PARAM_EXAM_ID, required = false) final Long examId, |             @RequestParam(name = API.EXAM_API_PARAM_EXAM_ID, required = false) final Long examId, | ||||||
|             @RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, |  | ||||||
|             @RequestParam(name = API.EXAM_API_USER_SESSION_ID, required = false) final String userSessionId, |             @RequestParam(name = API.EXAM_API_USER_SESSION_ID, required = false) final String userSessionId, | ||||||
|             final Principal principal, |             final Principal principal, | ||||||
|             final HttpServletRequest request) { |             final HttpServletRequest request) { | ||||||
|  | @ -210,8 +217,8 @@ public class ExamAPI_V1_Controller { | ||||||
|             path = API.EXAM_API_HANDSHAKE_ENDPOINT, |             path = API.EXAM_API_HANDSHAKE_ENDPOINT, | ||||||
|             method = RequestMethod.DELETE, |             method = RequestMethod.DELETE, | ||||||
|             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) |             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) | ||||||
|     public void handshakeEstablish( |     public void handshakeDelete( | ||||||
|             @RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, |             @RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||||
|             final Principal principal, |             final Principal principal, | ||||||
|             final HttpServletRequest request) { |             final HttpServletRequest request) { | ||||||
| 
 | 
 | ||||||
|  | @ -239,25 +246,30 @@ public class ExamAPI_V1_Controller { | ||||||
|             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, |             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, | ||||||
|             produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) |             produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) | ||||||
|     public ResponseEntity<StreamingResponseBody> getConfig( |     public ResponseEntity<StreamingResponseBody> getConfig( | ||||||
|             @RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, |             @RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken) { | ||||||
|             final Principal principal) { |  | ||||||
| 
 | 
 | ||||||
|         final Long institutionId = getInstitutionId(principal); |         final StreamingResponseBody stream = out -> { | ||||||
|         final StreamingResponseBody stream = out -> this.examSessionService.streamDefaultExamConfig( |             try { | ||||||
|                 institutionId, |                 this.examSessionService | ||||||
|                 connectionToken, |                         .streamDefaultExamConfig( | ||||||
|                 out); |                                 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); |         return new ResponseEntity<>(stream, HttpStatus.OK); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @RequestMapping( |     @RequestMapping( | ||||||
|             path = API.EXAM_API_PING_ENDPOINT, |             path = API.EXAM_API_PING_ENDPOINT, | ||||||
|             method = RequestMethod.PUT, |             method = RequestMethod.POST, | ||||||
|             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, |             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, | ||||||
|             produces = MediaType.APPLICATION_JSON_UTF8_VALUE) |             produces = MediaType.APPLICATION_JSON_UTF8_VALUE) | ||||||
|     public PingResponse ping( |     public PingResponse ping( | ||||||
|             @RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, |             @RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||||
|             @RequestParam(name = API.EXAM_API_PING_TIMESTAMP, required = true) final long timestamp, |             @RequestParam(name = API.EXAM_API_PING_TIMESTAMP, required = true) final long timestamp, | ||||||
|             @RequestParam(name = API.EXAM_API_PING_NUMBER, required = false) final int pingNumber) { |             @RequestParam(name = API.EXAM_API_PING_NUMBER, required = false) final int pingNumber) { | ||||||
| 
 | 
 | ||||||
|  | @ -272,9 +284,9 @@ public class ExamAPI_V1_Controller { | ||||||
|     @RequestMapping( |     @RequestMapping( | ||||||
|             path = API.EXAM_API_EVENT_ENDPOINT, |             path = API.EXAM_API_EVENT_ENDPOINT, | ||||||
|             method = RequestMethod.POST, |             method = RequestMethod.POST, | ||||||
|             consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) |             consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) | ||||||
|     public void event( |     public void event( | ||||||
|             @RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, |             @RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, | ||||||
|             @RequestBody(required = true) final ClientEvent event) { |             @RequestBody(required = true) final ClientEvent event) { | ||||||
| 
 | 
 | ||||||
|         this.sebClientConnectionService.notifyClientEvent(connectionToken, event); |         this.sebClientConnectionService.notifyClientEvent(connectionToken, event); | ||||||
|  |  | ||||||
|  | @ -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; | package ch.ethz.seb.sebserver.webservice.integration.api.exam; | ||||||
| 
 | 
 | ||||||
| import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; | ||||||
| import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; | ||||||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; | ||||||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||||||
| 
 | 
 | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| 
 | 
 | ||||||
|  | import org.apache.commons.lang3.StringUtils; | ||||||
|  | import org.joda.time.DateTime; | ||||||
|  | import org.joda.time.DateTimeZone; | ||||||
| import org.junit.Before; | import org.junit.Before; | ||||||
| import org.junit.runner.RunWith; | import org.junit.runner.RunWith; | ||||||
| import org.mockito.Mockito; | import org.mockito.Mockito; | ||||||
|  | @ -25,6 +28,9 @@ import org.springframework.boot.json.JacksonJsonParser; | ||||||
| import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; | ||||||
| import org.springframework.boot.test.context.SpringBootTest; | import org.springframework.boot.test.context.SpringBootTest; | ||||||
| import org.springframework.boot.test.mock.mockito.MockBean; | import org.springframework.boot.test.mock.mockito.MockBean; | ||||||
|  | import org.springframework.cache.CacheManager; | ||||||
|  | import org.springframework.http.MediaType; | ||||||
|  | import org.springframework.mock.web.MockHttpServletResponse; | ||||||
| import org.springframework.security.crypto.password.PasswordEncoder; | import org.springframework.security.crypto.password.PasswordEncoder; | ||||||
| import org.springframework.security.oauth2.provider.ClientDetails; | import org.springframework.security.oauth2.provider.ClientDetails; | ||||||
| import org.springframework.security.oauth2.provider.client.BaseClientDetails; | import org.springframework.security.oauth2.provider.client.BaseClientDetails; | ||||||
|  | @ -32,7 +38,9 @@ import org.springframework.security.web.FilterChainProxy; | ||||||
| import org.springframework.test.context.ActiveProfiles; | import org.springframework.test.context.ActiveProfiles; | ||||||
| import org.springframework.test.context.junit4.SpringRunner; | import org.springframework.test.context.junit4.SpringRunner; | ||||||
| import org.springframework.test.web.servlet.MockMvc; | import org.springframework.test.web.servlet.MockMvc; | ||||||
|  | import org.springframework.test.web.servlet.MvcResult; | ||||||
| import org.springframework.test.web.servlet.ResultActions; | import org.springframework.test.web.servlet.ResultActions; | ||||||
|  | import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; | ||||||
| import org.springframework.test.web.servlet.setup.MockMvcBuilders; | import org.springframework.test.web.servlet.setup.MockMvcBuilders; | ||||||
| import org.springframework.util.LinkedMultiValueMap; | import org.springframework.util.LinkedMultiValueMap; | ||||||
| import org.springframework.util.MultiValueMap; | import org.springframework.util.MultiValueMap; | ||||||
|  | @ -40,6 +48,7 @@ import org.springframework.web.context.WebApplicationContext; | ||||||
| 
 | 
 | ||||||
| import ch.ethz.seb.sebserver.SEBServer; | import ch.ethz.seb.sebserver.SEBServer; | ||||||
| import ch.ethz.seb.sebserver.WebSecurityConfig; | import ch.ethz.seb.sebserver.WebSecurityConfig; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.api.API; | ||||||
| import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebClientConfigService; | import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebClientConfigService; | ||||||
| import ch.ethz.seb.sebserver.webservice.weblayer.oauth.AdminAPIClientDetails; | import ch.ethz.seb.sebserver.webservice.weblayer.oauth.AdminAPIClientDetails; | ||||||
|  | @ -66,6 +75,9 @@ public abstract class ExamAPIIntegrationTester { | ||||||
| 
 | 
 | ||||||
|     protected MockMvc mockMvc; |     protected MockMvc mockMvc; | ||||||
| 
 | 
 | ||||||
|  |     @Autowired | ||||||
|  |     protected CacheManager cacheManager; | ||||||
|  | 
 | ||||||
|     @MockBean |     @MockBean | ||||||
|     public WebClientDetailsService webClientDetailsService; |     public WebClientDetailsService webClientDetailsService; | ||||||
| 
 | 
 | ||||||
|  | @ -75,6 +87,12 @@ public abstract class ExamAPIIntegrationTester { | ||||||
|                 .addFilter(this.springSecurityFilterChain).build(); |                 .addFilter(this.springSecurityFilterChain).build(); | ||||||
|         Mockito.when(this.webClientDetailsService.loadClientByClientId(Mockito.anyString())).thenReturn( |         Mockito.when(this.webClientDetailsService.loadClientByClientId(Mockito.anyString())).thenReturn( | ||||||
|                 getForExamClientAPI()); |                 getForExamClientAPI()); | ||||||
|  | 
 | ||||||
|  |         // clear all caches before a test | ||||||
|  |         this.cacheManager.getCacheNames() | ||||||
|  |                 .stream() | ||||||
|  |                 .map(name -> this.cacheManager.getCache(name)) | ||||||
|  |                 .forEach(cache -> cache.clear()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected ClientDetails getForExamClientAPI() { |     protected ClientDetails getForExamClientAPI() { | ||||||
|  | @ -103,9 +121,9 @@ public abstract class ExamAPIIntegrationTester { | ||||||
|         final ResultActions result = this.mockMvc.perform(post("/oauth/token") |         final ResultActions result = this.mockMvc.perform(post("/oauth/token") | ||||||
|                 .params(params) |                 .params(params) | ||||||
|                 .with(httpBasic(clientId, clientSecret)) |                 .with(httpBasic(clientId, clientSecret)) | ||||||
|                 .accept("application/json;charset=UTF-8")) |                 .accept(MediaType.APPLICATION_JSON_UTF8_VALUE)) | ||||||
|                 .andExpect(status().isOk()) |                 .andExpect(status().isOk()) | ||||||
|                 .andExpect(content().contentType("application/json;charset=UTF-8")); |                 .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)); | ||||||
| 
 | 
 | ||||||
|         final String resultString = result.andReturn().getResponse().getContentAsString(); |         final String resultString = result.andReturn().getResponse().getContentAsString(); | ||||||
| 
 | 
 | ||||||
|  | @ -113,6 +131,152 @@ public abstract class ExamAPIIntegrationTester { | ||||||
|         return jsonParser.parseMap(resultString).get("access_token").toString(); |         return jsonParser.parseMap(resultString).get("access_token").toString(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     protected MockHttpServletResponse createConnection( | ||||||
|  |             final String accessToken, | ||||||
|  |             final Long institutionId, | ||||||
|  |             final Long examId) throws Exception { | ||||||
|  | 
 | ||||||
|  |         final MockHttpServletRequestBuilder builder = get(this.endpoint + "/handshake") | ||||||
|  |                 .header("Content-Type", "application/x-www-form-urlencoded") | ||||||
|  |                 .header("Authorization", "Bearer " + accessToken) | ||||||
|  |                 .accept(MediaType.APPLICATION_JSON_UTF8_VALUE); | ||||||
|  | 
 | ||||||
|  |         String body = ""; | ||||||
|  | 
 | ||||||
|  |         if (institutionId != null) { | ||||||
|  |             body += "institutionId=" + institutionId; | ||||||
|  |         } | ||||||
|  |         if (examId != null) { | ||||||
|  |             body += "&examId=" + examId; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         builder.content(body); | ||||||
|  | 
 | ||||||
|  |         final ResultActions result = this.mockMvc.perform(builder) | ||||||
|  |                 .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)); | ||||||
|  | 
 | ||||||
|  |         return result.andReturn().getResponse(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected MockHttpServletResponse updateConnection( | ||||||
|  |             final String accessToken, | ||||||
|  |             final String connectionToken, | ||||||
|  |             final Long examId, | ||||||
|  |             final String userSessionId) throws Exception { | ||||||
|  | 
 | ||||||
|  |         return updateConnection(accessToken, connectionToken, examId, userSessionId, false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected MockHttpServletResponse establishConnection( | ||||||
|  |             final String accessToken, | ||||||
|  |             final String connectionToken, | ||||||
|  |             final Long examId, | ||||||
|  |             final String userSessionId) throws Exception { | ||||||
|  | 
 | ||||||
|  |         return updateConnection(accessToken, connectionToken, examId, userSessionId, true); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected MockHttpServletResponse updateConnection( | ||||||
|  |             final String accessToken, | ||||||
|  |             final String connectionToken, | ||||||
|  |             final Long examId, | ||||||
|  |             final String userSessionId, | ||||||
|  |             final boolean establish) throws Exception { | ||||||
|  | 
 | ||||||
|  |         final MockHttpServletRequestBuilder builder = (establish) | ||||||
|  |                 ? put(this.endpoint + "/handshake") | ||||||
|  |                         .header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE) | ||||||
|  |                         .header("Authorization", "Bearer " + accessToken) | ||||||
|  |                         .header(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken) | ||||||
|  |                         .accept(MediaType.APPLICATION_JSON_UTF8_VALUE) | ||||||
|  |                 : patch(this.endpoint + "/handshake") | ||||||
|  |                         .header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE) | ||||||
|  |                         .header("Authorization", "Bearer " + accessToken) | ||||||
|  |                         .header(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken) | ||||||
|  |                         .accept(MediaType.APPLICATION_JSON_UTF8_VALUE); | ||||||
|  | 
 | ||||||
|  |         String body = ""; | ||||||
|  |         if (examId != null) { | ||||||
|  |             body += "examId=" + examId; | ||||||
|  |         } | ||||||
|  |         if (userSessionId != null) { | ||||||
|  |             if (!StringUtils.isBlank(body)) { | ||||||
|  |                 body += "&"; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             body += API.EXAM_API_USER_SESSION_ID + "=" + userSessionId; | ||||||
|  |         } | ||||||
|  |         builder.content(body); | ||||||
|  | 
 | ||||||
|  |         final ResultActions result = this.mockMvc.perform(builder); | ||||||
|  | 
 | ||||||
|  |         return result.andReturn().getResponse(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected MockHttpServletResponse closeConnection(final String accessToken, final String connectionToken) | ||||||
|  |             throws Exception { | ||||||
|  |         final MockHttpServletRequestBuilder builder = delete(this.endpoint + "/handshake") | ||||||
|  |                 .header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE) | ||||||
|  |                 .header("Authorization", "Bearer " + accessToken) | ||||||
|  |                 .header(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken) | ||||||
|  |                 .accept(MediaType.APPLICATION_JSON_UTF8_VALUE); | ||||||
|  |         final ResultActions result = this.mockMvc.perform(builder); | ||||||
|  |         return result.andReturn().getResponse(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected MockHttpServletResponse sendPing( | ||||||
|  |             final String accessToken, | ||||||
|  |             final String connectionToken, | ||||||
|  |             final int num) throws Exception { | ||||||
|  |         final MockHttpServletRequestBuilder builder = post(this.endpoint + API.EXAM_API_PING_ENDPOINT) | ||||||
|  |                 .header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE) | ||||||
|  |                 .header("Authorization", "Bearer " + accessToken) | ||||||
|  |                 .header(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken) | ||||||
|  |                 .accept(MediaType.APPLICATION_JSON_UTF8_VALUE); | ||||||
|  | 
 | ||||||
|  |         final String body = API.EXAM_API_PING_TIMESTAMP + "=" + DateTime.now(DateTimeZone.UTC).getMillis() | ||||||
|  |                 + "&" + API.EXAM_API_PING_NUMBER + "=" + num; | ||||||
|  |         builder.content(body); | ||||||
|  | 
 | ||||||
|  |         final ResultActions result = this.mockMvc.perform(builder); | ||||||
|  |         return result.andReturn().getResponse(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected MockHttpServletResponse sendEvent( | ||||||
|  |             final String accessToken, | ||||||
|  |             final String connectionToken, | ||||||
|  |             final String type, | ||||||
|  |             final long timestamp, | ||||||
|  |             final double value, | ||||||
|  |             final String text) throws Exception { | ||||||
|  | 
 | ||||||
|  |         final MockHttpServletRequestBuilder builder = post(this.endpoint + API.EXAM_API_EVENT_ENDPOINT) | ||||||
|  |                 .header("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE) | ||||||
|  |                 .header("Authorization", "Bearer " + accessToken) | ||||||
|  |                 .header(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken) | ||||||
|  |                 .accept(MediaType.APPLICATION_JSON_UTF8_VALUE); | ||||||
|  | 
 | ||||||
|  |         final String body = "{ \"type\": \"%s\", \"timestamp\": %s, \"numericValue\": %s, \"text\": \"%s\" }"; | ||||||
|  |         builder.content(String.format(body, type, timestamp, value, text)); | ||||||
|  |         final ResultActions result = this.mockMvc.perform(builder); | ||||||
|  |         return result.andReturn().getResponse(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected MockHttpServletResponse getExamConfig(final String accessToken, final String connectionToken) | ||||||
|  |             throws Exception { | ||||||
|  |         final MockHttpServletRequestBuilder builder = get(this.endpoint + API.EXAM_API_CONFIGURATION_REQUEST_ENDPOINT) | ||||||
|  |                 .header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE) | ||||||
|  |                 .header("Authorization", "Bearer " + accessToken) | ||||||
|  |                 .header(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken) | ||||||
|  |                 .accept(MediaType.APPLICATION_OCTET_STREAM_VALUE); | ||||||
|  | 
 | ||||||
|  |         final ResultActions result = this.mockMvc | ||||||
|  |                 .perform(builder) | ||||||
|  |                 .andDo(MvcResult::getAsyncResult); | ||||||
|  | 
 | ||||||
|  |         return result.andReturn().getResponse(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Autowired |     @Autowired | ||||||
|     AdminAPIClientDetails adminClientDetails; |     AdminAPIClientDetails adminClientDetails; | ||||||
|     @Autowired |     @Autowired | ||||||
|  |  | ||||||
|  | @ -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.endpoint.v1=${sebserver.webservice.api.exam.endpoint}/v1 | ||||||
| sebserver.webservice.api.exam.accessTokenValiditySeconds=1800 | sebserver.webservice.api.exam.accessTokenValiditySeconds=1800 | ||||||
| sebserver.webservice.api.exam.refreshTokenValiditySeconds=-1 | sebserver.webservice.api.exam.refreshTokenValiditySeconds=-1 | ||||||
| sebserver.webservice.internalSecret=TO_SET |  | ||||||
| sebserver.webservice.api.redirect.unauthorized=none | sebserver.webservice.api.redirect.unauthorized=none | ||||||
| # comma separated list of known possible OpenEdX API access token request endpoints | # comma separated list of known possible OpenEdX API access token request endpoints | ||||||
| sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token | sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 anhefti
						anhefti