From 82966029f3c55fd0bca999527e5d4a60fb85d0c4 Mon Sep 17 00:00:00 2001 From: Simon Entholzer Date: Mon, 16 Dec 2024 22:14:10 +0100 Subject: [PATCH 1/5] add ssh and token authentication --- build.gradle | 17 +- .../artemisModel/ArtemisAuthMechanism.java | 9 + .../de/tum/cit/ase/domain/ArtemisUser.java | 43 ++ .../de/tum/cit/ase/domain/RequestType.java | 8 + .../de/tum/cit/ase/domain/Simulation.java | 50 +++ .../tum/cit/ase/service/CiStatusService.java | 6 +- .../service/artemis/ArtemisUserService.java | 33 +- .../interaction/SimulatedArtemisAdmin.java | 13 + .../interaction/SimulatedArtemisStudent.java | 421 +++++++++++++----- .../interaction/SimulatedArtemisUser.java | 49 +- .../artemis/util/UserSshPublicKeyDTO.java | 17 + .../simulation/SimulationDataService.java | 7 +- .../SimulationExecutionService.java | 161 +++++-- .../simulation/SimulationResultService.java | 14 +- .../java/de/tum/cit/ase/util/SshUtils.java | 75 ++++ ...000000000019_add_token_SSH_key_to_user.xml | 17 + ...020_add_participation_mode_percentages.xml | 25 ++ .../resources/config/liquibase/master.xml | 3 + .../app/entities/simulation/simulation.ts | 19 +- .../create-simulation-box.component.html | 55 ++- .../create-simulation-box.component.ts | 20 +- .../simulation-card.component.html | 1 - .../simulation-card.component.ts | 9 +- .../simulations-overview.component.html | 8 +- 24 files changed, 851 insertions(+), 229 deletions(-) create mode 100644 src/main/java/de/tum/cit/ase/artemisModel/ArtemisAuthMechanism.java create mode 100644 src/main/java/de/tum/cit/ase/service/artemis/util/UserSshPublicKeyDTO.java create mode 100644 src/main/java/de/tum/cit/ase/util/SshUtils.java create mode 100644 src/main/resources/config/liquibase/changelog/00000000000019_add_token_SSH_key_to_user.xml create mode 100644 src/main/resources/config/liquibase/changelog/00000000000020_add_participation_mode_percentages.xml diff --git a/build.gradle b/build.gradle index 2c0888f9..89f3eca7 100644 --- a/build.gradle +++ b/build.gradle @@ -219,7 +219,22 @@ dependencies { implementation "org.springframework.security:spring-security-messaging:${springSecurityVersion}" implementation "commons-io:commons-io:2.16.1" implementation "com.thedeanda:lorem:2.2" - implementation "org.eclipse.jgit:org.eclipse.jgit:6.9.0.202403050737-r" + 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 'com.opencsv:opencsv:5.9' diff --git a/src/main/java/de/tum/cit/ase/artemisModel/ArtemisAuthMechanism.java b/src/main/java/de/tum/cit/ase/artemisModel/ArtemisAuthMechanism.java new file mode 100644 index 00000000..01bd832f --- /dev/null +++ b/src/main/java/de/tum/cit/ase/artemisModel/ArtemisAuthMechanism.java @@ -0,0 +1,9 @@ +package de.tum.cit.ase.artemisModel; + +public enum ArtemisAuthMechanism { + ONLINE_IDE, + PASSWORD, + PARTICIPATION_TOKEN, + SSH, + USER_TOKEN, +} diff --git a/src/main/java/de/tum/cit/ase/domain/ArtemisUser.java b/src/main/java/de/tum/cit/ase/domain/ArtemisUser.java index 0c25bbc4..ce761000 100644 --- a/src/main/java/de/tum/cit/ase/domain/ArtemisUser.java +++ b/src/main/java/de/tum/cit/ase/domain/ArtemisUser.java @@ -4,7 +4,21 @@ import com.opencsv.bean.CsvBindByName; import de.tum.cit.ase.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/ase/domain/RequestType.java b/src/main/java/de/tum/cit/ase/domain/RequestType.java index bb0fd5b9..036ed7de 100644 --- a/src/main/java/de/tum/cit/ase/domain/RequestType.java +++ b/src/main/java/de/tum/cit/ase/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/ase/domain/Simulation.java b/src/main/java/de/tum/cit/ase/domain/Simulation.java index f52eeb33..755efca2 100644 --- a/src/main/java/de/tum/cit/ase/domain/Simulation.java +++ b/src/main/java/de/tum/cit/ase/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/ase/service/CiStatusService.java b/src/main/java/de/tum/cit/ase/service/CiStatusService.java index 35d566e8..c7197a3e 100644 --- a/src/main/java/de/tum/cit/ase/service/CiStatusService.java +++ b/src/main/java/de/tum/cit/ase/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/ase/service/artemis/ArtemisUserService.java b/src/main/java/de/tum/cit/ase/service/artemis/ArtemisUserService.java index 8c9bcde2..7cf0e0ff 100644 --- a/src/main/java/de/tum/cit/ase/service/artemis/ArtemisUserService.java +++ b/src/main/java/de/tum/cit/ase/service/artemis/ArtemisUserService.java @@ -10,11 +10,15 @@ import de.tum.cit.ase.service.dto.ArtemisUserPatternDTO; import de.tum.cit.ase.util.ArtemisServer; import de.tum.cit.ase.util.NumberRangeParser; +import de.tum.cit.ase.util.SshUtils; import de.tum.cit.ase.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; @@ -61,14 +65,28 @@ public List createArtemisUsersByPattern(ArtemisServer server, Artem if (admin == null) { throw new BadRequestAlertException("No admin user found for server", "artemisUser", "missingAdmin"); } - simulatedArtemisAdmin = - SimulatedArtemisUser.createArtemisAdminFromCredentials( - artemisConfiguration.getUrl(server), - admin.getUsername(), - admin.getPassword() - ); + simulatedArtemisAdmin = SimulatedArtemisUser.createArtemisAdminFromCredentials( + artemisConfiguration.getUrl(server), + admin.getUsername(), + admin.getPassword() + ); 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++) { @@ -79,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 @@ -112,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/ase/service/artemis/interaction/SimulatedArtemisAdmin.java b/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisAdmin.java index ca7ce191..2f060fd3 100644 --- a/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisAdmin.java +++ b/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisAdmin.java @@ -178,6 +178,19 @@ 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(); + } + /** * Create an exam for benchmarking. * @param course the course for which to create the exam diff --git a/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisStudent.java b/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisStudent.java index ca236b8c..5543c973 100644 --- a/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisStudent.java +++ b/src/main/java/de/tum/cit/ase/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.ase.artemisModel.*; import de.tum.cit.ase.domain.ArtemisUser; @@ -14,19 +15,45 @@ import de.tum.cit.ase.service.artemis.util.ArtemisServerInfo; import de.tum.cit.ase.service.artemis.util.CourseDashboardDTO; import de.tum.cit.ase.service.artemis.util.ScienceEventDTO; +import de.tum.cit.ase.service.artemis.util.UserSshPublicKeyDTO; +import de.tum.cit.ase.util.SshUtils; import de.tum.cit.ase.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) { @@ -191,14 +230,13 @@ private RequestStat getNotifications() { long start = System.nanoTime(); webClient .get() - .uri( - uriBuilder -> - uriBuilder - .path("api/notifications") - .queryParam("page", 0) - .queryParam("size", 25) - .queryParam("sort", "notificationDate,desc") - .build() + .uri(uriBuilder -> + uriBuilder + .path("api/notifications") + .queryParam("page", 0) + .queryParam("size", 25) + .queryParam("sort", "notificationDate,desc") + .build() ) .retrieve() .toBodilessEntity() @@ -206,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(); @@ -262,9 +329,8 @@ private void getUnreadMessages() { private void getExerciseChannelAndMessages(long exerciseId) { Map channelResponse = webClient .get() - .uri( - uriBuilder -> - uriBuilder.pathSegment("api", "courses", courseIdString, "exercises", String.valueOf(exerciseId), "channel").build() + .uri(uriBuilder -> + uriBuilder.pathSegment("api", "courses", courseIdString, "exercises", String.valueOf(exerciseId), "channel").build() ) .retrieve() .bodyToMono(new ParameterizedTypeReference>() {}) @@ -278,17 +344,16 @@ private void getExerciseChannelAndMessages(long exerciseId) { webClient .get() - .uri( - uriBuilder -> - uriBuilder - .pathSegment("api", "courses", courseIdString, "messages") - .queryParam("conversationId", channelId) - .queryParam("PostSortCriterion", "CREATION_DATE") - .queryParam("SortingOrder", "DESCENDING") - .queryParam("pagingEnabled", true) - .queryParam("page", 0) - .queryParam("size", 50) - .build() + .uri(uriBuilder -> + uriBuilder + .pathSegment("api", "courses", courseIdString, "messages") + .queryParam("conversationId", channelId) + .queryParam("PostSortCriterion", "CREATION_DATE") + .queryParam("SortingOrder", "DESCENDING") + .queryParam("pagingEnabled", true) + .queryParam("page", 0) + .queryParam("size", 50) + .build() ) .retrieve() .toBodilessEntity() @@ -299,16 +364,10 @@ private void getExerciseChannelAndMessages(long exerciseId) { private void getLatestResult(long participationId) { webClient .get() - .uri( - uriBuilder -> - uriBuilder - .pathSegment( - "api", - "programming-exercise-participations", - String.valueOf(participationId), - "latest-pending-submission" - ) - .build() + .uri(uriBuilder -> + uriBuilder + .pathSegment("api", "programming-exercise-participations", String.valueOf(participationId), "latest-pending-submission") + .build() ) .retrieve() .toBodilessEntity() @@ -386,20 +445,19 @@ private RequestStat startExam() { long start = System.nanoTime(); studentExam = webClient .get() - .uri( - uriBuilder -> - uriBuilder - .pathSegment( - "api", - "courses", - courseIdString, - "exams", - examIdString, - "student-exams", - studentExamId.toString(), - "conduction" - ) - .build() + .uri(uriBuilder -> + uriBuilder + .pathSegment( + "api", + "courses", + courseIdString, + "exams", + examIdString, + "student-exams", + studentExamId.toString(), + "conduction" + ) + .build() ) .retrieve() .bodyToMono(StudentExam.class) @@ -411,9 +469,8 @@ private RequestStat fetchLiveEvents() { long start = System.nanoTime(); webClient .get() - .uri( - uriBuilder -> - uriBuilder.pathSegment("api", "courses", courseIdString, "exams", examIdString, "student-exams", "live-events").build() + .uri(uriBuilder -> + uriBuilder.pathSegment("api", "courses", courseIdString, "exams", examIdString, "student-exams", "live-events").build() ) .retrieve() .toBodilessEntity() @@ -421,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) { @@ -431,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; @@ -451,9 +508,8 @@ private RequestStat solveAndSubmitModelingExercise(ModelingExercise modelingExer long start = System.nanoTime(); webClient .put() - .uri( - uriBuilder -> - uriBuilder.pathSegment("api", "exercises", modelingExercise.getId().toString(), "modeling-submissions").build() + .uri(uriBuilder -> + uriBuilder.pathSegment("api", "exercises", modelingExercise.getId().toString(), "modeling-submissions").build() ) .bodyValue(modelingSubmission) .retrieve() @@ -490,8 +546,8 @@ private RequestStat solveAndSubmitQuizExercise(QuizExercise quizExercise) { long start = System.nanoTime(); webClient .put() - .uri( - uriBuilder -> uriBuilder.pathSegment("api", "exercises", quizExercise.getId().toString(), "submissions", "exam").build() + .uri(uriBuilder -> + uriBuilder.pathSegment("api", "exercises", quizExercise.getId().toString(), "submissions", "exam").build() ) .bodyValue(quizSubmission) .retrieve() @@ -502,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() @@ -519,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 @@ -534,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) { @@ -546,13 +605,28 @@ 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 .post() - .uri( - uriBuilder -> - uriBuilder.pathSegment("api", "courses", courseIdString, "exams", examIdString, "student-exams", "submit").build() + .uri(uriBuilder -> + uriBuilder.pathSegment("api", "courses", courseIdString, "exams", examIdString, "student-exams", "submit").build() ) .bodyValue(studentExam) .retrieve() @@ -565,20 +639,19 @@ private RequestStat loadExamSummary() { long start = System.nanoTime(); webClient .get() - .uri( - uriBuilder -> - uriBuilder - .pathSegment( - "api", - "courses", - courseIdString, - "exams", - examIdString, - "student-exams", - studentExamId.toString(), - "summary" - ) - .build() + .uri(uriBuilder -> + uriBuilder + .pathSegment( + "api", + "courses", + courseIdString, + "exams", + examIdString, + "student-exams", + studentExamId.toString(), + "summary" + ) + .build() ) .retrieve() .toBodilessEntity() @@ -615,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); @@ -623,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 { @@ -677,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()); } @@ -754,16 +847,70 @@ 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 duration = System.nanoTime() - start; var git = Git.cloneRepository() .setURI(repositoryUrl) .setDirectory(localPath.toFile()) - .setCredentialsProvider(getCredentialsProvider()) + .setCredentialsProvider(credentialsProvider) .call(); + git.close(); + log.debug("Done " + repositoryUrl); + 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++; + 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 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(); - log.debug("Done " + repositoryUrl); - return new RequestStat(now(), duration, CLONE); + + return new RequestStat(now(), duration, CLONE_SSH); } catch (Exception e) { log.warn("Error while cloning repository for {{}}: {{}}", username, e.getMessage()); attempt++; @@ -774,11 +921,71 @@ private RequestStat cloneRepo(String repositoryUrl) throws IOException { } } } + 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/ase/service/artemis/interaction/SimulatedArtemisUser.java b/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisUser.java index 4a554ee5..a1769d93 100644 --- a/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisUser.java +++ b/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisUser.java @@ -3,6 +3,7 @@ import static de.tum.cit.ase.domain.RequestType.AUTHENTICATION; import static java.time.ZonedDateTime.now; +import de.tum.cit.ase.artemisModel.ArtemisAuthMechanism; import de.tum.cit.ase.domain.ArtemisUser; import de.tum.cit.ase.domain.RequestStat; import de.tum.cit.ase.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(); } /** @@ -82,15 +87,13 @@ public List login() { if (artemisUser != null && artemisUser.getJwtToken() != null && artemisUser.getTokenExpirationDate().isAfter(now())) { log.debug("Using cached token for user {}", username); authToken = new AuthToken(artemisUser.getJwtToken(), null, null, artemisUser.getTokenExpirationDate()); - webClient = - WebClient - .builder() - .clientConnector(new ReactorClientHttpConnector(createHttpClient())) - .baseUrl(artemisUrl) - .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .defaultHeader("Cookie", authToken.jwtToken()) - .build(); + webClient = WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(createHttpClient())) + .baseUrl(artemisUrl) + .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .defaultHeader("Cookie", authToken.jwtToken()) + .build(); checkAccess(); if (authenticated) { return List.of(); @@ -100,8 +103,7 @@ public List login() { log.info("Logging in as {{}}", username); List requestStats = new ArrayList<>(); - WebClient webClient = WebClient - .builder() + WebClient webClient = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(createHttpClient())) .baseUrl(artemisUrl) .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) @@ -128,15 +130,13 @@ public List login() { artemisUser.setTokenExpirationDate(authToken.expireDate()); artemisUser = artemisUserService.updateArtemisUser(artemisUser.getId(), artemisUser); } - this.webClient = - WebClient - .builder() - .clientConnector(new ReactorClientHttpConnector(createHttpClient())) - .baseUrl(artemisUrl) - .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .defaultHeader("Cookie", authToken.jwtToken()) - .build(); + this.webClient = WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(createHttpClient())) + .baseUrl(artemisUrl) + .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .defaultHeader("Cookie", authToken.jwtToken()) + .build(); checkAccess(); log.debug("Logged in as {}", username); return requestStats; @@ -167,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 ); } @@ -207,8 +209,7 @@ public static SimulatedArtemisAdmin createArtemisAdminFromCredentials(String art } private static HttpClient createHttpClient() { - return HttpClient - .create() + return HttpClient.create() .doOnConnected(conn -> conn.addHandlerFirst(new ReadTimeoutHandler(20, TimeUnit.MINUTES)).addHandlerFirst(new WriteTimeoutHandler(30)) ) diff --git a/src/main/java/de/tum/cit/ase/service/artemis/util/UserSshPublicKeyDTO.java b/src/main/java/de/tum/cit/ase/service/artemis/util/UserSshPublicKeyDTO.java new file mode 100644 index 00000000..1affd9ed --- /dev/null +++ b/src/main/java/de/tum/cit/ase/service/artemis/util/UserSshPublicKeyDTO.java @@ -0,0 +1,17 @@ +package de.tum.cit.ase.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/ase/service/simulation/SimulationDataService.java b/src/main/java/de/tum/cit/ase/service/simulation/SimulationDataService.java index e5495ab2..84b76b7e 100644 --- a/src/main/java/de/tum/cit/ase/service/simulation/SimulationDataService.java +++ b/src/main/java/de/tum/cit/ase/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/ase/service/simulation/SimulationExecutionService.java b/src/main/java/de/tum/cit/ase/service/simulation/SimulationExecutionService.java index a9eafbb6..35585c7a 100644 --- a/src/main/java/de/tum/cit/ase/service/simulation/SimulationExecutionService.java +++ b/src/main/java/de/tum/cit/ase/service/simulation/SimulationExecutionService.java @@ -2,6 +2,7 @@ import static java.lang.Thread.sleep; +import de.tum.cit.ase.artemisModel.ArtemisAuthMechanism; import de.tum.cit.ase.artemisModel.Course; import de.tum.cit.ase.artemisModel.Exam; import de.tum.cit.ase.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 */ @@ -206,12 +208,14 @@ public synchronized void simulateExam(SimulationRun simulationRun) { return; } } + logAndSend(false, simulationRun, "Trying to cancel all build jobs..."); + cancelAllQueuedBuildJobs(admin); // 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()); - } + // 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 +226,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 */ @@ -255,18 +260,12 @@ private List simulateExamParticipations( logAndSend(false, simulationRun, "Participating in exam..."); requestStats.addAll( - performActionWithAll( - threadCount, - simulation.getNumberOfUsers(), - i -> students[i].startExamParticipation(courseId, examId, programmingExerciseId) + performActionWithAll(threadCount, simulation.getNumberOfUsers(), i -> + students[i].startExamParticipation(courseId, examId, programmingExerciseId) ) ); 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)) @@ -285,6 +284,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) { @@ -294,8 +294,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 */ @@ -332,7 +333,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 */ @@ -349,7 +350,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 @@ -365,13 +367,18 @@ private Course createCourse(SimulatedArtemisAdmin admin, SimulationRun simulatio } } + private void cancelAllQueuedBuildJobs(SimulatedArtemisAdmin admin) { + admin.cancelAllQueuedBuildJobs(); + } + /** * 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( @@ -394,9 +401,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 */ @@ -424,9 +432,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 */ @@ -445,10 +454,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) { @@ -467,10 +477,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) { @@ -489,10 +500,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) { @@ -529,15 +541,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()); @@ -553,9 +595,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) { @@ -585,10 +627,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) { @@ -604,10 +647,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) { @@ -642,10 +685,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()) { @@ -695,6 +739,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) { @@ -707,6 +752,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) { @@ -715,4 +761,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/ase/service/simulation/SimulationResultService.java b/src/main/java/de/tum/cit/ase/service/simulation/SimulationResultService.java index eb3abb53..bb564051 100644 --- a/src/main/java/de/tum/cit/ase/service/simulation/SimulationResultService.java +++ b/src/main/java/de/tum/cit/ase/service/simulation/SimulationResultService.java @@ -68,8 +68,14 @@ 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..736a359c 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, @@ -93,18 +97,22 @@ export class CreateSimulationBoxComponent implements OnInit { } inputValid(): boolean { + console.log('sss'); const basicRequirements: boolean = 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; + console.log(this.onlineIdePercentage, this.sshPercentage, this.passwordPercentage, this.tokenPercentage, basicRequirements); if (this.mode === Mode.CREATE_COURSE_AND_EXAM) { return basicRequirements; } 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 @@

