SEBSERV-469 implementation

This commit is contained in:
anhefti 2024-02-22 09:01:15 +01:00
parent 074b63580b
commit 29265dd7e1
16 changed files with 200 additions and 87 deletions

View file

@ -11,6 +11,11 @@ package ch.ethz.seb.sebserver.gbl.model.sebconfig;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.Collection;
import java.util.Set;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.validator.constraints.URL;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
@ -52,6 +57,7 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
public static final String ATTR_ENCRYPT_CERTIFICATE_ASYM = "cert_encryption_asym";
public static final String FILTER_ATTR_CREATION_DATE = "creation_date";
public static final String ATTR_EXAM_SELECTION = "exam_selection";
public enum ConfigPurpose {
START_EXAM,
@ -174,6 +180,8 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
@JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_LAST_UPDATE_USER)
public final String lastUpdateUser;
@JsonProperty(SEBClientConfig.ATTR_EXAM_SELECTION)
public final Set<Long> selectedExams;
@JsonCreator
public SEBClientConfig(
@ -204,7 +212,8 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
@JsonProperty(ATTR_ENCRYPT_CERTIFICATE_ASYM) final Boolean encryptCertificateAsym,
@JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ACTIVE) final Boolean active,
@JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_LAST_UPDATE_TIME) final DateTime lastUpdateTime,
@JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_LAST_UPDATE_USER) final String lastUpdateUser) {
@JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_LAST_UPDATE_USER) final String lastUpdateUser,
@JsonProperty(SEBClientConfig.ATTR_EXAM_SELECTION) final Set<Long> selectedExams) {
this.id = id;
this.institutionId = institutionId;
@ -240,6 +249,7 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
this.active = active;
this.lastUpdateTime = lastUpdateTime;
this.lastUpdateUser = lastUpdateUser;
this.selectedExams = Utils.immutableSetOf(selectedExams);
}
public SEBClientConfig(final Long institutionId, final POSTMapper postParams) {
@ -281,6 +291,7 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
this.active = false;
this.lastUpdateTime = postParams.getDateTime(CONFIGURATION_NODE.ATTR_LAST_UPDATE_TIME);
this.lastUpdateUser = postParams.getString(CONFIGURATION_NODE.ATTR_LAST_UPDATE_USER);
this.selectedExams = Utils.immutableSetOf(Utils.getIdsFromString(postParams.getString(SEBClientConfig.ATTR_EXAM_SELECTION)));
}
@Override
@ -424,6 +435,10 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
return this.lastUpdateUser;
}
public Set<Long> getSelectedExams() {
return selectedExams;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
@ -503,7 +518,8 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
this.encryptCertificateAsym,
this.active,
this.lastUpdateTime,
this.lastUpdateUser);
this.lastUpdateUser,
this.selectedExams);
}
public static SEBClientConfig createNew(final Long institutionId, final long pingIterval) {
@ -533,6 +549,7 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
false,
false,
null,
null,
null);
}

View file

