diff --git a/build.gradle b/build.gradle index 198a68ac..259c5e73 100644 --- a/build.gradle +++ b/build.gradle @@ -204,6 +204,24 @@ dependencies { implementation "commons-io:commons-io:2.18.0" implementation "com.thedeanda:lorem:2.2" implementation "org.eclipse.jgit:org.eclipse.jgit:7.1.0.202411261347-r" + // https://search.maven.org/artifact/org.eclipse.jgit/org.eclipse.jgit + implementation "org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.1.0.202411261347-r" + // Note: jgit.htt.server is not compatible with jakarta yet and neither is there a timeline. Hence, we had to add the source files to our repository. + // Once the compatibility is given, we can switch back to the maven dependency. + implementation "org.eclipse.jgit:org.eclipse.jgit.http.server:7.1.0.202411261347-r" + + // apache ssh enabled the ssh git operations in LocalVC together with JGit + implementation "org.apache.sshd:sshd-core:2.14.0" + implementation "org.apache.sshd:sshd-git:2.14.0" + implementation "org.apache.sshd:sshd-osgi:2.14.0" + implementation "org.apache.sshd:sshd-sftp:2.14.0" + + implementation "org.bouncycastle:bcpkix-jdk18on:1.79" + implementation "org.bouncycastle:bcprov-jdk18on:1.79" + + implementation "io.reactivex.rxjava3:rxjava:3.1.8" + implementation 'io.netty:netty-resolver-dns-native-macos:4.1.100.Final:osx-aarch_64' + implementation "org.eclipse.jgit:org.eclipse.jgit:7.1.0.202411261347-r" implementation "io.reactivex.rxjava3:rxjava:3.1.10" implementation 'io.netty:netty-resolver-dns-native-macos:4.1.116.Final:osx-aarch_64' implementation 'com.opencsv:opencsv:5.9' diff --git a/src/main/java/de/tum/cit/aet/artemisModel/ArtemisAuthMechanism.java b/src/main/java/de/tum/cit/aet/artemisModel/ArtemisAuthMechanism.java new file mode 100644 index 00000000..9506a469 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemisModel/ArtemisAuthMechanism.java @@ -0,0 +1,9 @@ +package de.tum.cit.aet.artemisModel; + +public enum ArtemisAuthMechanism { + ONLINE_IDE, + PASSWORD, + PARTICIPATION_TOKEN, + SSH, + USER_TOKEN, +} diff --git a/src/main/java/de/tum/cit/aet/domain/ArtemisUser.java b/src/main/java/de/tum/cit/aet/domain/ArtemisUser.java index 33731bfd..fc649dcc 100644 --- a/src/main/java/de/tum/cit/aet/domain/ArtemisUser.java +++ b/src/main/java/de/tum/cit/aet/domain/ArtemisUser.java @@ -4,7 +4,21 @@ import com.opencsv.bean.CsvBindByName; import de.tum.cit.aet.util.ArtemisServer; import jakarta.persistence.*; +import java.io.ByteArrayOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.*; +import java.security.interfaces.RSAPublicKey; import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Base64; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; +import org.springframework.data.util.Pair; @Entity @Table(name = "artemis_user") @@ -35,6 +49,14 @@ public class ArtemisUser { @JsonIgnore private ZonedDateTime tokenExpirationDate; + @Column(name = "public_ssh_key") + @JsonIgnore + private String publicKey; + + @Column(name = "private_ssh_key") + @JsonIgnore + private String privateKey; + public Long getId() { return id; } @@ -90,4 +112,25 @@ public ZonedDateTime getTokenExpirationDate() { public void setTokenExpirationDate(ZonedDateTime tokenExpirationDate) { this.tokenExpirationDate = tokenExpirationDate; } + + public String getPrivateKey() { + return privateKey; + } + + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } + + public String getPublicKey() { + return publicKey; + } + + public void setPublicKey(String publicKey) { + this.publicKey = publicKey; + } + + public void setKeyPair(Pair keyPair) { + this.publicKey = keyPair.getFirst(); + this.privateKey = keyPair.getSecond(); + } } diff --git a/src/main/java/de/tum/cit/aet/domain/RequestType.java b/src/main/java/de/tum/cit/aet/domain/RequestType.java index 2d48c15e..4ccd11f5 100644 --- a/src/main/java/de/tum/cit/aet/domain/RequestType.java +++ b/src/main/java/de/tum/cit/aet/domain/RequestType.java @@ -13,4 +13,12 @@ public enum RequestType { REPOSITORY_INFO, REPOSITORY_FILES, MISC, + SETUP_SSH_KEYS, + FETCH_PARTICIPATION_VCS_ACCESS_TOKEN, + CLONE_SSH, + CLONE_TOKEN, + CLONE_PASSWORD, + PUSH_SSH, + PUSH_TOKEN, + PUSH_PASSWORD, } diff --git a/src/main/java/de/tum/cit/aet/domain/Simulation.java b/src/main/java/de/tum/cit/aet/domain/Simulation.java index b407169b..09521eed 100644 --- a/src/main/java/de/tum/cit/aet/domain/Simulation.java +++ b/src/main/java/de/tum/cit/aet/domain/Simulation.java @@ -45,10 +45,24 @@ public class Simulation { @Column(name = "user_range") private String userRange; + @Deprecated + @JsonIgnore @Enumerated(EnumType.STRING) @Column(name = "ide_type", nullable = false) private IDEType ideType; + @Column(name = "onlineide_percentage", nullable = false) + private double onlineIdePercentage; + + @Column(name = "password_percentage", nullable = false) + private double passwordPercentage; + + @Column(name = "token_percentage", nullable = false) + private double tokenPercentage; + + @Column(name = "ssh_percentage", nullable = false) + private double sshPercentage; + @Column(name = "number_of_commits_and_pushes_from") private int numberOfCommitsAndPushesFrom; @@ -212,6 +226,42 @@ public boolean instructorCredentialsProvided() { return instructorUsername != null && instructorPassword != null; } + public double getOnlineIdePercentage() { + return onlineIdePercentage; + } + + public void setOnlineIdePercentage(double onlineIdePercentage) { + this.onlineIdePercentage = onlineIdePercentage; + } + + public double getPasswordPercentage() { + return passwordPercentage; + } + + public void setPasswordPercentage(double passwordPercentage) { + this.passwordPercentage = passwordPercentage; + } + + public double getTokenPercentage() { + return tokenPercentage; + } + + public void setTokenPercentage(double tokenPercentage) { + this.tokenPercentage = tokenPercentage; + } + + public double getSshPercentage() { + return sshPercentage; + } + + public void setSshPercentage(double sshPercentage) { + this.sshPercentage = sshPercentage; + } + + public boolean participationPercentagesSumUpToHundredPercent() { + return (this.onlineIdePercentage + this.passwordPercentage + this.tokenPercentage + this.sshPercentage) == 100.0; + } + public enum Mode { /** * We create a temporary course and exam, prepare the exam and delete everything afterwards. diff --git a/src/main/java/de/tum/cit/aet/service/CiStatusService.java b/src/main/java/de/tum/cit/aet/service/CiStatusService.java index 92bb49be..3782445b 100644 --- a/src/main/java/de/tum/cit/aet/service/CiStatusService.java +++ b/src/main/java/de/tum/cit/aet/service/CiStatusService.java @@ -134,18 +134,22 @@ public CompletableFuture subscribeToCiStatusViaResults(SimulationRun simul } catch (InterruptedException e) { Thread.currentThread().interrupt(); } - log.debug("Updating CI status for simulation run {}", simulationRun.getId()); + log.info("Updating CI status for simulation run {}", simulationRun.getId()); + submissions = new ArrayList<>(); for (var participation : participations) { submissions.addAll(admin.getSubmissions(participation.getId())); } numberOfQueuedJobs = submissions.size() - getNumberOfResults(submissions); + log.info("Currently queued buildjobs: {}", numberOfQueuedJobs); + status.setQueuedJobs(numberOfQueuedJobs); status.setTimeInMinutes(status.getTimeInMinutes() + 1); status.setAvgJobsPerMinute((double) (status.getTotalJobs() - status.getQueuedJobs()) / status.getTimeInMinutes()); status = ciStatusRepository.save(status); websocketService.sendRunCiUpdate(simulationRun.getId(), status); } while (numberOfQueuedJobs > 0); + status.setFinished(true); status = ciStatusRepository.save(status); websocketService.sendRunCiUpdate(simulationRun.getId(), status); diff --git a/src/main/java/de/tum/cit/aet/service/artemis/ArtemisUserService.java b/src/main/java/de/tum/cit/aet/service/artemis/ArtemisUserService.java index a03d23fb..fb721054 100644 --- a/src/main/java/de/tum/cit/aet/service/artemis/ArtemisUserService.java +++ b/src/main/java/de/tum/cit/aet/service/artemis/ArtemisUserService.java @@ -10,11 +10,15 @@ import de.tum.cit.aet.service.dto.ArtemisUserPatternDTO; import de.tum.cit.aet.util.ArtemisServer; import de.tum.cit.aet.util.NumberRangeParser; +import de.tum.cit.aet.util.SshUtils; import de.tum.cit.aet.web.rest.errors.BadRequestAlertException; import java.io.*; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.util.Pair; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -68,6 +72,21 @@ public List createArtemisUsersByPattern(ArtemisServer server, Artem ); simulatedArtemisAdmin.login(); } + log.info("Generate SSH keys... this might take some time"); + AtomicInteger sshKeyCounter = new AtomicInteger(0); + int totalKeys = pattern.getTo() - pattern.getFrom(); + Pair[] pregeneratedSSHkeys = new Pair[totalKeys + 1]; + + IntStream.range(pattern.getFrom(), pattern.getTo() + 1) + .parallel() + .forEach(i -> { + if (sshKeyCounter.get() % 100 == 0) { + log.info("{{}} of {{}} keys created...", sshKeyCounter.get(), totalKeys); + } + pregeneratedSSHkeys[i - pattern.getFrom()] = SshUtils.generateSshKeyPair(); + sshKeyCounter.getAndIncrement(); + }); + log.info("Done generating {{}} SSH keys", totalKeys); List createdUsers = new ArrayList<>(); for (int i = pattern.getFrom(); i < pattern.getTo(); i++) { @@ -78,6 +97,8 @@ public List createArtemisUsersByPattern(ArtemisServer server, Artem var password = pattern.getPasswordPattern().replace("{i}", String.valueOf(i)); artemisUser.setUsername(username); artemisUser.setPassword(password); + artemisUser.setKeyPair(pregeneratedSSHkeys[i - pattern.getFrom()]); + try { ArtemisUser createdUser = saveArtemisUser(artemisUser); // Create user on Artemis if necessary @@ -111,6 +132,7 @@ public ArtemisUser createArtemisUser(ArtemisServer server, ArtemisUserForCreatio artemisUser.setServer(server); artemisUser.setUsername(artemisUserDTO.getUsername()); artemisUser.setPassword(artemisUserDTO.getPassword()); + artemisUser.setKeyPair(SshUtils.generateSshKeyPair()); if (artemisUserDTO.getServerWideId() != null) { artemisUser.setServerWideId(artemisUserDTO.getServerWideId()); diff --git a/src/main/java/de/tum/cit/aet/service/artemis/interaction/SimulatedArtemisAdmin.java b/src/main/java/de/tum/cit/aet/service/artemis/interaction/SimulatedArtemisAdmin.java index 14245ca9..4172a0a3 100644 --- a/src/main/java/de/tum/cit/aet/service/artemis/interaction/SimulatedArtemisAdmin.java +++ b/src/main/java/de/tum/cit/aet/service/artemis/interaction/SimulatedArtemisAdmin.java @@ -178,6 +178,32 @@ public Course createCourse() { .block(); } + public void cancelAllQueuedBuildJobs() { + if (!authenticated) { + throw new IllegalStateException("User " + username + " is not logged in or does not have the necessary access rights."); + } + + webClient + .delete() + .uri(uriBuilder -> uriBuilder.pathSegment("api", "admin", "cancel-all-queued-jobs").build()) + .retrieve() + .toBodilessEntity() + .block(); + } + + public void cancelAllRunningBuildJobs() { + if (!authenticated) { + throw new IllegalStateException("User " + username + " is not logged in or does not have the necessary access rights."); + } + + webClient + .delete() + .uri(uriBuilder -> uriBuilder.pathSegment("api", "admin", "cancel-all-running-jobs").build()) + .retrieve() + .toBodilessEntity() + .block(); + } + /** * Create an exam for benchmarking. * @param course the course for which to create the exam diff --git a/src/main/java/de/tum/cit/aet/service/artemis/interaction/SimulatedArtemisStudent.java b/src/main/java/de/tum/cit/aet/service/artemis/interaction/SimulatedArtemisStudent.java index d024d31e..1cd8ef59 100644 --- a/src/main/java/de/tum/cit/aet/service/artemis/interaction/SimulatedArtemisStudent.java +++ b/src/main/java/de/tum/cit/aet/service/artemis/interaction/SimulatedArtemisStudent.java @@ -5,6 +5,7 @@ import static java.lang.Thread.sleep; import static java.time.ZonedDateTime.now; +import com.mysql.cj.xdevapi.SessionFactory; import com.thedeanda.lorem.LoremIpsum; import de.tum.cit.aet.artemisModel.*; import de.tum.cit.aet.domain.ArtemisUser; @@ -14,19 +15,45 @@ import de.tum.cit.aet.service.artemis.util.ArtemisServerInfo; import de.tum.cit.aet.service.artemis.util.CourseDashboardDTO; import de.tum.cit.aet.service.artemis.util.ScienceEventDTO; +import de.tum.cit.aet.service.artemis.util.UserSshPublicKeyDTO; +import de.tum.cit.aet.util.SshUtils; import de.tum.cit.aet.util.UMLClassDiagrams; import jakarta.annotation.Nullable; -import java.io.IOException; +import java.io.*; +import java.net.InetSocketAddress; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Random; +import java.security.*; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.*; import org.apache.commons.io.FileUtils; +import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; +import org.apache.sshd.common.config.keys.PublicKeyEntry; +import org.apache.sshd.common.config.keys.loader.KeyPairResourceLoader; +import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.session.Session; +import org.apache.sshd.common.session.SessionContext; +import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.TransportConfigCallback; import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.transport.*; +import org.eclipse.jgit.transport.sshd.JGitKeyCache; +import org.eclipse.jgit.transport.sshd.ServerKeyDatabase; +import org.eclipse.jgit.transport.sshd.SshdSessionFactory; +import org.eclipse.jgit.transport.sshd.SshdSessionFactoryBuilder; +import org.eclipse.jgit.util.FS; import org.slf4j.LoggerFactory; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; @@ -43,6 +70,9 @@ public class SimulatedArtemisStudent extends SimulatedArtemisUser { private String examIdString; private Long studentExamId; private StudentExam studentExam; + private String participationVcsAccessToken; + private ArtemisAuthMechanism authenticationMechanism; + private final int numberOfCommitsAndPushesFrom; private final int numberOfCommitsAndPushesTo; @@ -53,12 +83,16 @@ public SimulatedArtemisStudent( ArtemisUser artemisUser, ArtemisUserService artemisUserService, int numberOfCommitsAndPushesFrom, - int numberOfCommitsAndPushesTo + int numberOfCommitsAndPushesTo, + ArtemisAuthMechanism authMechanism ) { super(artemisUrl, artemisUser, artemisUserService); log = LoggerFactory.getLogger(SimulatedArtemisStudent.class.getName() + "." + username); this.numberOfCommitsAndPushesFrom = numberOfCommitsAndPushesFrom; this.numberOfCommitsAndPushesTo = numberOfCommitsAndPushesTo; + this.authenticationMechanism = authMechanism; + this.publicKeyString = artemisUser.getPublicKey(); + this.privateKeyString = artemisUser.getPrivateKey(); } @Override @@ -69,6 +103,7 @@ protected void checkAccess() { /** * Perform miscellaneous calls to Artemis, e.g. to get the user info, system notifications, account, notification settings, and courses. + * * @return the list of request stats */ public List performInitialCalls() { @@ -83,17 +118,19 @@ public List performInitialCalls() { getNotificationSettings(), getCourses(), getMutedConversations(), - getNotifications() + getNotifications(), + configureSSH() ); } /** * Participate in an exam, i.e. solve and submit the exercises and fetch live events. + * * @param courseId the ID of the course - * @param examId the ID of the exam + * @param examId the ID of the exam * @return the list of request stats */ - public List participateInExam(long courseId, long examId, boolean onlineIde) { + public List participateInExam(long courseId, long examId) { if (!authenticated) { throw new IllegalStateException("User " + username + " is not logged in or not a student."); } @@ -103,15 +140,16 @@ public List participateInExam(long courseId, long examId, boolean o List requestStats = new ArrayList<>(); requestStats.add(fetchLiveEvents()); - requestStats.addAll(handleExercises(onlineIde)); + requestStats.addAll(handleExercises()); return requestStats; } /** * Start participating in an exam, i.e. navigate into the exam and start the exam. + * * @param courseId the ID of the course - * @param examId the ID of the exam + * @param examId the ID of the exam * @return the list of request stats */ public List startExamParticipation(long courseId, long examId, long courseProgrammingExerciseId) { @@ -141,8 +179,9 @@ public List startExamParticipation(long courseId, long examId, long /** * Submit and end an exam, i.e. submit the student exam and load the exam summary. + * * @param courseId the ID of the course - * @param examId the ID of the exam + * @param examId the ID of the exam * @return the list of request stats */ public List submitAndEndExam(long courseId, long examId) { @@ -205,6 +244,35 @@ private RequestStat getNotifications() { return new RequestStat(now(), System.nanoTime() - start, MISC); } + private RequestStat configureSSH() { + long start = System.nanoTime(); + List keys = webClient + .get() + .uri(uriBuilder -> uriBuilder.path("api/ssh-settings/public-keys").build()) + .retrieve() + .bodyToFlux(UserSshPublicKeyDTO.class) + .collectList() + .block(); + + var hasArtemisKeyStoredAlready = keys.stream().anyMatch(key -> key.publicKey().equals(publicKeyString)); + + if (!hasArtemisKeyStoredAlready) { + try { + webClient + .post() + .uri("api/ssh-settings/public-key") + .bodyValue(UserSshPublicKeyDTO.of(publicKeyString)) + .retrieve() + .toBodilessEntity() + .block(); + } catch (Exception e) { + log.error("Error while adding SSH key for {{}}: {{}}", username, e.getMessage()); + } + } + + return new RequestStat(now(), System.nanoTime() - start, SETUP_SSH_KEYS); + } + private RequestStat getCourses() { long start = System.nanoTime(); webClient.get().uri("api/courses/for-dashboard").retrieve().toBodilessEntity().block(); @@ -410,7 +478,7 @@ private RequestStat fetchLiveEvents() { return new RequestStat(now(), System.nanoTime() - start, MISC); } - private List handleExercises(boolean onlineIde) { + private List handleExercises() { List requestStats = new ArrayList<>(); for (var exercise : studentExam.getExercises()) { if (exercise instanceof ModelingExercise) { @@ -420,7 +488,7 @@ private List handleExercises(boolean onlineIde) { } else if (exercise instanceof QuizExercise) { requestStats.add(solveAndSubmitQuizExercise((QuizExercise) exercise)); } else if (exercise instanceof ProgrammingExercise) { - requestStats.addAll(solveAndSubmitProgrammingExercise((ProgrammingExercise) exercise, onlineIde)); + requestStats.addAll(solveAndSubmitProgrammingExercise((ProgrammingExercise) exercise)); } } return requestStats; @@ -490,16 +558,19 @@ private RequestStat solveAndSubmitQuizExercise(QuizExercise quizExercise) { return null; } - private void commitAndPush(List requestStats, boolean onlineIde, Long participationId, String changedFileContent) - throws IOException, GitAPIException { - if (onlineIde) { - makeOnlineIDECommitAndPush(requestStats, participationId, changedFileContent); - } else { - makeOfflineIDECommitAndPush(requestStats); + private void commitAndPush( + List requestStats, + ArtemisAuthMechanism mechanism, + Long participationId, + String changedFileContent + ) throws IOException, GitAPIException, GeneralSecurityException { + switch (mechanism) { + case ONLINE_IDE -> makeOnlineIDECommitAndPush(requestStats, participationId, changedFileContent); + default -> makeOfflineIDECommitAndPush(requestStats); } } - private List solveAndSubmitProgrammingExercise(ProgrammingExercise programmingExercise, boolean onlineIDE) { + private List solveAndSubmitProgrammingExercise(ProgrammingExercise programmingExercise) { var programmingParticipation = (ProgrammingExerciseStudentParticipation) programmingExercise .getStudentParticipations() .iterator() @@ -507,14 +578,14 @@ private List solveAndSubmitProgrammingExercise(ProgrammingExercise List requestStats = new ArrayList<>(); var repositoryCloneUrl = programmingParticipation.getRepositoryUri(); var participationId = programmingParticipation.getId(); - + requestStats.add(fetchParticipationVcsAccessToken(participationId)); try { long start = System.nanoTime(); - if (onlineIDE) { - makeInitialProgrammingExerciseOnlineIDECalls(requestStats, participationId); - } else { - requestStats.add(cloneRepo(repositoryCloneUrl)); + switch (authenticationMechanism) { + case ONLINE_IDE -> makeInitialProgrammingExerciseOnlineIDECalls(requestStats, participationId); + case SSH -> requestStats.add(cloneRepoOverSSH(repositoryCloneUrl)); + default -> requestStats.add(cloneRepo(repositoryCloneUrl)); } int n = new Random().nextInt(numberOfCommitsAndPushesFrom, numberOfCommitsAndPushesTo); // we do a random number of commits and pushes to make some noise @@ -522,10 +593,10 @@ private List solveAndSubmitProgrammingExercise(ProgrammingExercise for (int j = 0; j < n; j++) { sleep(100); var makeInvalidChange = new Random().nextBoolean(); - var writeToFile = !onlineIDE; + var writeToFile = !this.authenticationMechanism.equals(ArtemisAuthMechanism.ONLINE_IDE); var changedFileContent = changeFiles(makeInvalidChange, writeToFile); - commitAndPush(requestStats, onlineIDE, participationId, changedFileContent); + commitAndPush(requestStats, this.authenticationMechanism, participationId, changedFileContent); } log.debug(" Clone and commit+push done in " + formatDurationFrom(start)); } catch (Exception e) { @@ -534,6 +605,22 @@ private List solveAndSubmitProgrammingExercise(ProgrammingExercise return requestStats; } + private RequestStat fetchParticipationVcsAccessToken(Long participationId) { + long start = System.nanoTime(); + this.participationVcsAccessToken = webClient + .get() + .uri(uriBuilder -> + uriBuilder + .pathSegment("api", "account", "participation-vcs-access-token") + .queryParam("participationId", participationId) + .build() + ) + .retrieve() + .bodyToMono(String.class) + .block(); + return new RequestStat(now(), System.nanoTime() - start, FETCH_PARTICIPATION_VCS_ACCESS_TOKEN); + } + private RequestStat submitStudentExam() { long start = System.nanoTime(); webClient @@ -601,7 +688,7 @@ private static S getSubmissionOfType(Exercise exercise, C return null; } - private RequestStat commitAndPushRepo() throws IOException, GitAPIException { + private RequestStat commitAndPushRepo() throws IOException, GitAPIException, GeneralSecurityException { var localPath = Path.of("repos", username); log.debug("Commit and push to " + localPath); @@ -609,12 +696,32 @@ private RequestStat commitAndPushRepo() throws IOException, GitAPIException { git.add().addFilepattern("src").call(); git.commit().setMessage("local test").setAllowEmpty(true).setSign(false).call(); + var keyPair = loadKeys(privateKeyString); long start = System.nanoTime(); - git.push().setCredentialsProvider(getCredentialsProvider()).call(); + + switch (this.authenticationMechanism) { + case ONLINE_IDE -> throw new IllegalStateException("Cannot push to Online IDE via jgit"); + case PASSWORD -> git.push().setCredentialsProvider(getCredentialsProvider()).call(); + case PARTICIPATION_TOKEN -> git.push().setCredentialsProvider(getCredentialsProviderWithToken()).call(); + case SSH -> git + .push() + .setTransportConfigCallback(transport -> { + SshTransport sshTransport = (SshTransport) transport; + sshTransport.setSshSessionFactory(getSessionFactory(keyPair)); + }) + .call(); + } + long duration = System.nanoTime() - start; git.close(); - return new RequestStat(now(), duration, PUSH); + + return switch (this.authenticationMechanism) { + case PASSWORD -> new RequestStat(now(), duration, PUSH_PASSWORD); + case PARTICIPATION_TOKEN -> new RequestStat(now(), duration, PUSH_TOKEN); + case SSH -> new RequestStat(now(), duration, PUSH_SSH); + default -> new RequestStat(now(), duration, PUSH); + }; } private String changeFiles(boolean invalidChange, boolean writeToFile) throws IOException { @@ -663,7 +770,7 @@ private void makeInitialProgrammingExerciseOnlineIDECalls(List requ requestStats.add(fetchFiles(participationId)); } - private void makeOfflineIDECommitAndPush(List requestStats) throws IOException, GitAPIException { + private void makeOfflineIDECommitAndPush(List requestStats) throws IOException, GitAPIException, GeneralSecurityException { requestStats.add(commitAndPushRepo()); } @@ -740,16 +847,28 @@ private RequestStat cloneRepo(String repositoryUrl) throws IOException { while (attempt < MAX_RETRIES) { try { long start = System.nanoTime(); + UsernamePasswordCredentialsProvider credentialsProvider; + switch (authenticationMechanism) { + case ONLINE_IDE -> throw new IOException("Cannot pull from Online IDE"); + case PASSWORD -> credentialsProvider = getCredentialsProvider(); + case PARTICIPATION_TOKEN -> credentialsProvider = getCredentialsProviderWithToken(); + default -> throw new IllegalStateException("Not implemented"); + } + var git = Git.cloneRepository() .setURI(repositoryUrl) .setDirectory(localPath.toFile()) - .setCredentialsProvider(getCredentialsProvider()) + .setCredentialsProvider(credentialsProvider) .call(); - var duration = System.nanoTime() - start; + var duration = System.nanoTime() - start; git.close(); log.debug("Done " + repositoryUrl); - return new RequestStat(now(), duration, CLONE); + return switch (authenticationMechanism) { + case PASSWORD -> new RequestStat(now(), duration, CLONE_PASSWORD); + case PARTICIPATION_TOKEN -> new RequestStat(now(), duration, CLONE_TOKEN); + default -> new RequestStat(now(), duration, CLONE); + }; } catch (Exception e) { log.warn("Error while cloning repository for {{}}: {{}}", username, e.getMessage()); attempt++; @@ -764,7 +883,110 @@ private RequestStat cloneRepo(String repositoryUrl) throws IOException { throw new RuntimeException("Failed to clone repository for " + username); } + public RequestStat cloneRepoOverSSH(String repositoryUrl) throws IOException, GeneralSecurityException { + log.debug("Clone " + repositoryUrl); + + var localPath = Path.of("repos", username); + FileUtils.deleteDirectory(localPath.toFile()); + + var sshRepositoryUrl = getSshCloneUrl(repositoryUrl); + + int attempt = 0; + + var keyPair = loadKeys(privateKeyString); + while (attempt < MAX_RETRIES) { + try { + long start = System.nanoTime(); + + Git git = Git.cloneRepository() + .setURI(sshRepositoryUrl) + .setDirectory(localPath.toFile()) + .setTransportConfigCallback(transport -> { + SshTransport sshTransport = (SshTransport) transport; + sshTransport.setSshSessionFactory(getSessionFactory(keyPair)); + }) + .call(); + + var duration = System.nanoTime() - start; + + git.close(); + + return new RequestStat(now(), duration, CLONE_SSH); + } catch (Exception e) { + log.warn("Error while cloning repository for {{}}: {{}}", username, e.getMessage()); + attempt++; + try { + sleep(RETRY_DELAY_MS); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + + log.error("Failed to clone repository for {{}}", username); + throw new RuntimeException("Failed to clone repository for " + username); + } + + public Iterable loadKeys(String privateKey) throws IOException, GeneralSecurityException { + try { + Object parsed = new PEMParser(new StringReader(privateKey)).readObject(); + KeyPair pair; + pair = new JcaPEMKeyConverter().getKeyPair((PEMKeyPair) parsed); + + return Collections.singleton(pair); + } catch (Exception e) { + throw new RuntimeException("Failed to load SSH keys", e); + } + } + + private SshdSessionFactory getSessionFactory(Iterable keyPairs) { + // Create a temporary directory to use for the home directory and SSH directory + // This is required by the SshdSessionFactory object despite us not using them + Path temporaryDirectory; + try { + temporaryDirectory = Files.createTempDirectory("ssh-temp-dir-user-1"); + } catch (IOException e) { + throw new RuntimeException("Failed to create temporary directory", e); + } + + return new SshdSessionFactoryBuilder() + .setPreferredAuthentications("publickey") + .setDefaultKeysProvider(ignoredSshDirBecauseWeUseAnInMemorySetOfKeyPairs -> keyPairs) + .setHomeDirectory(temporaryDirectory.toFile()) + .setSshDirectory(temporaryDirectory.toFile()) + .setServerKeyDatabase((ignoredHomeDir, ignoredSshDir) -> + new ServerKeyDatabase() { + @Override + public List lookup(String connectAddress, InetSocketAddress remoteAddress, Configuration config) { + return Collections.emptyList(); + } + + @Override + public boolean accept( + String connectAddress, + InetSocketAddress remoteAddress, + PublicKey serverKey, + Configuration config, + CredentialsProvider provider + ) { + return true; + } + } + ) + //The JGitKeyCache handles the caching of keys to avoid unnecessary disk I/O and improve performance + .build(new JGitKeyCache()); + } + + private String getSshCloneUrl(String cloneUrl) { + var artemisServerHostname = artemisUrl.substring(artemisUrl.indexOf("//") + 2).split("/")[0].split(":")[0]; + return "ssh://git@" + artemisServerHostname + ":7921" + cloneUrl.substring(cloneUrl.indexOf("/git/")); + } + private UsernamePasswordCredentialsProvider getCredentialsProvider() { return new UsernamePasswordCredentialsProvider(username, password); } + + private UsernamePasswordCredentialsProvider getCredentialsProviderWithToken() { + return new UsernamePasswordCredentialsProvider(username, participationVcsAccessToken); + } } diff --git a/src/main/java/de/tum/cit/aet/service/artemis/interaction/SimulatedArtemisUser.java b/src/main/java/de/tum/cit/aet/service/artemis/interaction/SimulatedArtemisUser.java index 1d7dbd6d..f66d3b04 100644 --- a/src/main/java/de/tum/cit/aet/service/artemis/interaction/SimulatedArtemisUser.java +++ b/src/main/java/de/tum/cit/aet/service/artemis/interaction/SimulatedArtemisUser.java @@ -3,6 +3,7 @@ import static de.tum.cit.aet.domain.RequestType.AUTHENTICATION; import static java.time.ZonedDateTime.now; +import de.tum.cit.aet.artemisModel.ArtemisAuthMechanism; import de.tum.cit.aet.domain.ArtemisUser; import de.tum.cit.aet.domain.RequestStat; import de.tum.cit.aet.service.artemis.ArtemisUserService; @@ -35,6 +36,8 @@ public abstract class SimulatedArtemisUser { protected final String username; protected final String password; + protected String privateKeyString; + protected String publicKeyString; protected final String artemisUrl; protected WebClient webClient; protected AuthToken authToken; @@ -56,6 +59,8 @@ public SimulatedArtemisUser(String artemisUrl, ArtemisUser artemisUser, ArtemisU this.artemisUrl = artemisUrl; this.artemisUser = artemisUser; this.artemisUserService = artemisUserService; + this.privateKeyString = artemisUser.getPrivateKey(); + this.publicKeyString = artemisUser.getPublicKey(); } /** @@ -162,14 +167,16 @@ public static SimulatedArtemisStudent createArtemisStudent( ArtemisUser artemisUser, ArtemisUserService artemisUserService, int numberOfCommitsAndPushesFrom, - int numberOfCommitsAndPushesTo + int numberOfCommitsAndPushesTo, + ArtemisAuthMechanism authMechanism ) { return new SimulatedArtemisStudent( artemisUrl, artemisUser, artemisUserService, numberOfCommitsAndPushesFrom, - numberOfCommitsAndPushesTo + numberOfCommitsAndPushesTo, + authMechanism ); } diff --git a/src/main/java/de/tum/cit/aet/service/artemis/util/UserSshPublicKeyDTO.java b/src/main/java/de/tum/cit/aet/service/artemis/util/UserSshPublicKeyDTO.java new file mode 100644 index 00000000..cc65ca0b --- /dev/null +++ b/src/main/java/de/tum/cit/aet/service/artemis/util/UserSshPublicKeyDTO.java @@ -0,0 +1,17 @@ +package de.tum.cit.aet.service.artemis.util; + +import java.time.ZonedDateTime; + +public record UserSshPublicKeyDTO( + Long id, + String label, + String publicKey, + String keyHash, + ZonedDateTime creationDate, + ZonedDateTime lastUsedDate, + ZonedDateTime expiryDate +) { + public static UserSshPublicKeyDTO of(String publicKey) { + return new UserSshPublicKeyDTO(null, "Key", publicKey, null, null, null, null); + } +} diff --git a/src/main/java/de/tum/cit/aet/service/simulation/SimulationDataService.java b/src/main/java/de/tum/cit/aet/service/simulation/SimulationDataService.java index 230bc54a..daeee397 100644 --- a/src/main/java/de/tum/cit/aet/service/simulation/SimulationDataService.java +++ b/src/main/java/de/tum/cit/aet/service/simulation/SimulationDataService.java @@ -71,7 +71,12 @@ public Simulation createSimulation(Simulation simulation) { throw new IllegalArgumentException("Invalid simulation"); } if (simulation.getIdeType() == null) { - throw new IllegalArgumentException("IDE type must not be null"); + // Deprecated + simulation.setIdeType(Simulation.IDEType.OFFLINE); + } + + if (!simulation.participationPercentagesSumUpToHundredPercent()) { + throw new IllegalArgumentException("Participation mode percentages must add up to 100%"); } // If only one of the instructor credentials is set, remove both if ((simulation.getInstructorUsername() != null) ^ (simulation.getInstructorPassword() != null)) { diff --git a/src/main/java/de/tum/cit/aet/service/simulation/SimulationExecutionService.java b/src/main/java/de/tum/cit/aet/service/simulation/SimulationExecutionService.java index 731909a7..ccff3d71 100644 --- a/src/main/java/de/tum/cit/aet/service/simulation/SimulationExecutionService.java +++ b/src/main/java/de/tum/cit/aet/service/simulation/SimulationExecutionService.java @@ -2,6 +2,7 @@ import static java.lang.Thread.sleep; +import de.tum.cit.aet.artemisModel.ArtemisAuthMechanism; import de.tum.cit.aet.artemisModel.Course; import de.tum.cit.aet.artemisModel.Exam; import de.tum.cit.aet.artemisModel.ProgrammingExercise; @@ -74,6 +75,7 @@ public SimulationExecutionService( *

* The steps of the simulation depend on the simulation mode, see {@link Simulation.Mode}. * This method sends status updates, log messages and results to the client via websockets. + * * @param simulationRun the simulation run to execute * @throws SimulationFailedException if an error occurs while executing the simulation */ @@ -189,7 +191,8 @@ public synchronized void simulateExam(SimulationRun simulationRun) { ); logAndSend(false, simulationRun, "Simulation finished."); - // TODO: Cleanup deletes running build jobs. Need to find another approach to cleanup + + // Cleanup deletes running build jobs. When it is enabled subscribing to CI status is disabled cleanupAsync(admin, simulationRun, courseId, examId); // Calculate, save and send result @@ -206,11 +209,14 @@ public synchronized void simulateExam(SimulationRun simulationRun) { return; } } - // Subscribe to CI status, as we can only safely delete the course after all CI jobs have finished - try { - ciStatusService.subscribeToCiStatusViaResults(runWithResult, admin, examId).get(); - } catch (ExecutionException | InterruptedException e) { - logAndSend(true, simulationRun, "Error while subscribing to CI status: %s", e.getMessage()); + + // Subscribe to CI status, as we can only safely delete the course after all CI jobs have finished. + if (!artemisConfiguration.getCleanup(simulationRun.getSimulation().getServer())) { + try { + ciStatusService.subscribeToCiStatusViaResults(runWithResult, admin, examId).get(); + } catch (ExecutionException | InterruptedException e) { + logAndSend(true, simulationRun, "Error while subscribing to CI status: %s", e.getMessage()); + } } } } @@ -222,11 +228,12 @@ public synchronized void simulateExam(SimulationRun simulationRun) { *

* Fails the simulation run if an error occurs while performing the simulations. * Does not fail for exceptions occurring for individual students. + * * @param simulationRun the simulation run to perform the exam participations for - * @param students the students to perform the exam participations with - * @param admin the admin to use for cleanup if necessary - * @param courseId the ID of the course the exam is in - * @param examId the ID of the exam to participate in + * @param students the students to perform the exam participations with + * @param admin the admin to use for cleanup if necessary + * @param courseId the ID of the course the exam is in + * @param examId the ID of the exam to participate in * @return a list of request stats for all performed actions * @throws SimulationFailedException if an error occurs while performing the simulations */ @@ -260,9 +267,7 @@ private List simulateExamParticipations( ) ); requestStats.addAll( - performActionWithAll(threadCount, simulation.getNumberOfUsers(), i -> - students[i].participateInExam(courseId, examId, simulation.getIdeType() == Simulation.IDEType.ONLINE) - ) + performActionWithAll(threadCount, simulation.getNumberOfUsers(), i -> students[i].participateInExam(courseId, examId)) ); requestStats.addAll( performActionWithAll(threadCount, simulation.getNumberOfUsers(), i -> students[i].submitAndEndExam(courseId, examId)) @@ -281,6 +286,7 @@ private List simulateExamParticipations( * Sets the doNotSleep flag. * If the flag is set to true, the simulation will not wait for user group synchronization. * The flag should only be set to true for testing purposes when the connection to Artemis is mocked. + * * @param doNotSleep the value to set the flag to */ public void setDoNotSleep(boolean doNotSleep) { @@ -290,8 +296,9 @@ public void setDoNotSleep(boolean doNotSleep) { /** * Initializes and logs in the admin for the given simulation run. * Fails the simulation run if an error occurs while initializing the admin. + * * @param simulationRun the simulation run to initialize the admin for - * @param accountDTO the account to use for logging in (only necessary for production instance) + * @param accountDTO the account to use for logging in (only necessary for production instance) * @return the initialized and logged in admin * @throws SimulationFailedException if an error occurs while initializing the admin */ @@ -328,7 +335,7 @@ private SimulatedArtemisAdmin initializeAdminFromUserManagement(ArtemisServer se /** * Initializes the admin for the given server with the given account and logs in. * - * @param server the Artemis Server to initialize the admin for + * @param server the Artemis Server to initialize the admin for * @param artemisAccountDTO the account to use for logging in * @return the initialized and logged in admin */ @@ -345,7 +352,8 @@ private SimulatedArtemisAdmin initializeAdminWithAccount(ArtemisServer server, A /** * Creates a course for the given admin and simulation run. * Fails the simulation run if an error occurs while creating the course. - * @param admin the admin to use for creating the course + * + * @param admin the admin to use for creating the course * @param simulationRun the simulation run to create the course for * @return the created course * @throws SimulationFailedException if an error occurs while creating the course @@ -361,13 +369,19 @@ private Course createCourse(SimulatedArtemisAdmin admin, SimulationRun simulatio } } + private void cancelAllBuildJobs(SimulatedArtemisAdmin admin) { + admin.cancelAllQueuedBuildJobs(); + admin.cancelAllRunningBuildJobs(); + } + /** * Registers the given students for the given course using the given admin and simulation run. * Fails the simulation run if an error occurs while registering the students. - * @param admin the admin to use for registering the students + * + * @param admin the admin to use for registering the students * @param simulationRun the simulation run to register the students for - * @param courseId the ID of the course to register the students for - * @param students the students to register + * @param courseId the ID of the course to register the students for + * @param students the students to register * @throws SimulationFailedException if an error occurs while registering the students */ private void registerStudentsForCourse( @@ -390,9 +404,10 @@ private void registerStudentsForCourse( /** * Fetches the course with the given ID using the given admin and simulation run. * Fails the simulation run if an error occurs while fetching the course. - * @param admin the admin to use for fetching the course + * + * @param admin the admin to use for fetching the course * @param simulationRun the simulation run to fetch the course for - * @param courseId the ID of the course to fetch + * @param courseId the ID of the course to fetch * @return the fetched course * @throws SimulationFailedException if an error occurs while fetching the course */ @@ -420,9 +435,10 @@ private ProgrammingExercise createCourseProgrammingExercise(SimulatedArtemisAdmi /** * Creates an exam for the given simulation run in the given course using the given admin. * Fails the simulation run if an error occurs while creating the exam. - * @param admin the admin to use for creating the exam + * + * @param admin the admin to use for creating the exam * @param simulationRun the simulation run to create the exam for - * @param course the course to create the exam in + * @param course the course to create the exam in * @return the created exam * @throws SimulationFailedException if an error occurs while creating the exam */ @@ -441,10 +457,11 @@ private Exam createExam(SimulatedArtemisAdmin admin, SimulationRun simulationRun /** * Creates the exercises for the given exam using the given admin and simulation run. * Fails the simulation run if an error occurs while creating the exercises. - * @param admin the admin to use for creating the exercises + * + * @param admin the admin to use for creating the exercises * @param simulationRun the simulation run to create the exercises for - * @param courseId the ID of the course the exam is in - * @param exam the exam to create the exercises for + * @param courseId the ID of the course the exam is in + * @param exam the exam to create the exercises for * @throws SimulationFailedException if an error occurs while creating the exercises */ private void createExamExercises(SimulatedArtemisAdmin admin, SimulationRun simulationRun, long courseId, Exam exam) { @@ -463,10 +480,11 @@ private void createExamExercises(SimulatedArtemisAdmin admin, SimulationRun simu * Registers the students for the given exam using the given admin and simulation run. * Registers all students of the course. * Fails the simulation run if an error occurs while registering the students. - * @param admin the admin to use for registering the students + * + * @param admin the admin to use for registering the students * @param simulationRun the simulation run to register the students for - * @param courseId the ID of the course the exam is in - * @param examId the ID of the exam to register the students for + * @param courseId the ID of the course the exam is in + * @param examId the ID of the exam to register the students for * @throws SimulationFailedException if an error occurs while registering the students */ private void registerStudentsForExam(SimulatedArtemisAdmin admin, SimulationRun simulationRun, long courseId, long examId) { @@ -485,10 +503,11 @@ private void registerStudentsForExam(SimulatedArtemisAdmin admin, SimulationRun * Prepares the exam for conduction using the given admin and simulation run. * This includes generating the student exams and preparing the exercises. * Fails the simulation run if an error occurs while preparing the exam. - * @param admin the admin to use for preparing the exam + * + * @param admin the admin to use for preparing the exam * @param simulationRun the simulation run to prepare the exam for - * @param courseId the ID of the course the exam is in - * @param examId the ID of the exam to prepare + * @param courseId the ID of the course the exam is in + * @param examId the ID of the exam to prepare * @throws SimulationFailedException if an error occurs while preparing the exam */ private void prepareExam(SimulatedArtemisAdmin admin, SimulationRun simulationRun, long courseId, long examId) { @@ -525,15 +544,45 @@ private SimulatedArtemisStudent[] initializeStudents(SimulationRun simulationRun } SimulatedArtemisStudent[] users = new SimulatedArtemisStudent[artemisUsers.size()]; + int onlineIde, password, token, ssh; + onlineIde = password = token = ssh = 0; + for (int i = 0; i < artemisUsers.size(); i++) { + var mechanism = getArtemisAuthMechanism(simulation); + switch (mechanism) { + case ONLINE_IDE -> onlineIde++; + case PASSWORD -> password++; + case PARTICIPATION_TOKEN -> token++; + case SSH -> ssh++; + } + users[i] = SimulatedArtemisUser.createArtemisStudent( artemisConfiguration.getUrl(simulation.getServer()), artemisUsers.get(i), artemisUserService, simulation.getNumberOfCommitsAndPushesFrom(), - simulation.getNumberOfCommitsAndPushesTo() + simulation.getNumberOfCommitsAndPushesTo(), + mechanism ); } + + log.info( + "Users will use authentication mechanisms: onlineIDE {{}} | password {{}} | token {{}} | SSH {{}}", + onlineIde, + password, + token, + ssh + ); + logAndSend( + false, + simulationRun, + "User authentication: onlineIDE %s | password %s | token %s | SSH %s", + onlineIde, + password, + token, + ssh + ); + return users; } catch (Exception e) { logAndSend(true, simulationRun, "Error while initializing students: %s", e.getMessage()); @@ -549,9 +598,9 @@ private SimulatedArtemisStudent[] initializeStudents(SimulationRun simulationRun * If an exception occurs while performing the action for a user, the exception is logged and the user is skipped. * Exceptions occurring for one user do not affect the execution of the action for other users and are not rethrown. * - * @param threadCount the number of threads to use + * @param threadCount the number of threads to use * @param numberOfUsers the number of users to perform the action for - * @param action the action to perform + * @param action the action to perform * @return a list of request stats for all performed actions */ private List performActionWithAll(int threadCount, int numberOfUsers, Function> action) { @@ -581,10 +630,11 @@ private List performActionWithAll(int threadCount, int numberOfUser /** * Calls {@link #cleanup(SimulatedArtemisAdmin, SimulationRun, long, long)} asynchronously. - * @param admin the admin to use for cleanup + * + * @param admin the admin to use for cleanup * @param simulationRun the simulation run to cleanup - * @param courseId the ID of the course to cleanup - * @param examId the ID of the exam to cleanup + * @param courseId the ID of the course to cleanup + * @param examId the ID of the exam to cleanup */ private void cleanupAsync(SimulatedArtemisAdmin admin, SimulationRun simulationRun, long courseId, long examId) { if (Thread.currentThread().isInterrupted() || admin == null) { @@ -600,10 +650,10 @@ private void cleanupAsync(SimulatedArtemisAdmin admin, SimulationRun simulationR *

* It is recommended to call this method asynchronously via {@link #cleanupAsync(SimulatedArtemisAdmin, SimulationRun, long, long)}. * - * @param admin the admin to use for cleanup + * @param admin the admin to use for cleanup * @param simulationRun the simulation run to cleanup - * @param courseId the ID of the course to cleanup - * @param examId the ID of the exam to cleanup + * @param courseId the ID of the course to cleanup + * @param examId the ID of the exam to cleanup */ private void cleanup(SimulatedArtemisAdmin admin, SimulationRun simulationRun, long courseId, long examId) { if (Thread.currentThread().isInterrupted() || admin == null) { @@ -617,6 +667,15 @@ private void cleanup(SimulatedArtemisAdmin admin, SimulationRun simulationRun, l return; } + logAndSend(false, simulationRun, "Trying to cancel all build jobs..."); + cancelAllBuildJobs(admin); + try { + sleep(1_000 * 15); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + logAndSend(false, simulationRun, "Done cancelling all build jobs"); + logAndSend(false, simulationRun, "Cleaning up... This may take a while."); try { if (mode == Simulation.Mode.EXISTING_COURSE_CREATE_EXAM && examId != 0) { @@ -638,10 +697,11 @@ private void cleanup(SimulatedArtemisAdmin admin, SimulationRun simulationRun, l /** * Logs the given message and sends it to the client via websockets. * Also saves the message to the database. - * @param error whether the message is an error message + * + * @param error whether the message is an error message * @param simulationRun the simulation run to send the message for - * @param format the format string - * @param args the arguments for the format string + * @param format the format string + * @param args the arguments for the format string */ private void logAndSend(boolean error, SimulationRun simulationRun, String format, Object... args) { if (Thread.currentThread().isInterrupted()) { @@ -691,6 +751,7 @@ private void failSimulationRun(SimulationRun simulationRun) { /** * Sets the simulation run status to finished and sends the result to the client via websockets. + * * @param simulationRun the simulation run to finish */ private void finishSimulationRun(SimulationRun simulationRun) { @@ -703,6 +764,7 @@ private void finishSimulationRun(SimulationRun simulationRun) { /** * Sends the result of the given simulation run to the client via websockets. * Also sends a mail with the result if the simulation run is part of a schedule. + * * @param simulationRun the simulation run to send the result for */ private void sendRunResult(SimulationRun simulationRun) { @@ -711,4 +773,21 @@ private void sendRunResult(SimulationRun simulationRun) { mailService.sendRunResultMail(simulationRun, simulationRun.getSchedule()); } } + + private ArtemisAuthMechanism getArtemisAuthMechanism(Simulation simulation) { + Random random = new Random(); + double randomValue = random.nextDouble() * 100; + + if (randomValue <= simulation.getOnlineIdePercentage()) { + return ArtemisAuthMechanism.ONLINE_IDE; + } else if (randomValue <= simulation.getOnlineIdePercentage() + simulation.getPasswordPercentage()) { + return ArtemisAuthMechanism.PASSWORD; + } else if ( + randomValue <= simulation.getOnlineIdePercentage() + simulation.getPasswordPercentage() + simulation.getTokenPercentage() + ) { + return ArtemisAuthMechanism.PARTICIPATION_TOKEN; + } else { + return ArtemisAuthMechanism.SSH; + } + } } diff --git a/src/main/java/de/tum/cit/aet/service/simulation/SimulationResultService.java b/src/main/java/de/tum/cit/aet/service/simulation/SimulationResultService.java index e24d0fd8..c79dd90f 100644 --- a/src/main/java/de/tum/cit/aet/service/simulation/SimulationResultService.java +++ b/src/main/java/de/tum/cit/aet/service/simulation/SimulationResultService.java @@ -67,9 +67,13 @@ public SimulationRun calculateAndSaveResult(SimulationRun simulationRun, List generateSshKeyPair() { + try { + // Generate RSA key pair + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); // Use 2048-bit key size + KeyPair keyPair = keyPairGen.generateKeyPair(); + + var privateKey = exportPrivateKey(keyPair.getPrivate()); + var publicKey = exportPublicKey(keyPair); + return Pair.of(publicKey, privateKey); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static String exportPrivateKey(java.security.PrivateKey privateKey) throws IOException { + StringWriter stringWriter = new StringWriter(); + try (JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) { + pemWriter.writeObject(privateKey); + } + return stringWriter.toString(); + } + + private static String exportPublicKey(KeyPair keyPair) { + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + return "ssh-rsa " + Base64.getEncoder().encodeToString(encodePublicKey(publicKey)); + } + + private static byte[] encodePublicKey(RSAPublicKey publicKey) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writeString(out, "ssh-rsa"); + writeBigInteger(out, publicKey.getPublicExponent()); + writeBigInteger(out, publicKey.getModulus()); + + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("Error encoding public key", e); + } + } + + private static void writeString(ByteArrayOutputStream out, String data) throws IOException { + byte[] bytes = data.getBytes(); + writeInt(out, bytes.length); + out.write(bytes); + } + + private static void writeBigInteger(ByteArrayOutputStream out, BigInteger value) throws IOException { + byte[] bytes = value.toByteArray(); + writeInt(out, bytes.length); + out.write(bytes); + } + + private static void writeInt(ByteArrayOutputStream out, int value) throws IOException { + out.write((value >> 24) & 0xFF); + out.write((value >> 16) & 0xFF); + out.write((value >> 8) & 0xFF); + out.write(value & 0xFF); + } +} diff --git a/src/main/resources/config/liquibase/changelog/00000000000019_add_token_SSH_key_to_user.xml b/src/main/resources/config/liquibase/changelog/00000000000019_add_token_SSH_key_to_user.xml new file mode 100644 index 00000000..bae71120 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/00000000000019_add_token_SSH_key_to_user.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/00000000000020_add_participation_mode_percentages.xml b/src/main/resources/config/liquibase/changelog/00000000000020_add_participation_mode_percentages.xml new file mode 100644 index 00000000..07300600 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/00000000000020_add_participation_mode_percentages.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 5fe3cd22..3936f0e4 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -27,6 +27,9 @@ + + + diff --git a/src/main/webapp/app/entities/simulation/simulation.ts b/src/main/webapp/app/entities/simulation/simulation.ts index 115b2627..ed5d1f6c 100644 --- a/src/main/webapp/app/entities/simulation/simulation.ts +++ b/src/main/webapp/app/entities/simulation/simulation.ts @@ -13,9 +13,12 @@ export class Simulation { public runs: SimulationRun[], public creationDate: Date, public customizeUserRange: boolean, - public ideType: IdeType, public numberOfCommitsAndPushesFrom: number, public numberOfCommitsAndPushesTo: number, + public onlineIdePercentage: number, + public passwordPercentage: number, + public tokenPercentage: number, + public sshPercentage: number, public userRange?: string, public instructorUsername?: string | null, public instructorPassword?: string | null, @@ -49,17 +52,3 @@ export function getTextRepresentation(mode: Mode): string { return 'Existing course, create exam'; } } - -export enum IdeType { - ONLINE = 'ONLINE', - OFFLINE = 'OFFLINE', -} - -export function getTextRepresentationIdeType(ideType: IdeType): string { - switch (ideType) { - case IdeType.OFFLINE: - return 'Offline'; - case IdeType.ONLINE: - return 'Online'; - } -} diff --git a/src/main/webapp/app/layouts/create-simulation-box/create-simulation-box.component.html b/src/main/webapp/app/layouts/create-simulation-box/create-simulation-box.component.html index ad093783..0e3aa4c7 100644 --- a/src/main/webapp/app/layouts/create-simulation-box/create-simulation-box.component.html +++ b/src/main/webapp/app/layouts/create-simulation-box/create-simulation-box.component.html @@ -33,13 +33,56 @@ } + + +

- - + + +
+
+ + +
+
+ + +
+
+ +
diff --git a/src/main/webapp/app/layouts/create-simulation-box/create-simulation-box.component.ts b/src/main/webapp/app/layouts/create-simulation-box/create-simulation-box.component.ts index e754e7da..7e810e3c 100644 --- a/src/main/webapp/app/layouts/create-simulation-box/create-simulation-box.component.ts +++ b/src/main/webapp/app/layouts/create-simulation-box/create-simulation-box.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, OnInit, Output } from '@angular/core'; -import { getTextRepresentation, getTextRepresentationIdeType, IdeType, Mode, Simulation } from '../../entities/simulation/simulation'; +import { getTextRepresentation, Mode, Simulation } from '../../entities/simulation/simulation'; import { ArtemisServer } from '../../core/util/artemisServer'; import { ProfileService } from '../profiles/profile.service'; import { SimulationsService } from '../../simulations/simulations.service'; @@ -24,11 +24,14 @@ export class CreateSimulationBoxComponent implements OnInit { mode: Mode = Mode.CREATE_COURSE_AND_EXAM; customizeUserRange: boolean = false; userRange: string = ''; - ideType: IdeType = IdeType.OFFLINE; numberOfCommitsAndPushesFrom: number = 8; numberOfCommitsAndPushesTo: number = 15; instructorUsername: string = ''; instructorPassword: string = ''; + passwordPercentage: number = 100; + tokenPercentage: number = 0; + sshPercentage: number = 0; + onlineIdePercentage: number = 0; availableServers = Object.values(ArtemisServer); availableModes = [ @@ -37,14 +40,12 @@ export class CreateSimulationBoxComponent implements OnInit { Mode.EXISTING_COURSE_PREPARED_EXAM, Mode.EXISTING_COURSE_UNPREPARED_EXAM, ]; - availableIdeTypes = [IdeType.OFFLINE, IdeType.ONLINE]; serversWithCleanupEnabled: ArtemisServer[] = []; showPassword: boolean = false; protected readonly Mode = Mode; protected readonly ArtemisServer = ArtemisServer; protected readonly getTextRepresentation = getTextRepresentation; - protected readonly getTextRepresentationIdeType = getTextRepresentationIdeType; constructor( private profileService: ProfileService, @@ -78,9 +79,12 @@ export class CreateSimulationBoxComponent implements OnInit { [], new Date(), this.customizeUserRange, - this.ideType, this.numberOfCommitsAndPushesFrom, this.numberOfCommitsAndPushesTo, + this.onlineIdePercentage, + this.passwordPercentage, + this.tokenPercentage, + this.sshPercentage, this.userRange, this.instructorUsername.length > 0 ? this.instructorUsername : undefined, this.instructorPassword.length > 0 ? this.instructorPassword : undefined, @@ -97,7 +101,8 @@ export class CreateSimulationBoxComponent implements OnInit { this.name.length > 0 && ((!this.customizeUserRange && this.numberOfUsers > 0) || (this.customizeUserRange && this.userRange.length > 0)) && this.numberOfCommitsAndPushesFrom > 0 && - this.numberOfCommitsAndPushesTo > this.numberOfCommitsAndPushesFrom; + this.numberOfCommitsAndPushesTo > this.numberOfCommitsAndPushesFrom && + this.sshPercentage + this.tokenPercentage + this.passwordPercentage + this.onlineIdePercentage === 100; if (this.mode === Mode.CREATE_COURSE_AND_EXAM) { return basicRequirements; @@ -105,6 +110,7 @@ export class CreateSimulationBoxComponent implements OnInit { if (this.mode === Mode.EXISTING_COURSE_CREATE_EXAM) { return basicRequirements && this.courseId > 0; } + return basicRequirements && this.courseId > 0 && this.examId > 0; } } diff --git a/src/main/webapp/app/layouts/simulation-card/simulation-card.component.html b/src/main/webapp/app/layouts/simulation-card/simulation-card.component.html index 80d04129..bc741efc 100644 --- a/src/main/webapp/app/layouts/simulation-card/simulation-card.component.html +++ b/src/main/webapp/app/layouts/simulation-card/simulation-card.component.html @@ -41,7 +41,6 @@ Users: {{ simulation.numberOfUsers }} }

-

IDE Type: {{ getTextRepresentationIdeType(simulation.ideType) }}

Commits: {{ simulation.numberOfCommitsAndPushesFrom }} - {{ simulation.numberOfCommitsAndPushesTo }}

@if (simulation.mode != Mode.CREATE_COURSE_AND_EXAM) {
diff --git a/src/main/webapp/app/layouts/simulation-card/simulation-card.component.ts b/src/main/webapp/app/layouts/simulation-card/simulation-card.component.ts index a56b76c8..4459bce6 100644 --- a/src/main/webapp/app/layouts/simulation-card/simulation-card.component.ts +++ b/src/main/webapp/app/layouts/simulation-card/simulation-card.component.ts @@ -1,11 +1,5 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { - getTextRepresentation, - getTextRepresentationIdeType, - instructorCredentialsProvided, - Mode, - Simulation, -} from '../../entities/simulation/simulation'; +import { getTextRepresentation, instructorCredentialsProvided, Mode, Simulation } from '../../entities/simulation/simulation'; import { SimulationRun, Status } from '../../entities/simulation/simulationRun'; import { SimulationsService } from '../../simulations/simulations.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @@ -48,7 +42,6 @@ export class SimulationCardComponent implements OnInit { protected readonly Mode = Mode; protected readonly Status = Status; protected readonly getTextRepresentation = getTextRepresentation; - protected readonly getTextRepresentationIdeType = getTextRepresentationIdeType; protected readonly ArtemisServer = ArtemisServer; protected readonly instructorCredentialsProvided = instructorCredentialsProvided; diff --git a/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.html b/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.html index 2edb0e68..c83651ed 100644 --- a/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.html +++ b/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.html @@ -106,9 +106,11 @@

- - - +
diff --git a/src/test/java/de/tum/cit/aet/service/SimulationExecutionServiceIT.java b/src/test/java/de/tum/cit/aet/service/SimulationExecutionServiceIT.java index f32cc3b7..a60d1c56 100644 --- a/src/test/java/de/tum/cit/aet/service/SimulationExecutionServiceIT.java +++ b/src/test/java/de/tum/cit/aet/service/SimulationExecutionServiceIT.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.*; import de.tum.cit.aet.IntegrationTest; +import de.tum.cit.aet.artemisModel.ArtemisAuthMechanism; import de.tum.cit.aet.artemisModel.Course; import de.tum.cit.aet.artemisModel.Exam; import de.tum.cit.aet.domain.ArtemisUser; @@ -34,7 +35,6 @@ import org.junit.jupiter.api.*; import org.mockito.*; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; @@ -123,13 +123,13 @@ public void init() { .thenReturn(simulatedArtemisAdmin); mockedSimulatedArtemisUser.when(() -> createArtemisAdminFromCredentials("", "admin", "admin")).thenReturn(simulatedArtemisAdmin); mockedSimulatedArtemisUser - .when(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15)) + .when(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE)) .thenReturn(simulatedArtemisStudent1); mockedSimulatedArtemisUser - .when(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15)) + .when(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE)) .thenReturn(simulatedArtemisStudent2); mockedSimulatedArtemisUser - .when(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15)) + .when(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE)) .thenReturn(simulatedArtemisStudent3); when(simulatedArtemisAdmin.login()).thenReturn(List.of()); @@ -145,9 +145,9 @@ public void init() { when(simulatedArtemisStudent2.startExamParticipation(1, 1, 0)).thenReturn(List.of()); when(simulatedArtemisStudent3.startExamParticipation(1, 1, 0)).thenReturn(List.of()); - when((simulatedArtemisStudent1.participateInExam(1, 1, false))).thenReturn(List.of()); - when((simulatedArtemisStudent2.participateInExam(1, 1, false))).thenReturn(List.of()); - when((simulatedArtemisStudent3.participateInExam(1, 1, false))).thenReturn(List.of()); + when((simulatedArtemisStudent1.participateInExam(1, 1))).thenReturn(List.of()); + when((simulatedArtemisStudent2.participateInExam(1, 1))).thenReturn(List.of()); + when((simulatedArtemisStudent3.participateInExam(1, 1))).thenReturn(List.of()); when(simulatedArtemisStudent1.startExamParticipation(1, 1, 0)).thenReturn(List.of()); when(simulatedArtemisStudent2.startExamParticipation(1, 1, 0)).thenReturn(List.of()); @@ -220,7 +220,7 @@ public void testCreateCourseAndExam_cleanupEnabled_success() { verify(simulatedStudent, times(1)).login(); verify(simulatedStudent, times(1)).performInitialCalls(); verify(simulatedStudent, times(1)).startExamParticipation(1, 1, 0); - verify(simulatedStudent, times(1)).participateInExam(1, 1, false); + verify(simulatedStudent, times(1)).participateInExam(1, 1); verify(simulatedStudent, times(1)).submitAndEndExam(1, 1); } @@ -235,9 +235,18 @@ public void testCreateCourseAndExam_cleanupEnabled_success() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser("", adminUser, artemisUserService), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -280,7 +289,7 @@ public void testCreateCourseAndExam_cleanupDisabled_success() { verify(simulatedStudent, times(1)).login(); verify(simulatedStudent, times(1)).performInitialCalls(); verify(simulatedStudent, times(1)).startExamParticipation(1, 1, 0); - verify(simulatedStudent, times(1)).participateInExam(1, 1, false); + verify(simulatedStudent, times(1)).participateInExam(1, 1); verify(simulatedStudent, times(1)).submitAndEndExam(1, 1); } @@ -295,9 +304,18 @@ public void testCreateCourseAndExam_cleanupDisabled_success() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser("", adminUser, artemisUserService), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -337,7 +355,7 @@ public void testExistingCourseCreateExam_cleanupEnabled_success() { verify(simulatedStudent, times(1)).login(); verify(simulatedStudent, times(1)).performInitialCalls(); verify(simulatedStudent, times(1)).startExamParticipation(1, 1, 0); - verify(simulatedStudent, times(1)).participateInExam(1, 1, false); + verify(simulatedStudent, times(1)).participateInExam(1, 1); verify(simulatedStudent, times(1)).submitAndEndExam(1, 1); } @@ -352,9 +370,18 @@ public void testExistingCourseCreateExam_cleanupEnabled_success() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser("", adminUser, artemisUserService), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -394,7 +421,7 @@ public void testExistingCourseCreateExam_cleanupDisabled_success() { verify(simulatedStudent, times(1)).login(); verify(simulatedStudent, times(1)).performInitialCalls(); verify(simulatedStudent, times(1)).startExamParticipation(1, 1, 0); - verify(simulatedStudent, times(1)).participateInExam(1, 1, false); + verify(simulatedStudent, times(1)).participateInExam(1, 1); verify(simulatedStudent, times(1)).submitAndEndExam(1, 1); } @@ -409,9 +436,18 @@ public void testExistingCourseCreateExam_cleanupDisabled_success() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser("", adminUser, artemisUserService), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -452,7 +488,7 @@ public void testExistingCourseUnpreparedExam_success() { verify(simulatedStudent, times(1)).login(); verify(simulatedStudent, times(1)).performInitialCalls(); verify(simulatedStudent, times(1)).startExamParticipation(1, 1, 0); - verify(simulatedStudent, times(1)).participateInExam(1, 1, false); + verify(simulatedStudent, times(1)).participateInExam(1, 1); verify(simulatedStudent, times(1)).submitAndEndExam(1, 1); } @@ -467,9 +503,18 @@ public void testExistingCourseUnpreparedExam_success() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser("", adminUser, artemisUserService), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -512,7 +557,7 @@ public void testExistingCoursePreparedExam_success() { verify(simulatedStudent, times(1)).login(); verify(simulatedStudent, times(1)).performInitialCalls(); verify(simulatedStudent, times(1)).startExamParticipation(1, 1, 0); - verify(simulatedStudent, times(1)).participateInExam(1, 1, false); + verify(simulatedStudent, times(1)).participateInExam(1, 1); verify(simulatedStudent, times(1)).submitAndEndExam(1, 1); } @@ -527,9 +572,18 @@ public void testExistingCoursePreparedExam_success() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser(any(), any(), any()), times(0)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -576,7 +630,7 @@ public void testCreateCourseAndExam_cleanupEnabled_production_success() { verify(simulatedStudent, times(1)).login(); verify(simulatedStudent, times(1)).performInitialCalls(); verify(simulatedStudent, times(1)).startExamParticipation(1, 1, 0); - verify(simulatedStudent, times(1)).participateInExam(1, 1, false); + verify(simulatedStudent, times(1)).participateInExam(1, 1); verify(simulatedStudent, times(1)).submitAndEndExam(1, 1); } @@ -592,9 +646,18 @@ public void testCreateCourseAndExam_cleanupEnabled_production_success() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser(any(), any(), any()), times(0)); mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromCredentials("", "admin", "admin"), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -634,7 +697,7 @@ public void testCreateCourseAndExam_fail_onInitializeStudents() { verify(simulatedStudent, times(0)).login(); verify(simulatedStudent, times(0)).performInitialCalls(); verify(simulatedStudent, times(0)).startExamParticipation(anyLong(), anyLong(), anyLong()); - verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong(), anyBoolean()); + verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong()); verify(simulatedStudent, times(0)).submitAndEndExam(anyLong(), anyLong()); } @@ -649,7 +712,10 @@ public void testCreateCourseAndExam_fail_onInitializeStudents() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser(any(), any(), any()), times(0)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent(any(), any(), any(), anyInt(), anyInt()), times(0)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent(any(), any(), any(), anyInt(), anyInt(), ArtemisAuthMechanism.ONLINE_IDE), + times(0) + ); } @Test @@ -689,7 +755,7 @@ public void testCreateCourseAndExam_fail_onInitializeAdmin() { verify(simulatedStudent, times(0)).login(); verify(simulatedStudent, times(0)).performInitialCalls(); verify(simulatedStudent, times(0)).startExamParticipation(anyLong(), anyLong(), anyLong()); - verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong(), anyBoolean()); + verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong()); verify(simulatedStudent, times(0)).submitAndEndExam(anyLong(), anyLong()); } @@ -704,9 +770,18 @@ public void testCreateCourseAndExam_fail_onInitializeAdmin() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser(any(), any(), any()), times(0)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -746,7 +821,7 @@ public void testCreateCourseAndExam_fail_onCreateCourse() { verify(simulatedStudent, times(0)).login(); verify(simulatedStudent, times(0)).performInitialCalls(); verify(simulatedStudent, times(0)).startExamParticipation(anyLong(), anyLong(), anyLong()); - verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong(), anyBoolean()); + verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong()); verify(simulatedStudent, times(0)).submitAndEndExam(anyLong(), anyLong()); } @@ -761,9 +836,18 @@ public void testCreateCourseAndExam_fail_onCreateCourse() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser("", adminUser, artemisUserService), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -806,7 +890,7 @@ public void testCreateCourseAndExam_fail_onRegisterStudentsForCourse() { verify(simulatedStudent, times(0)).login(); verify(simulatedStudent, times(0)).performInitialCalls(); verify(simulatedStudent, times(0)).startExamParticipation(anyLong(), anyLong(), anyLong()); - verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong(), anyBoolean()); + verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong()); verify(simulatedStudent, times(0)).submitAndEndExam(anyLong(), anyLong()); } @@ -821,9 +905,18 @@ public void testCreateCourseAndExam_fail_onRegisterStudentsForCourse() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser("", adminUser, artemisUserService), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -864,7 +957,7 @@ public void testExistingCourseCreateExam_fail_onGetCourse() { verify(simulatedStudent, times(0)).login(); verify(simulatedStudent, times(0)).performInitialCalls(); verify(simulatedStudent, times(0)).startExamParticipation(anyLong(), anyLong(), anyLong()); - verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong(), anyBoolean()); + verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong()); verify(simulatedStudent, times(0)).submitAndEndExam(anyLong(), anyLong()); } @@ -879,9 +972,18 @@ public void testExistingCourseCreateExam_fail_onGetCourse() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser("", adminUser, artemisUserService), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -922,7 +1024,7 @@ public void testExistingCourseCreateExam_fail_onCreateExam() { verify(simulatedStudent, times(0)).login(); verify(simulatedStudent, times(0)).performInitialCalls(); verify(simulatedStudent, times(0)).startExamParticipation(anyLong(), anyLong(), anyLong()); - verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong(), anyBoolean()); + verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong()); verify(simulatedStudent, times(0)).submitAndEndExam(anyLong(), anyLong()); } @@ -937,9 +1039,18 @@ public void testExistingCourseCreateExam_fail_onCreateExam() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser("", adminUser, artemisUserService), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -980,7 +1091,7 @@ public void testExistingCourseCreateExam_fail_onCreateExamExercises() { verify(simulatedStudent, times(0)).login(); verify(simulatedStudent, times(0)).performInitialCalls(); verify(simulatedStudent, times(0)).startExamParticipation(anyLong(), anyLong(), anyLong()); - verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong(), anyBoolean()); + verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong()); verify(simulatedStudent, times(0)).submitAndEndExam(anyLong(), anyLong()); } @@ -995,9 +1106,18 @@ public void testExistingCourseCreateExam_fail_onCreateExamExercises() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser("", adminUser, artemisUserService), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -1038,7 +1158,7 @@ public void testExistingCourseCreateExam_fail_onRegisterStudentsForExam() { verify(simulatedStudent, times(0)).login(); verify(simulatedStudent, times(0)).performInitialCalls(); verify(simulatedStudent, times(0)).startExamParticipation(anyLong(), anyLong(), anyLong()); - verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong(), anyBoolean()); + verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong()); verify(simulatedStudent, times(0)).submitAndEndExam(anyLong(), anyLong()); } @@ -1053,9 +1173,18 @@ public void testExistingCourseCreateExam_fail_onRegisterStudentsForExam() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser("", adminUser, artemisUserService), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -1096,7 +1225,7 @@ public void testExistingCourseCreateExam_fail_onPrepareExam() { verify(simulatedStudent, times(0)).login(); verify(simulatedStudent, times(0)).performInitialCalls(); verify(simulatedStudent, times(0)).startExamParticipation(anyLong(), anyLong(), anyLong()); - verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong(), anyBoolean()); + verify(simulatedStudent, times(0)).participateInExam(anyLong(), anyLong()); verify(simulatedStudent, times(0)).submitAndEndExam(anyLong(), anyLong()); } @@ -1111,9 +1240,18 @@ public void testExistingCourseCreateExam_fail_onPrepareExam() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser("", adminUser, artemisUserService), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -1147,15 +1285,9 @@ public void testExistingCourseCreateExam_success_studentsCannotConnect() { when(simulatedArtemisStudent3.startExamParticipation(anyLong(), anyLong(), anyLong())).thenThrow( new RuntimeException("Test exception") ); - when(simulatedArtemisStudent1.participateInExam(anyLong(), anyLong(), anyBoolean())).thenThrow( - new RuntimeException("Test exception") - ); - when(simulatedArtemisStudent2.participateInExam(anyLong(), anyLong(), anyBoolean())).thenThrow( - new RuntimeException("Test exception") - ); - when(simulatedArtemisStudent3.participateInExam(anyLong(), anyLong(), anyBoolean())).thenThrow( - new RuntimeException("Test exception") - ); + when(simulatedArtemisStudent1.participateInExam(anyLong(), anyLong())).thenThrow(new RuntimeException("Test exception")); + when(simulatedArtemisStudent2.participateInExam(anyLong(), anyLong())).thenThrow(new RuntimeException("Test exception")); + when(simulatedArtemisStudent3.participateInExam(anyLong(), anyLong())).thenThrow(new RuntimeException("Test exception")); when(simulatedArtemisStudent1.submitAndEndExam(anyLong(), anyLong())).thenThrow(new RuntimeException("Test exception")); when(simulatedArtemisStudent2.submitAndEndExam(anyLong(), anyLong())).thenThrow(new RuntimeException("Test exception")); when(simulatedArtemisStudent3.submitAndEndExam(anyLong(), anyLong())).thenThrow(new RuntimeException("Test exception")); @@ -1181,7 +1313,7 @@ public void testExistingCourseCreateExam_success_studentsCannotConnect() { verify(simulatedStudent, times(1)).login(); verify(simulatedStudent, times(1)).performInitialCalls(); verify(simulatedStudent, times(1)).startExamParticipation(1, 1, 0); - verify(simulatedStudent, times(1)).participateInExam(1, 1, false); + verify(simulatedStudent, times(1)).participateInExam(1, 1); verify(simulatedStudent, times(1)).submitAndEndExam(1, 1); } @@ -1196,9 +1328,18 @@ public void testExistingCourseCreateExam_success_studentsCannotConnect() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser("", adminUser, artemisUserService), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } @Test @@ -1239,7 +1380,7 @@ public void testExistingCourseCreateExam_success_failOnCleanup() { verify(simulatedStudent, times(1)).login(); verify(simulatedStudent, times(1)).performInitialCalls(); verify(simulatedStudent, times(1)).startExamParticipation(1, 1, 0); - verify(simulatedStudent, times(1)).participateInExam(1, 1, false); + verify(simulatedStudent, times(1)).participateInExam(1, 1); verify(simulatedStudent, times(1)).submitAndEndExam(1, 1); } @@ -1254,8 +1395,17 @@ public void testExistingCourseCreateExam_success_failOnCleanup() { mockedSimulatedArtemisUser.verify(() -> createArtemisAdminFromUser("", adminUser, artemisUserService), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15), times(1)); - mockedSimulatedArtemisUser.verify(() -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15), times(1)); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser1, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser2, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); + mockedSimulatedArtemisUser.verify( + () -> createArtemisStudent("", studentUser3, artemisUserService, 8, 15, ArtemisAuthMechanism.ONLINE_IDE), + times(1) + ); } }