- - - +
From e32b4740390162ae37673bcd21d8fd26bb018aed Mon Sep 17 00:00:00 2001 From: Simon Entholzer Date: Wed, 18 Dec 2024 18:51:12 +0100 Subject: [PATCH 2/5] fix tests --- .../interaction/SimulatedArtemisStudent.java | 3 +- .../service/SimulationExecutionServiceIT.java | 321 +++++++++++++----- 2 files changed, 238 insertions(+), 86 deletions(-) diff --git a/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisStudent.java b/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisStudent.java index 5543c973..3552ab74 100644 --- a/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisStudent.java +++ b/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisStudent.java @@ -855,12 +855,13 @@ private RequestStat cloneRepo(String repositoryUrl) throws IOException { default -> throw new IllegalStateException("Not implemented"); } - var duration = System.nanoTime() - start; var git = Git.cloneRepository() .setURI(repositoryUrl) .setDirectory(localPath.toFile()) .setCredentialsProvider(credentialsProvider) .call(); + + var duration = System.nanoTime() - start; git.close(); log.debug("Done " + repositoryUrl); return switch (authenticationMechanism) { diff --git a/src/test/java/de/tum/cit/ase/service/SimulationExecutionServiceIT.java b/src/test/java/de/tum/cit/ase/service/SimulationExecutionServiceIT.java index ed65bf2a..f26e736c 100644 --- a/src/test/java/de/tum/cit/ase/service/SimulationExecutionServiceIT.java +++ b/src/test/java/de/tum/cit/ase/service/SimulationExecutionServiceIT.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.*; import de.tum.cit.ase.IntegrationTest; +import de.tum.cit.ase.artemisModel.ArtemisAuthMechanism; import de.tum.cit.ase.artemisModel.Course; import de.tum.cit.ase.artemisModel.Exam; import de.tum.cit.ase.domain.ArtemisUser; @@ -122,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()); @@ -144,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()); @@ -219,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); } @@ -234,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 @@ -279,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); } @@ -294,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 @@ -336,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); } @@ -351,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 @@ -393,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); } @@ -408,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 @@ -451,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); } @@ -466,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 @@ -511,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); } @@ -526,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 @@ -575,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); } @@ -591,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 @@ -633,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()); } @@ -648,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 @@ -688,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()); } @@ -703,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 @@ -745,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()); } @@ -760,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 @@ -805,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()); } @@ -820,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 @@ -863,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()); } @@ -878,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 @@ -921,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()); } @@ -936,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 @@ -979,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()); } @@ -994,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 @@ -1037,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()); } @@ -1052,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 @@ -1095,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()); } @@ -1110,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 @@ -1146,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")); @@ -1180,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); } @@ -1195,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 @@ -1238,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); } @@ -1253,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) + ); } } From ce4823066c05666c820da8e1846c9bc3b24dc72b Mon Sep 17 00:00:00 2001 From: Simon Entholzer Date: Mon, 23 Dec 2024 12:05:32 +0100 Subject: [PATCH 3/5] adding build job cancelling --- .../interaction/SimulatedArtemisAdmin.java | 13 ++++++++ .../SimulationExecutionService.java | 32 +++++++++++++------ 2 files changed, 35 insertions(+), 10 deletions(-) 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 6eb4c5e0..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 @@ -191,6 +191,19 @@ public void cancelAllQueuedBuildJobs() { .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/simulation/SimulationExecutionService.java b/src/main/java/de/tum/cit/aet/service/simulation/SimulationExecutionService.java index 7f2dde93..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 @@ -191,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 @@ -208,14 +209,15 @@ public synchronized void simulateExam(SimulationRun simulationRun) { return; } } - logAndSend(false, simulationRun, "Trying to cancel all build jobs..."); - cancelAllQueuedBuildJobs(admin); - // 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()); + } + } } } @@ -367,8 +369,9 @@ private Course createCourse(SimulatedArtemisAdmin admin, SimulationRun simulatio } } - private void cancelAllQueuedBuildJobs(SimulatedArtemisAdmin admin) { + private void cancelAllBuildJobs(SimulatedArtemisAdmin admin) { admin.cancelAllQueuedBuildJobs(); + admin.cancelAllRunningBuildJobs(); } /** @@ -664,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) { From 8ee81a43143f8ae3952e38b3d47cef02b27e443a Mon Sep 17 00:00:00 2001 From: Simon Entholzer Date: Mon, 23 Dec 2024 12:10:37 +0100 Subject: [PATCH 4/5] remove now unused default PUSH/CLONE stats --- .../cit/aet/service/simulation/SimulationResultService.java | 4 ---- 1 file changed, 4 deletions(-) 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 33ec1b59..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,12 +67,10 @@ public SimulationRun calculateAndSaveResult(SimulationRun simulationRun, List Date: Mon, 23 Dec 2024 21:30:12 +0100 Subject: [PATCH 5/5] remove console logs --- .../create-simulation-box/create-simulation-box.component.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 736a359c..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 @@ -97,15 +97,13 @@ export class CreateSimulationBoxComponent implements OnInit { } inputValid(): boolean { - console.log('sss'); const basicRequirements: boolean = this.name.length > 0 && ((!this.customizeUserRange && this.numberOfUsers > 0) || (this.customizeUserRange && this.userRange.length > 0)) && this.numberOfCommitsAndPushesFrom > 0 && this.numberOfCommitsAndPushesTo > this.numberOfCommitsAndPushesFrom && - this.sshPercentage + this.tokenPercentage + this.passwordPercentage + this.onlineIdePercentage == 100; + this.sshPercentage + this.tokenPercentage + this.passwordPercentage + this.onlineIdePercentage === 100; - console.log(this.onlineIdePercentage, this.sshPercentage, this.passwordPercentage, this.tokenPercentage, basicRequirements); if (this.mode === Mode.CREATE_COURSE_AND_EXAM) { return basicRequirements; }