From fb8df62f5959597129f173df4d6631030aee24d1 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 4 Jul 2024 15:29:43 +0200 Subject: [PATCH] SEBSERV-419 implementation and local testing --- .../seb/sebserver/gbl/model/exam/Exam.java | 4 + .../MonitoringProctoringService.java | 6 +- .../webservice/servicelayer/dao/ExamDAO.java | 7 +- .../servicelayer/dao/impl/ExamDAOImpl.java | 27 +- .../session/impl/ExamSessionCacheService.java | 3 +- .../InternalClientConnectionDataFactory.java | 2 +- .../impl/SEBClientPingBatchService.java | 67 ++- .../RemoteProctoringRoomServiceImpl.java | 74 +-- .../ScreenProctoringAPIBinding.java | 75 ++- .../api/ExamMonitoringController.java | 1 + .../integration/UseCasesIntegrationTest.java | 461 +++++++++--------- 11 files changed, 409 insertions(+), 318 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java index 782687a52..465a258b3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java @@ -86,6 +86,10 @@ public enum ExamType { ExamStatus.TEST_RUN.name(), ExamStatus.RUNNING.name()); + public static final List RUNNING_STATE_NAMES = Arrays.asList( + ExamStatus.TEST_RUN.name(), + ExamStatus.RUNNING.name()); + @JsonProperty(EXAM.ATTR_ID) public final Long id; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java index 22fa09d22..2192e439b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java @@ -202,10 +202,10 @@ private void updateScreenProctoringAction( final EntityKey entityKey = pageContext.getEntityKey(); final I18nSupport i18nSupport = this.pageService.getI18nSupport(); - final TreeItem screeProcotringGroupAction = proctoringGUIService.getScreeProcotringGroupAction(group); - if (screeProcotringGroupAction != null) { + final TreeItem screeProctoringGroupAction = proctoringGUIService.getScreeProcotringGroupAction(group); + if (screeProctoringGroupAction != null) { // update action - screeProcotringGroupAction.setText(i18nSupport.getText(new LocTextKey( + screeProctoringGroupAction.setText(i18nSupport.getText(new LocTextKey( ActionDefinition.MONITOR_EXAM_VIEW_SCREEN_PROCTOR_GROUP.title.name, group.name, group.size))); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java index 0ccfc6e2f..ac504cec7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java @@ -9,7 +9,6 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao; import java.util.Collection; -import java.util.List; import java.util.function.Predicate; import org.springframework.cache.annotation.CacheEvict; @@ -124,6 +123,12 @@ Result> getExamsForStatus( * @return Result refer to a collection of exams or to an error if happened */ Result> allThatNeedsStatusUpdate(long leadTime, long followupTime); + /** Quickly checks if an Exam is running or not (Sate RUNNING or TEST_RUN) + * + * @param examId The identifier of the exam to check + * @return true if the exam is in a running state */ + boolean isRunning(Long examId); + /** Get a collection of all currently running exam identifiers * * @return collection of all currently running exam identifiers */ diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java index 927c1eada..0a9ddbba8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java @@ -335,7 +335,7 @@ public Result> allRunningExamIds() { isEqualTo(BooleanUtils.toInteger(true))) .and( ExamRecordDynamicSqlSupport.status, - isEqualTo(ExamStatus.RUNNING.name())) + isIn(Exam.RUNNING_STATE_NAMES)) .and( ExamRecordDynamicSqlSupport.updating, isEqualTo(BooleanUtils.toInteger(false))) @@ -392,6 +392,27 @@ public Result> allThatNeedsStatusUpdate(final long leadTime, fi .flatMap(this::toDomainModel); } + @Override + @Transactional(readOnly = true) + public boolean isRunning(final Long examId) { + try { + final Long exists = this.examRecordMapper.countByExample() + .where( + id, + isEqualTo(examId)) + .and( + status, + isIn(Exam.RUNNING_STATE_NAMES)) + .build() + .execute(); + + return exists >= 1; + } catch (final Exception e) { + log.error("Failed to check if exam is running: {} error: {}", examId, e.getMessage()); + return false; + } + } + @Override @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) public Result placeLock(final Long examId, final String updateId) { @@ -736,7 +757,7 @@ public Result> allIdsOfRunningWithScreenProctoringEnabled() { isEqualToWhenPresent(BooleanUtils.toIntegerObject(true))) .and( ExamRecordDynamicSqlSupport.status, - isEqualTo(ExamStatus.RUNNING.name())) + isIn(Exam.RUNNING_STATE_NAMES)) .build() .execute(); }); @@ -754,7 +775,7 @@ public Result> allIdsOfRunning(final Long institutionId) { isEqualToWhenPresent(BooleanUtils.toIntegerObject(true))) .and( ExamRecordDynamicSqlSupport.status, - isEqualTo(ExamStatus.RUNNING.name())) + isIn(Exam.RUNNING_STATE_NAMES)) .and( ExamRecordDynamicSqlSupport.lmsAvailable, isEqualToWhenPresent(BooleanUtils.toIntegerObject(true))) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java index a788426a0..dc7f35060 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java @@ -137,7 +137,8 @@ public ClientConnectionDataInternal getClientConnection(final String connectionT } final ClientConnection clientConnection = getClientConnectionByToken(connectionToken); - if (clientConnection == null) { + // TODO check running exam within cache instead of DB call + if (clientConnection == null || (clientConnection.examId != null && !examDAO.isRunning(clientConnection.examId))) { return null; } else { return this.internalClientConnectionDataFactory.createClientConnectionData(clientConnection); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/InternalClientConnectionDataFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/InternalClientConnectionDataFactory.java index 48f94fe95..358c9e670 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/InternalClientConnectionDataFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/InternalClientConnectionDataFactory.java @@ -52,7 +52,7 @@ public InternalClientConnectionDataFactory( public ClientConnectionDataInternal createClientConnectionData(final ClientConnection clientConnection) { - ClientConnectionDataInternal result; + final ClientConnectionDataInternal result; if (clientConnection.status == ConnectionStatus.CLOSED || clientConnection.status == ConnectionStatus.DISABLED) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientPingBatchService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientPingBatchService.java index 2641b70f2..77b98f331 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientPingBatchService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientPingBatchService.java @@ -12,21 +12,18 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.concurrent.ScheduledFuture; - -import javax.annotation.PreDestroy; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; import org.apache.commons.lang3.StringUtils; import org.ehcache.impl.internal.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.stereotype.Component; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; @@ -44,6 +41,7 @@ public class SEBClientPingBatchService implements SEBClientPingService { private final ExamSessionCacheService examSessionCacheService; private final SEBClientInstructionService sebClientInstructionService; + private final ClientConnectionDAO clientConnectionDAO; private final Set pingKeys = new HashSet<>(); private final Map pings = new ConcurrentHashMap<>(); @@ -51,10 +49,12 @@ public class SEBClientPingBatchService implements SEBClientPingService { public SEBClientPingBatchService( final ExamSessionCacheService examSessionCacheService, - final SEBClientInstructionService sebClientInstructionService) { + final SEBClientInstructionService sebClientInstructionService, + final ClientConnectionDAO clientConnectionDAO) { this.examSessionCacheService = examSessionCacheService; this.sebClientInstructionService = sebClientInstructionService; + this.clientConnectionDAO = clientConnectionDAO; } @Scheduled(fixedDelayString = "${sebserver.webservice.api.exam.session.ping.batch.interval:500}") @@ -71,7 +71,7 @@ public void processPings() { try { this.pingKeys.clear(); this.pingKeys.addAll(this.pings.keySet()); - this.pingKeys.stream().forEach(cid -> processPing( + this.pingKeys.forEach(cid -> processPing( cid, this.pings.remove(cid), Utils.getMillisecondsNow())); @@ -94,10 +94,8 @@ public final String notifyPing( final String instruction = this.instructions.remove(connectionToken); if (instructionConfirm != null) { - System.out.println("************ put instructionConfirm: " + instructionConfirm + " instructions: " - + this.instructions); + this.pings.put(connectionToken, instructionConfirm); -// // TODO is this a good idea or is there another better way to deal with instruction confirm synchronization? if (instruction != null && instruction.contains("\"instruction-confirm\":\"" + instructionConfirm + "\"")) { return null; } @@ -105,9 +103,6 @@ public final String notifyPing( this.pings.put(connectionToken, StringUtils.EMPTY); } -// System.out.println( -// "**************** notifyPing instructionConfirm: " + instructionConfirm + " pings: " + this.pings); - return instruction; } @@ -126,19 +121,13 @@ private void processPing( if (connectionData != null) { if (connectionData.clientConnection.status == ClientConnection.ConnectionStatus.DISABLED) { // SEBSERV-440 send quit instruction to SEB - sebClientInstructionService.registerInstruction( - connectionData.clientConnection.examId, - ClientInstruction.InstructionType.SEB_QUIT, - Collections.emptyMap(), - connectionData.clientConnection.connectionToken, - false, - false - ); + sendQuitInstruction(connectionToken, connectionData.clientConnection.examId); } connectionData.notifyPing(timestamp); } else { - log.error("Failed to get ClientConnectionDataInternal for: {}", connectionToken); + log.warn("Failed to get ClientConnectionDataInternal probably due to finished Exam for: {}.", connectionToken); + sendQuitInstruction(connectionToken,null); } if (StringUtils.isNotBlank(instructionConfirm)) { @@ -154,4 +143,38 @@ private void processPing( this.instructions.put(connectionToken, instructionJSON); } } + + private void sendQuitInstruction(final String connectionToken, final Long examId) { + + Long _examId = examId; + if (examId == null) { + final Result clientConnectionResult = clientConnectionDAO + .byConnectionToken(connectionToken); + + if (clientConnectionResult.hasError()) { + log.error( + "Failed to get examId for client connection token: {} error: {}", + connectionToken, + clientConnectionResult.getError().getMessage()); + } + + _examId = clientConnectionResult.get().examId; + } + + if (_examId != null) { + + log.info("Send automated quit instruction to SEB for connection token: {}", connectionToken); + + // TODO add SEB event log that SEB Server has automatically send quit instruction to SEB + + sebClientInstructionService.registerInstruction( + _examId, + ClientInstruction.InstructionType.SEB_QUIT, + Collections.emptyMap(), + connectionToken, + false, + false + ); + } + } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/RemoteProctoringRoomServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/RemoteProctoringRoomServiceImpl.java index d32d9e31c..31b407722 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/RemoteProctoringRoomServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/RemoteProctoringRoomServiceImpl.java @@ -115,7 +115,7 @@ public Result> getActiveCollectingRoomConnections( final Collection currentlyInBreakoutRooms = this.remoteProctoringRoomDAO .getConnectionsInBreakoutRooms(examId) - .getOrElse(() -> Collections.emptyList()); + .getOrElse(Collections::emptyList); if (currentlyInBreakoutRooms.isEmpty()) { return this.clientConnectionDAO @@ -132,52 +132,58 @@ public Result> getActiveCollectingRoomConnections( @Override public void updateProctoringCollectingRooms() { - try { - - // Applying to collecting room - this.clientConnectionDAO - .getAllForProctoringUpdateActive() - .getOrThrow() - .stream() - .forEach(this::assignToCollectingRoom); - - // Dispose from collecting room - this.clientConnectionDAO - .getAllForProctoringUpdateInactive() - .getOrThrow() - .stream() - .forEach(this::removeFromRoom); - } catch (final Exception e) { - log.error("Unexpected error while trying to update proctoring collecting rooms: ", e); - } + // NOTE: Since life proctoring is not supported anymore, we disable automated updates here + +// try { +// +// // Applying to collecting room +// this.clientConnectionDAO +// .getAllForProctoringUpdateActive() +// .getOrThrow() +// .forEach(this::assignToCollectingRoom); +// +// // Dispose from collecting room +// this.clientConnectionDAO +// .getAllForProctoringUpdateInactive() +// .getOrThrow() +// .forEach(this::removeFromRoom); +// +// } catch (final Exception e) { +// log.error("Unexpected error while trying to update proctoring collecting rooms: ", e); +// } } @EventListener(ExamDeletionEvent.class) public void notifyExamDeletionEvent(final ExamDeletionEvent event) { - event.ids.forEach(examId -> { - try { - this.examAdminService - .examForPK(examId) - .flatMap(this::disposeRoomsForExam) - .getOrThrow(); + // NOTE: Since life proctoring is not supported anymore, we disable automated updates here - } catch (final Exception e) { - log.error("Failed to delete depending proctoring data for exam: {}", examId, e); - } - }); +// event.ids.forEach(examId -> { +// try { +// +// this.examAdminService +// .examForPK(examId) +// .flatMap(this::disposeRoomsForExam) +// .getOrThrow(); +// +// } catch (final Exception e) { +// log.error("Failed to delete depending proctoring data for exam: {}", examId, e); +// } +// }); } @EventListener public void notifyExamFinished(final ExamFinishedEvent event) { - if (log.isDebugEnabled()) { - log.debug("ExamFinishedEvent received, process disposeRoomsForExam..."); - } + // NOTE: Since life proctoring is not supported anymore, we disable automated updates here - disposeRoomsForExam(event.exam) - .onError(error -> log.error("Failed to dispose rooms for finished exam: {}", event.exam, error)); +// if (log.isDebugEnabled()) { +// log.debug("ExamFinishedEvent received, process disposeRoomsForExam..."); +// } +// +// disposeRoomsForExam(event.exam) +// .onError(error -> log.error("Failed to dispose rooms for finished exam: {}", event.exam, error)); } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringAPIBinding.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringAPIBinding.java index 4d204b6cc..eb0fa5d8d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringAPIBinding.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringAPIBinding.java @@ -386,7 +386,7 @@ void synchronizeUserAccount(final String userUUID) { void synchronizeUserAccounts(final Exam exam) { try { - + final ScreenProctoringServiceOAuthTemplate apiTemplate = getAPITemplate(null); exam.supporter.forEach(userUUID -> synchronizeUserAccount(userUUID, apiTemplate)); if (exam.owner != null) { synchronizeUserAccount(exam.owner, apiTemplate); @@ -1156,12 +1156,24 @@ private void rollbackOnSPS( } } - private ScreenProctoringServiceOAuthTemplate apiTemplate = null; - + private ScreenProctoringServiceOAuthTemplate apiTemplateExam = null; + private ScreenProctoringServiceOAuthTemplate apiTemplateBundle = null; private ScreenProctoringServiceOAuthTemplate getAPITemplate(final Long examId) { - if (this.apiTemplate == null || !this.apiTemplate.isValid(examId)) { - if (examId != null) { + if (examId == null) { + if (apiTemplateBundle == null) { + if (log.isDebugEnabled()) { + log.debug("Create new ScreenProctoringServiceOAuthTemplate for bundle"); + } + + final WebserviceInfo.ScreenProctoringServiceBundle bundle = this.webserviceInfo + .getScreenProctoringServiceBundle(); + this.testConnection(bundle).getOrThrow(); + this.apiTemplateBundle = new ScreenProctoringServiceOAuthTemplate(this, bundle); + } + return apiTemplateBundle; + } else { + if (this.apiTemplateExam == null || !this.apiTemplateExam.isValid(examId)) { if (log.isDebugEnabled()) { log.debug("Create new ScreenProctoringServiceOAuthTemplate for exam: {}", examId); } @@ -1170,27 +1182,44 @@ private ScreenProctoringServiceOAuthTemplate getAPITemplate(final Long examId) { .getScreenProctoringSettings(new EntityKey(examId, EntityType.EXAM)) .getOrThrow(); this.testConnection(settings).getOrThrow(); - this.apiTemplate = new ScreenProctoringServiceOAuthTemplate(this, settings); - - } else if (this.webserviceInfo.getScreenProctoringServiceBundle().bundled) { - - if (log.isDebugEnabled()) { - log.debug("Create new ScreenProctoringServiceOAuthTemplate for exam: {}", examId); - } - - final WebserviceInfo.ScreenProctoringServiceBundle bundle = this.webserviceInfo - .getScreenProctoringServiceBundle(); - - this.testConnection(bundle).getOrThrow(); - this.apiTemplate = new ScreenProctoringServiceOAuthTemplate(this, bundle); - - - } else { - throw new IllegalStateException("No SPS API access information found!"); + this.apiTemplateExam = new ScreenProctoringServiceOAuthTemplate(this, settings); } + + return apiTemplateExam; } - return this.apiTemplate; +// if (this.apiTemplate == null || !this.apiTemplate.isValid(examId)) { +// if (examId != null) { +// +// if (log.isDebugEnabled()) { +// log.debug("Create new ScreenProctoringServiceOAuthTemplate for exam: {}", examId); +// } +// +// final ScreenProctoringSettings settings = this.proctoringSettingsDAO +// .getScreenProctoringSettings(new EntityKey(examId, EntityType.EXAM)) +// .getOrThrow(); +// this.testConnection(settings).getOrThrow(); +// this.apiTemplate = new ScreenProctoringServiceOAuthTemplate(this, settings); +// +// } else if (this.webserviceInfo.getScreenProctoringServiceBundle().bundled) { +// +// if (log.isDebugEnabled()) { +// log.debug("Create new ScreenProctoringServiceOAuthTemplate for exam: {}", examId); +// } +// +// final WebserviceInfo.ScreenProctoringServiceBundle bundle = this.webserviceInfo +// .getScreenProctoringServiceBundle(); +// +// this.testConnection(bundle).getOrThrow(); +// this.apiTemplate = new ScreenProctoringServiceOAuthTemplate(this, bundle); +// +// +// } else { +// throw new IllegalStateException("No SPS API access information found!"); +// } +// } +// +// return this.apiTemplate; } private static List getSupporterIds(final Exam exam) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java index 6465307b5..026dcca89 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java @@ -284,6 +284,7 @@ public Exam toggleTestRunForExam( .flatMap(authorization::checkModify) .flatMap(examSessionService::toggleTestRun) .map(exam -> { + examSessionService.flushCache(exam); if (exam.status == Exam.ExamStatus.TEST_RUN) { applicationEventPublisher.publishEvent(new ExamStartedEvent(exam)); } else if (exam.status == Exam.ExamStatus.UP_COMING) { diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java index 337ceef49..63c4e50e9 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java @@ -3772,236 +3772,237 @@ public void testUsecase27_SetProctoringSettingsJitsiForExam() throws IOException @Autowired private ClientConnectionDAO clientConnectionDAO; - @Test - @Order(28) - // ************************************* - // Use Case 28: Login as admin and connect with SEBs to running exam with procotring enabled - // - Get Exam (running) - // - start some SEB clients connecting to running exam - // - Check collecting rooms created - public void testUsecase28_TestExamProctoring() throws IOException { - - final RestServiceImpl restService = createRestServiceForUser( - "admin", - "admin", - new GetExamPage(), - new GetExamProctoringSettings(), - new SaveExamProctoringSettings(), - new IsTownhallRoomAvailable(), - new GetCollectingRooms(), - new GetClientConfigPage(), - new ActivateClientConfig(), - new NewClientConfig(), - new GetClientConfig(), - new GetProctorRoomConnection(), - new GetCollectingRoomConnections(), - new NotifyProctoringRoomOpened(), - new SendProctoringReconfigurationAttributes(), - new GetTownhallRoom(), - new OpenTownhallRoom(), - new CloseProctoringRoom()); - - // get exam - final Result> exams = restService - .getBuilder(GetExamPage.class) - .call(); - - assertNotNull(exams); - assertFalse(exams.hasError()); - final Page examPage = exams.get(); - assertFalse(examPage.isEmpty()); - - final Exam runningExam = examPage.content - .stream() - .filter(exam -> exam.status == ExamStatus.RUNNING) - .findFirst() - .orElse(null); - - assertNotNull(runningExam); - assertTrue(runningExam.status == ExamStatus.RUNNING); - - final Result pSettings = restService - .getBuilder(GetExamProctoringSettings.class) - .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) - .call(); - - assertNotNull(pSettings); - assertFalse(pSettings.hasError()); - final ProctoringServiceSettings proctoringServiceSettings = pSettings.get(); - assertTrue(proctoringServiceSettings.enableProctoring); - assertEquals("https://test.proc/service", proctoringServiceSettings.serverURL); - - // start some SEB connections for this exam - - // create SEB Client Config without password protection - Result newConfigResponse = restService - .getBuilder(NewClientConfig.class) - .withFormParam(Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME, "No Password Protection") - .withFormParam(SEBClientConfig.ATTR_FALLBACK, Constants.TRUE_STRING) - .withFormParam(SEBClientConfig.ATTR_FALLBACK_START_URL, "http://fallback.com/fallback") - .withFormParam(SEBClientConfig.ATTR_FALLBACK_TIMEOUT, "100") - .withFormParam(SEBClientConfig.ATTR_FALLBACK_ATTEMPTS, "5") - .withFormParam(SEBClientConfig.ATTR_FALLBACK_ATTEMPT_INTERVAL, "5") - .withFormParam(SEBClientConfig.ATTR_CONFIG_PURPOSE, SEBClientConfig.ConfigPurpose.START_EXAM.name()) - .call(); - - assertNotNull(newConfigResponse); - assertFalse(newConfigResponse.hasError()); - final SEBClientConfig sebClientConfig = newConfigResponse.get(); - assertEquals("No Password Protection", sebClientConfig.name); - assertFalse(sebClientConfig.isActive()); - assertEquals("http://fallback.com/fallback", sebClientConfig.fallbackStartURL); - - // activate the new Client Configuration - restService - .getBuilder(ActivateClientConfig.class) - .withURIVariable(API.PARAM_MODEL_ID, sebClientConfig.getModelId()) - .call(); - - newConfigResponse = restService.getBuilder(GetClientConfig.class) - .withURIVariable(API.PARAM_MODEL_ID, sebClientConfig.getModelId()) - .call(); - - final SEBClientConfig clientConfig = newConfigResponse.get(); - assertTrue(clientConfig.isActive()); - final ClientCredentials credentials = this.sebClientConfigDAO - .getSEBClientCredentials(clientConfig.getModelId()) - .getOrThrow(); - - assertTrue(clientConfig.isActive()); - - // simulate a SEB connection... - try { - new SEBClientBot( - credentials.clientIdAsString(), - this.cryptor.decrypt(credentials.secret).getOrThrow().toString(), - runningExam.getModelId(), - String.valueOf(runningExam.institutionId), - false); - - Thread.sleep(1000); - - this.examProcotringRoomService.updateProctoringCollectingRooms(); - - // check collecting room was created - final Collection collectingRooms = restService - .getBuilder(GetCollectingRooms.class) - .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) - .call() - .get(); - - assertNotNull(collectingRooms); - assertFalse(collectingRooms.isEmpty()); - // Two rooms a two people for four connections - assertEquals(2, collectingRooms.size()); - final RemoteProctoringRoom room1 = collectingRooms.iterator().next(); - assertEquals(2, room1.roomSize.intValue()); - assertFalse(room1.townhallRoom); - - final ProctoringRoomConnection proctoringRoomConnection = restService - .getBuilder(GetProctorRoomConnection.class) - .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) - .withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room1.name) - .call() - .get(); - - assertNotNull(proctoringRoomConnection); - assertEquals(room1.name, proctoringRoomConnection.roomName); - assertNotNull(proctoringRoomConnection.accessToken); - - // notify room open - restService - .getBuilder(NotifyProctoringRoomOpened.class) - .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) - .withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room1.name) - .call() - .get(); - - // reconfigure clients in room - restService - .getBuilder(SendProctoringReconfigurationAttributes.class) - .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) - .withQueryParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, room1.name) - .withQueryParam(API.EXAM_PROCTORING_ATTR_RECEIVE_AUDIO, "true") - .withQueryParam(API.EXAM_PROCTORING_ATTR_RECEIVE_VIDEO, "true") - .withQueryParam(API.EXAM_PROCTORING_ATTR_ALLOW_CHAT, "true") - .call() - .get(); - - final Collection collection = restService - .getBuilder(GetCollectingRoomConnections.class) - .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) - .withQueryParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, room1.name) - .call() - .get(); - - assertNotNull(collection); - assertFalse(collection.isEmpty()); - assertEquals(2, collection.size()); - final ClientConnection connection = collection.iterator().next(); - assertEquals(runningExam.id, connection.examId); - // this is because the Json model do not contian certain attributes due to performance - assertNull(connection.remoteProctoringRoomId); - // we can geht the room number by getting it directyl from the record - final ClientConnection clientConnection = this.clientConnectionDAO.byPK(connection.id).get(); - assertNotNull(clientConnection.remoteProctoringRoomId); - - // get and open townhall - final String townhallActive = restService - .getBuilder(IsTownhallRoomAvailable.class) - .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) - .call() - .get(); - - assertEquals("true", townhallActive); - - // check no Townhallroom yet - RemoteProctoringRoom townhallRoom = restService - .getBuilder(GetTownhallRoom.class) - .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) - .call() - .get(); - assertEquals(RemoteProctoringRoom.NULL_ROOM, townhallRoom); - - // open townhall room - final ProctoringRoomConnection townhallRoomConntection = restService - .getBuilder(OpenTownhallRoom.class) - .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) - .call() - .get(); - - assertNotNull(townhallRoomConntection); - - // check Townhallroom is available yet - townhallRoom = restService - .getBuilder(GetTownhallRoom.class) - .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) - .call() - .get(); - assertTrue(townhallRoom.townhallRoom); - assertEquals(townhallRoom.name, townhallRoomConntection.roomName); - - // close townhall room - restService - .getBuilder(CloseProctoringRoom.class) - .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) - .withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, townhallRoom.name) - .call() - .get(); - - townhallRoom = restService - .getBuilder(GetTownhallRoom.class) - .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) - .call() - .get(); - assertEquals(RemoteProctoringRoom.NULL_ROOM, townhallRoom); - - Thread.sleep(5000); - - } catch (final Exception e) { - fail(e.getMessage()); - } - } +// NOTE: Not supported anymore +// @Test +// @Order(28) +// // ************************************* +// // Use Case 28: Login as admin and connect with SEBs to running exam with procotring enabled +// // - Get Exam (running) +// // - start some SEB clients connecting to running exam +// // - Check collecting rooms created +// public void testUsecase28_TestExamProctoring() throws IOException { +// +// final RestServiceImpl restService = createRestServiceForUser( +// "admin", +// "admin", +// new GetExamPage(), +// new GetExamProctoringSettings(), +// new SaveExamProctoringSettings(), +// new IsTownhallRoomAvailable(), +// new GetCollectingRooms(), +// new GetClientConfigPage(), +// new ActivateClientConfig(), +// new NewClientConfig(), +// new GetClientConfig(), +// new GetProctorRoomConnection(), +// new GetCollectingRoomConnections(), +// new NotifyProctoringRoomOpened(), +// new SendProctoringReconfigurationAttributes(), +// new GetTownhallRoom(), +// new OpenTownhallRoom(), +// new CloseProctoringRoom()); +// +// // get exam +// final Result> exams = restService +// .getBuilder(GetExamPage.class) +// .call(); +// +// assertNotNull(exams); +// assertFalse(exams.hasError()); +// final Page examPage = exams.get(); +// assertFalse(examPage.isEmpty()); +// +// final Exam runningExam = examPage.content +// .stream() +// .filter(exam -> exam.status == ExamStatus.RUNNING) +// .findFirst() +// .orElse(null); +// +// assertNotNull(runningExam); +// assertTrue(runningExam.status == ExamStatus.RUNNING); +// +// final Result pSettings = restService +// .getBuilder(GetExamProctoringSettings.class) +// .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) +// .call(); +// +// assertNotNull(pSettings); +// assertFalse(pSettings.hasError()); +// final ProctoringServiceSettings proctoringServiceSettings = pSettings.get(); +// assertTrue(proctoringServiceSettings.enableProctoring); +// assertEquals("https://test.proc/service", proctoringServiceSettings.serverURL); +// +// // start some SEB connections for this exam +// +// // create SEB Client Config without password protection +// Result newConfigResponse = restService +// .getBuilder(NewClientConfig.class) +// .withFormParam(Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME, "No Password Protection") +// .withFormParam(SEBClientConfig.ATTR_FALLBACK, Constants.TRUE_STRING) +// .withFormParam(SEBClientConfig.ATTR_FALLBACK_START_URL, "http://fallback.com/fallback") +// .withFormParam(SEBClientConfig.ATTR_FALLBACK_TIMEOUT, "100") +// .withFormParam(SEBClientConfig.ATTR_FALLBACK_ATTEMPTS, "5") +// .withFormParam(SEBClientConfig.ATTR_FALLBACK_ATTEMPT_INTERVAL, "5") +// .withFormParam(SEBClientConfig.ATTR_CONFIG_PURPOSE, SEBClientConfig.ConfigPurpose.START_EXAM.name()) +// .call(); +// +// assertNotNull(newConfigResponse); +// assertFalse(newConfigResponse.hasError()); +// final SEBClientConfig sebClientConfig = newConfigResponse.get(); +// assertEquals("No Password Protection", sebClientConfig.name); +// assertFalse(sebClientConfig.isActive()); +// assertEquals("http://fallback.com/fallback", sebClientConfig.fallbackStartURL); +// +// // activate the new Client Configuration +// restService +// .getBuilder(ActivateClientConfig.class) +// .withURIVariable(API.PARAM_MODEL_ID, sebClientConfig.getModelId()) +// .call(); +// +// newConfigResponse = restService.getBuilder(GetClientConfig.class) +// .withURIVariable(API.PARAM_MODEL_ID, sebClientConfig.getModelId()) +// .call(); +// +// final SEBClientConfig clientConfig = newConfigResponse.get(); +// assertTrue(clientConfig.isActive()); +// final ClientCredentials credentials = this.sebClientConfigDAO +// .getSEBClientCredentials(clientConfig.getModelId()) +// .getOrThrow(); +// +// assertTrue(clientConfig.isActive()); +// +// // simulate a SEB connection... +// try { +// new SEBClientBot( +// credentials.clientIdAsString(), +// this.cryptor.decrypt(credentials.secret).getOrThrow().toString(), +// runningExam.getModelId(), +// String.valueOf(runningExam.institutionId), +// false); +// +// Thread.sleep(1000); +// +// this.examProcotringRoomService.updateProctoringCollectingRooms(); +// +// // check collecting room was created +// final Collection collectingRooms = restService +// .getBuilder(GetCollectingRooms.class) +// .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) +// .call() +// .get(); +// +// assertNotNull(collectingRooms); +// assertFalse(collectingRooms.isEmpty()); +// // Two rooms a two people for four connections +// assertEquals(2, collectingRooms.size()); +// final RemoteProctoringRoom room1 = collectingRooms.iterator().next(); +// assertEquals(2, room1.roomSize.intValue()); +// assertFalse(room1.townhallRoom); +// +// final ProctoringRoomConnection proctoringRoomConnection = restService +// .getBuilder(GetProctorRoomConnection.class) +// .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) +// .withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room1.name) +// .call() +// .get(); +// +// assertNotNull(proctoringRoomConnection); +// assertEquals(room1.name, proctoringRoomConnection.roomName); +// assertNotNull(proctoringRoomConnection.accessToken); +// +// // notify room open +// restService +// .getBuilder(NotifyProctoringRoomOpened.class) +// .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) +// .withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room1.name) +// .call() +// .get(); +// +// // reconfigure clients in room +// restService +// .getBuilder(SendProctoringReconfigurationAttributes.class) +// .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) +// .withQueryParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, room1.name) +// .withQueryParam(API.EXAM_PROCTORING_ATTR_RECEIVE_AUDIO, "true") +// .withQueryParam(API.EXAM_PROCTORING_ATTR_RECEIVE_VIDEO, "true") +// .withQueryParam(API.EXAM_PROCTORING_ATTR_ALLOW_CHAT, "true") +// .call() +// .get(); +// +// final Collection collection = restService +// .getBuilder(GetCollectingRoomConnections.class) +// .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) +// .withQueryParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, room1.name) +// .call() +// .get(); +// +// assertNotNull(collection); +// assertFalse(collection.isEmpty()); +// assertEquals(2, collection.size()); +// final ClientConnection connection = collection.iterator().next(); +// assertEquals(runningExam.id, connection.examId); +// // this is because the Json model do not contian certain attributes due to performance +// assertNull(connection.remoteProctoringRoomId); +// // we can geht the room number by getting it directyl from the record +// final ClientConnection clientConnection = this.clientConnectionDAO.byPK(connection.id).get(); +// assertNotNull(clientConnection.remoteProctoringRoomId); +// +// // get and open townhall +// final String townhallActive = restService +// .getBuilder(IsTownhallRoomAvailable.class) +// .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) +// .call() +// .get(); +// +// assertEquals("true", townhallActive); +// +// // check no Townhallroom yet +// RemoteProctoringRoom townhallRoom = restService +// .getBuilder(GetTownhallRoom.class) +// .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) +// .call() +// .get(); +// assertEquals(RemoteProctoringRoom.NULL_ROOM, townhallRoom); +// +// // open townhall room +// final ProctoringRoomConnection townhallRoomConntection = restService +// .getBuilder(OpenTownhallRoom.class) +// .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) +// .call() +// .get(); +// +// assertNotNull(townhallRoomConntection); +// +// // check Townhallroom is available yet +// townhallRoom = restService +// .getBuilder(GetTownhallRoom.class) +// .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) +// .call() +// .get(); +// assertTrue(townhallRoom.townhallRoom); +// assertEquals(townhallRoom.name, townhallRoomConntection.roomName); +// +// // close townhall room +// restService +// .getBuilder(CloseProctoringRoom.class) +// .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) +// .withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, townhallRoom.name) +// .call() +// .get(); +// +// townhallRoom = restService +// .getBuilder(GetTownhallRoom.class) +// .withURIVariable(API.PARAM_MODEL_ID, runningExam.getModelId()) +// .call() +// .get(); +// assertEquals(RemoteProctoringRoom.NULL_ROOM, townhallRoom); +// +// Thread.sleep(5000); +// +// } catch (final Exception e) { +// fail(e.getMessage()); +// } +// } @Test @Order(29)