@ -935,4 +935,22 @@ public final class Utils {
return "Basic " + base64Creds;
}
public static Set<Long> getIdsFromString(final String idsString) {
if (StringUtils.isBlank(idsString)) {
return Collections.emptySet();
}
return Arrays
.stream(StringUtils.split(idsString, Constants.LIST_SEPARATOR_CHAR))
.map(s -> {
try {
return Long.valueOf(s);
} catch (final Exception e) {
log.warn("Failed to parse String: {} to Long", s);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
}

View file

@ -134,6 +134,9 @@ public class SEBClientConfigForm implements TemplateComposer {
private static final LocTextKey FORM_CONFIRM_ENCRYPT_SECRET_TEXT_KEY =
new LocTextKey("sebserver.clientconfig.form.encryptSecret.confirm");
private static final LocTextKey FORM_EXAM_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.clientconfig.form.examselection");
private static final LocTextKey DELETE_CONFIRM =
new LocTextKey("sebserver.clientconfig.action.delete.confirm");
private static final LocTextKey DELETE_SUCCESS =
@ -409,20 +412,28 @@ public class SEBClientConfigForm implements TemplateComposer {
.mandatory(!isReadonly))
.withDefaultSpanEmptyCell(3)
.addField(FormBuilder.multiComboSelection(
SEBClientConfig.ATTR_EXAM_SELECTION,
FORM_EXAM_SELECTION_TEXT_KEY,
StringUtils.join(clientConfig.selectedExams, Constants.LIST_SEPARATOR),
() -> pageService.getResourceService().getExamLogSelectionResources())
.withInputSpan(5))
.withDefaultSpanEmptyCell(1);
// VDI
.withDefaultSpanInput(2)
.addFieldIf(
() -> false, // TODO skipped for version 1.2 --> 1.3 or 1.4
() -> FormBuilder.singleSelection(
SEBClientConfig.ATTR_VDI_TYPE,
VDI_TYPE_TEXT_KEY,
clientConfig.vdiType != null
? clientConfig.vdiType.name()
: SEBClientConfig.VDIType.NO.name(),
() -> this.pageService.getResourceService().vdiTypeResources())
.mandatory(!isReadonly))
.withDefaultSpanEmptyCell(3);
// .withDefaultSpanInput(2)
// .addFieldIf(
// () -> false, // TODO skipped for version 1.2 --> 1.3 or 1.4
// () -> FormBuilder.singleSelection(
// SEBClientConfig.ATTR_VDI_TYPE,
// VDI_TYPE_TEXT_KEY,
// clientConfig.vdiType != null
// ? clientConfig.vdiType.name()
// : SEBClientConfig.VDIType.NO.name(),
// () -> this.pageService.getResourceService().vdiTypeResources())
// .mandatory(!isReadonly))
// .withDefaultSpanEmptyCell(3);
// VDI Attributes
@ -460,7 +471,10 @@ public class SEBClientConfigForm implements TemplateComposer {
FALLBACK_TEXT_KEY,
clientConfig.fallback != null
? clientConfig.fallback.toString()
: Constants.FALSE_STRING));
: Constants.FALSE_STRING))
.withDefaultSpanEmptyCell(3)
;
// Fallback Attributes

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.gui.form;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
@ -122,6 +123,7 @@ public final class SelectionFieldBuilder extends FieldBuilder<String> {
this.itemsSupplier.get()
.stream()
.filter(tuple -> keys.contains(tuple._1))
.sorted((t1, t2) -> String.CASE_INSENSITIVE_ORDER.compare(t1._2, t2._2))
.map(tuple -> tuple._1)
.forEach(v -> buildReadonlyLabel(builder, composite, v, 1));
}

View file

@ -8,22 +8,17 @@
package ch.ethz.seb.sebserver.gui.widget;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.*;
import java.util.List;
import java.util.Optional;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.widgets.DropDown;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -33,13 +28,11 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService;
public final class MultiSelectionCombo extends Composite implements Selection {
public static final LocTextKey DESELECT_TOOLTIP = new LocTextKey( "sebserver.form.multiselect.deselect.tooltip" );
private static final Logger log = LoggerFactory.getLogger(MultiSelectionCombo.class);
private static final long serialVersionUID = -7787134114963647332L;
private final WidgetFactory widgetFactory;
private final List<Control> selectionControls = new ArrayList<>();
private final List<Tuple<String>> valueMapping = new ArrayList<>();
private final List<Tuple<String>> availableValues = new ArrayList<>();
private final List<Tuple<String>> selectedValues = new ArrayList<>();
@ -50,7 +43,10 @@ public final class MultiSelectionCombo extends Composite implements Selection {
private final Composite updateAnchor;
private final String testKey;
private final Table selectionTable;
private Listener listener = null;
private WidgetFactory widgetFactory;
MultiSelectionCombo(
final Composite parent,
@ -78,7 +74,7 @@ public final class MultiSelectionCombo extends Composite implements Selection {
this.textInput.addListener(SWT.FocusIn, event -> openDropDown());
this.textInput.addListener(SWT.Modify, event -> openDropDown());
this.textInput.addListener(SWT.MouseUp, event -> openDropDown());
this.dropDown.addListener(SWT.Selection, event -> {
this.dropDown.addListener(SWT.DefaultSelection, event -> {
final int selectionIndex = this.dropDown.getSelectionIndex();
if (selectionIndex >= 0) {
final String selectedItem = this.dropDown.getItems()[selectionIndex];
@ -86,21 +82,29 @@ public final class MultiSelectionCombo extends Composite implements Selection {
}
});
selectionTable = widgetFactory.tableLocalized(this, SWT.NONE);
final GridLayout tableLayout = new GridLayout(1, true);
selectionTable.setLayout(tableLayout);
final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, false);
selectionTable.setLayoutData(gridData);
//selectionTable.setToolTipText();
selectionTable.addListener(SWT.MouseDoubleClick, this::removeComboSelection);
selectionTable.addListener(SWT.Selection, event -> {
selectionTable.setSelection(-1);
});
selectionTable.setHeaderVisible(false);
selectionTable.setLinesVisible(true);
this.updateAnchor = updateAnchor;
}
private void openDropDown() {
final String text = this.textInput.getText();
if (text == null) {
this.dropDown.setVisible(false);
@Override
public void setToolTipText(final String tooltipText) {
if (tooltipText == null) {
super.setToolTipText(widgetFactory.getI18nSupport().getText(DESELECT_TOOLTIP));
return;
}
this.dropDown.setItems(this.availableValues
.stream()
.filter(it -> it._2 != null && it._2.startsWith(text))
.map(t -> t._2).toArray(String[]::new));
this.dropDown.setSelectionIndex(0);
this.dropDown.setVisible(true);
super.setToolTipText(tooltipText + "\n\n" + widgetFactory.getI18nSupport().getText(DESELECT_TOOLTIP));
}
@Override
@ -157,9 +161,6 @@ public final class MultiSelectionCombo extends Composite implements Selection {
@Override
public void clear() {
this.selectedValues.clear();
this.selectionControls
.forEach(Control::dispose);
this.selectionControls.clear();
this.availableValues.clear();
this.availableValues.addAll(this.valueMapping);
}
@ -170,42 +171,39 @@ public final class MultiSelectionCombo extends Composite implements Selection {
}
this.selectedValues.add(item);
final Label label = this.widgetFactory.label(this, item._2);
label.setData(OPTION_VALUE, item._2);
final GridData textCell = new GridData(SWT.LEFT, SWT.CENTER, true, true);
label.setLayoutData(textCell);
label.addListener(SWT.MouseDoubleClick, this::removeComboSelection);
this.selectionControls.add(label);
WidgetFactory.setARIALabel(label, item._2);
WidgetFactory.setTestId(label, (this.testKey != null) ? this.testKey + "_" + item._1 : item._1);
sortSelectedTable();
this.availableValues.remove(item);
PageService.updateScrolledComposite(this);
this.updateAnchor.layout(true, true);
}
private void sortSelectedTable() {
selectionTable.removeAll();
selectedValues.sort((t1, t2) -> String.CASE_INSENSITIVE_ORDER.compare(t1._2, t2._2));
selectedValues.stream().forEach(t -> {
final TableItem tItem = new TableItem(selectionTable, SWT.NONE);
tItem.setText(0, t._2);
tItem.setData("tuple", t);
WidgetFactory.setARIALabel(tItem, t._2);
WidgetFactory.setTestId(tItem, (this.testKey != null) ? this.testKey + "_" + t._1 : t._1);
});
}
private void removeComboSelection(final Event event) {
if (event.widget == null) {
return;
}
final String selectionKey = (String) event.widget.getData(OPTION_VALUE);
final Optional<Control> findFirst = this.selectionControls.stream()
.filter(t -> selectionKey.equals(t.getData(OPTION_VALUE)))
.findFirst();
if (!findFirst.isPresent()) {
return;
}
final Control control = findFirst.get();
final int indexOf = this.selectionControls.indexOf(control);
this.selectionControls.remove(control);
control.dispose();
final Tuple<String> value = this.selectedValues.remove(indexOf);
final TableItem item = selectionTable.getItem(new Point(event.x, event.y));
@SuppressWarnings("unchecked")
final Tuple<String> value = (Tuple<String>) item.getData("tuple");
this.selectedValues.remove(value);
this.availableValues.add(value);
sortSelectedTable();
PageService.updateScrolledComposite(this);
this.updateAnchor.layout(true, true);
if (this.listener != null) {
@ -238,4 +236,18 @@ public final class MultiSelectionCombo extends Composite implements Selection {
return findFirst.orElse(null);
}
private void openDropDown() {
final String text = this.textInput.getText();
if (text == null) {
this.dropDown.setVisible(false);
return;
}
this.dropDown.setItems(this.availableValues
.stream()
.filter(it -> it._2 != null && it._2.startsWith(text))
.map(t -> t._2).toArray(String[]::new));
this.dropDown.setSelectionIndex(0);
this.dropDown.setVisible(true);
}
}

View file

@ -489,7 +489,11 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
additionalAttributes.containsKey(SEBClientConfig.ATTR_ENCRYPT_CERTIFICATE_ASYM),
BooleanUtils.toBooleanObject(record.getActive()),
Utils.toDateTimeUTC(record.getLastUpdateTime()),
record.getLastUpdateUser()));
record.getLastUpdateUser(),
Utils.getIdsFromString(
additionalAttributes.containsKey(SEBClientConfig.ATTR_EXAM_SELECTION)
? additionalAttributes.get(SEBClientConfig.ATTR_EXAM_SELECTION).getValue()
: null)));
}
private String getEncryptionPassword(final SEBClientConfig sebClientConfig) {
@ -685,6 +689,21 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
configId,
SEBClientConfig.ATTR_ENCRYPT_CERTIFICATE_ASYM);
}
final Set<Long> selectedExams = sebClientConfig.getSelectedExams();
if (selectedExams != null && !selectedExams.isEmpty()) {
final String ids = StringUtils.join(selectedExams, Constants.LIST_SEPARATOR);
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.SEB_CLIENT_CONFIGURATION,
configId,
SEBClientConfig.ATTR_EXAM_SELECTION,
ids);
} else {
this.additionalAttributesDAO.delete(
EntityType.SEB_CLIENT_CONFIGURATION,
configId,
SEBClientConfig.ATTR_EXAM_SELECTION);
}
}
private Long disposeSEBClientConfig(final Long pk) {

View file

@ -63,5 +63,5 @@ public interface ClientConfigService {
boolean checkAccess(SEBClientConfig config);
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
void initalCheckAccess(SEBClientConfig config);
void initialCheckAccess(SEBClientConfig config);
}

View file

@ -504,7 +504,7 @@ public class ClientConfigServiceImpl implements ClientConfigService {
}
@Override
public void initalCheckAccess(final SEBClientConfig config) {
public void initialCheckAccess(final SEBClientConfig config) {
checkAccess(config);
}

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import java.io.OutputStream;
import java.security.Principal;
import java.util.Collection;
import java.util.Set;
import java.util.function.Predicate;
@ -130,9 +131,10 @@ public interface ExamSessionService {
/** Gets all currently running Exams for a particular Institution.
*
* @param institutionId the Institution identifier
* @param examSelectionFilter Exam selection filter from SEB connection configuration
* @return Result referencing the list of all currently running Exams of the institution or to an error if
* happened. */
Result<Collection<Exam>> getRunningExamsForInstitution(Long institutionId);
Result<Collection<Exam>> getRunningExams(Long institutionId, Predicate<Long> examSelectionFilter);
/** Gets all currently running Exams for a particular FilterMap.
*

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.io.IOException;
import java.io.OutputStream;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -251,9 +252,10 @@ public class ExamSessionServiceImpl implements ExamSessionService {
}
@Override
public Result<Collection<Exam>> getRunningExamsForInstitution(final Long institutionId) {
public Result<Collection<Exam>> getRunningExams(final Long institutionId, final Predicate<Long> examSelectionFilter) {
return this.examDAO.allIdsOfRunning(institutionId)
.map(col -> col.stream()
.filter(examSelectionFilter)
.map(this::getRunningExam)
.filter(Result::hasValue)
.map(Result::get)

View file

@ -11,17 +11,17 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.SEBClientConfig;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -132,7 +132,10 @@ public class ExamAPI_V1_Controller {
// Crate list of running exams
final List<RunningExamInfo> result;
if (examId == null) {
result = this.examSessionService.getRunningExamsForInstitution(institutionId)
result = this.examSessionService.getRunningExams(
institutionId,
getExamSelectionPredicate(principal.getName()))
.getOrThrow()
.stream()
.map(this::createRunningExamInfo)
@ -161,17 +164,7 @@ public class ExamAPI_V1_Controller {
this.executor);
}
private boolean checkConsistency(final RunningExamInfo info) {
if (StringUtils.isNotBlank(info.name) &&
StringUtils.isNotBlank(info.url) &&
StringUtils.isNotBlank(info.examId)) {
return true;
}
log.warn("Invalid running exam detected. Filter out exam : {}", info);
return false;
}
@RequestMapping(
path = API.EXAM_API_HANDSHAKE_ENDPOINT,
@ -420,4 +413,31 @@ public class ExamAPI_V1_Controller {
.onSuccess(bek -> response.setHeader(API.EXAM_API_EXAM_ALT_BEK, bek));
}
private Predicate<Long> getExamSelectionPredicate(final String clientName) {
return this.sebClientConfigDAO
.byClientName(clientName)
.map(this::getExamSelectionPredicate)
.onError(error -> log.warn("Failed to get SEB connection configuration by name: {}", clientName))
.getOr(Utils.truePredicate());
}
private Predicate<Long> getExamSelectionPredicate(final SEBClientConfig config) {
if (config == null || config.selectedExams.isEmpty()) {
return Utils.truePredicate();
}
return config.getSelectedExams()::contains;
}
private boolean checkConsistency(final RunningExamInfo info) {
if (StringUtils.isNotBlank(info.name) &&
StringUtils.isNotBlank(info.url) &&
StringUtils.isNotBlank(info.examId)) {
return true;
}
log.warn("Invalid running exam detected. Filter out exam : {}", info);
return false;
}
}

View file

@ -182,7 +182,7 @@ public class SEBClientConfigController extends ActivatableEntityController<SEBCl
protected Result<SEBClientConfig> notifySaved(final SEBClientConfig entity) {
if (entity.isActive()) {
// try to get access token for SEB client
this.sebClientConfigService.initalCheckAccess(entity);
this.sebClientConfigService.initialCheckAccess(entity);
}
return super.notifySaved(entity);
}

View file

@ -129,6 +129,7 @@ sebserver.form.mandatory.label={0} mandatory
sebserver.form.confirm.label=confirm {0}
sebserver.form.tablefilter.label={0} table-column filter
sebserver.table.column.sort.default.tooltip=Click on the column header to sort the table within this column
sebserver.form.multiselect.deselect.tooltip=Please use double-click to deselect selected items.
sebserver.dialog.confirm.deactivation=Note that there are {0} other entities that belong to this entity.<br/>Those will also be deactivated by deactivating this entity.<br/><br/>Are you sure to deactivate this entity?
sebserver.dialog.confirm.deactivation.noDependencies=Are you sure you want to deactivate?
@ -1033,6 +1034,8 @@ sebserver.clientconfig.form.certificate=Encrypt with Certificate
sebserver.clientconfig.form.certificate.tooltip=Choose identity certificate to be used for encrypting the connection configuration
sebserver.clientconfig.form.type.async=Use asymmetric-only encryption
sebserver.clientconfig.form.type.async.tooltip=Use old asymmetric-only encryption (for SEB < 2.2)
sebserver.clientconfig.form.examselection=Exams
sebserver.clientconfig.form.examselection.tooltip=List of Exams selected to work with this Connection Configuration.
sebserver.clientconfig.form.credentials.title=Client Credentials of Connection Configuration
sebserver.clientconfig.form.credentials.info=A SEB client that loads this connection configuration<br/>uses the following credentials to securely connect to the SEB Server.

View file

@ -138,7 +138,8 @@ public class ModelObjectJSONGenerator {
false,
true,
DateTime.now(),
"user123");
"user123",
null);
System.out.println(domainObject.getClass().getSimpleName() + ":");
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));

View file

@ -128,6 +128,7 @@ public class ClientConfigTest extends GuiIntegrationTest {
false,
null,
null,
null,
null))
.call();
@ -161,6 +162,7 @@ public class ClientConfigTest extends GuiIntegrationTest {
false,
null,
null,
null,
null))
.call()
.getOrThrow();

View file

@ -12,6 +12,7 @@ import static org.junit.Assert.*;
import java.util.Collection;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.Order;
@ -61,7 +62,7 @@ public class ExamProctoringRoomServiceTest extends AdministrationAPIIntegrationT
@Order(1)
public void test01_checkExamRunning() {
final Result<Collection<Exam>> runningExamsForInstitution =
this.examSessionService.getRunningExamsForInstitution(1L);
this.examSessionService.getRunningExams(1L, Utils.truePredicate());
assertFalse(runningExamsForInstitution.hasError());
final Collection<Exam> collection = runningExamsForInstitution.get();
assertFalse(collection.isEmpty());