SEBSERV-139 Jitsi Javascript API integration

This commit is contained in:
anhefti 2020-08-20 08:28:31 +02:00
parent 59e3d29e3d
commit 0e293c7602
7 changed files with 195 additions and 61 deletions

View file

@ -15,11 +15,15 @@ import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public class SEBClientProctoringConnectionData { public class SEBClientProctoringConnectionData {
public static final String ATTR_SERVER_HOST = "serverHost";
public static final String ATTR_SERVER_URL = "serverURL"; public static final String ATTR_SERVER_URL = "serverURL";
public static final String ATTR_ROOM_NAME = "roomName"; public static final String ATTR_ROOM_NAME = "roomName";
public static final String ATTR_ACCESS_TOKEN = "accessToken"; public static final String ATTR_ACCESS_TOKEN = "accessToken";
public static final String ATTR_CONNECTION_URL = "connectionURL"; public static final String ATTR_CONNECTION_URL = "connectionURL";
@JsonProperty(ATTR_SERVER_HOST)
public final String serverHost;
@JsonProperty(ATTR_SERVER_URL) @JsonProperty(ATTR_SERVER_URL)
public final String serverURL; public final String serverURL;
@ -34,17 +38,23 @@ public class SEBClientProctoringConnectionData {
@JsonCreator @JsonCreator
public SEBClientProctoringConnectionData( public SEBClientProctoringConnectionData(
@JsonProperty(ATTR_SERVER_HOST) final String serverHost,
@JsonProperty(ATTR_SERVER_URL) final String serverURL, @JsonProperty(ATTR_SERVER_URL) final String serverURL,
@JsonProperty(ATTR_ROOM_NAME) final String roomName, @JsonProperty(ATTR_ROOM_NAME) final String roomName,
@JsonProperty(ATTR_ACCESS_TOKEN) final String accessToken, @JsonProperty(ATTR_ACCESS_TOKEN) final String accessToken,
@JsonProperty(ATTR_CONNECTION_URL) final String connectionURL) { @JsonProperty(ATTR_CONNECTION_URL) final String connectionURL) {
this.serverHost = serverHost;
this.serverURL = serverURL; this.serverURL = serverURL;
this.roomName = roomName; this.roomName = roomName;
this.accessToken = accessToken; this.accessToken = accessToken;
this.connectionURL = connectionURL; this.connectionURL = connectionURL;
} }
public String getServerHost() {
return this.serverHost;
}
public String getAccessToken() { public String getAccessToken() {
return this.accessToken; return this.accessToken;
} }
@ -64,7 +74,9 @@ public class SEBClientProctoringConnectionData {
@Override @Override
public String toString() { public String toString() {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();
builder.append("SEBClientProctoringConnectionData [serverURL="); builder.append("SEBClientProctoringConnectionData [serverHost=");
builder.append(this.serverHost);
builder.append(", serverURL=");
builder.append(this.serverURL); builder.append(this.serverURL);
builder.append(", roomName="); builder.append(", roomName=");
builder.append(this.roomName); builder.append(this.roomName);

View file

@ -0,0 +1,102 @@
/*
* Copyright (c) 2020 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.gui;
import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext;
@Component
@GuiProfile
public class ProctoringServlet extends HttpServlet {
private static final long serialVersionUID = 3475978419653411800L;
public static final String SESSION_ATTR_PROCTORING_DATA = "SESSION_ATTR_PROCTORING_DATA";
// @formatter:off
private static final String HTML =
"<!DOCTYPE html>" +
"<html>" +
"<head>" +
" <title></title>" +
" <script src='https://%s/external_api.js'></script>" +
"</head>" +
"" +
"<body>" +
"<div id=\"proctoring\"></div> " +
"</body>" +
"<script>" +
" const options = {\r\n" +
" parentNode: document.querySelector('#proctoring'),\r\n" +
" roomName: '%s',\r\n" +
" width: 600,\r\n" +
" height: 400,\r\n" +
" jwt: '%s'\r\n" +
" }\r\n" +
" meetAPI = new JitsiMeetExternalAPI(\"%s\", options);\r\n" +
"</script>" +
"</html>";
// @formatter:on
@Override
protected void doGet(
final HttpServletRequest req,
final HttpServletResponse resp)
throws ServletException, IOException {
final HttpSession httpSession = req.getSession();
final ServletContext servletContext = httpSession.getServletContext();
final WebApplicationContext webApplicationContext = WebApplicationContextUtils
.getRequiredWebApplicationContext(servletContext);
final boolean authenticated = isAuthenticated(httpSession, webApplicationContext);
if (!authenticated) {
resp.setStatus(HttpStatus.FORBIDDEN.value());
return;
}
final SEBClientProctoringConnectionData proctoringConnectionData =
(SEBClientProctoringConnectionData) httpSession.getAttribute(SESSION_ATTR_PROCTORING_DATA);
final String accessToken = proctoringConnectionData.getAccessToken();
final String roomName = proctoringConnectionData.roomName;
final String server = "seb-jitsi.ethz.ch";
final String script = String.format(HTML, server, roomName, accessToken, server);
resp.getOutputStream().println(script);
}
private boolean isAuthenticated(
final HttpSession httpSession,
final WebApplicationContext webApplicationContext) {
final AuthorizationContextHolder authorizationContextHolder = webApplicationContext
.getBean(AuthorizationContextHolder.class);
final SEBServerAuthorizationContext authorizationContext = authorizationContextHolder
.getAuthorizationContext(httpSession);
return authorizationContext.isValid() && authorizationContext.isLoggedIn();
}
}

View file

@ -8,14 +8,8 @@
package ch.ethz.seb.sebserver.gui; package ch.ethz.seb.sebserver.gui;
import java.io.IOException;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.ServletContextListener; import javax.servlet.ServletContextListener;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.rap.rwt.engine.RWTServlet; import org.eclipse.rap.rwt.engine.RWTServlet;
import org.eclipse.rap.rwt.engine.RWTServletContextListener; import org.eclipse.rap.rwt.engine.RWTServletContextListener;
@ -25,6 +19,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -63,25 +58,12 @@ public class RAPSpringConfig {
} }
@Bean @Bean
public ServletRegistrationBean<ProctoringServlet> servletProctoringRegistrationBean() { public ServletRegistrationBean<ProctoringServlet> servletProctoringRegistrationBean(
return new ServletRegistrationBean<>(new ProctoringServlet(), "/proctoring/*"); final ApplicationContext applicationContext) {
}
private static class ProctoringServlet extends HttpServlet {
private static final long serialVersionUID = 3475978419653411800L;
@Override
protected void doGet(
final HttpServletRequest req,
final HttpServletResponse resp)
throws ServletException, IOException {
System.out.println("********************************");
resp.getOutputStream().println("Hello");
}
final ProctoringServlet proctoringServlet = applicationContext
.getBean(ProctoringServlet.class);
return new ServletRegistrationBean<>(proctoringServlet, "/proctoring/*");
} }
@Bean @Bean

View file

@ -8,6 +8,8 @@
package ch.ethz.seb.sebserver.gui.content; package ch.ethz.seb.sebserver.gui.content;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Collection; import java.util.Collection;
import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.RWT;
@ -33,6 +35,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.ProctoringServlet;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.service.ResourceService; import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
@ -276,16 +279,18 @@ public class MonitoringClientConnection implements TemplateComposer {
} }
private PageAction openProctorScreen(final PageAction action, final String connectionToken) { // @formatter:off
// private static final String OPEN_SINGEL_ROOM_SCRIPT =
// final ProctorDialog dialog = new ProctorDialog(action.pageContext().getParent().getShell()); "var existingWin = window.open('', '%s', 'height=420,width=620,location=no,scrollbars=yes,status=no,menubar=yes,toolbar=yes,titlebar=yes');\n" +
// dialog.open(EVENT_LIST_TITLE_KEY, "if(existingWin.location.href === 'about:blank'){\n" +
// "https://seb-jitsi.ethz.ch/TestRoomABC?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0Ijp7InVzZXIiOnsiYXZhdGFyIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9qb2huLWRvZSIsIm5hbWUiOiJEaXNwbGF5IE5hbWUiLCJlbWFpbCI6Im5hbWVAZXhhbXBsZS5jb20ifX0sImF1ZCI6InNlYi1qaXRzaSIsImlzcyI6InNlYi1qaXRzaSIsInN1YiI6Im1lZXQuaml0c2kiLCJyb29tIjoiKiJ9.SD9Zs78mMFqxS1tpalPTykYYaubIYsj_406WAOhcqxQ"); " existingWin.location.href = '%s/proctoring/%s';\n" +
// " existingWin.focus();\n" +
// final UrlLauncher urlLauncher = RWT.getClient().getService(UrlLauncher.class); "} else {\n" +
// urlLauncher.openURL( " existingWin.focus();\n" +
// "https://seb-jitsi.ethz.ch/TestRoomABC?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0Ijp7InVzZXIiOnsiYXZhdGFyIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9qb2huLWRvZSIsIm5hbWUiOiJEaXNwbGF5IE5hbWUiLCJlbWFpbCI6Im5hbWVAZXhhbXBsZS5jb20ifX0sImF1ZCI6InNlYi1qaXRzaSIsImlzcyI6InNlYi1qaXRzaSIsInN1YiI6Im1lZXQuaml0c2kiLCJyb29tIjoiKiJ9.SD9Zs78mMFqxS1tpalPTykYYaubIYsj_406WAOhcqxQ"); "}";
// @formatter:on
private PageAction openProctorScreen(final PageAction action, final String connectionToken) {
final SEBClientProctoringConnectionData proctoringConnectionData = final SEBClientProctoringConnectionData proctoringConnectionData =
this.pageService.getRestService().getBuilder(GetProctorURLForClient.class) this.pageService.getRestService().getBuilder(GetProctorURLForClient.class)
.withURIVariable(API.PARAM_MODEL_ID, action.getEntityKey().modelId) .withURIVariable(API.PARAM_MODEL_ID, action.getEntityKey().modelId)
@ -293,20 +298,20 @@ public class MonitoringClientConnection implements TemplateComposer {
.call() .call()
.getOrThrow(); .getOrThrow();
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String roomName = urlEncoder.encodeToString(Utils.toByteArray(connectionToken));
RWT.getUISession().getHttpSession().setAttribute(
ProctoringServlet.SESSION_ATTR_PROCTORING_DATA,
proctoringConnectionData);
final String webserviceServerAddress = this.pageService.getAuthorizationContextHolder()
.getWebserviceURIService()
.getWebserviceServerAddress();
final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class); final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class);
javaScriptExecutor.execute( final String script = String.format(OPEN_SINGEL_ROOM_SCRIPT, roomName, webserviceServerAddress, roomName);
"window.open(" javaScriptExecutor.execute(script);
+ "'" + proctoringConnectionData.connectionURL + "',"
+ "'proctoring',"
+ "'height=400,width=800,location=no,scrollbars=yes,status=no,menubar=yes,toolbar=yes,titlebar=yes');");
// final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class);
// javaScriptExecutor.execute(
// "window.open("
// + "'http://localhost:8080/proctoring',"
// + "'proctoring',"
// + "'height=400,width=800,location=no,scrollbars=yes,status=no,menubar=yes,toolbar=yes,titlebar=yes');");
return action; return action;
} }

View file

@ -31,6 +31,7 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Cryptor; import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamProctoringService; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamProctoringService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@ -45,13 +46,16 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
private static final String JITSI_ACCESS_TOKEN_PAYLOAD = private static final String JITSI_ACCESS_TOKEN_PAYLOAD =
"{\"context\":{\"user\":{\"name\":\"%s\"}},\"iss\":\"%s\",\"aud\":\"%s\",\"sub\":\"%s\",\"room\":\"%s\"%s}"; "{\"context\":{\"user\":{\"name\":\"%s\"}},\"iss\":\"%s\",\"aud\":\"%s\",\"sub\":\"%s\",\"room\":\"%s\"%s}";
private final AuthorizationService authorizationService;
private final ExamSessionService examSessionService; private final ExamSessionService examSessionService;
private final Cryptor cryptor; private final Cryptor cryptor;
protected ExamJITSIProctoringService( protected ExamJITSIProctoringService(
final AuthorizationService authorizationService,
final ExamSessionService examSessionService, final ExamSessionService examSessionService,
final Cryptor cryptor) { final Cryptor cryptor) {
this.authorizationService = authorizationService;
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
this.cryptor = cryptor; this.cryptor = cryptor;
} }
@ -113,7 +117,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
examProctoring.serverURL, examProctoring.serverURL,
examProctoring.appKey, examProctoring.appKey,
examProctoring.getAppSecret(), examProctoring.getAppSecret(),
clientConnection.userSessionId, this.authorizationService.getUserService().getCurrentUser().getUsername(),
(server) ? "seb-server" : "seb-client", (server) ? "seb-server" : "seb-client",
roomName, roomName,
expTime) expTime)
@ -154,6 +158,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
.append(token).toString(); .append(token).toString();
return new SEBClientProctoringConnectionData( return new SEBClientProctoringConnectionData(
host,
roomUrl, roomUrl,
roomName, roomName,
token, token,
@ -166,9 +171,12 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
final String roomName) { final String roomName) {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();
return builder.append(url) builder.append(url);
.append("/") if (!url.endsWith(Constants.URL_PATH_SEPARATOR)) {
.append(roomName) builder.append(Constants.URL_PATH_SEPARATOR);
}
return builder.append(roomName)
.toString(); .toString();
} }

View file

@ -16,6 +16,8 @@ import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
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;
@ -49,6 +51,8 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
private final WebserviceInfo webserviceInfo; private final WebserviceInfo webserviceInfo;
private final ClientConnectionDAO clientConnectionDAO; private final ClientConnectionDAO clientConnectionDAO;
private final ClientInstructionDAO clientInstructionDAO; private final ClientInstructionDAO clientInstructionDAO;
private final JSONMapper jsonMapper;
private final Map<String, ClientInstructionRecord> instructions; private final Map<String, ClientInstructionRecord> instructions;
private long lastRefresh = 0; private long lastRefresh = 0;
@ -56,11 +60,13 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
public SEBInstructionServiceImpl( public SEBInstructionServiceImpl(
final WebserviceInfo webserviceInfo, final WebserviceInfo webserviceInfo,
final ClientConnectionDAO clientConnectionDAO, final ClientConnectionDAO clientConnectionDAO,
final ClientInstructionDAO clientInstructionDAO) { final ClientInstructionDAO clientInstructionDAO,
final JSONMapper jsonMapper) {
this.webserviceInfo = webserviceInfo; this.webserviceInfo = webserviceInfo;
this.clientConnectionDAO = clientConnectionDAO; this.clientConnectionDAO = clientConnectionDAO;
this.clientInstructionDAO = clientInstructionDAO; this.clientInstructionDAO = clientInstructionDAO;
this.jsonMapper = jsonMapper;
this.instructions = new ConcurrentHashMap<>(); this.instructions = new ConcurrentHashMap<>();
} }
@ -101,15 +107,13 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
final boolean isActive = this.clientConnectionDAO final boolean isActive = this.clientConnectionDAO
.isActiveConnection(examId, connectionToken) .isActiveConnection(examId, connectionToken)
.getOr(false); .getOr(false);
if (isActive) { if (isActive) {
try { try {
final String attributesString = new JSONMapper().writeValueAsString(attributes); final String attributesString = this.jsonMapper.writeValueAsString(attributes);
this.clientInstructionDAO this.clientInstructionDAO
.insert(examId, type, attributesString, connectionToken, needsConfirm) .insert(examId, type, attributesString, connectionToken, needsConfirm)
.map(inst -> { .map(this::chacheInstruction)
this.instructions.putIfAbsent(inst.getConnectionToken(), inst);
return inst;
})
.onError(error -> log.error("Failed to put instruction: ", error)) .onError(error -> log.error("Failed to put instruction: ", error))
.getOrThrow(); .getOrThrow();
} catch (final Exception e) { } catch (final Exception e) {
@ -139,10 +143,10 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
.filter(activeConnections::contains) .filter(activeConnections::contains)
.map(token -> this.clientInstructionDAO.insert(examId, type, attributesString, token, needsConfirm)) .map(token -> this.clientInstructionDAO.insert(examId, type, attributesString, token, needsConfirm))
.map(result -> result.get( .map(result -> result.get(
error -> log.error("Failed to put instruction: ", error), error -> log.error("Failed to register instruction: ", error),
() -> null)) () -> null))
.filter(Objects::nonNull) .filter(Objects::nonNull)
.forEach(inst -> this.instructions.putIfAbsent(inst.getConnectionToken(), inst)); .forEach(this::chacheInstruction);
}); });
} }
@ -229,4 +233,24 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
.forEach(inst -> this.instructions.putIfAbsent(inst.getConnectionToken(), inst))); .forEach(inst -> this.instructions.putIfAbsent(inst.getConnectionToken(), inst)));
} }
private ClientInstructionRecord chacheInstruction(final ClientInstructionRecord instruction) {
final String connectionToken = instruction.getConnectionToken();
if (this.instructions.containsKey(connectionToken)) {
// check if previous instruction is still valid
final ClientInstructionRecord clientInstructionRecord = this.instructions.get(connectionToken);
if (BooleanUtils.toBoolean(BooleanUtils.toBooleanObject(clientInstructionRecord.getNeedsConfirmation()))) {
// check if time is out
final long now = DateTime.now(DateTimeZone.UTC).getMillis();
final Long timestamp = clientInstructionRecord.getTimestamp();
if (timestamp != null && now - timestamp > Constants.MINUTE_IN_MILLIS) {
// remove old instruction and add new one
this.instructions.put(connectionToken, instruction);
}
}
} else {
this.instructions.put(connectionToken, instruction);
}
return instruction;
}
} }

View file

@ -21,9 +21,10 @@ public class ExamJITSIProctoringServiceTest {
@Test @Test
public void testCreateProctoringURL() { public void testCreateProctoringURL() {
Cryptor cryptorMock = Mockito.mock(Cryptor.class); final Cryptor cryptorMock = Mockito.mock(Cryptor.class);
Mockito.when(cryptorMock.decrypt(Mockito.any())).thenReturn("fbvgeghergrgrthrehreg123"); Mockito.when(cryptorMock.decrypt(Mockito.any())).thenReturn("fbvgeghergrgrthrehreg123");
final ExamJITSIProctoringService examJITSIProctoringService = new ExamJITSIProctoringService(null, cryptorMock); final ExamJITSIProctoringService examJITSIProctoringService =
new ExamJITSIProctoringService(null, null, cryptorMock);
final SEBClientProctoringConnectionData data = examJITSIProctoringService.createProctoringConnectionData( final SEBClientProctoringConnectionData data = examJITSIProctoringService.createProctoringConnectionData(
"https://seb-jitsi.example.ch", "https://seb-jitsi.example.ch",
"test-app", "test-app",