diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamFactory.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamFactory.java index aa1ca83f67c8..35ec60736223 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ExamFactory.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamFactory.java @@ -3,11 +3,12 @@ import static java.time.ZonedDateTime.now; import java.time.ZonedDateTime; +import java.util.HashSet; +import java.util.Set; import de.tum.in.www1.artemis.domain.Course; -import de.tum.in.www1.artemis.domain.exam.Exam; -import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; -import de.tum.in.www1.artemis.domain.exam.StudentExam; +import de.tum.in.www1.artemis.domain.exam.*; +import de.tum.in.www1.artemis.web.rest.dto.*; /** * Factory for creating Exams and related objects. @@ -200,4 +201,28 @@ public static Exam generateExamWithExerciseGroup(Course course, boolean mandator return exam; } + + /** + * creates exam session DTOs + * + * @param session1 firts exam session + * @param session2 second exam session + * @return set of exam session DTOs + */ + public static Set createExpectedExamSessionDTOs(ExamSession session1, ExamSession session2) { + var expectedDTOs = new HashSet(); + var firstStudentExamDTO = new StudentExamWithIdAndExamAndUserDTO(session1.getStudentExam().getId(), + new ExamWithIdAndCourseDTO(session1.getStudentExam().getExam().getId(), new CourseWithIdDTO(session1.getStudentExam().getExam().getCourse().getId())), + new UserWithIdAndLoginDTO(session1.getStudentExam().getUser().getId(), session1.getStudentExam().getUser().getLogin())); + var secondStudentExamDTO = new StudentExamWithIdAndExamAndUserDTO(session2.getStudentExam().getId(), + new ExamWithIdAndCourseDTO(session2.getStudentExam().getExam().getId(), new CourseWithIdDTO(session2.getStudentExam().getExam().getCourse().getId())), + new UserWithIdAndLoginDTO(session2.getStudentExam().getUser().getId(), session2.getStudentExam().getUser().getLogin())); + var firstExamSessionDTO = new ExamSessionDTO(session1.getId(), session1.getBrowserFingerprintHash(), session1.getIpAddress(), session1.getSuspiciousReasons(), + session1.getCreatedDate(), firstStudentExamDTO); + var secondExamSessionDTO = new ExamSessionDTO(session2.getId(), session2.getBrowserFingerprintHash(), session2.getIpAddress(), session2.getSuspiciousReasons(), + session2.getCreatedDate(), secondStudentExamDTO); + expectedDTOs.add(firstExamSessionDTO); + expectedDTOs.add(secondExamSessionDTO); + return expectedDTOs; + } } diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java index b325756679bd..f1016817f6aa 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java @@ -5,95 +5,60 @@ import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.*; import static org.springframework.http.HttpStatus.CREATED; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; -import java.time.Duration; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.junit.jupiter.api.*; -import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import com.fasterxml.jackson.databind.ObjectMapper; - import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; -import de.tum.in.www1.artemis.assessment.GradingScaleUtilService; -import de.tum.in.www1.artemis.bonus.BonusFactory; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; -import de.tum.in.www1.artemis.domain.enumeration.*; +import de.tum.in.www1.artemis.domain.enumeration.ExerciseType; import de.tum.in.www1.artemis.domain.exam.*; import de.tum.in.www1.artemis.domain.metis.conversation.Channel; -import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission; -import de.tum.in.www1.artemis.domain.participation.*; -import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismCase; -import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismVerdict; +import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.domain.quiz.QuizExercise; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseFactory; import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseTestService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseFactory; import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; -import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository; -import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismCaseRepository; -import de.tum.in.www1.artemis.security.SecurityUtils; -import de.tum.in.www1.artemis.service.QuizSubmissionService; -import de.tum.in.www1.artemis.service.connectors.vcs.VersionControlRepositoryPermission; import de.tum.in.www1.artemis.service.dto.StudentDTO; -import de.tum.in.www1.artemis.service.exam.*; -import de.tum.in.www1.artemis.service.ldap.LdapUserDto; +import de.tum.in.www1.artemis.service.exam.ExamAccessService; +import de.tum.in.www1.artemis.service.exam.ExamDateService; +import de.tum.in.www1.artemis.service.exam.ExamService; import de.tum.in.www1.artemis.service.scheduled.ParticipantScoreScheduleService; import de.tum.in.www1.artemis.service.user.PasswordService; -import de.tum.in.www1.artemis.team.TeamUtilService; import de.tum.in.www1.artemis.user.UserFactory; import de.tum.in.www1.artemis.user.UserUtilService; -import de.tum.in.www1.artemis.util.*; +import de.tum.in.www1.artemis.util.PageableSearchUtilService; +import de.tum.in.www1.artemis.util.ZipFileTestUtilService; import de.tum.in.www1.artemis.web.rest.dto.*; -import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; class ExamIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { private static final String TEST_PREFIX = "examintegration"; - public static final String STUDENT_111 = TEST_PREFIX + "student111"; - - private final Logger log = LoggerFactory.getLogger(getClass()); - @Autowired private QuizExerciseRepository quizExerciseRepository; - @Autowired - private QuizSubmissionRepository quizSubmissionRepository; - - @Autowired - private QuizSubmissionService quizSubmissionService; - @Autowired private CourseRepository courseRepo; @@ -106,45 +71,21 @@ class ExamIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTe @Autowired private ExamRepository examRepository; - @Autowired - private ExamUserRepository examUserRepository; - @Autowired private ExamService examService; - @Autowired - private StudentExamService studentExamService; - @Autowired private ExamDateService examDateService; - @Autowired - private ExamRegistrationService examRegistrationService; - - @Autowired - private ExerciseGroupRepository exerciseGroupRepository; - @Autowired private StudentExamRepository studentExamRepository; - @Autowired - private ProgrammingExerciseRepository programmingExerciseRepository; - @Autowired private StudentParticipationRepository studentParticipationRepository; @Autowired private SubmissionRepository submissionRepository; - @Autowired - private ResultRepository resultRepository; - - @Autowired - private ParticipationTestRepository participationTestRepository; - - @Autowired - private GradingScaleRepository gradingScaleRepository; - @Autowired private PasswordService passwordService; @@ -154,24 +95,6 @@ class ExamIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTe @Autowired private ExamAccessService examAccessService; - @Autowired - private TeamRepository teamRepository; - - @Autowired - private BonusRepository bonusRepository; - - @Autowired - private PlagiarismCaseRepository plagiarismCaseRepository; - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private ParticipantScoreRepository participantScoreRepository; - - @Autowired - private ProgrammingExerciseTestService programmingExerciseTestService; - @Autowired private ChannelRepository channelRepository; @@ -187,24 +110,12 @@ class ExamIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTe @Autowired private TextExerciseUtilService textExerciseUtilService; - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - @Autowired private ModelingExerciseUtilService modelingExerciseUtilService; @Autowired private ExerciseUtilService exerciseUtilService; - @Autowired - private ParticipationUtilService participationUtilService; - - @Autowired - private TeamUtilService teamUtilService; - - @Autowired - private GradingScaleUtilService gradingScaleUtilService; - @Autowired private PageableSearchUtilService pageableSearchUtilService; @@ -218,13 +129,9 @@ class ExamIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTe private Exam exam2; - private Exam testExam1; - - private static final int NUMBER_OF_STUDENTS = 3; + private static final int NUMBER_OF_STUDENTS = 2; - private static final int NUMBER_OF_TUTORS = 2; - - private final List studentRepos = new ArrayList<>(); + private static final int NUMBER_OF_TUTORS = 1; private User student1; @@ -256,8 +163,6 @@ void initTestCase() { examUtilService.addExamChannel(exam1, "exam1 channel"); exam2 = examUtilService.addExamWithExerciseGroup(course1, true); examUtilService.addExamChannel(exam2, "exam2 channel"); - testExam1 = examUtilService.addTestExam(course1); - examUtilService.addStudentExamForTestExam(testExam1, student1); bitbucketRequestMockProvider.enableMockingOfRequests(); @@ -265,210 +170,9 @@ void initTestCase() { participantScoreScheduleService.activate(); } - @AfterEach - void tearDown() throws Exception { - bitbucketRequestMockProvider.reset(); - bambooRequestMockProvider.reset(); - if (programmingExerciseTestService.exerciseRepo != null) { - programmingExerciseTestService.tearDown(); - } - - for (var repo : studentRepos) { - repo.resetLocalRepo(); - } - - ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 500; - participantScoreScheduleService.shutdown(); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testRegisterUserInExam_addedToCourseStudentsGroup() throws Exception { - User student42 = userUtilService.getUserByLogin(TEST_PREFIX + "student42"); - jiraRequestMockProvider.enableMockingOfRequests(); - jiraRequestMockProvider.mockAddUserToGroup(course1.getStudentGroupName(), false); - bitbucketRequestMockProvider.mockUpdateUserDetails(student42.getLogin(), student42.getEmail(), student42.getName()); - bitbucketRequestMockProvider.mockAddUserToGroups(); - - Set studentsInCourseBefore = userRepo.findAllInGroupWithAuthorities(course1.getStudentGroupName()); - request.postWithoutLocation("/api/courses/" + course1.getId() + "/exams/" + exam1.getId() + "/students/" + TEST_PREFIX + "student42", null, HttpStatus.OK, null); - Set studentsInCourseAfter = userRepo.findAllInGroupWithAuthorities(course1.getStudentGroupName()); - studentsInCourseBefore.add(student42); - assertThat(studentsInCourseBefore).containsExactlyInAnyOrderElementsOf(studentsInCourseAfter); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testAddStudentToExam_testExam() throws Exception { - request.postWithoutLocation("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/students/" + TEST_PREFIX + "student42", null, HttpStatus.BAD_REQUEST, - null); - } - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testRemoveStudentToExam_testExam() throws Exception { - request.delete("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/students/" + TEST_PREFIX + "student42", HttpStatus.BAD_REQUEST); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testRegisterUsersInExam() throws Exception { - jiraRequestMockProvider.enableMockingOfRequests(); - - var savedExam = examUtilService.addExam(course1); - - List registrationNumbers = Arrays.asList("1111111", "1111112", "1111113"); - List students = userUtilService.setRegistrationNumberOfStudents(registrationNumbers, TEST_PREFIX); - - User student1 = students.get(0); - User student2 = students.get(1); - User student3 = students.get(2); - - var registrationNumber3WithTypo = "1111113" + "0"; - var registrationNumber4WithTypo = "1111115" + "1"; - var registrationNumber99 = "1111199"; - var registrationNumber111 = "1111100"; - var emptyRegistrationNumber = ""; - - // mock the ldap service - doReturn(Optional.empty()).when(ldapUserService).findByRegistrationNumber(registrationNumber3WithTypo); - doReturn(Optional.empty()).when(ldapUserService).findByRegistrationNumber(emptyRegistrationNumber); - doReturn(Optional.empty()).when(ldapUserService).findByRegistrationNumber(registrationNumber4WithTypo); - - var ldapUser111Dto = new LdapUserDto().registrationNumber(registrationNumber111).firstName(STUDENT_111).lastName(STUDENT_111).username(STUDENT_111) - .email(STUDENT_111 + "@tum.de"); - doReturn(Optional.of(ldapUser111Dto)).when(ldapUserService).findByRegistrationNumber(registrationNumber111); - - // first mocked call is expected to add student 99 to the course student group - jiraRequestMockProvider.mockAddUserToGroup(course1.getStudentGroupName(), false); - // second mocked call expected to create student 111 - jiraRequestMockProvider.mockCreateUserInExternalUserManagement(ldapUser111Dto.getUsername(), ldapUser111Dto.getFirstName() + " " + ldapUser111Dto.getLastName(), - ldapUser111Dto.getEmail()); - // the last mocked call is expected to add student 111 to the course student group - jiraRequestMockProvider.mockAddUserToGroup(course1.getStudentGroupName(), false); - - User student99 = userUtilService.createAndSaveUser("student99"); // not registered for the course - userUtilService.setRegistrationNumberOfUserAndSave("student99", registrationNumber99); - - bitbucketRequestMockProvider.mockUpdateUserDetails(student99.getLogin(), student99.getEmail(), student99.getName()); - bitbucketRequestMockProvider.mockAddUserToGroups(); - student99 = userRepo.findOneWithGroupsAndAuthoritiesByLogin("student99").orElseThrow(); - assertThat(student99.getGroups()).doesNotContain(course1.getStudentGroupName()); - - // Note: student111 is not yet a user of Artemis and should be retrieved from the LDAP - request.postWithoutLocation("/api/courses/" + course1.getId() + "/exams/" + savedExam.getId() + "/students/" + TEST_PREFIX + "student1", null, HttpStatus.OK, null); - request.postWithoutLocation("/api/courses/" + course1.getId() + "/exams/" + savedExam.getId() + "/students/nonExistingStudent", null, HttpStatus.NOT_FOUND, null); - - Exam storedExam = examRepository.findWithExamUsersById(savedExam.getId()).orElseThrow(); - ExamUser examUserStudent1 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student1.getId()).orElseThrow(); - assertThat(storedExam.getExamUsers()).containsExactly(examUserStudent1); - - request.delete("/api/courses/" + course1.getId() + "/exams/" + savedExam.getId() + "/students/" + TEST_PREFIX + "student1", HttpStatus.OK); - request.delete("/api/courses/" + course1.getId() + "/exams/" + savedExam.getId() + "/students/nonExistingStudent", HttpStatus.NOT_FOUND); - storedExam = examRepository.findWithExamUsersById(savedExam.getId()).orElseThrow(); - assertThat(storedExam.getExamUsers()).isEmpty(); - - var studentDto1 = UserFactory.generateStudentDTOWithRegistrationNumber(student1.getRegistrationNumber()); - var studentDto2 = UserFactory.generateStudentDTOWithRegistrationNumber(student2.getRegistrationNumber()); - var studentDto3 = new StudentDTO(student3.getLogin(), null, null, registrationNumber3WithTypo, null); // explicit typo, should be a registration failure later - var studentDto4 = UserFactory.generateStudentDTOWithRegistrationNumber(registrationNumber4WithTypo); // explicit typo, should fall back to login name later - var studentDto10 = UserFactory.generateStudentDTOWithRegistrationNumber(null); // completely empty - - var studentDto99 = new StudentDTO(student99.getLogin(), null, null, registrationNumber99, null); - var studentDto111 = new StudentDTO(null, null, null, registrationNumber111, null); - - // Add a student with login but empty registration number - var studentsToRegister = List.of(studentDto1, studentDto2, studentDto3, studentDto4, studentDto99, studentDto111, studentDto10); - - // now we register all these students for the exam. - List registrationFailures = request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + savedExam.getId() + "/students", - studentsToRegister, StudentDTO.class, HttpStatus.OK); - // all students get registered if they can be found in the LDAP - assertThat(registrationFailures).containsExactlyInAnyOrder(studentDto4, studentDto10); - - // TODO check audit events stored properly - - storedExam = examRepository.findWithExamUsersById(savedExam.getId()).orElseThrow(); - - // now a new user student101 should exist - var student111 = userUtilService.getUserByLogin(STUDENT_111); - - var examUser1 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student1.getId()).orElseThrow(); - var examUser2 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student2.getId()).orElseThrow(); - var examUser3 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student3.getId()).orElseThrow(); - var examUser99 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student99.getId()).orElseThrow(); - var examUser111 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student111.getId()).orElseThrow(); - - assertThat(storedExam.getExamUsers()).containsExactlyInAnyOrder(examUser1, examUser2, examUser3, examUser99, examUser111); - - for (var examUser : storedExam.getExamUsers()) { - // all registered users must have access to the course - var user = userRepo.findOneWithGroupsAndAuthoritiesByLogin(examUser.getUser().getLogin()).orElseThrow(); - assertThat(user.getGroups()).contains(course1.getStudentGroupName()); - } - - // Make sure delete also works if so many objects have been created before - request.delete("/api/courses/" + course1.getId() + "/exams/" + savedExam.getId(), HttpStatus.OK); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testRegisterLDAPUsersInExam() throws Exception { - jiraRequestMockProvider.enableMockingOfRequests(); - var savedExam = examUtilService.addExam(course1); - String student100 = TEST_PREFIX + "student100"; - String student200 = TEST_PREFIX + "student200"; - String student300 = TEST_PREFIX + "student300"; - - // setup mocks - var ldapUser1Dto = new LdapUserDto().firstName(student100).lastName(student100).username(student100).registrationNumber("100000").email(student100 + "@tum.de"); - doReturn(Optional.of(ldapUser1Dto)).when(ldapUserService).findByUsername(student100); - jiraRequestMockProvider.mockCreateUserInExternalUserManagement(ldapUser1Dto.getUsername(), ldapUser1Dto.getFirstName() + " " + ldapUser1Dto.getLastName(), null); - jiraRequestMockProvider.mockAddUserToGroup(course1.getStudentGroupName(), false); - - var ldapUser2Dto = new LdapUserDto().firstName(student200).lastName(student200).username(student200).registrationNumber("200000").email(student200 + "@tum.de"); - doReturn(Optional.of(ldapUser2Dto)).when(ldapUserService).findByEmail(student200 + "@tum.de"); - jiraRequestMockProvider.mockCreateUserInExternalUserManagement(ldapUser2Dto.getUsername(), ldapUser2Dto.getFirstName() + " " + ldapUser2Dto.getLastName(), null); - jiraRequestMockProvider.mockAddUserToGroup(course1.getStudentGroupName(), false); - - var ldapUser3Dto = new LdapUserDto().firstName(student300).lastName(student300).username(student300).registrationNumber("3000000").email(student300 + "@tum.de"); - doReturn(Optional.of(ldapUser3Dto)).when(ldapUserService).findByRegistrationNumber("3000000"); - jiraRequestMockProvider.mockCreateUserInExternalUserManagement(ldapUser3Dto.getUsername(), ldapUser3Dto.getFirstName() + " " + ldapUser3Dto.getLastName(), null); - jiraRequestMockProvider.mockAddUserToGroup(course1.getStudentGroupName(), false); - - // user with login - StudentDTO dto1 = new StudentDTO(student100, student100, student100, null, null); - // user with email - StudentDTO dto2 = new StudentDTO(null, student200, student200, null, student200 + "@tum.de"); - // user with registration number - StudentDTO dto3 = new StudentDTO(null, student300, student300, "3000000", null); - // user without anything - StudentDTO dto4 = new StudentDTO(null, null, null, null, null); - - List registrationFailures = request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + savedExam.getId() + "/students", - List.of(dto1, dto2, dto3, dto4), StudentDTO.class, HttpStatus.OK); - assertThat(registrationFailures).containsExactly(dto4); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testAddStudentsToExam_testExam() throws Exception { - userUtilService.setRegistrationNumberOfUserAndSave(TEST_PREFIX + "student1", "1111111"); - - StudentDTO studentDto1 = UserFactory.generateStudentDTOWithRegistrationNumber("1111111"); - List studentDTOS = List.of(studentDto1); - request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/students", studentDTOS, StudentDTO.class, HttpStatus.FORBIDDEN); - } - - @Test - @WithMockUser(username = "admin", roles = "ADMIN") + @WithMockUser(username = TEST_PREFIX + "instructor10", roles = "INSTRUCTOR") void testGetAllActiveExams() throws Exception { - jiraRequestMockProvider.enableMockingOfRequests(); - jiraRequestMockProvider.mockCreateGroup(course10.getInstructorGroupName()); - jiraRequestMockProvider.mockAddUserToGroup(course10.getInstructorGroupName(), false); - - // switch to instructor10 - SecurityContextHolder.getContext().setAuthentication(SecurityUtils.makeAuthorizationObject(TEST_PREFIX + "instructor10")); // add additional active exam var exam3 = examUtilService.addExam(course10, ZonedDateTime.now().plusDays(1), ZonedDateTime.now().plusDays(2), ZonedDateTime.now().plusDays(3)); @@ -480,192 +184,6 @@ void testGetAllActiveExams() throws Exception { assertThat(activeExams).containsExactly(exam3); } - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testRemoveAllStudentsFromExam_testExam() throws Exception { - request.delete("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/students", HttpStatus.BAD_REQUEST); - } - - // TODO IMPORTANT test more complex exam configurations (mixed exercise type, more variants and more registered students) - @Nested - class ExamStartTest { - - private Set registeredUsers; - - private final List createdStudentExams = new ArrayList<>(); - - @BeforeEach - void init() throws Exception { - doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); - - // registering users - User student2 = userUtilService.getUserByLogin(TEST_PREFIX + "student2"); - registeredUsers = Set.of(student1, student2); - exam2.setExamUsers(Set.of(new ExamUser())); - // setting dates - exam2.setStartDate(now().plusHours(2)); - exam2.setEndDate(now().plusHours(3)); - exam2.setVisibleDate(now().plusHours(1)); - } - - @AfterEach - void cleanup() { - // Cleanup of Bidirectional Relationships - for (StudentExam studentExam : createdStudentExams) { - exam2.removeStudentExam(studentExam); - } - examRepository.save(exam2); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testStartExercisesWithTextExercise() throws Exception { - // creating exercise - ExerciseGroup exerciseGroup = exam2.getExerciseGroups().get(0); - - TextExercise textExercise = TextExerciseFactory.generateTextExerciseForExam(exerciseGroup); - exerciseGroup.addExercise(textExercise); - exerciseGroupRepository.save(exerciseGroup); - textExercise = exerciseRepo.save(textExercise); - - createStudentExams(textExercise); - - List studentParticipations = invokePrepareExerciseStart(); - - for (Participation participation : studentParticipations) { - assertThat(participation.getExercise()).isEqualTo(textExercise); - assertThat(participation.getExercise().getCourseViaExerciseGroupOrCourseMember()).isNotNull(); - assertThat(participation.getExercise().getExerciseGroup()).isEqualTo(exam2.getExerciseGroups().get(0)); - assertThat(participation.getSubmissions()).hasSize(1); - var textSubmission = (TextSubmission) participation.getSubmissions().iterator().next(); - assertThat(textSubmission.getText()).isNull(); - } - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testStartExercisesWithModelingExercise() throws Exception { - // creating exercise - ModelingExercise modelingExercise = ModelingExerciseFactory.generateModelingExerciseForExam(DiagramType.ClassDiagram, exam2.getExerciseGroups().get(0)); - exam2.getExerciseGroups().get(0).addExercise(modelingExercise); - exerciseGroupRepository.save(exam2.getExerciseGroups().get(0)); - modelingExercise = exerciseRepo.save(modelingExercise); - - createStudentExams(modelingExercise); - - List studentParticipations = invokePrepareExerciseStart(); - - for (Participation participation : studentParticipations) { - assertThat(participation.getExercise()).isEqualTo(modelingExercise); - assertThat(participation.getExercise().getCourseViaExerciseGroupOrCourseMember()).isNotNull(); - assertThat(participation.getExercise().getExerciseGroup()).isEqualTo(exam2.getExerciseGroups().get(0)); - assertThat(participation.getSubmissions()).hasSize(1); - var modelingSubmission = (ModelingSubmission) participation.getSubmissions().iterator().next(); - assertThat(modelingSubmission.getModel()).isNull(); - assertThat(modelingSubmission.getExplanationText()).isNull(); - } - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testStartExerciseWithProgrammingExercise() throws Exception { - bitbucketRequestMockProvider.enableMockingOfRequests(true); - bambooRequestMockProvider.enableMockingOfRequests(true); - - ProgrammingExercise programmingExercise = createProgrammingExercise(); - - participationUtilService.mockCreationOfExerciseParticipation(programmingExercise, versionControlService, continuousIntegrationService); - - createStudentExams(programmingExercise); - - var studentParticipations = invokePrepareExerciseStart(); - - for (Participation participation : studentParticipations) { - assertThat(participation.getExercise()).isEqualTo(programmingExercise); - assertThat(participation.getExercise().getCourseViaExerciseGroupOrCourseMember()).isNotNull(); - assertThat(participation.getExercise().getExerciseGroup()).isEqualTo(exam2.getExerciseGroups().get(0)); - // No initial submissions should be created for programming exercises - assertThat(participation.getSubmissions()).isEmpty(); - assertThat(((ProgrammingExerciseParticipation) participation).isLocked()).isTrue(); - verify(versionControlService, never()).configureRepository(eq(programmingExercise), (ProgrammingExerciseStudentParticipation) eq(participation), eq(true)); - } - } - - private static class ExamStartDateSource implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(Arguments.of(ZonedDateTime.now().minusHours(1)), // after exam start - Arguments.arguments(ZonedDateTime.now().plusMinutes(3)) // before exam start but after pe unlock date - ); - } - } - - @ParameterizedTest(name = "{displayName} [{index}]") - @ArgumentsSource(ExamStartDateSource.class) - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testStartExerciseWithProgrammingExercise_participationUnlocked(ZonedDateTime startDate) throws Exception { - exam2.setVisibleDate(ZonedDateTime.now().minusHours(2)); - exam2.setStartDate(startDate); - examRepository.save(exam2); - - bitbucketRequestMockProvider.enableMockingOfRequests(true); - bambooRequestMockProvider.enableMockingOfRequests(true); - - ProgrammingExercise programmingExercise = createProgrammingExercise(); - - participationUtilService.mockCreationOfExerciseParticipation(programmingExercise, versionControlService, continuousIntegrationService); - - createStudentExams(programmingExercise); - - var studentParticipations = invokePrepareExerciseStart(); - - for (Participation participation : studentParticipations) { - assertThat(participation.getExercise()).isEqualTo(programmingExercise); - assertThat(participation.getExercise().getCourseViaExerciseGroupOrCourseMember()).isNotNull(); - assertThat(participation.getExercise().getExerciseGroup()).isEqualTo(exam2.getExerciseGroups().get(0)); - // No initial submissions should be created for programming exercises - assertThat(participation.getSubmissions()).isEmpty(); - ProgrammingExerciseStudentParticipation studentParticipation = (ProgrammingExerciseStudentParticipation) participation; - // The participation should not get locked if it gets created after the exam already started - assertThat(studentParticipation.isLocked()).isFalse(); - verify(versionControlService).addMemberToRepository(studentParticipation.getVcsRepositoryUrl(), studentParticipation.getStudent().orElseThrow(), - VersionControlRepositoryPermission.REPO_WRITE); - } - } - - private void createStudentExams(Exercise exercise) { - // creating student exams - for (User user : registeredUsers) { - StudentExam studentExam = new StudentExam(); - studentExam.addExercise(exercise); - studentExam.setUser(user); - exam2.addStudentExam(studentExam); - createdStudentExams.add(studentExamRepository.save(studentExam)); - } - - exam2 = examRepository.save(exam2); - } - - private ProgrammingExercise createProgrammingExercise() { - ProgrammingExercise programmingExercise = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(exam2.getExerciseGroups().get(0)); - programmingExercise = exerciseRepo.save(programmingExercise); - programmingExercise = programmingExerciseUtilService.addTemplateParticipationForProgrammingExercise(programmingExercise); - exam2.getExerciseGroups().get(0).addExercise(programmingExercise); - exerciseGroupRepository.save(exam2.getExerciseGroups().get(0)); - return programmingExercise; - } - - private List invokePrepareExerciseStart() throws Exception { - // invoke start exercises - int noGeneratedParticipations = prepareExerciseStart(exam2); - verify(gitService, times(getNumberOfProgrammingExercises(exam2))).combineAllCommitsOfRepositoryIntoOne(any()); - assertThat(noGeneratedParticipations).isEqualTo(exam2.getStudentExams().size()); - return participationTestRepository.findByExercise_ExerciseGroup_Exam_Id(exam2.getId()); - } - - } - @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGenerateStudentExams() throws Exception { @@ -684,49 +202,6 @@ void testGenerateStudentExams() throws Exception { assertThat(studentExam.getExam()).isEqualTo(exam); // TODO: check exercise configuration, each mandatory exercise group has to appear, one optional exercise should appear } - - // Make sure delete also works if so many objects have been created before - request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testGenerateStudentExamsCleanupOldParticipations() throws Exception { - Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, NUMBER_OF_STUDENTS); - - request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/generate-student-exams", Optional.empty(), StudentExam.class, - HttpStatus.OK); - - List studentParticipations = participationTestRepository.findByExercise_ExerciseGroup_Exam_Id(exam.getId()); - assertThat(studentParticipations).isEmpty(); - - // invoke start exercises - studentExamService.startExercises(exam.getId()).join(); - - studentParticipations = participationTestRepository.findByExercise_ExerciseGroup_Exam_Id(exam.getId()); - assertThat(studentParticipations).hasSize(12); - - request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/generate-student-exams", Optional.empty(), StudentExam.class, - HttpStatus.OK); - - studentParticipations = participationTestRepository.findByExercise_ExerciseGroup_Exam_Id(exam.getId()); - assertThat(studentParticipations).isEmpty(); - - // invoke start exercises - studentExamService.startExercises(exam.getId()).join(); - - studentParticipations = participationTestRepository.findByExercise_ExerciseGroup_Exam_Id(exam.getId()); - assertThat(studentParticipations).hasSize(12); - - // Make sure delete also works if so many objects have been created before - request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testGenerateStudentExams_testExam() throws Exception { - request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/generate-student-exams", Optional.empty(), StudentExam.class, - HttpStatus.BAD_REQUEST); } @Test @@ -778,14 +253,14 @@ void testGenerateStudentExamsTooManyMandatoryExerciseGroups_badRequest() throws @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGenerateMissingStudentExams() throws Exception { - Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, 2); + Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, 1); // Generate student exams List studentExams = request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/generate-student-exams", Optional.empty(), StudentExam.class, HttpStatus.OK); assertThat(studentExams).hasSize(exam.getExamUsers().size()); - // Register two new students - examUtilService.registerUsersForExamAndSaveExam(exam, TEST_PREFIX, 3, 3); + // Register one new students + examUtilService.registerUsersForExamAndSaveExam(exam, TEST_PREFIX, 2, 2); // Generate individual exams for the two missing students List missingStudentExams = request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/generate-missing-student-exams", @@ -803,122 +278,6 @@ void testGenerateMissingStudentExams() throws Exception { studentExamsDB = request.getList("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams", HttpStatus.OK, StudentExam.class); assertThat(studentExamsDB).hasSize(exam.getExamUsers().size()); - - // Make sure delete also works if so many objects have been created before - request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testGenerateMissingStudentExams_testExam() throws Exception { - request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/generate-missing-student-exams", Optional.empty(), StudentExam.class, - HttpStatus.BAD_REQUEST); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testEvaluateQuizExercises_testExam() throws Exception { - request.post("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/student-exams/evaluate-quiz-exercises", Optional.empty(), HttpStatus.BAD_REQUEST); - } - - @Test - @WithMockUser(username = "admin", roles = "ADMIN") - void testRemovingAllStudents() throws Exception { - doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); - Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, 3); - - // Generate student exams - List studentExams = request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/generate-student-exams", - Optional.empty(), StudentExam.class, HttpStatus.OK); - assertThat(studentExams).hasSize(3); - assertThat(exam.getExamUsers()).hasSize(3); - - int numberOfGeneratedParticipations = prepareExerciseStart(exam); - assertThat(numberOfGeneratedParticipations).isEqualTo(12); - - verify(gitService, times(getNumberOfProgrammingExercises(exam))).combineAllCommitsOfRepositoryIntoOne(any()); - // Fetch student exams - List studentExamsDB = request.getList("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams", HttpStatus.OK, StudentExam.class); - assertThat(studentExamsDB).hasSize(3); - List participationList = new ArrayList<>(); - Exercise[] exercises = examRepository.findAllExercisesByExamId(exam.getId()).toArray(new Exercise[0]); - for (Exercise value : exercises) { - participationList.addAll(studentParticipationRepository.findByExerciseId(value.getId())); - } - assertThat(participationList).hasSize(12); - - // TODO there should be some participation but no submissions unfortunately - // remove all students - request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/students", HttpStatus.OK); - - // Get the exam with all registered users - var params = new LinkedMultiValueMap(); - params.add("withStudents", "true"); - Exam storedExam = request.get("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK, Exam.class, params); - assertThat(storedExam.getExamUsers()).isEmpty(); - - // Fetch student exams - studentExamsDB = request.getList("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams", HttpStatus.OK, StudentExam.class); - assertThat(studentExamsDB).isEmpty(); - - // Fetch participations - exercises = examRepository.findAllExercisesByExamId(exam.getId()).toArray(new Exercise[0]); - participationList = new ArrayList<>(); - for (Exercise exercise : exercises) { - participationList.addAll(studentParticipationRepository.findByExerciseId(exercise.getId())); - } - assertThat(participationList).hasSize(12); - - } - - @Test - @WithMockUser(username = "admin", roles = "ADMIN") - void testRemovingAllStudentsAndParticipations() throws Exception { - doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); - Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, 3); - - // Generate student exams - List studentExams = request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/generate-student-exams", - Optional.empty(), StudentExam.class, HttpStatus.OK); - assertThat(studentExams).hasSize(3); - assertThat(exam.getExamUsers()).hasSize(3); - - int numberOfGeneratedParticipations = prepareExerciseStart(exam); - verify(gitService, times(getNumberOfProgrammingExercises(exam))).combineAllCommitsOfRepositoryIntoOne(any()); - assertThat(numberOfGeneratedParticipations).isEqualTo(12); - // Fetch student exams - List studentExamsDB = request.getList("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams", HttpStatus.OK, StudentExam.class); - assertThat(studentExamsDB).hasSize(3); - List participationList = new ArrayList<>(); - Exercise[] exercises = examRepository.findAllExercisesByExamId(exam.getId()).toArray(new Exercise[0]); - for (Exercise value : exercises) { - participationList.addAll(studentParticipationRepository.findByExerciseId(value.getId())); - } - assertThat(participationList).hasSize(12); - - // TODO there should be some participation but no submissions unfortunately - // remove all students - var paramsParticipations = new LinkedMultiValueMap(); - paramsParticipations.add("withParticipationsAndSubmission", "true"); - request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/students", HttpStatus.OK, paramsParticipations); - - // Get the exam with all registered users - var params = new LinkedMultiValueMap(); - params.add("withStudents", "true"); - Exam storedExam = request.get("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK, Exam.class, params); - assertThat(storedExam.getExamUsers()).isEmpty(); - - // Fetch student exams - studentExamsDB = request.getList("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams", HttpStatus.OK, StudentExam.class); - assertThat(studentExamsDB).isEmpty(); - - // Fetch participations - exercises = examRepository.findAllExercisesByExamId(exam.getId()).toArray(new Exercise[0]); - participationList = new ArrayList<>(); - for (Exercise exercise : exercises) { - participationList.addAll(studentParticipationRepository.findByExerciseId(exercise.getId())); - } - assertThat(participationList).isEmpty(); } @Test @@ -1023,92 +382,6 @@ private List createExamsWithInvalidDates(Course course) { return List.of(examA, examB, examC, examD, examE, examF, examG); } - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testCreateTestExam_asInstructor() throws Exception { - // Test the creation of a test exam - Exam examA = ExamFactory.generateTestExam(course1); - URI examUri = request.post("/api/courses/" + course1.getId() + "/exams", examA, HttpStatus.CREATED); - Exam savedExam = request.get(String.valueOf(examUri), HttpStatus.OK, Exam.class); - - verify(examAccessService).checkCourseAccessForInstructorElseThrow(course1.getId()); - Channel channelFromDB = channelRepository.findChannelByExamId(savedExam.getId()); - assertThat(channelFromDB).isNotNull(); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testCreateTestExam_asInstructor_withVisibleDateEqualsStartDate() throws Exception { - // Test the creation of a test exam, where visibleDate equals StartDate - Exam examB = ExamFactory.generateTestExam(course1); - examB.setVisibleDate(examB.getStartDate()); - request.post("/api/courses/" + course1.getId() + "/exams", examB, HttpStatus.CREATED); - - verify(examAccessService).checkCourseAccessForInstructorElseThrow(course1.getId()); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testCreateTestExam_asInstructor_badRequestWithWorkingTimeGreaterThanWorkingWindow() throws Exception { - // Test for bad request, where workingTime is greater than difference between StartDate and EndDate - Exam examC = ExamFactory.generateTestExam(course1); - examC.setWorkingTime(5000); - request.post("/api/courses/" + course1.getId() + "/exams", examC, HttpStatus.BAD_REQUEST); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testCreateTestExam_asInstructor_badRequestWithWorkingTimeSetToZero() throws Exception { - // Test for bad request, if the working time is 0 - Exam examD = ExamFactory.generateTestExam(course1); - examD.setWorkingTime(0); - request.post("/api/courses/" + course1.getId() + "/exams", examD, HttpStatus.BAD_REQUEST); - - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testCreateTestExam_asInstructor_testExam_CorrectionRoundViolation() throws Exception { - Exam exam = ExamFactory.generateTestExam(course1); - exam.setNumberOfCorrectionRoundsInExam(1); - request.post("/api/courses/" + course1.getId() + "/exams", exam, HttpStatus.BAD_REQUEST); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testCreateTestExam_asInstructor_realExam_CorrectionRoundViolation() throws Exception { - Exam exam = ExamFactory.generateExam(course1); - exam.setNumberOfCorrectionRoundsInExam(0); - request.post("/api/courses/" + course1.getId() + "/exams", exam, HttpStatus.BAD_REQUEST); - - exam.setNumberOfCorrectionRoundsInExam(3); - request.post("/api/courses/" + course1.getId() + "/exams", exam, HttpStatus.BAD_REQUEST); - - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testUpdateTestExam_asInstructor_withExamModeChanged() throws Exception { - // The Exam-Mode should not be changeable with a PUT / update operation, a CONFLICT should be returned instead - // Case 1: test exam should be updated to real exam - Exam examA = ExamFactory.generateTestExam(course1); - Exam createdExamA = request.postWithResponseBody("/api/courses/" + course1.getId() + "/exams", examA, Exam.class, HttpStatus.CREATED); - createdExamA.setNumberOfCorrectionRoundsInExam(1); - createdExamA.setTestExam(false); - request.putWithResponseBody("/api/courses/" + course1.getId() + "/exams", createdExamA, Exam.class, HttpStatus.CONFLICT); - - // Case 2: real exam should be updated to test exam - Exam examB = ExamFactory.generateTestExam(course1); - examB.setNumberOfCorrectionRoundsInExam(1); - examB.setTestExam(false); - examB.setChannelName("examB"); - Exam createdExamB = request.postWithResponseBody("/api/courses/" + course1.getId() + "/exams", examB, Exam.class, HttpStatus.CREATED); - createdExamB.setTestExam(true); - createdExamB.setNumberOfCorrectionRoundsInExam(0); - request.putWithResponseBody("/api/courses/" + course1.getId() + "/exams", createdExamB, Exam.class, HttpStatus.CONFLICT); - - } - @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testUpdateExam_asInstructor() throws Exception { @@ -1146,47 +419,6 @@ void testUpdateExam_asInstructor() throws Exception { verify(instanceMessageSendService, never()).sendProgrammingExerciseSchedule(any()); } - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testUpdateExam_reschedule_visibleAndStartDateChanged() throws Exception { - // Add a programming exercise to the exam and change the dates in order to invoke a rescheduling - var programmingEx = programmingExerciseUtilService.addCourseExamExerciseGroupWithOneProgrammingExerciseAndTestCases(); - var examWithProgrammingEx = programmingEx.getExerciseGroup().getExam(); - - ZonedDateTime visibleDate = examWithProgrammingEx.getVisibleDate(); - ZonedDateTime startDate = examWithProgrammingEx.getStartDate(); - ZonedDateTime endDate = examWithProgrammingEx.getEndDate(); - examUtilService.setVisibleStartAndEndDateOfExam(examWithProgrammingEx, visibleDate.plusSeconds(1), startDate.plusSeconds(1), endDate); - - request.put("/api/courses/" + examWithProgrammingEx.getCourse().getId() + "/exams", examWithProgrammingEx, HttpStatus.OK); - verify(instanceMessageSendService).sendProgrammingExerciseSchedule(programmingEx.getId()); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testUpdateExam_reschedule_visibleDateChanged() throws Exception { - var programmingEx = programmingExerciseUtilService.addCourseExamExerciseGroupWithOneProgrammingExerciseAndTestCases(); - var examWithProgrammingEx = programmingEx.getExerciseGroup().getExam(); - examWithProgrammingEx.setVisibleDate(examWithProgrammingEx.getVisibleDate().plusSeconds(1)); - request.put("/api/courses/" + examWithProgrammingEx.getCourse().getId() + "/exams", examWithProgrammingEx, HttpStatus.OK); - verify(instanceMessageSendService).sendProgrammingExerciseSchedule(programmingEx.getId()); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testUpdateExam_reschedule_startDateChanged() throws Exception { - var programmingEx = programmingExerciseUtilService.addCourseExamExerciseGroupWithOneProgrammingExerciseAndTestCases(); - var examWithProgrammingEx = programmingEx.getExerciseGroup().getExam(); - - ZonedDateTime visibleDate = examWithProgrammingEx.getVisibleDate(); - ZonedDateTime startDate = examWithProgrammingEx.getStartDate(); - ZonedDateTime endDate = examWithProgrammingEx.getEndDate(); - examUtilService.setVisibleStartAndEndDateOfExam(examWithProgrammingEx, visibleDate, startDate.plusSeconds(1), endDate); - - request.put("/api/courses/" + examWithProgrammingEx.getCourse().getId() + "/exams", examWithProgrammingEx, HttpStatus.OK); - verify(instanceMessageSendService).sendProgrammingExerciseSchedule(programmingEx.getId()); - } - @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testUpdateExam_rescheduleModeling_endDateChanged() throws Exception { @@ -1237,7 +469,6 @@ void testUpdateExam_exampleSolutionPublicationDateChanged() throws Exception { Exam fetchedExam = examRepository.findWithExerciseGroupsAndExercisesByIdOrElseThrow(examWithModelingEx.getId()); Exercise exercise = fetchedExam.getExerciseGroups().get(0).getExercises().stream().findFirst().orElseThrow(); assertThat(exercise.isExampleSolutionPublished()).isTrue(); - } @Test @@ -1285,7 +516,6 @@ void testGetExam_asInstructor_WithTestRunQuizExerciseSubmissions() throws Except @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetExamsForCourse_asInstructor() throws Exception { - var exams = request.getList("/api/courses/" + course1.getId() + "/exams", HttpStatus.OK, Exam.class); verify(examAccessService).checkCourseAccessForTeachingAssistantElseThrow(course1.getId()); @@ -1407,82 +637,6 @@ void testResetExamWithQuizExercise_asInstructor() throws Exception { assertThat(quizExercise.getDueDate()).isNull(); } - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testDeleteStudent() throws Exception { - doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); - // Create an exam with registered students - Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, 3); - var student2 = userUtilService.getUserByLogin(TEST_PREFIX + "student2"); - - // Remove student1 from the exam - request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/students/" + TEST_PREFIX + "student1", HttpStatus.OK); - - // Get the exam with all registered users - var params = new LinkedMultiValueMap(); - params.add("withStudents", "true"); - Exam storedExam = request.get("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK, Exam.class, params); - - // Ensure that student1 was removed from the exam - var examUser = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student1.getId()); - assertThat(examUser).isEmpty(); - assertThat(storedExam.getExamUsers()).hasSize(2); - - // Create individual student exams - List generatedStudentExams = request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/generate-student-exams", - Optional.empty(), StudentExam.class, HttpStatus.OK); - assertThat(generatedStudentExams).hasSize(storedExam.getExamUsers().size()); - - // Start the exam to create participations - prepareExerciseStart(exam); - - verify(gitService, times(getNumberOfProgrammingExercises(exam))).combineAllCommitsOfRepositoryIntoOne(any()); - // Get the student exam of student2 - Optional optionalStudent1Exam = generatedStudentExams.stream().filter(studentExam -> studentExam.getUser().equals(student2)).findFirst(); - assertThat(optionalStudent1Exam.orElseThrow()).isNotNull(); - var studentExam2 = optionalStudent1Exam.get(); - - // explicitly set the user again to prevent issues in the following server call due to the use of SecurityUtils.setAuthorizationObject(); - userUtilService.changeUser(TEST_PREFIX + "instructor1"); - // Remove student2 from the exam - request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/students/" + TEST_PREFIX + "student2", HttpStatus.OK); - - // Get the exam with all registered users - params = new LinkedMultiValueMap<>(); - params.add("withStudents", "true"); - storedExam = request.get("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK, Exam.class, params); - - // Ensure that student2 was removed from the exam - var examUser2 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student2.getId()); - assertThat(examUser2).isEmpty(); - assertThat(storedExam.getExamUsers()).hasSize(1); - - // Ensure that the student exam of student2 was deleted - List studentExams = request.getList("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams", HttpStatus.OK, StudentExam.class); - assertThat(studentExams).hasSameSizeAs(storedExam.getExamUsers()).doesNotContain(studentExam2); - - // Ensure that the participations were not deleted - List participationsStudent2 = studentParticipationRepository - .findByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(student2.getId(), studentExam2.getExercises()); - assertThat(participationsStudent2).hasSize(studentExam2.getExercises().size()); - - // Make sure delete also works if so many objects have been created before - request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testDeleteStudentForTestExam_badRequest() throws Exception { - doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); - // Create an exam with registered students - Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, 1); - exam.setTestExam(true); - examRepository.save(exam); - - // Remove student1 from the exam - request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/students/" + TEST_PREFIX + "student1", HttpStatus.BAD_REQUEST); - } - @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetExamWithOptions() throws Exception { @@ -1524,60 +678,6 @@ void testGetExamWithOptions() throws Exception { }); } - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testDeleteStudentWithParticipationsAndSubmissions() throws Exception { - doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); - // Create an exam with registered students - Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, 3); - - // Create individual student exams - List generatedStudentExams = request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/generate-student-exams", - Optional.empty(), StudentExam.class, HttpStatus.OK); - - // Get the student exam of student1 - Optional optionalStudent1Exam = generatedStudentExams.stream().filter(studentExam -> studentExam.getUser().equals(student1)).findFirst(); - assertThat(optionalStudent1Exam.orElseThrow()).isNotNull(); - var studentExam1 = optionalStudent1Exam.get(); - - // Start the exam to create participations - prepareExerciseStart(exam); - verify(gitService, times(getNumberOfProgrammingExercises(exam))).combineAllCommitsOfRepositoryIntoOne(any()); - List participationsStudent1 = studentParticipationRepository - .findByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(student1.getId(), studentExam1.getExercises()); - assertThat(participationsStudent1).hasSize(studentExam1.getExercises().size()); - - // explicitly set the user again to prevent issues in the following server call due to the use of SecurityUtils.setAuthorizationObject(); - userUtilService.changeUser(TEST_PREFIX + "instructor1"); - - // Remove student1 from the exam and his participations - var params = new LinkedMultiValueMap(); - params.add("withParticipationsAndSubmission", "true"); - request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/students/" + TEST_PREFIX + "student1", HttpStatus.OK, params); - - // Get the exam with all registered users - params = new LinkedMultiValueMap<>(); - params.add("withStudents", "true"); - Exam storedExam = request.get("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK, Exam.class, params); - - // Ensure that student1 was removed from the exam - var examUser1 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student1.getId()); - assertThat(examUser1).isEmpty(); - assertThat(storedExam.getExamUsers()).hasSize(2); - - // Ensure that the student exam of student1 was deleted - List studentExams = request.getList("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams", HttpStatus.OK, StudentExam.class); - assertThat(studentExams).hasSameSizeAs(storedExam.getExamUsers()).doesNotContain(studentExam1); - - // Ensure that the participations of student1 were deleted - participationsStudent1 = studentParticipationRepository.findByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(student1.getId(), - studentExam1.getExercises()); - assertThat(participationsStudent1).isEmpty(); - - // Make sure delete also works if so many objects have been created before - request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK); - } - @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testGetExamForTestRunDashboard_forbidden() throws Exception { @@ -1587,242 +687,65 @@ void testGetExamForTestRunDashboard_forbidden() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetExamForTestRunDashboard_badRequest() throws Exception { - request.get("/api/courses/" + course2.getId() + "/exams/" + exam1.getId() + "/exam-for-test-run-assessment-dashboard", HttpStatus.BAD_REQUEST, Exam.class); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testDeleteExamWithOneTestRuns() throws Exception { - var exam = examUtilService.addExam(course1); - exam = examUtilService.addTextModelingProgrammingExercisesToExam(exam, false, false); - examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); - request.delete("/api/courses/" + exam.getCourse().getId() + "/exams/" + exam.getId(), HttpStatus.OK); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testDeleteExamWithMultipleTestRuns() throws Exception { - bitbucketRequestMockProvider.enableMockingOfRequests(true); - bambooRequestMockProvider.enableMockingOfRequests(true); - - var exam = examUtilService.addExam(course1); - exam = examUtilService.addTextModelingProgrammingExercisesToExam(exam, true, true); - mockDeleteProgrammingExercise(exerciseUtilService.getFirstExerciseWithType(exam, ProgrammingExercise.class), Set.of(instructor)); - - examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); - examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); - examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); - assertThat(studentExamRepository.findAllTestRunsByExamId(exam.getId())).hasSize(3); - request.delete("/api/courses/" + exam.getCourse().getId() + "/exams/" + exam.getId(), HttpStatus.OK); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testDeleteCourseWithMultipleTestRuns() throws Exception { - var exam = examUtilService.addExam(course1); - exam = examUtilService.addTextModelingProgrammingExercisesToExam(exam, false, false); - examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); - examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); - examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); - assertThat(studentExamRepository.findAllTestRunsByExamId(exam.getId())).hasSize(3); - request.delete("/api/courses/" + exam.getCourse().getId(), HttpStatus.OK); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testGetExamForTestRunDashboard_ok() throws Exception { - var exam = examUtilService.addExam(course1); - exam = examUtilService.addTextModelingProgrammingExercisesToExam(exam, false, false); - examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); - exam = request.get("/api/courses/" + exam.getCourse().getId() + "/exams/" + exam.getId() + "/exam-for-test-run-assessment-dashboard", HttpStatus.OK, Exam.class); - assertThat(exam.getExerciseGroups().stream().flatMap(exerciseGroup -> exerciseGroup.getExercises().stream()).toList()).isNotEmpty(); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testDeleteStudentThatDoesNotExist() throws Exception { - Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, 1); - request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/students/nonExistingStudent", HttpStatus.NOT_FOUND); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testGetStudentExamForStart() throws Exception { - Exam exam = examUtilService.addActiveExamWithRegisteredUser(course1, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); - exam.setVisibleDate(ZonedDateTime.now().minusHours(1).minusMinutes(5)); - StudentExam response = request.get("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/start", HttpStatus.OK, StudentExam.class); - assertThat(response.getExam()).isEqualTo(exam); - verify(examAccessService).getExamInCourseElseThrow(course1.getId(), exam.getId()); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testAddAllRegisteredUsersToExam() throws Exception { - Exam exam = examUtilService.addExam(course1); - Channel examChannel = examUtilService.addExamChannel(exam, "testchannel"); - int numberOfStudentsInCourse = userRepo.findAllInGroup(course1.getStudentGroupName()).size(); - - User student99 = userUtilService.createAndSaveUser(TEST_PREFIX + "student99"); // not registered for the course - student99.setGroups(Collections.singleton("tumuser")); - userUtilService.setRegistrationNumberOfUserAndSave(student99, "1234"); - assertThat(student99.getGroups()).contains(course1.getStudentGroupName()); - - var examUser99 = examUserRepository.findByExamIdAndUserId(exam.getId(), student99.getId()); - assertThat(examUser99).isEmpty(); - - request.postWithoutLocation("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/register-course-students", null, HttpStatus.OK, null); - - exam = examRepository.findWithExamUsersById(exam.getId()).orElseThrow(); - examUser99 = examUserRepository.findByExamIdAndUserId(exam.getId(), student99.getId()); - - // the course students + our custom student99 - assertThat(exam.getExamUsers()).hasSize(numberOfStudentsInCourse + 1); - assertThat(exam.getExamUsers()).contains(examUser99.orElseThrow()); - verify(examAccessService).checkCourseAndExamAccessForInstructorElseThrow(course1.getId(), exam.getId()); - - Channel channelFromDB = channelRepository.findChannelByExamId(exam.getId()); - assertThat(channelFromDB).isNotNull(); - assertThat(channelFromDB.getExam()).isEqualTo(exam); - assertThat(channelFromDB.getName()).isEqualTo(examChannel.getName()); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testRegisterCourseStudents_testExam() throws Exception { - request.postWithoutLocation("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/register-course-students", null, HttpStatus.BAD_REQUEST, null); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testUpdateOrderOfExerciseGroups() throws Exception { - Exam exam = ExamFactory.generateExam(course1); - ExerciseGroup exerciseGroup1 = ExamFactory.generateExerciseGroupWithTitle(true, exam, "first"); - ExerciseGroup exerciseGroup2 = ExamFactory.generateExerciseGroupWithTitle(true, exam, "second"); - ExerciseGroup exerciseGroup3 = ExamFactory.generateExerciseGroupWithTitle(true, exam, "third"); - examRepository.save(exam); - - TextExercise exercise1_1 = textExerciseUtilService.createTextExerciseForExam(exerciseGroup1); - TextExercise exercise1_2 = textExerciseUtilService.createTextExerciseForExam(exerciseGroup1); - TextExercise exercise2_1 = textExerciseUtilService.createTextExerciseForExam(exerciseGroup2); - TextExercise exercise3_1 = textExerciseUtilService.createTextExerciseForExam(exerciseGroup3); - TextExercise exercise3_2 = textExerciseUtilService.createTextExerciseForExam(exerciseGroup3); - TextExercise exercise3_3 = textExerciseUtilService.createTextExerciseForExam(exerciseGroup3); - - List orderedExerciseGroups = new ArrayList<>(List.of(exerciseGroup2, exerciseGroup3, exerciseGroup1)); - // Should save new order - request.put("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/exercise-groups-order", orderedExerciseGroups, HttpStatus.OK); - verify(examAccessService).checkCourseAndExamAccessForEditorElseThrow(course1.getId(), exam.getId()); - - List savedExerciseGroups = examRepository.findWithExerciseGroupsById(exam.getId()).orElseThrow().getExerciseGroups(); - assertThat(savedExerciseGroups.get(0).getTitle()).isEqualTo("second"); - assertThat(savedExerciseGroups.get(1).getTitle()).isEqualTo("third"); - assertThat(savedExerciseGroups.get(2).getTitle()).isEqualTo("first"); - - // Exercises should be preserved - Exam savedExam = examRepository.findWithExerciseGroupsAndExercisesById(exam.getId()).orElseThrow(); - ExerciseGroup savedExerciseGroup1 = savedExam.getExerciseGroups().get(2); - ExerciseGroup savedExerciseGroup2 = savedExam.getExerciseGroups().get(0); - ExerciseGroup savedExerciseGroup3 = savedExam.getExerciseGroups().get(1); - assertThat(savedExerciseGroup1.getExercises()).containsExactlyInAnyOrder(exercise1_1, exercise1_2); - assertThat(savedExerciseGroup2.getExercises()).containsExactlyInAnyOrder(exercise2_1); - assertThat(savedExerciseGroup3.getExercises()).containsExactlyInAnyOrder(exercise3_1, exercise3_2, exercise3_3); - - // Should fail with too many exercise groups - orderedExerciseGroups.add(exerciseGroup1); - request.put("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/exercise-groups-order", orderedExerciseGroups, HttpStatus.BAD_REQUEST); - - // Should fail with too few exercise groups - orderedExerciseGroups.remove(3); - orderedExerciseGroups.remove(2); - request.put("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/exercise-groups-order", orderedExerciseGroups, HttpStatus.BAD_REQUEST); - - // Should fail with different exercise group - orderedExerciseGroups = Arrays.asList(exerciseGroup2, exerciseGroup3, ExamFactory.generateExerciseGroup(true, exam)); - request.put("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/exercise-groups-order", orderedExerciseGroups, HttpStatus.BAD_REQUEST); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void lockAllRepositories_noInstructor() throws Exception { - request.postWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam1.getId() + "/student-exams/lock-all-repositories", Optional.empty(), Integer.class, - HttpStatus.FORBIDDEN); + request.get("/api/courses/" + course2.getId() + "/exams/" + exam1.getId() + "/exam-for-test-run-assessment-dashboard", HttpStatus.BAD_REQUEST, Exam.class); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void lockAllRepositories() throws Exception { - Exam exam = examUtilService.addExamWithExerciseGroup(course1, true); - - Exam examWithExerciseGroups = examRepository.findWithExerciseGroupsAndExercisesById(exam.getId()).orElseThrow(); - ExerciseGroup exerciseGroup1 = examWithExerciseGroups.getExerciseGroups().get(0); - - ProgrammingExercise programmingExercise = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(exerciseGroup1); - programmingExerciseRepository.save(programmingExercise); - - ProgrammingExercise programmingExercise2 = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(exerciseGroup1); - programmingExerciseRepository.save(programmingExercise2); + void testDeleteExamWithOneTestRuns() throws Exception { + var exam = examUtilService.addExam(course1); + exam = examUtilService.addTextModelingProgrammingExercisesToExam(exam, false, false); + examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); + request.delete("/api/courses/" + exam.getCourse().getId() + "/exams/" + exam.getId(), HttpStatus.OK); + } - Integer numOfLockedExercises = request.postWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams/lock-all-repositories", - Optional.empty(), Integer.class, HttpStatus.OK); + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testDeleteExamWithMultipleTestRuns() throws Exception { + bitbucketRequestMockProvider.enableMockingOfRequests(true); + bambooRequestMockProvider.enableMockingOfRequests(true); - assertThat(numOfLockedExercises).isEqualTo(2); + var exam = examUtilService.addExam(course1); + exam = examUtilService.addTextModelingProgrammingExercisesToExam(exam, true, true); + mockDeleteProgrammingExercise(exerciseUtilService.getFirstExerciseWithType(exam, ProgrammingExercise.class), Set.of(instructor)); - verify(programmingExerciseScheduleService).lockAllStudentRepositories(programmingExercise); - verify(programmingExerciseScheduleService).lockAllStudentRepositories(programmingExercise2); + examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); + examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); + examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); + assertThat(studentExamRepository.findAllTestRunsByExamId(exam.getId())).hasSize(3); + request.delete("/api/courses/" + exam.getCourse().getId() + "/exams/" + exam.getId(), HttpStatus.OK); } @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void unlockAllRepositories_preAuthNoInstructor() throws Exception { - request.postWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam1.getId() + "/student-exams/unlock-all-repositories", Optional.empty(), Integer.class, - HttpStatus.FORBIDDEN); + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testDeleteCourseWithMultipleTestRuns() throws Exception { + var exam = examUtilService.addExam(course1); + exam = examUtilService.addTextModelingProgrammingExercisesToExam(exam, false, false); + examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); + examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); + examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); + assertThat(studentExamRepository.findAllTestRunsByExamId(exam.getId())).hasSize(3); + request.delete("/api/courses/" + exam.getCourse().getId(), HttpStatus.OK); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void unlockAllRepositories() throws Exception { - bitbucketRequestMockProvider.enableMockingOfRequests(true); - assertThat(studentExamRepository.findStudentExam(new ProgrammingExercise(), null)).isEmpty(); - - Exam exam = examUtilService.addExamWithExerciseGroup(course1, true); - ExerciseGroup exerciseGroup1 = exam.getExerciseGroups().get(0); - - ProgrammingExercise programmingExercise = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(exerciseGroup1); - programmingExerciseRepository.save(programmingExercise); - - ProgrammingExercise programmingExercise2 = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(exerciseGroup1); - programmingExerciseRepository.save(programmingExercise2); - - User student2 = userUtilService.getUserByLogin(TEST_PREFIX + "student2"); - var studentExam1 = examUtilService.addStudentExamWithUser(exam, student1, 10); - studentExam1.setExercises(List.of(programmingExercise, programmingExercise2)); - var studentExam2 = examUtilService.addStudentExamWithUser(exam, student2, 0); - studentExam2.setExercises(List.of(programmingExercise, programmingExercise2)); - studentExamRepository.saveAll(Set.of(studentExam1, studentExam2)); - - var participationExSt1 = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, TEST_PREFIX + "student1"); - var participationExSt2 = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, TEST_PREFIX + "student2"); - - var participationEx2St1 = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise2, TEST_PREFIX + "student1"); - var participationEx2St2 = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise2, TEST_PREFIX + "student2"); - - assertThat(studentExamRepository.findStudentExam(programmingExercise, participationExSt1)).contains(studentExam1); - assertThat(studentExamRepository.findStudentExam(programmingExercise, participationExSt2)).contains(studentExam2); - assertThat(studentExamRepository.findStudentExam(programmingExercise2, participationEx2St1)).contains(studentExam1); - assertThat(studentExamRepository.findStudentExam(programmingExercise2, participationEx2St2)).contains(studentExam2); - - mockConfigureRepository(programmingExercise, TEST_PREFIX + "student1", Set.of(student1), true); - mockConfigureRepository(programmingExercise, TEST_PREFIX + "student2", Set.of(student2), true); - mockConfigureRepository(programmingExercise2, TEST_PREFIX + "student1", Set.of(student1), true); - mockConfigureRepository(programmingExercise2, TEST_PREFIX + "student2", Set.of(student2), true); - - Integer numOfUnlockedExercises = request.postWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams/unlock-all-repositories", - Optional.empty(), Integer.class, HttpStatus.OK); - - assertThat(numOfUnlockedExercises).isEqualTo(2); + void testGetExamForTestRunDashboard_ok() throws Exception { + var exam = examUtilService.addExam(course1); + exam = examUtilService.addTextModelingProgrammingExercisesToExam(exam, false, false); + examUtilService.setupTestRunForExamWithExerciseGroupsForInstructor(exam, instructor, exam.getExerciseGroups()); + exam = request.get("/api/courses/" + exam.getCourse().getId() + "/exams/" + exam.getId() + "/exam-for-test-run-assessment-dashboard", HttpStatus.OK, Exam.class); + assertThat(exam.getExerciseGroups().stream().flatMap(exerciseGroup -> exerciseGroup.getExercises().stream()).toList()).isNotEmpty(); + } - verify(programmingExerciseScheduleService).unlockAllStudentRepositories(programmingExercise); - verify(programmingExerciseScheduleService).unlockAllStudentRepositories(programmingExercise2); + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testGetStudentExamForStart() throws Exception { + Exam exam = examUtilService.addActiveExamWithRegisteredUser(course1, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); + exam.setVisibleDate(ZonedDateTime.now().minusHours(1).minusMinutes(5)); + StudentExam response = request.get("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/start", HttpStatus.OK, StudentExam.class); + assertThat(response.getExam()).isEqualTo(exam); + verify(examAccessService).getExamInCourseElseThrow(course1.getId(), exam.getId()); } @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @@ -1894,441 +817,6 @@ void testGetExamScore_tutor_forbidden() throws Exception { request.get("/api/courses/" + course1.getId() + "/exams/" + exam1.getId() + "/scores", HttpStatus.FORBIDDEN, ExamScoresDTO.class); } - private int getNumberOfProgrammingExercises(Exam exam) { - exam = examRepository.findWithExerciseGroupsAndExercisesByIdOrElseThrow(exam.getId()); - int count = 0; - for (var exerciseGroup : exam.getExerciseGroups()) { - for (var exercise : exerciseGroup.getExercises()) { - if (exercise instanceof ProgrammingExercise) { - count++; - } - } - } - return count; - } - - private void configureCourseAsBonusWithIndividualAndTeamResults(Course course, GradingScale bonusToGradingScale) { - ZonedDateTime pastTimestamp = ZonedDateTime.now().minusDays(5); - TextExercise textExercise = textExerciseUtilService.createIndividualTextExercise(course, pastTimestamp, pastTimestamp, pastTimestamp); - Long individualTextExerciseId = textExercise.getId(); - textExerciseUtilService.createIndividualTextExercise(course, pastTimestamp, pastTimestamp, pastTimestamp); - - Exercise teamExercise = textExerciseUtilService.createTeamTextExercise(course, pastTimestamp, pastTimestamp, pastTimestamp); - User tutor1 = userRepo.findOneByLogin(TEST_PREFIX + "tutor1").orElseThrow(); - Long teamTextExerciseId = teamExercise.getId(); - Long team1Id = teamUtilService.createTeam(Set.of(student1), tutor1, teamExercise, TEST_PREFIX + "team1").getId(); - User student2 = userRepo.findOneByLogin(TEST_PREFIX + "student2").orElseThrow(); - User student3 = userRepo.findOneByLogin(TEST_PREFIX + "student3").orElseThrow(); - User tutor2 = userRepo.findOneByLogin(TEST_PREFIX + "tutor2").orElseThrow(); - Long team2Id = teamUtilService.createTeam(Set.of(student2, student3), tutor2, teamExercise, TEST_PREFIX + "team2").getId(); - - participationUtilService.createParticipationSubmissionAndResult(individualTextExerciseId, student1, 10.0, 10.0, 50, true); - - Team team1 = teamRepository.findById(team1Id).orElseThrow(); - var result = participationUtilService.createParticipationSubmissionAndResult(teamTextExerciseId, team1, 10.0, 10.0, 40, true); - // Creating a second results for team1 to test handling multiple results. - participationUtilService.createSubmissionAndResult((StudentParticipation) result.getParticipation(), 50, true); - - var student2Result = participationUtilService.createParticipationSubmissionAndResult(individualTextExerciseId, student2, 10.0, 10.0, 50, true); - - var student3Result = participationUtilService.createParticipationSubmissionAndResult(individualTextExerciseId, student3, 10.0, 10.0, 30, true); - - Team team2 = teamRepository.findById(team2Id).orElseThrow(); - participationUtilService.createParticipationSubmissionAndResult(teamTextExerciseId, team2, 10.0, 10.0, 80, true); - - // Adding plagiarism cases - var bonusPlagiarismCase = new PlagiarismCase(); - bonusPlagiarismCase.setStudent(student3); - bonusPlagiarismCase.setExercise(student3Result.getParticipation().getExercise()); - bonusPlagiarismCase.setVerdict(PlagiarismVerdict.PLAGIARISM); - plagiarismCaseRepository.save(bonusPlagiarismCase); - - var bonusPlagiarismCase2 = new PlagiarismCase(); - bonusPlagiarismCase2.setStudent(student2); - bonusPlagiarismCase2.setExercise(student2Result.getParticipation().getExercise()); - bonusPlagiarismCase2.setVerdict(PlagiarismVerdict.POINT_DEDUCTION); - bonusPlagiarismCase2.setVerdictPointDeduction(50); - plagiarismCaseRepository.save(bonusPlagiarismCase2); - - BonusStrategy bonusStrategy = BonusStrategy.GRADES_CONTINUOUS; - bonusToGradingScale.setBonusStrategy(bonusStrategy); - gradingScaleRepository.save(bonusToGradingScale); - - GradingScale sourceGradingScale = gradingScaleUtilService.generateGradingScaleWithStickyStep(new double[] { 60, 40, 50 }, Optional.of(new String[] { "0", "0.3", "0.6" }), - true, 1); - sourceGradingScale.setGradeType(GradeType.BONUS); - sourceGradingScale.setCourse(course); - gradingScaleRepository.save(sourceGradingScale); - - var bonus = BonusFactory.generateBonus(bonusStrategy, -1.0, sourceGradingScale.getId(), bonusToGradingScale.getId()); - bonusRepository.save(bonus); - - course.setMaxPoints(100); - course.setPresentationScore(null); - courseRepo.save(course); - - } - - @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") - @CsvSource({ "false, false", "true, false", "false, true", "true, true" }) - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testGetExamScore(boolean withCourseBonus, boolean withSecondCorrectionAndStarted) throws Exception { - programmingExerciseTestService.setup(this, versionControlService, continuousIntegrationService); - bitbucketRequestMockProvider.enableMockingOfRequests(true); - bambooRequestMockProvider.enableMockingOfRequests(true); - - doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); - - var visibleDate = now().minusMinutes(5); - var startDate = now().plusMinutes(5); - var endDate = now().plusMinutes(20); - - // register users. Instructors are ignored from scores as they are exclusive for test run exercises - Set registeredStudents = getRegisteredStudentsForExam(); - - var studentExams = programmingExerciseTestService.prepareStudentExamsForConduction(TEST_PREFIX, visibleDate, startDate, endDate, registeredStudents, studentRepos); - Exam exam = examRepository.findByIdWithExamUsersExerciseGroupsAndExercisesElseThrow(studentExams.get(0).getExam().getId()); - Course course = exam.getCourse(); - - Integer noGeneratedParticipations = registeredStudents.size() * exam.getExerciseGroups().size(); - - verify(gitService, times(getNumberOfProgrammingExercises(exam))).combineAllCommitsOfRepositoryIntoOne(any()); - // explicitly set the user again to prevent issues in the following server call due to the use of SecurityUtils.setAuthorizationObject(); - userUtilService.changeUser(TEST_PREFIX + "instructor1"); - - // instructor exam checklist checks - ExamChecklistDTO examChecklistDTO = examService.getStatsForChecklist(exam, true); - assertThat(examChecklistDTO).isNotNull(); - assertThat(examChecklistDTO.getNumberOfGeneratedStudentExams()).isEqualTo(exam.getExamUsers().size()); - assertThat(examChecklistDTO.getAllExamExercisesAllStudentsPrepared()).isTrue(); - assertThat(examChecklistDTO.getNumberOfTotalParticipationsForAssessment()).isZero(); - - // check that an adapted version is computed for tutors - userUtilService.changeUser(TEST_PREFIX + "tutor1"); - - examChecklistDTO = examService.getStatsForChecklist(exam, false); - assertThat(examChecklistDTO).isNotNull(); - assertThat(examChecklistDTO.getNumberOfGeneratedStudentExams()).isNull(); - assertThat(examChecklistDTO.getAllExamExercisesAllStudentsPrepared()).isFalse(); - assertThat(examChecklistDTO.getNumberOfTotalParticipationsForAssessment()).isZero(); - - userUtilService.changeUser(TEST_PREFIX + "instructor1"); - - // set start and submitted date as results are created below - studentExams.forEach(studentExam -> { - studentExam.setStartedAndStartDate(now().minusMinutes(2)); - studentExam.setSubmitted(true); - studentExam.setSubmissionDate(now().minusMinutes(1)); - }); - studentExamRepository.saveAll(studentExams); - - // Fetch the created participations and assign them to the exercises - int participationCounter = 0; - List exercisesInExam = exam.getExerciseGroups().stream().map(ExerciseGroup::getExercises).flatMap(Collection::stream).toList(); - for (var exercise : exercisesInExam) { - List participations = studentParticipationRepository.findByExerciseIdAndTestRunWithEagerLegalSubmissionsResult(exercise.getId(), false); - exercise.setStudentParticipations(new HashSet<>(participations)); - participationCounter += exercise.getStudentParticipations().size(); - } - assertThat(noGeneratedParticipations).isEqualTo(participationCounter); - - if (withSecondCorrectionAndStarted) { - exercisesInExam.forEach(exercise -> exercise.setSecondCorrectionEnabled(true)); - exerciseRepo.saveAll(exercisesInExam); - } - - // Scores used for all exercise results - double correctionResultScore = 60D; - double resultScore = 75D; - - // Assign results to participations and submissions - for (var exercise : exercisesInExam) { - for (var participation : exercise.getStudentParticipations()) { - Submission submission; - // Programming exercises don't have a submission yet - if (exercise instanceof ProgrammingExercise) { - assertThat(participation.getSubmissions()).isEmpty(); - submission = new ProgrammingSubmission(); - submission.setParticipation(participation); - submission = submissionRepository.save(submission); - } - else { - // There should only be one submission for text, quiz, modeling and file upload - assertThat(participation.getSubmissions()).hasSize(1); - submission = participation.getSubmissions().iterator().next(); - } - - // make sure to create submitted answers - if (exercise instanceof QuizExercise quizExercise) { - var quizQuestions = quizExerciseRepository.findByIdWithQuestionsElseThrow(exercise.getId()).getQuizQuestions(); - for (var quizQuestion : quizQuestions) { - var submittedAnswer = QuizExerciseFactory.generateSubmittedAnswerFor(quizQuestion, true); - var quizSubmission = quizSubmissionRepository.findWithEagerSubmittedAnswersById(submission.getId()); - quizSubmission.addSubmittedAnswers(submittedAnswer); - quizSubmissionService.saveSubmissionForExamMode(quizExercise, quizSubmission, participation.getStudent().orElseThrow()); - } - } - - // Create results - if (withSecondCorrectionAndStarted) { - var firstResult = new Result().score(correctionResultScore).rated(true).completionDate(now().minusMinutes(5)); - firstResult.setParticipation(participation); - firstResult.setAssessor(instructor); - firstResult = resultRepository.save(firstResult); - firstResult.setSubmission(submission); - submission.addResult(firstResult); - } - - var finalResult = new Result().score(resultScore).rated(true).completionDate(now().minusMinutes(5)); - finalResult.setParticipation(participation); - finalResult.setAssessor(instructor); - finalResult = resultRepository.save(finalResult); - finalResult.setSubmission(submission); - submission.addResult(finalResult); - - submission.submitted(true); - submission.setSubmissionDate(now().minusMinutes(6)); - submissionRepository.save(submission); - } - } - // explicitly set the user again to prevent issues in the following server call due to the use of SecurityUtils.setAuthorizationObject(); - userUtilService.changeUser(TEST_PREFIX + "instructor1"); - final var exerciseWithNoUsers = TextExerciseFactory.generateTextExerciseForExam(exam.getExerciseGroups().get(0)); - exerciseRepo.save(exerciseWithNoUsers); - - GradingScale gradingScale = gradingScaleUtilService.generateGradingScaleWithStickyStep(new double[] { 60, 25, 15, 50 }, - Optional.of(new String[] { "5.0", "3.0", "1.0", "1.0" }), true, 1); - gradingScale.setExam(exam); - gradingScale = gradingScaleRepository.save(gradingScale); - - waitForParticipantScores(); - - if (withCourseBonus) { - configureCourseAsBonusWithIndividualAndTeamResults(course, gradingScale); - } - - await().timeout(Duration.ofMinutes(1)).until(() -> { - for (Exercise exercise : exercisesInExam) { - if (participantScoreRepository.findAllByExercise(exercise).size() != exercise.getStudentParticipations().size()) { - return false; - } - } - return true; - }); - - var examScores = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/scores", HttpStatus.OK, ExamScoresDTO.class); - - // Compare generated results to data in ExamScoresDTO - // Compare top-level DTO properties - assertThat(examScores.maxPoints()).isEqualTo(exam.getExamMaxPoints()); - - assertThat(examScores.hasSecondCorrectionAndStarted()).isEqualTo(withSecondCorrectionAndStarted); - - // For calculation assume that all exercises within an exerciseGroups have the same max points - double calculatedAverageScore = 0.0; - for (var exerciseGroup : exam.getExerciseGroups()) { - var exercise = exerciseGroup.getExercises().stream().findAny().orElseThrow(); - if (exercise.getIncludedInOverallScore().equals(IncludedInOverallScore.NOT_INCLUDED)) { - continue; - } - calculatedAverageScore += Math.round(exercise.getMaxPoints() * resultScore / 100.00 * 10) / 10.0; - } - - assertThat(examScores.averagePointsAchieved()).isEqualTo(calculatedAverageScore); - assertThat(examScores.title()).isEqualTo(exam.getTitle()); - assertThat(examScores.examId()).isEqualTo(exam.getId()); - - // Ensure that all exerciseGroups of the exam are present in the DTO - Set exerciseGroupIdsInDTO = examScores.exerciseGroups().stream().map(ExamScoresDTO.ExerciseGroup::id).collect(Collectors.toSet()); - Set exerciseGroupIdsInExam = exam.getExerciseGroups().stream().map(ExerciseGroup::getId).collect(Collectors.toSet()); - assertThat(exerciseGroupIdsInExam).isEqualTo(exerciseGroupIdsInDTO); - - // Compare exerciseGroups in DTO to exam exerciseGroups - // Tolerated absolute difference for floating-point number comparisons - double EPSILON = 0000.1; - for (var exerciseGroupDTO : examScores.exerciseGroups()) { - // Find the original exerciseGroup of the exam using the id in ExerciseGroupId - ExerciseGroup originalExerciseGroup = exam.getExerciseGroups().stream().filter(exerciseGroup -> exerciseGroup.getId().equals(exerciseGroupDTO.id())).findFirst() - .orElseThrow(); - - // Assume that all exercises in a group have the same max score - Double groupMaxScoreFromExam = originalExerciseGroup.getExercises().stream().findAny().orElseThrow().getMaxPoints(); - assertThat(exerciseGroupDTO.maxPoints()).isEqualTo(originalExerciseGroup.getExercises().stream().findAny().orElseThrow().getMaxPoints()); - assertThat(groupMaxScoreFromExam).isEqualTo(exerciseGroupDTO.maxPoints(), withPrecision(EPSILON)); - - // EPSILON - // Compare exercise information - long noOfExerciseGroupParticipations = 0; - for (var originalExercise : originalExerciseGroup.getExercises()) { - // Find the corresponding ExerciseInfo object - var exerciseDTO = exerciseGroupDTO.containedExercises().stream().filter(exerciseInfo -> exerciseInfo.exerciseId().equals(originalExercise.getId())).findFirst() - .orElseThrow(); - // Check the exercise title - assertThat(originalExercise.getTitle()).isEqualTo(exerciseDTO.title()); - // Check the max points of the exercise - assertThat(originalExercise.getMaxPoints()).isEqualTo(exerciseDTO.maxPoints()); - // Check the number of exercise participants and update the group participant counter - var noOfExerciseParticipations = originalExercise.getStudentParticipations().size(); - noOfExerciseGroupParticipations += noOfExerciseParticipations; - assertThat(Long.valueOf(originalExercise.getStudentParticipations().size())).isEqualTo(exerciseDTO.numberOfParticipants()); - } - assertThat(noOfExerciseGroupParticipations).isEqualTo(exerciseGroupDTO.numberOfParticipants()); - } - - // Ensure that all registered students have a StudentResult - Set studentIdsWithStudentResults = examScores.studentResults().stream().map(ExamScoresDTO.StudentResult::userId).collect(Collectors.toSet()); - Set registeredUsers = exam.getRegisteredUsers(); - Set registeredUsersIds = registeredUsers.stream().map(User::getId).collect(Collectors.toSet()); - assertThat(studentIdsWithStudentResults).isEqualTo(registeredUsersIds); - - // Compare StudentResult with the generated results - for (var studentResult : examScores.studentResults()) { - // Find the original user using the id in StudentResult - User originalUser = userRepo.findByIdElseThrow(studentResult.userId()); - StudentExam studentExamOfUser = studentExams.stream().filter(studentExam -> studentExam.getUser().equals(originalUser)).findFirst().orElseThrow(); - - assertThat(studentResult.name()).isEqualTo(originalUser.getName()); - assertThat(studentResult.email()).isEqualTo(originalUser.getEmail()); - assertThat(studentResult.login()).isEqualTo(originalUser.getLogin()); - assertThat(studentResult.registrationNumber()).isEqualTo(originalUser.getRegistrationNumber()); - - // Calculate overall points achieved - - var calculatedOverallPoints = calculateOverallPoints(resultScore, studentExamOfUser); - - assertThat(studentResult.overallPointsAchieved()).isEqualTo(calculatedOverallPoints, withPrecision(EPSILON)); - - double expectedPointsAchievedInFirstCorrection = withSecondCorrectionAndStarted ? calculateOverallPoints(correctionResultScore, studentExamOfUser) : 0.0; - assertThat(studentResult.overallPointsAchievedInFirstCorrection()).isEqualTo(expectedPointsAchievedInFirstCorrection, withPrecision(EPSILON)); - - // Calculate overall score achieved - var calculatedOverallScore = calculatedOverallPoints / examScores.maxPoints() * 100; - assertThat(studentResult.overallScoreAchieved()).isEqualTo(calculatedOverallScore, withPrecision(EPSILON)); - - assertThat(studentResult.overallGrade()).isNotNull(); - assertThat(studentResult.hasPassed()).isNotNull(); - assertThat(studentResult.mostSeverePlagiarismVerdict()).isNull(); - if (withCourseBonus) { - String studentLogin = studentResult.login(); - assertThat(studentResult.gradeWithBonus().bonusStrategy()).isEqualTo(BonusStrategy.GRADES_CONTINUOUS); - switch (studentLogin) { - case TEST_PREFIX + "student1" -> { - assertThat(studentResult.gradeWithBonus().mostSeverePlagiarismVerdict()).isNull(); - assertThat(studentResult.gradeWithBonus().studentPointsOfBonusSource()).isEqualTo(10.0); - assertThat(studentResult.gradeWithBonus().bonusGrade()).isEqualTo("0.0"); - assertThat(studentResult.gradeWithBonus().finalGrade()).isEqualTo("1.0"); - } - case TEST_PREFIX + "student2" -> { - assertThat(studentResult.gradeWithBonus().mostSeverePlagiarismVerdict()).isEqualTo(PlagiarismVerdict.POINT_DEDUCTION); - assertThat(studentResult.gradeWithBonus().studentPointsOfBonusSource()).isEqualTo(10.5); // 10.5 = 8 + 5 * 50% plagiarism point deduction. - assertThat(studentResult.gradeWithBonus().finalGrade()).isEqualTo("1.0"); - } - case TEST_PREFIX + "student3" -> { - assertThat(studentResult.gradeWithBonus().mostSeverePlagiarismVerdict()).isEqualTo(PlagiarismVerdict.PLAGIARISM); - assertThat(studentResult.gradeWithBonus().studentPointsOfBonusSource()).isZero(); - assertThat(studentResult.gradeWithBonus().bonusGrade()).isEqualTo(GradingScale.DEFAULT_PLAGIARISM_GRADE); - assertThat(studentResult.gradeWithBonus().finalGrade()).isEqualTo("1.0"); - } - default -> { - } - } - } - else { - assertThat(studentResult.gradeWithBonus()).isNull(); - } - - // Ensure that the exercise ids of the student exam are the same as the exercise ids in the students exercise results - Set exerciseIdsOfStudentResult = studentResult.exerciseGroupIdToExerciseResult().values().stream().map(ExamScoresDTO.ExerciseResult::exerciseId) - .collect(Collectors.toSet()); - Set exerciseIdsInStudentExam = studentExamOfUser.getExercises().stream().map(DomainObject::getId).collect(Collectors.toSet()); - assertThat(exerciseIdsOfStudentResult).isEqualTo(exerciseIdsInStudentExam); - for (Map.Entry entry : studentResult.exerciseGroupIdToExerciseResult().entrySet()) { - var exerciseResult = entry.getValue(); - - // Find the original exercise using the id in ExerciseResult - Exercise originalExercise = studentExamOfUser.getExercises().stream().filter(exercise -> exercise.getId().equals(exerciseResult.exerciseId())).findFirst() - .orElseThrow(); - - // Check that the key is associated with the exerciseGroup which actually contains the exercise in the exerciseResult - assertThat(originalExercise.getExerciseGroup().getId()).isEqualTo(entry.getKey()); - - assertThat(exerciseResult.title()).isEqualTo(originalExercise.getTitle()); - assertThat(exerciseResult.maxScore()).isEqualTo(originalExercise.getMaxPoints()); - assertThat(exerciseResult.achievedScore()).isEqualTo(resultScore); - if (originalExercise instanceof QuizExercise) { - assertThat(exerciseResult.hasNonEmptySubmission()).isTrue(); - } - else { - assertThat(exerciseResult.hasNonEmptySubmission()).isFalse(); - } - // TODO: create a test where hasNonEmptySubmission() is false for a quiz - assertThat(exerciseResult.achievedPoints()).isEqualTo(originalExercise.getMaxPoints() * resultScore / 100, withPrecision(EPSILON)); - } - } - - // change back to instructor user - userUtilService.changeUser(TEST_PREFIX + "instructor1"); - - var expectedTotalExamAssessmentsFinishedByCorrectionRound = new Long[] { noGeneratedParticipations.longValue(), noGeneratedParticipations.longValue() }; - if (!withSecondCorrectionAndStarted) { - // The second correction has not started in this case. - expectedTotalExamAssessmentsFinishedByCorrectionRound[1] = 0L; - } - - // check if stats are set correctly for the instructor - examChecklistDTO = examService.getStatsForChecklist(exam, true); - assertThat(examChecklistDTO).isNotNull(); - var size = examScores.studentResults().size(); - assertThat(examChecklistDTO.getNumberOfGeneratedStudentExams()).isEqualTo(size); - assertThat(examChecklistDTO.getNumberOfExamsSubmitted()).isEqualTo(size); - assertThat(examChecklistDTO.getNumberOfExamsStarted()).isEqualTo(size); - assertThat(examChecklistDTO.getAllExamExercisesAllStudentsPrepared()).isTrue(); - assertThat(examChecklistDTO.getNumberOfTotalParticipationsForAssessment()).isEqualTo(size * 6L); - assertThat(examChecklistDTO.getNumberOfTestRuns()).isZero(); - assertThat(examChecklistDTO.getNumberOfTotalExamAssessmentsFinishedByCorrectionRound()).hasSize(2).containsExactly(expectedTotalExamAssessmentsFinishedByCorrectionRound); - - // change to a tutor - userUtilService.changeUser(TEST_PREFIX + "tutor1"); - - // check that a modified version is returned - // check if stats are set correctly for the instructor - examChecklistDTO = examService.getStatsForChecklist(exam, false); - assertThat(examChecklistDTO).isNotNull(); - assertThat(examChecklistDTO.getNumberOfGeneratedStudentExams()).isNull(); - assertThat(examChecklistDTO.getNumberOfExamsSubmitted()).isNull(); - assertThat(examChecklistDTO.getNumberOfExamsStarted()).isNull(); - assertThat(examChecklistDTO.getAllExamExercisesAllStudentsPrepared()).isFalse(); - assertThat(examChecklistDTO.getNumberOfTotalParticipationsForAssessment()).isEqualTo(size * 6L); - assertThat(examChecklistDTO.getNumberOfTestRuns()).isNull(); - assertThat(examChecklistDTO.getNumberOfTotalExamAssessmentsFinishedByCorrectionRound()).hasSize(2).containsExactly(expectedTotalExamAssessmentsFinishedByCorrectionRound); - - bambooRequestMockProvider.reset(); - - final ProgrammingExercise programmingExercise = (ProgrammingExercise) exam.getExerciseGroups().get(6).getExercises().iterator().next(); - - var usersOfExam = exam.getRegisteredUsers(); - mockDeleteProgrammingExercise(programmingExercise, usersOfExam); - - await().until(() -> participantScoreScheduleService.isIdle()); - - // change back to instructor user - userUtilService.changeUser(TEST_PREFIX + "instructor1"); - // Make sure delete also works if so many objects have been created before - waitForParticipantScores(); - request.delete("/api/courses/" + course.getId() + "/exams/" + exam.getId(), HttpStatus.OK); - assertThat(examRepository.findById(exam.getId())).isEmpty(); - } - - private void waitForParticipantScores() { - participantScoreScheduleService.executeScheduledTasks(); - await().until(() -> participantScoreScheduleService.isIdle()); - } - - private double calculateOverallPoints(Double correctionResultScore, StudentExam studentExamOfUser) { - return studentExamOfUser.getExercises().stream().filter(exercise -> !exercise.getIncludedInOverallScore().equals(IncludedInOverallScore.NOT_INCLUDED)) - .map(Exercise::getMaxPoints).reduce(0.0, (total, maxScore) -> (Math.round((total + maxScore * correctionResultScore / 100) * 10) / 10.0)); - } - @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetExamStatistics() throws Exception { @@ -2471,27 +959,6 @@ void testIsExamOver_GracePeriod() { assertThat(isOver).isFalse(); } - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testIsUserRegisteredForExam() { - var examUser = new ExamUser(); - examUser.setExam(exam1); - examUser.setUser(student1); - examUser = examUserRepository.save(examUser); - exam1.addExamUser(examUser); - final var exam = examRepository.save(exam1); - final var isUserRegistered = examRegistrationService.isUserRegisteredForExam(exam.getId(), student1.getId()); - final var isCurrentUserRegistered = examRegistrationService.isCurrentUserRegisteredForExam(exam.getId()); - assertThat(isUserRegistered).isTrue(); - assertThat(isCurrentUserRegistered).isFalse(); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testRegisterInstructorToExam() throws Exception { - request.postWithoutLocation("/api/courses/" + course1.getId() + "/exams/" + exam1.getId() + "/students/" + TEST_PREFIX + "instructor1", null, HttpStatus.FORBIDDEN, null); - } - @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testArchiveCourseWithExam() throws Exception { @@ -2635,322 +1102,6 @@ private void assertSubmissionFilename(List expectedFilenames, Submission s assertThat(expectedFilenames).contains(Path.of(filename)); } - @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") - @ValueSource(ints = { 0, 1, 2 }) - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testGetStatsForExamAssessmentDashboard(int numberOfCorrectionRounds) throws Exception { - log.debug("testGetStatsForExamAssessmentDashboard: step 1 done"); - doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); - - User examTutor1 = userRepo.findOneByLogin(TEST_PREFIX + "tutor1").orElseThrow(); - User examTutor2 = userRepo.findOneByLogin(TEST_PREFIX + "tutor2").orElseThrow(); - - var examVisibleDate = now().minusMinutes(5); - var examStartDate = now().plusMinutes(5); - var examEndDate = now().plusMinutes(20); - Course course = courseUtilService.addEmptyCourse(); - Exam exam = examUtilService.addExam(course, examVisibleDate, examStartDate, examEndDate); - exam.setNumberOfCorrectionRoundsInExam(numberOfCorrectionRounds); - exam = examRepository.save(exam); - exam = examUtilService.addExerciseGroupsAndExercisesToExam(exam, false); - - log.debug("testGetStatsForExamAssessmentDashboard: step 2 done"); - - var stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class); - assertThat(stats.getNumberOfSubmissions()).isInstanceOf(DueDateStat.class); - assertThat(stats.getTutorLeaderboardEntries()).isInstanceOf(List.class); - if (numberOfCorrectionRounds != 0) { - assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()).isInstanceOf(DueDateStat[].class); - assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isZero(); - } - else { - assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()).isNull(); - } - assertThat(stats.getNumberOfAssessmentLocks()).isZero(); - assertThat(stats.getNumberOfSubmissions().inTime()).isZero(); - if (numberOfCorrectionRounds > 0) { - assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isZero(); - } - else { - assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()).isNull(); - } - assertThat(stats.getTotalNumberOfAssessmentLocks()).isZero(); - - if (numberOfCorrectionRounds == 0) { - // We do not need any more assertions, as numberOfCorrectionRounds is only 0 for test exams (no manual assessment) - return; - } - - var lockedSubmissions = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/lockedSubmissions", HttpStatus.OK, List.class); - assertThat(lockedSubmissions).isEmpty(); - - log.debug("testGetStatsForExamAssessmentDashboard: step 3 done"); - - // register users. Instructors are ignored from scores as they are exclusive for test run exercises - Set registeredStudents = getRegisteredStudentsForExam(); - for (var student : registeredStudents) { - var registeredExamUser = new ExamUser(); - registeredExamUser.setExam(exam); - registeredExamUser.setUser(student); - exam.addExamUser(registeredExamUser); - } - exam.setNumberOfExercisesInExam(exam.getExerciseGroups().size()); - exam.setRandomizeExerciseOrder(false); - exam = examRepository.save(exam); - exam = examRepository.findWithExamUsersAndExerciseGroupsAndExercisesById(exam.getId()).orElseThrow(); - - log.debug("testGetStatsForExamAssessmentDashboard: step 4 done"); - - // generate individual student exams - List studentExams = request.postListWithResponseBody("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/generate-student-exams", Optional.empty(), - StudentExam.class, HttpStatus.OK); - int noGeneratedParticipations = ExamPrepareExercisesTestUtil.prepareExerciseStart(request, exam, course); - verify(gitService, times(getNumberOfProgrammingExercises(exam))).combineAllCommitsOfRepositoryIntoOne(any()); - // set start and submitted date as results are created below - studentExams.forEach(studentExam -> { - studentExam.setStartedAndStartDate(now().minusMinutes(2)); - studentExam.setSubmitted(true); - studentExam.setSubmissionDate(now().minusMinutes(1)); - }); - studentExamRepository.saveAll(studentExams); - - log.debug("testGetStatsForExamAssessmentDashboard: step 5 done"); - - // Fetch the created participations and assign them to the exercises - int participationCounter = 0; - List exercisesInExam = exam.getExerciseGroups().stream().map(ExerciseGroup::getExercises).flatMap(Collection::stream).toList(); - for (var exercise : exercisesInExam) { - List participations = studentParticipationRepository.findByExerciseIdAndTestRunWithEagerLegalSubmissionsResult(exercise.getId(), false); - exercise.setStudentParticipations(new HashSet<>(participations)); - participationCounter += exercise.getStudentParticipations().size(); - } - assertThat(noGeneratedParticipations).isEqualTo(participationCounter); - - log.debug("testGetStatsForExamAssessmentDashboard: step 6 done"); - - // Assign submissions to the participations - for (var exercise : exercisesInExam) { - for (var participation : exercise.getStudentParticipations()) { - assertThat(participation.getSubmissions()).hasSize(1); - Submission submission = participation.getSubmissions().iterator().next(); - submission.submitted(true); - submission.setSubmissionDate(now().minusMinutes(6)); - submissionRepository.save(submission); - } - } - - log.debug("testGetStatsForExamAssessmentDashboard: step 7 done"); - - // check the stats again - check the count of submitted submissions - stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class); - assertThat(stats.getNumberOfAssessmentLocks()).isZero(); - // 85 = (17 users * 5 exercises); quiz submissions are not counted - assertThat(stats.getNumberOfSubmissions().inTime()).isEqualTo(studentExams.size() * 5L); - assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isZero(); - assertThat(stats.getNumberOfComplaints()).isZero(); - assertThat(stats.getTotalNumberOfAssessmentLocks()).isZero(); - - // Score used for all exercise results - Double resultScore = 75.0; - - log.debug("testGetStatsForExamAssessmentDashboard: step 7 done"); - - // Lock all submissions - for (var exercise : exercisesInExam) { - for (var participation : exercise.getStudentParticipations()) { - Submission submission; - assertThat(participation.getSubmissions()).hasSize(1); - submission = participation.getSubmissions().iterator().next(); - // Create results - var result = new Result().score(resultScore); - if (exercise instanceof QuizExercise) { - result.completionDate(now().minusMinutes(4)); - result.setRated(true); - } - result.setAssessmentType(AssessmentType.SEMI_AUTOMATIC); - result.setParticipation(participation); - result.setAssessor(examTutor1); - result = resultRepository.save(result); - result.setSubmission(submission); - submission.addResult(result); - submissionRepository.save(submission); - } - } - log.debug("testGetStatsForExamAssessmentDashboard: step 8 done"); - - // check the stats again - userUtilService.changeUser(TEST_PREFIX + "tutor1"); - stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class); - - assertThat(stats.getNumberOfAssessmentLocks()).isEqualTo(studentExams.size() * 5L); - // (studentExams.size() users * 5 exercises); quiz submissions are not counted - assertThat(stats.getNumberOfSubmissions().inTime()).isEqualTo(studentExams.size() * 5L); - // the studentExams.size() quiz submissions are already assessed - assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isEqualTo(studentExams.size()); - assertThat(stats.getNumberOfComplaints()).isZero(); - assertThat(stats.getTotalNumberOfAssessmentLocks()).isEqualTo(studentExams.size() * 5L); - - log.debug("testGetStatsForExamAssessmentDashboard: step 9 done"); - - // test the query needed for assessment information - userUtilService.changeUser(TEST_PREFIX + "tutor2"); - exam.getExerciseGroups().forEach(group -> { - var locks = group.getExercises().stream().map( - exercise -> resultRepository.countNumberOfLockedAssessmentsByOtherTutorsForExamExerciseForCorrectionRounds(exercise, numberOfCorrectionRounds, examTutor2)[0] - .inTime()) - .reduce(Long::sum).orElseThrow(); - if (group.getExercises().stream().anyMatch(exercise -> !(exercise instanceof QuizExercise))) - assertThat(locks).isEqualTo(studentExams.size()); - }); - - log.debug("testGetStatsForExamAssessmentDashboard: step 10 done"); - - userUtilService.changeUser(TEST_PREFIX + "instructor1"); - lockedSubmissions = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/lockedSubmissions", HttpStatus.OK, List.class); - assertThat(lockedSubmissions).hasSize(studentExams.size() * 5); - - log.debug("testGetStatsForExamAssessmentDashboard: step 11 done"); - - // Finish assessment of all submissions - for (var exercise : exercisesInExam) { - for (var participation : exercise.getStudentParticipations()) { - Submission submission; - assertThat(participation.getSubmissions()).hasSize(1); - submission = participation.getSubmissions().iterator().next(); - var result = submission.getLatestResult().completionDate(now().minusMinutes(5)); - result.setRated(true); - resultRepository.save(result); - } - } - - log.debug("testGetStatsForExamAssessmentDashboard: step 12 done"); - - // check the stats again - stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class); - assertThat(stats.getNumberOfAssessmentLocks()).isZero(); - // 75 = (15 users * 5 exercises); quiz submissions are not counted - assertThat(stats.getNumberOfSubmissions().inTime()).isEqualTo(studentExams.size() * 5L); - // 75 + the 19 quiz submissions - assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isEqualTo(studentExams.size() * 5L + studentExams.size()); - assertThat(stats.getNumberOfComplaints()).isZero(); - assertThat(stats.getTotalNumberOfAssessmentLocks()).isZero(); - - log.debug("testGetStatsForExamAssessmentDashboard: step 13 done"); - - lockedSubmissions = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/lockedSubmissions", HttpStatus.OK, List.class); - assertThat(lockedSubmissions).isEmpty(); - if (numberOfCorrectionRounds == 2) { - lockAndAssessForSecondCorrection(exam, course, studentExams, exercisesInExam, numberOfCorrectionRounds); - } - - log.debug("testGetStatsForExamAssessmentDashboard: step 14 done"); - } - - private void lockAndAssessForSecondCorrection(Exam exam, Course course, List studentExams, List exercisesInExam, int numberOfCorrectionRounds) - throws Exception { - // Lock all submissions - User examInstructor = userRepo.findOneByLogin(TEST_PREFIX + "instructor1").orElseThrow(); - User examTutor2 = userRepo.findOneByLogin(TEST_PREFIX + "tutor2").orElseThrow(); - - for (var exercise : exercisesInExam) { - for (var participation : exercise.getStudentParticipations()) { - assertThat(participation.getSubmissions()).hasSize(1); - Submission submission = participation.getSubmissions().iterator().next(); - // Create results - var result = new Result().score(50D).rated(true); - if (exercise instanceof QuizExercise) { - result.completionDate(now().minusMinutes(3)); - } - result.setAssessmentType(AssessmentType.SEMI_AUTOMATIC); - result.setParticipation(participation); - result.setAssessor(examInstructor); - result = resultRepository.save(result); - result.setSubmission(submission); - submission.addResult(result); - submissionRepository.save(submission); - } - } - // check the stats again - userUtilService.changeUser(TEST_PREFIX + "instructor1"); - var stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class); - assertThat(stats.getNumberOfAssessmentLocks()).isEqualTo(studentExams.size() * 5L); - // 75 = (15 users * 5 exercises); quiz submissions are not counted - assertThat(stats.getNumberOfSubmissions().inTime()).isEqualTo(studentExams.size() * 5L); - // the 15 quiz submissions are already assessed - and all are assessed in the first correctionRound - assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isEqualTo(studentExams.size() * 6L); - assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[1].inTime()).isEqualTo(studentExams.size()); - assertThat(stats.getNumberOfComplaints()).isZero(); - assertThat(stats.getTotalNumberOfAssessmentLocks()).isEqualTo(studentExams.size() * 5L); - - // test the query needed for assessment information - userUtilService.changeUser(TEST_PREFIX + "tutor2"); - exam.getExerciseGroups().forEach(group -> { - var locksRound1 = group.getExercises().stream().map( - exercise -> resultRepository.countNumberOfLockedAssessmentsByOtherTutorsForExamExerciseForCorrectionRounds(exercise, numberOfCorrectionRounds, examTutor2)[0] - .inTime()) - .reduce(Long::sum).orElseThrow(); - if (group.getExercises().stream().anyMatch(exercise -> !(exercise instanceof QuizExercise))) { - assertThat(locksRound1).isZero(); - } - - var locksRound2 = group.getExercises().stream().map( - exercise -> resultRepository.countNumberOfLockedAssessmentsByOtherTutorsForExamExerciseForCorrectionRounds(exercise, numberOfCorrectionRounds, examTutor2)[1] - .inTime()) - .reduce(Long::sum).orElseThrow(); - if (group.getExercises().stream().anyMatch(exercise -> !(exercise instanceof QuizExercise))) { - assertThat(locksRound2).isEqualTo(studentExams.size()); - } - }); - - userUtilService.changeUser(TEST_PREFIX + "instructor1"); - var lockedSubmissions = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/lockedSubmissions", HttpStatus.OK, List.class); - assertThat(lockedSubmissions).hasSize(studentExams.size() * 5); - - // Finish assessment of all submissions - for (var exercise : exercisesInExam) { - for (var participation : exercise.getStudentParticipations()) { - Submission submission; - assertThat(participation.getSubmissions()).hasSize(1); - submission = participation.getSubmissions().iterator().next(); - var result = submission.getLatestResult().completionDate(now().minusMinutes(5)); - result.setRated(true); - resultRepository.save(result); - } - } - - // check the stats again - stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class); - assertThat(stats.getNumberOfAssessmentLocks()).isZero(); - // 75 = (15 users * 5 exercises); quiz submissions are not counted - assertThat(stats.getNumberOfSubmissions().inTime()).isEqualTo(studentExams.size() * 5L); - // 75 + the 15 quiz submissions - assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isEqualTo(studentExams.size() * 6L); - assertThat(stats.getNumberOfComplaints()).isZero(); - assertThat(stats.getTotalNumberOfAssessmentLocks()).isZero(); - - lockedSubmissions = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/lockedSubmissions", HttpStatus.OK, List.class); - assertThat(lockedSubmissions).isEmpty(); - - } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testGenerateStudentExamsTemplateCombine() throws Exception { - Exam examWithProgramming = examUtilService.addExerciseGroupsAndExercisesToExam(exam1, true); - doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); - - // invoke generate student exams - request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + examWithProgramming.getId() + "/generate-student-exams", Optional.empty(), - StudentExam.class, HttpStatus.OK); - - verify(gitService, never()).combineAllCommitsOfRepositoryIntoOne(any()); - - // invoke prepare exercise start - prepareExerciseStart(exam1); - - verify(gitService, times(getNumberOfProgrammingExercises(exam1))).combineAllCommitsOfRepositoryIntoOne(any()); - } - @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetExamTitleAsInstructor() throws Exception { @@ -2990,79 +1141,6 @@ void testGetExamTitleForNonExistingExam() throws Exception { request.get("/api/exams/123124123123/title", HttpStatus.NOT_FOUND, String.class); } - // ExamRegistration Service - checkRegistrationOrRegisterStudentToTestExam - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testCheckRegistrationOrRegisterStudentToTestExam_noTestExam() { - assertThatThrownBy( - () -> examRegistrationService.checkRegistrationOrRegisterStudentToTestExam(course1, exam1.getId(), userUtilService.getUserByLogin(TEST_PREFIX + "student1"))) - .isInstanceOf(BadRequestAlertException.class); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "student42", roles = "USER") - void testCheckRegistrationOrRegisterStudentToTestExam_studentNotPartOfCourse() { - assertThatThrownBy( - () -> examRegistrationService.checkRegistrationOrRegisterStudentToTestExam(course1, exam1.getId(), userUtilService.getUserByLogin(TEST_PREFIX + "student42"))) - .isInstanceOf(BadRequestAlertException.class); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testCheckRegistrationOrRegisterStudentToTestExam_successfulRegistration() { - Exam testExam = ExamFactory.generateTestExam(course1); - testExam = examRepository.save(testExam); - var examUser = new ExamUser(); - examUser.setExam(testExam); - examUser.setUser(student1); - examUser = examUserRepository.save(examUser); - testExam.addExamUser(examUser); - testExam = examRepository.save(testExam); - examRegistrationService.checkRegistrationOrRegisterStudentToTestExam(course1, testExam.getId(), student1); - Exam testExamReloaded = examRepository.findByIdWithExamUsersElseThrow(testExam.getId()); - assertThat(testExamReloaded.getExamUsers()).contains(examUser); - } - - // ExamResource - getStudentExamForTestExamForStart - @Test - @WithMockUser(username = TEST_PREFIX + "student42", roles = "USER") - void testGetStudentExamForTestExamForStart_notRegisteredInCourse() throws Exception { - request.get("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/start", HttpStatus.FORBIDDEN, String.class); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testGetStudentExamForTestExamForStart_notVisible() throws Exception { - testExam1.setVisibleDate(now().plusMinutes(60)); - testExam1 = examRepository.save(testExam1); - - request.get("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/start", HttpStatus.FORBIDDEN, StudentExam.class); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testGetStudentExamForTestExamForStart_ExamDoesNotBelongToCourse() throws Exception { - Exam testExam = examUtilService.addTestExam(course2); - - request.get("/api/courses/" + course1.getId() + "/exams/" + testExam.getId() + "/start", HttpStatus.CONFLICT, StudentExam.class); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testGetStudentExamForTestExamForStart_fetchExam_successful() throws Exception { - var testExam = examUtilService.addTestExam(course2); - testExam = examRepository.save(testExam); - var examUser = new ExamUser(); - examUser.setExam(testExam); - examUser.setUser(student1); - examUser = examUserRepository.save(examUser); - testExam.addExamUser(examUser); - examRepository.save(testExam); - var studentExam5 = examUtilService.addStudentExamForTestExam(testExam, student1); - StudentExam studentExamReceived = request.get("/api/courses/" + course2.getId() + "/exams/" + testExam.getId() + "/start", HttpStatus.OK, StudentExam.class); - assertThat(studentExamReceived).isEqualTo(studentExam5); - } - @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetExamForImportWithExercises_successful() throws Exception { @@ -3257,23 +1335,6 @@ void testImportExamWithExercises_correctionRoundConflict() throws Exception { request.postWithoutLocation("/api/courses/" + course1.getId() + "/exam-import", examC, HttpStatus.BAD_REQUEST, null); } - @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") - @CsvSource({ "A,A,B,C", "A,B,C,C", "A,A,B,B" }) - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testImportExamWithExercises_programmingExerciseSameShortNameOrTitle(String shortName1, String shortName2, String title1, String title2) throws Exception { - Exam exam = ExamFactory.generateExamWithExerciseGroup(course1, true); - ExerciseGroup exerciseGroup = exam.getExerciseGroups().get(0); - ProgrammingExercise exercise1 = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(exerciseGroup); - ProgrammingExercise exercise2 = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(exerciseGroup); - - exercise1.setShortName(shortName1); - exercise2.setShortName(shortName2); - exercise1.setTitle(title1); - exercise2.setTitle(title2); - - request.postWithoutLocation("/api/courses/" + course1.getId() + "/exam-import", exam, HttpStatus.BAD_REQUEST, null); - } - @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testImportExamWithExercises_successfulWithoutExercises() throws Exception { @@ -3340,25 +1401,6 @@ void testImportExamWithExercises_successfulWithImportToOtherCourse() throws Exce } } - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testImportExamWithExercises_preCheckFailed() throws Exception { - Exam exam = ExamFactory.generateExam(course1); - ExerciseGroup programmingGroup = ExamFactory.generateExerciseGroup(false, exam); - exam = examRepository.save(exam); - exam.setId(null); - ProgrammingExercise programming = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(programmingGroup, ProgrammingLanguage.JAVA); - programmingGroup.addExercise(programming); - exerciseRepo.save(programming); - - doReturn(true).when(versionControlService).checkIfProjectExists(any(), any()); - doReturn(null).when(continuousIntegrationService).checkIfProjectExists(any(), any()); - - request.getMvc().perform(post("/api/courses/" + course1.getId() + "/exam-import").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(exam))) - .andExpect(status().isBadRequest()) - .andExpect(result -> assertThat(result.getResolvedException()).hasMessage("Exam contains programming exercise(s) with invalid short name.")); - } - @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testGetExercisesWithPotentialPlagiarismAsTutor_forbidden() throws Exception { @@ -3434,40 +1476,6 @@ void testGetSuspiciousSessionsAsInstructor() throws Exception { var suspiciousSessions = suspiciousSessionTuples.stream().findFirst().get(); assertThat(suspiciousSessions.examSessions()).hasSize(2); assertThat(suspiciousSessions.examSessions()).usingRecursiveFieldByFieldElementComparatorIgnoringFields("createdDate") - .containsExactlyInAnyOrderElementsOf(createExpectedDTOs(firstExamSessionStudent1, secondExamSessionStudent1)); - } - - private Set createExpectedDTOs(ExamSession session1, ExamSession session2) { - var expectedDTOs = new HashSet(); - var firstStudentExamDTO = new StudentExamWithIdAndExamAndUserDTO(session1.getStudentExam().getId(), - new ExamWithIdAndCourseDTO(session1.getStudentExam().getExam().getId(), new CourseWithIdDTO(session1.getStudentExam().getExam().getCourse().getId())), - new UserWithIdAndLoginDTO(session1.getStudentExam().getUser().getId(), session1.getStudentExam().getUser().getLogin())); - var secondStudentExamDTO = new StudentExamWithIdAndExamAndUserDTO(session2.getStudentExam().getId(), - new ExamWithIdAndCourseDTO(session2.getStudentExam().getExam().getId(), new CourseWithIdDTO(session2.getStudentExam().getExam().getCourse().getId())), - new UserWithIdAndLoginDTO(session2.getStudentExam().getUser().getId(), session2.getStudentExam().getUser().getLogin())); - var firstExamSessionDTO = new ExamSessionDTO(session1.getId(), session1.getBrowserFingerprintHash(), session1.getIpAddress(), session1.getSuspiciousReasons(), - session1.getCreatedDate(), firstStudentExamDTO); - var secondExamSessionDTO = new ExamSessionDTO(session2.getId(), session2.getBrowserFingerprintHash(), session2.getIpAddress(), session2.getSuspiciousReasons(), - session2.getCreatedDate(), secondStudentExamDTO); - expectedDTOs.add(firstExamSessionDTO); - expectedDTOs.add(secondExamSessionDTO); - return expectedDTOs; - - } - - private int prepareExerciseStart(Exam exam) throws Exception { - return ExamPrepareExercisesTestUtil.prepareExerciseStart(request, exam, course1); - } - - private Set getRegisteredStudentsForExam() { - var registeredStudents = new HashSet(); - for (int i = 1; i <= NUMBER_OF_STUDENTS; i++) { - registeredStudents.add(userUtilService.getUserByLogin(TEST_PREFIX + "student" + i)); - } - for (int i = 1; i <= NUMBER_OF_TUTORS; i++) { - registeredStudents.add(userUtilService.getUserByLogin(TEST_PREFIX + "tutor" + i)); - } - - return registeredStudents; + .containsExactlyInAnyOrderElementsOf(ExamFactory.createExpectedExamSessionDTOs(firstExamSessionStudent1, secondExamSessionStudent1)); } } diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamParticipationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamParticipationIntegrationTest.java new file mode 100644 index 000000000000..c4df42113fb0 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamParticipationIntegrationTest.java @@ -0,0 +1,1169 @@ +package de.tum.in.www1.artemis.exam; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.*; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.util.LinkedMultiValueMap; + +import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.assessment.GradingScaleUtilService; +import de.tum.in.www1.artemis.bonus.BonusFactory; +import de.tum.in.www1.artemis.course.CourseUtilService; +import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; +import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; +import de.tum.in.www1.artemis.domain.exam.*; +import de.tum.in.www1.artemis.domain.participation.Participation; +import de.tum.in.www1.artemis.domain.participation.StudentParticipation; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismCase; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismVerdict; +import de.tum.in.www1.artemis.domain.quiz.QuizExercise; +import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseTestService; +import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseFactory; +import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.participation.ParticipationUtilService; +import de.tum.in.www1.artemis.repository.*; +import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismCaseRepository; +import de.tum.in.www1.artemis.service.QuizSubmissionService; +import de.tum.in.www1.artemis.service.exam.ExamService; +import de.tum.in.www1.artemis.service.exam.StudentExamService; +import de.tum.in.www1.artemis.service.scheduled.ParticipantScoreScheduleService; +import de.tum.in.www1.artemis.team.TeamUtilService; +import de.tum.in.www1.artemis.user.UserUtilService; +import de.tum.in.www1.artemis.util.ExamPrepareExercisesTestUtil; +import de.tum.in.www1.artemis.util.LocalRepository; +import de.tum.in.www1.artemis.web.rest.dto.*; + +class ExamParticipationIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { + + private static final String TEST_PREFIX = "examparticipationtest"; + + private final Logger log = LoggerFactory.getLogger(getClass()); + + @Autowired + private QuizExerciseRepository quizExerciseRepository; + + @Autowired + private QuizSubmissionRepository quizSubmissionRepository; + + @Autowired + private QuizSubmissionService quizSubmissionService; + + @Autowired + private CourseRepository courseRepo; + + @Autowired + private ExerciseRepository exerciseRepo; + + @Autowired + private UserRepository userRepo; + + @Autowired + private ExamRepository examRepository; + + @Autowired + private ExamUserRepository examUserRepository; + + @Autowired + private ExamService examService; + + @Autowired + private StudentExamService studentExamService; + + @Autowired + private StudentExamRepository studentExamRepository; + + @Autowired + private StudentParticipationRepository studentParticipationRepository; + + @Autowired + private SubmissionRepository submissionRepository; + + @Autowired + private ResultRepository resultRepository; + + @Autowired + private ParticipationTestRepository participationTestRepository; + + @Autowired + private GradingScaleRepository gradingScaleRepository; + + @Autowired + private TeamRepository teamRepository; + + @Autowired + private BonusRepository bonusRepository; + + @Autowired + private PlagiarismCaseRepository plagiarismCaseRepository; + + @Autowired + private ParticipantScoreRepository participantScoreRepository; + + @Autowired + private ProgrammingExerciseTestService programmingExerciseTestService; + + @Autowired + private UserUtilService userUtilService; + + @Autowired + private CourseUtilService courseUtilService; + + @Autowired + private ExamUtilService examUtilService; + + @Autowired + private TextExerciseUtilService textExerciseUtilService; + + @Autowired + private ParticipationUtilService participationUtilService; + + @Autowired + private TeamUtilService teamUtilService; + + @Autowired + private GradingScaleUtilService gradingScaleUtilService; + + private Course course1; + + private static final int NUMBER_OF_STUDENTS = 3; + + private static final int NUMBER_OF_TUTORS = 2; + + private final List studentRepos = new ArrayList<>(); + + private User student1; + + private User instructor; + + @BeforeEach + void initTestCase() { + userUtilService.addUsers(TEST_PREFIX, NUMBER_OF_STUDENTS, NUMBER_OF_TUTORS, 0, 1); + + course1 = courseUtilService.addEmptyCourse(); + student1 = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); + instructor = userUtilService.getUserByLogin(TEST_PREFIX + "instructor1"); + + bitbucketRequestMockProvider.enableMockingOfRequests(); + + ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 200; + participantScoreScheduleService.activate(); + } + + @AfterEach + void tearDown() throws Exception { + bitbucketRequestMockProvider.reset(); + bambooRequestMockProvider.reset(); + if (programmingExerciseTestService.exerciseRepo != null) { + programmingExerciseTestService.tearDown(); + } + + for (var repo : studentRepos) { + repo.resetLocalRepo(); + } + + ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 500; + participantScoreScheduleService.shutdown(); + } + + @Test + @WithMockUser(username = "admin", roles = "ADMIN") + void testRemovingAllStudents_AfterParticipatingInExam() throws Exception { + doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); + Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, 3); + + // Generate student exams + List studentExams = request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/generate-student-exams", + Optional.empty(), StudentExam.class, HttpStatus.OK); + assertThat(studentExams).hasSize(3); + assertThat(exam.getExamUsers()).hasSize(3); + + int numberOfGeneratedParticipations = ExamPrepareExercisesTestUtil.prepareExerciseStart(request, exam, course1); + assertThat(numberOfGeneratedParticipations).isEqualTo(12); + + verify(gitService, times(examUtilService.getNumberOfProgrammingExercises(exam.getId()))).combineAllCommitsOfRepositoryIntoOne(any()); + // Fetch student exams + List studentExamsDB = request.getList("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams", HttpStatus.OK, StudentExam.class); + assertThat(studentExamsDB).hasSize(3); + List participationList = new ArrayList<>(); + Exercise[] exercises = examRepository.findAllExercisesByExamId(exam.getId()).toArray(new Exercise[0]); + for (Exercise value : exercises) { + participationList.addAll(studentParticipationRepository.findByExerciseId(value.getId())); + } + assertThat(participationList).hasSize(12); + + // TODO there should be some participation but no submissions unfortunately + // remove all students + request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/students", HttpStatus.OK); + + // Get the exam with all registered users + var params = new LinkedMultiValueMap(); + params.add("withStudents", "true"); + Exam storedExam = request.get("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK, Exam.class, params); + assertThat(storedExam.getExamUsers()).isEmpty(); + + // Fetch student exams + studentExamsDB = request.getList("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams", HttpStatus.OK, StudentExam.class); + assertThat(studentExamsDB).isEmpty(); + + // Fetch participations + exercises = examRepository.findAllExercisesByExamId(exam.getId()).toArray(new Exercise[0]); + participationList = new ArrayList<>(); + for (Exercise exercise : exercises) { + participationList.addAll(studentParticipationRepository.findByExerciseId(exercise.getId())); + } + assertThat(participationList).hasSize(12); + } + + @Test + @WithMockUser(username = "admin", roles = "ADMIN") + void testRemovingAllStudentsAndParticipations() throws Exception { + doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); + Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, 3); + + // Generate student exams + List studentExams = request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/generate-student-exams", + Optional.empty(), StudentExam.class, HttpStatus.OK); + assertThat(studentExams).hasSize(3); + assertThat(exam.getExamUsers()).hasSize(3); + + int numberOfGeneratedParticipations = ExamPrepareExercisesTestUtil.prepareExerciseStart(request, exam, course1); + verify(gitService, times(examUtilService.getNumberOfProgrammingExercises(exam.getId()))).combineAllCommitsOfRepositoryIntoOne(any()); + assertThat(numberOfGeneratedParticipations).isEqualTo(12); + // Fetch student exams + List studentExamsDB = request.getList("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams", HttpStatus.OK, StudentExam.class); + assertThat(studentExamsDB).hasSize(3); + List participationList = new ArrayList<>(); + Exercise[] exercises = examRepository.findAllExercisesByExamId(exam.getId()).toArray(new Exercise[0]); + for (Exercise value : exercises) { + participationList.addAll(studentParticipationRepository.findByExerciseId(value.getId())); + } + assertThat(participationList).hasSize(12); + + // TODO there should be some participation but no submissions unfortunately + // remove all students + var paramsParticipations = new LinkedMultiValueMap(); + paramsParticipations.add("withParticipationsAndSubmission", "true"); + request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/students", HttpStatus.OK, paramsParticipations); + + // Get the exam with all registered users + var params = new LinkedMultiValueMap(); + params.add("withStudents", "true"); + Exam storedExam = request.get("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK, Exam.class, params); + assertThat(storedExam.getExamUsers()).isEmpty(); + + // Fetch student exams + studentExamsDB = request.getList("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams", HttpStatus.OK, StudentExam.class); + assertThat(studentExamsDB).isEmpty(); + + // Fetch participations + exercises = examRepository.findAllExercisesByExamId(exam.getId()).toArray(new Exercise[0]); + participationList = new ArrayList<>(); + for (Exercise exercise : exercises) { + participationList.addAll(studentParticipationRepository.findByExerciseId(exercise.getId())); + } + assertThat(participationList).isEmpty(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testDeleteStudent_AfterParticipatingInExam() throws Exception { + doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); + // Create an exam with registered students + Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, 3); + var student2 = userUtilService.getUserByLogin(TEST_PREFIX + "student2"); + + // Remove student1 from the exam + request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/students/" + TEST_PREFIX + "student1", HttpStatus.OK); + + // Get the exam with all registered users + var params = new LinkedMultiValueMap(); + params.add("withStudents", "true"); + Exam storedExam = request.get("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK, Exam.class, params); + + // Ensure that student1 was removed from the exam + var examUser = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student1.getId()); + assertThat(examUser).isEmpty(); + assertThat(storedExam.getExamUsers()).hasSize(2); + + // Create individual student exams + List generatedStudentExams = request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/generate-student-exams", + Optional.empty(), StudentExam.class, HttpStatus.OK); + assertThat(generatedStudentExams).hasSize(storedExam.getExamUsers().size()); + + // Start the exam to create participations + ExamPrepareExercisesTestUtil.prepareExerciseStart(request, exam, course1); + + verify(gitService, times(examUtilService.getNumberOfProgrammingExercises(exam.getId()))).combineAllCommitsOfRepositoryIntoOne(any()); + // Get the student exam of student2 + Optional optionalStudent1Exam = generatedStudentExams.stream().filter(studentExam -> studentExam.getUser().equals(student2)).findFirst(); + assertThat(optionalStudent1Exam.orElseThrow()).isNotNull(); + var studentExam2 = optionalStudent1Exam.get(); + + // explicitly set the user again to prevent issues in the following server call due to the use of SecurityUtils.setAuthorizationObject(); + userUtilService.changeUser(TEST_PREFIX + "instructor1"); + // Remove student2 from the exam + request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/students/" + TEST_PREFIX + "student2", HttpStatus.OK); + + // Get the exam with all registered users + params = new LinkedMultiValueMap<>(); + params.add("withStudents", "true"); + storedExam = request.get("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK, Exam.class, params); + + // Ensure that student2 was removed from the exam + var examUser2 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student2.getId()); + assertThat(examUser2).isEmpty(); + assertThat(storedExam.getExamUsers()).hasSize(1); + + // Ensure that the student exam of student2 was deleted + List studentExams = request.getList("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams", HttpStatus.OK, StudentExam.class); + assertThat(studentExams).hasSameSizeAs(storedExam.getExamUsers()).doesNotContain(studentExam2); + + // Ensure that the participations were not deleted + List participationsStudent2 = studentParticipationRepository + .findByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(student2.getId(), studentExam2.getExercises()); + assertThat(participationsStudent2).hasSize(studentExam2.getExercises().size()); + + // Make sure delete also works if so many objects have been created before + request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGenerateStudentExamsCleanupOldParticipations() throws Exception { + Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, NUMBER_OF_STUDENTS); + + request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/generate-student-exams", Optional.empty(), StudentExam.class, + HttpStatus.OK); + + List studentParticipations = participationTestRepository.findByExercise_ExerciseGroup_Exam_Id(exam.getId()); + assertThat(studentParticipations).isEmpty(); + + // invoke start exercises + studentExamService.startExercises(exam.getId()).join(); + + studentParticipations = participationTestRepository.findByExercise_ExerciseGroup_Exam_Id(exam.getId()); + assertThat(studentParticipations).hasSize(12); + + request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/generate-student-exams", Optional.empty(), StudentExam.class, + HttpStatus.OK); + + studentParticipations = participationTestRepository.findByExercise_ExerciseGroup_Exam_Id(exam.getId()); + assertThat(studentParticipations).isEmpty(); + + // invoke start exercises + studentExamService.startExercises(exam.getId()).join(); + + studentParticipations = participationTestRepository.findByExercise_ExerciseGroup_Exam_Id(exam.getId()); + assertThat(studentParticipations).hasSize(12); + + // Make sure delete also works if so many objects have been created before + request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testDeleteStudentWithParticipationsAndSubmissions() throws Exception { + doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); + // Create an exam with registered students + Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, 3); + + // Create individual student exams + List generatedStudentExams = request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/generate-student-exams", + Optional.empty(), StudentExam.class, HttpStatus.OK); + + // Get the student exam of student1 + Optional optionalStudent1Exam = generatedStudentExams.stream().filter(studentExam -> studentExam.getUser().equals(student1)).findFirst(); + assertThat(optionalStudent1Exam.orElseThrow()).isNotNull(); + var studentExam1 = optionalStudent1Exam.get(); + + // Start the exam to create participations + ExamPrepareExercisesTestUtil.prepareExerciseStart(request, exam, course1); + verify(gitService, times(examUtilService.getNumberOfProgrammingExercises(exam.getId()))).combineAllCommitsOfRepositoryIntoOne(any()); + List participationsStudent1 = studentParticipationRepository + .findByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(student1.getId(), studentExam1.getExercises()); + assertThat(participationsStudent1).hasSize(studentExam1.getExercises().size()); + + // explicitly set the user again to prevent issues in the following server call due to the use of SecurityUtils.setAuthorizationObject(); + userUtilService.changeUser(TEST_PREFIX + "instructor1"); + + // Remove student1 from the exam and his participations + var params = new LinkedMultiValueMap(); + params.add("withParticipationsAndSubmission", "true"); + request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/students/" + TEST_PREFIX + "student1", HttpStatus.OK, params); + + // Get the exam with all registered users + params = new LinkedMultiValueMap<>(); + params.add("withStudents", "true"); + Exam storedExam = request.get("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK, Exam.class, params); + + // Ensure that student1 was removed from the exam + var examUser1 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student1.getId()); + assertThat(examUser1).isEmpty(); + assertThat(storedExam.getExamUsers()).hasSize(2); + + // Ensure that the student exam of student1 was deleted + List studentExams = request.getList("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams", HttpStatus.OK, StudentExam.class); + assertThat(studentExams).hasSameSizeAs(storedExam.getExamUsers()).doesNotContain(studentExam1); + + // Ensure that the participations of student1 were deleted + participationsStudent1 = studentParticipationRepository.findByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(student1.getId(), + studentExam1.getExercises()); + assertThat(participationsStudent1).isEmpty(); + + // Make sure delete also works if so many objects have been created before + request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId(), HttpStatus.OK); + } + + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") + @ValueSource(ints = { 0, 1, 2 }) + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetStatsForExamAssessmentDashboard(int numberOfCorrectionRounds) throws Exception { + log.debug("testGetStatsForExamAssessmentDashboard: step 1 done"); + doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); + + User examTutor1 = userRepo.findOneByLogin(TEST_PREFIX + "tutor1").orElseThrow(); + User examTutor2 = userRepo.findOneByLogin(TEST_PREFIX + "tutor2").orElseThrow(); + + var examVisibleDate = ZonedDateTime.now().minusMinutes(5); + var examStartDate = ZonedDateTime.now().plusMinutes(5); + var examEndDate = ZonedDateTime.now().plusMinutes(20); + Course course = courseUtilService.addEmptyCourse(); + Exam exam = examUtilService.addExam(course, examVisibleDate, examStartDate, examEndDate); + exam.setNumberOfCorrectionRoundsInExam(numberOfCorrectionRounds); + exam = examRepository.save(exam); + exam = examUtilService.addExerciseGroupsAndExercisesToExam(exam, false); + + log.debug("testGetStatsForExamAssessmentDashboard: step 2 done"); + + var stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class); + assertThat(stats.getNumberOfSubmissions()).isInstanceOf(DueDateStat.class); + assertThat(stats.getTutorLeaderboardEntries()).isInstanceOf(List.class); + if (numberOfCorrectionRounds != 0) { + assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()).isInstanceOf(DueDateStat[].class); + assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isZero(); + } + else { + assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()).isNull(); + } + assertThat(stats.getNumberOfAssessmentLocks()).isZero(); + assertThat(stats.getNumberOfSubmissions().inTime()).isZero(); + if (numberOfCorrectionRounds > 0) { + assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isZero(); + } + else { + assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()).isNull(); + } + assertThat(stats.getTotalNumberOfAssessmentLocks()).isZero(); + + if (numberOfCorrectionRounds == 0) { + // We do not need any more assertions, as numberOfCorrectionRounds is only 0 for test exams (no manual assessment) + return; + } + + var lockedSubmissions = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/lockedSubmissions", HttpStatus.OK, List.class); + assertThat(lockedSubmissions).isEmpty(); + + log.debug("testGetStatsForExamAssessmentDashboard: step 3 done"); + + // register users. Instructors are ignored from scores as they are exclusive for test run exercises + Set registeredStudents = getRegisteredStudentsForExam(); + for (var student : registeredStudents) { + var registeredExamUser = new ExamUser(); + registeredExamUser.setExam(exam); + registeredExamUser.setUser(student); + exam.addExamUser(registeredExamUser); + } + exam.setNumberOfExercisesInExam(exam.getExerciseGroups().size()); + exam.setRandomizeExerciseOrder(false); + exam = examRepository.save(exam); + exam = examRepository.findWithExamUsersAndExerciseGroupsAndExercisesById(exam.getId()).orElseThrow(); + + log.debug("testGetStatsForExamAssessmentDashboard: step 4 done"); + + // generate individual student exams + List studentExams = request.postListWithResponseBody("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/generate-student-exams", Optional.empty(), + StudentExam.class, HttpStatus.OK); + int noGeneratedParticipations = ExamPrepareExercisesTestUtil.prepareExerciseStart(request, exam, course); + verify(gitService, times(examUtilService.getNumberOfProgrammingExercises(exam.getId()))).combineAllCommitsOfRepositoryIntoOne(any()); + // set start and submitted date as results are created below + studentExams.forEach(studentExam -> { + studentExam.setStartedAndStartDate(ZonedDateTime.now().minusMinutes(2)); + studentExam.setSubmitted(true); + studentExam.setSubmissionDate(ZonedDateTime.now().minusMinutes(1)); + }); + studentExamRepository.saveAll(studentExams); + + log.debug("testGetStatsForExamAssessmentDashboard: step 5 done"); + + // Fetch the created participations and assign them to the exercises + int participationCounter = 0; + List exercisesInExam = exam.getExerciseGroups().stream().map(ExerciseGroup::getExercises).flatMap(Collection::stream).toList(); + for (var exercise : exercisesInExam) { + List participations = studentParticipationRepository.findByExerciseIdAndTestRunWithEagerLegalSubmissionsResult(exercise.getId(), false); + exercise.setStudentParticipations(new HashSet<>(participations)); + participationCounter += exercise.getStudentParticipations().size(); + } + assertThat(noGeneratedParticipations).isEqualTo(participationCounter); + + log.debug("testGetStatsForExamAssessmentDashboard: step 6 done"); + + // Assign submissions to the participations + for (var exercise : exercisesInExam) { + for (var participation : exercise.getStudentParticipations()) { + assertThat(participation.getSubmissions()).hasSize(1); + Submission submission = participation.getSubmissions().iterator().next(); + submission.submitted(true); + submission.setSubmissionDate(ZonedDateTime.now().minusMinutes(6)); + submissionRepository.save(submission); + } + } + + log.debug("testGetStatsForExamAssessmentDashboard: step 7 done"); + + // check the stats again - check the count of submitted submissions + stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class); + assertThat(stats.getNumberOfAssessmentLocks()).isZero(); + // 85 = (17 users * 5 exercises); quiz submissions are not counted + assertThat(stats.getNumberOfSubmissions().inTime()).isEqualTo(studentExams.size() * 5L); + assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isZero(); + assertThat(stats.getNumberOfComplaints()).isZero(); + assertThat(stats.getTotalNumberOfAssessmentLocks()).isZero(); + + // Score used for all exercise results + Double resultScore = 75.0; + + log.debug("testGetStatsForExamAssessmentDashboard: step 7 done"); + + // Lock all submissions + for (var exercise : exercisesInExam) { + for (var participation : exercise.getStudentParticipations()) { + Submission submission; + assertThat(participation.getSubmissions()).hasSize(1); + submission = participation.getSubmissions().iterator().next(); + // Create results + var result = new Result().score(resultScore); + if (exercise instanceof QuizExercise) { + result.completionDate(ZonedDateTime.now().minusMinutes(4)); + result.setRated(true); + } + result.setAssessmentType(AssessmentType.SEMI_AUTOMATIC); + result.setParticipation(participation); + result.setAssessor(examTutor1); + result = resultRepository.save(result); + result.setSubmission(submission); + submission.addResult(result); + submissionRepository.save(submission); + } + } + log.debug("testGetStatsForExamAssessmentDashboard: step 8 done"); + + // check the stats again + userUtilService.changeUser(TEST_PREFIX + "tutor1"); + stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class); + + assertThat(stats.getNumberOfAssessmentLocks()).isEqualTo(studentExams.size() * 5L); + // (studentExams.size() users * 5 exercises); quiz submissions are not counted + assertThat(stats.getNumberOfSubmissions().inTime()).isEqualTo(studentExams.size() * 5L); + // the studentExams.size() quiz submissions are already assessed + assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isEqualTo(studentExams.size()); + assertThat(stats.getNumberOfComplaints()).isZero(); + assertThat(stats.getTotalNumberOfAssessmentLocks()).isEqualTo(studentExams.size() * 5L); + + log.debug("testGetStatsForExamAssessmentDashboard: step 9 done"); + + // test the query needed for assessment information + userUtilService.changeUser(TEST_PREFIX + "tutor2"); + exam.getExerciseGroups().forEach(group -> { + var locks = group.getExercises().stream().map( + exercise -> resultRepository.countNumberOfLockedAssessmentsByOtherTutorsForExamExerciseForCorrectionRounds(exercise, numberOfCorrectionRounds, examTutor2)[0] + .inTime()) + .reduce(Long::sum).orElseThrow(); + if (group.getExercises().stream().anyMatch(exercise -> !(exercise instanceof QuizExercise))) { + assertThat(locks).isEqualTo(studentExams.size()); + } + }); + + log.debug("testGetStatsForExamAssessmentDashboard: step 10 done"); + + userUtilService.changeUser(TEST_PREFIX + "instructor1"); + lockedSubmissions = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/lockedSubmissions", HttpStatus.OK, List.class); + assertThat(lockedSubmissions).hasSize(studentExams.size() * 5); + + log.debug("testGetStatsForExamAssessmentDashboard: step 11 done"); + + // Finish assessment of all submissions + for (var exercise : exercisesInExam) { + for (var participation : exercise.getStudentParticipations()) { + Submission submission; + assertThat(participation.getSubmissions()).hasSize(1); + submission = participation.getSubmissions().iterator().next(); + var result = submission.getLatestResult().completionDate(ZonedDateTime.now().minusMinutes(5)); + result.setRated(true); + resultRepository.save(result); + } + } + + log.debug("testGetStatsForExamAssessmentDashboard: step 12 done"); + + // check the stats again + stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class); + assertThat(stats.getNumberOfAssessmentLocks()).isZero(); + // 75 = (15 users * 5 exercises); quiz submissions are not counted + assertThat(stats.getNumberOfSubmissions().inTime()).isEqualTo(studentExams.size() * 5L); + // 75 + the 19 quiz submissions + assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isEqualTo(studentExams.size() * 5L + studentExams.size()); + assertThat(stats.getNumberOfComplaints()).isZero(); + assertThat(stats.getTotalNumberOfAssessmentLocks()).isZero(); + + log.debug("testGetStatsForExamAssessmentDashboard: step 13 done"); + + lockedSubmissions = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/lockedSubmissions", HttpStatus.OK, List.class); + assertThat(lockedSubmissions).isEmpty(); + if (numberOfCorrectionRounds == 2) { + lockAndAssessForSecondCorrection(exam, course, studentExams, exercisesInExam, numberOfCorrectionRounds); + } + + log.debug("testGetStatsForExamAssessmentDashboard: step 14 done"); + } + + private void lockAndAssessForSecondCorrection(Exam exam, Course course, List studentExams, List exercisesInExam, int numberOfCorrectionRounds) + throws Exception { + // Lock all submissions + User examInstructor = userRepo.findOneByLogin(TEST_PREFIX + "instructor1").orElseThrow(); + User examTutor2 = userRepo.findOneByLogin(TEST_PREFIX + "tutor2").orElseThrow(); + + for (var exercise : exercisesInExam) { + for (var participation : exercise.getStudentParticipations()) { + assertThat(participation.getSubmissions()).hasSize(1); + Submission submission = participation.getSubmissions().iterator().next(); + // Create results + var result = new Result().score(50D).rated(true); + if (exercise instanceof QuizExercise) { + result.completionDate(ZonedDateTime.now().minusMinutes(3)); + } + result.setAssessmentType(AssessmentType.SEMI_AUTOMATIC); + result.setParticipation(participation); + result.setAssessor(examInstructor); + result = resultRepository.save(result); + result.setSubmission(submission); + submission.addResult(result); + submissionRepository.save(submission); + } + } + // check the stats again + userUtilService.changeUser(TEST_PREFIX + "instructor1"); + var stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class); + assertThat(stats.getNumberOfAssessmentLocks()).isEqualTo(studentExams.size() * 5L); + // 75 = (15 users * 5 exercises); quiz submissions are not counted + assertThat(stats.getNumberOfSubmissions().inTime()).isEqualTo(studentExams.size() * 5L); + // the 15 quiz submissions are already assessed - and all are assessed in the first correctionRound + assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isEqualTo(studentExams.size() * 6L); + assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[1].inTime()).isEqualTo(studentExams.size()); + assertThat(stats.getNumberOfComplaints()).isZero(); + assertThat(stats.getTotalNumberOfAssessmentLocks()).isEqualTo(studentExams.size() * 5L); + + // test the query needed for assessment information + userUtilService.changeUser(TEST_PREFIX + "tutor2"); + exam.getExerciseGroups().forEach(group -> { + var locksRound1 = group.getExercises().stream().map( + exercise -> resultRepository.countNumberOfLockedAssessmentsByOtherTutorsForExamExerciseForCorrectionRounds(exercise, numberOfCorrectionRounds, examTutor2)[0] + .inTime()) + .reduce(Long::sum).orElseThrow(); + if (group.getExercises().stream().anyMatch(exercise -> !(exercise instanceof QuizExercise))) { + assertThat(locksRound1).isZero(); + } + + var locksRound2 = group.getExercises().stream().map( + exercise -> resultRepository.countNumberOfLockedAssessmentsByOtherTutorsForExamExerciseForCorrectionRounds(exercise, numberOfCorrectionRounds, examTutor2)[1] + .inTime()) + .reduce(Long::sum).orElseThrow(); + if (group.getExercises().stream().anyMatch(exercise -> !(exercise instanceof QuizExercise))) { + assertThat(locksRound2).isEqualTo(studentExams.size()); + } + }); + + userUtilService.changeUser(TEST_PREFIX + "instructor1"); + var lockedSubmissions = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/lockedSubmissions", HttpStatus.OK, List.class); + assertThat(lockedSubmissions).hasSize(studentExams.size() * 5); + + // Finish assessment of all submissions + for (var exercise : exercisesInExam) { + for (var participation : exercise.getStudentParticipations()) { + Submission submission; + assertThat(participation.getSubmissions()).hasSize(1); + submission = participation.getSubmissions().iterator().next(); + var result = submission.getLatestResult().completionDate(ZonedDateTime.now().minusMinutes(5)); + result.setRated(true); + resultRepository.save(result); + } + } + + // check the stats again + stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class); + assertThat(stats.getNumberOfAssessmentLocks()).isZero(); + // 75 = (15 users * 5 exercises); quiz submissions are not counted + assertThat(stats.getNumberOfSubmissions().inTime()).isEqualTo(studentExams.size() * 5L); + // 75 + the 15 quiz submissions + assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isEqualTo(studentExams.size() * 6L); + assertThat(stats.getNumberOfComplaints()).isZero(); + assertThat(stats.getTotalNumberOfAssessmentLocks()).isZero(); + + lockedSubmissions = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/lockedSubmissions", HttpStatus.OK, List.class); + assertThat(lockedSubmissions).isEmpty(); + } + + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") + @CsvSource({ "false, false", "true, false", "false, true", "true, true" }) + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetExamScore(boolean withCourseBonus, boolean withSecondCorrectionAndStarted) throws Exception { + programmingExerciseTestService.setup(this, versionControlService, continuousIntegrationService); + bitbucketRequestMockProvider.enableMockingOfRequests(true); + bambooRequestMockProvider.enableMockingOfRequests(true); + + doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); + + var visibleDate = ZonedDateTime.now().minusMinutes(5); + var startDate = ZonedDateTime.now().plusMinutes(5); + var endDate = ZonedDateTime.now().plusMinutes(20); + + // register users. Instructors are ignored from scores as they are exclusive for test run exercises + Set registeredStudents = getRegisteredStudentsForExam(); + + var studentExams = programmingExerciseTestService.prepareStudentExamsForConduction(TEST_PREFIX, visibleDate, startDate, endDate, registeredStudents, studentRepos); + Exam exam = examRepository.findByIdWithExamUsersExerciseGroupsAndExercisesElseThrow(studentExams.get(0).getExam().getId()); + Course course = exam.getCourse(); + + Integer noGeneratedParticipations = registeredStudents.size() * exam.getExerciseGroups().size(); + + verify(gitService, times(examUtilService.getNumberOfProgrammingExercises(exam.getId()))).combineAllCommitsOfRepositoryIntoOne(any()); + // explicitly set the user again to prevent issues in the following server call due to the use of SecurityUtils.setAuthorizationObject(); + userUtilService.changeUser(TEST_PREFIX + "instructor1"); + + // instructor exam checklist checks + ExamChecklistDTO examChecklistDTO = examService.getStatsForChecklist(exam, true); + assertThat(examChecklistDTO).isNotNull(); + assertThat(examChecklistDTO.getNumberOfGeneratedStudentExams()).isEqualTo(exam.getExamUsers().size()); + assertThat(examChecklistDTO.getAllExamExercisesAllStudentsPrepared()).isTrue(); + assertThat(examChecklistDTO.getNumberOfTotalParticipationsForAssessment()).isZero(); + + // check that an adapted version is computed for tutors + userUtilService.changeUser(TEST_PREFIX + "tutor1"); + + examChecklistDTO = examService.getStatsForChecklist(exam, false); + assertThat(examChecklistDTO).isNotNull(); + assertThat(examChecklistDTO.getNumberOfGeneratedStudentExams()).isNull(); + assertThat(examChecklistDTO.getAllExamExercisesAllStudentsPrepared()).isFalse(); + assertThat(examChecklistDTO.getNumberOfTotalParticipationsForAssessment()).isZero(); + + userUtilService.changeUser(TEST_PREFIX + "instructor1"); + + // set start and submitted date as results are created below + studentExams.forEach(studentExam -> { + studentExam.setStartedAndStartDate(ZonedDateTime.now().minusMinutes(2)); + studentExam.setSubmitted(true); + studentExam.setSubmissionDate(ZonedDateTime.now().minusMinutes(1)); + }); + studentExamRepository.saveAll(studentExams); + + // Fetch the created participations and assign them to the exercises + int participationCounter = 0; + List exercisesInExam = exam.getExerciseGroups().stream().map(ExerciseGroup::getExercises).flatMap(Collection::stream).toList(); + for (var exercise : exercisesInExam) { + List participations = studentParticipationRepository.findByExerciseIdAndTestRunWithEagerLegalSubmissionsResult(exercise.getId(), false); + exercise.setStudentParticipations(new HashSet<>(participations)); + participationCounter += exercise.getStudentParticipations().size(); + } + assertThat(noGeneratedParticipations).isEqualTo(participationCounter); + + if (withSecondCorrectionAndStarted) { + exercisesInExam.forEach(exercise -> exercise.setSecondCorrectionEnabled(true)); + exerciseRepo.saveAll(exercisesInExam); + } + + // Scores used for all exercise results + double correctionResultScore = 60D; + double resultScore = 75D; + + // Assign results to participations and submissions + for (var exercise : exercisesInExam) { + for (var participation : exercise.getStudentParticipations()) { + Submission submission; + // Programming exercises don't have a submission yet + if (exercise instanceof ProgrammingExercise) { + assertThat(participation.getSubmissions()).isEmpty(); + submission = new ProgrammingSubmission(); + submission.setParticipation(participation); + submission = submissionRepository.save(submission); + } + else { + // There should only be one submission for text, quiz, modeling and file upload + assertThat(participation.getSubmissions()).hasSize(1); + submission = participation.getSubmissions().iterator().next(); + } + + // make sure to create submitted answers + if (exercise instanceof QuizExercise quizExercise) { + var quizQuestions = quizExerciseRepository.findByIdWithQuestionsElseThrow(exercise.getId()).getQuizQuestions(); + for (var quizQuestion : quizQuestions) { + var submittedAnswer = QuizExerciseFactory.generateSubmittedAnswerFor(quizQuestion, true); + var quizSubmission = quizSubmissionRepository.findWithEagerSubmittedAnswersById(submission.getId()); + quizSubmission.addSubmittedAnswers(submittedAnswer); + quizSubmissionService.saveSubmissionForExamMode(quizExercise, quizSubmission, participation.getStudent().orElseThrow()); + } + } + + // Create results + if (withSecondCorrectionAndStarted) { + var firstResult = new Result().score(correctionResultScore).rated(true).completionDate(ZonedDateTime.now().minusMinutes(5)); + firstResult.setParticipation(participation); + firstResult.setAssessor(instructor); + firstResult = resultRepository.save(firstResult); + firstResult.setSubmission(submission); + submission.addResult(firstResult); + } + + var finalResult = new Result().score(resultScore).rated(true).completionDate(ZonedDateTime.now().minusMinutes(5)); + finalResult.setParticipation(participation); + finalResult.setAssessor(instructor); + finalResult = resultRepository.save(finalResult); + finalResult.setSubmission(submission); + submission.addResult(finalResult); + + submission.submitted(true); + submission.setSubmissionDate(ZonedDateTime.now().minusMinutes(6)); + submissionRepository.save(submission); + } + } + // explicitly set the user again to prevent issues in the following server call due to the use of SecurityUtils.setAuthorizationObject(); + userUtilService.changeUser(TEST_PREFIX + "instructor1"); + final var exerciseWithNoUsers = TextExerciseFactory.generateTextExerciseForExam(exam.getExerciseGroups().get(0)); + exerciseRepo.save(exerciseWithNoUsers); + + GradingScale gradingScale = gradingScaleUtilService.generateGradingScaleWithStickyStep(new double[] { 60, 25, 15, 50 }, + Optional.of(new String[] { "5.0", "3.0", "1.0", "1.0" }), true, 1); + gradingScale.setExam(exam); + gradingScale = gradingScaleRepository.save(gradingScale); + + waitForParticipantScores(); + + if (withCourseBonus) { + configureCourseAsBonusWithIndividualAndTeamResults(course, gradingScale); + } + + await().timeout(Duration.ofMinutes(1)).until(() -> { + for (Exercise exercise : exercisesInExam) { + if (participantScoreRepository.findAllByExercise(exercise).size() != exercise.getStudentParticipations().size()) { + return false; + } + } + return true; + }); + + var examScores = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/scores", HttpStatus.OK, ExamScoresDTO.class); + + // Compare generated results to data in ExamScoresDTO + // Compare top-level DTO properties + assertThat(examScores.maxPoints()).isEqualTo(exam.getExamMaxPoints()); + + assertThat(examScores.hasSecondCorrectionAndStarted()).isEqualTo(withSecondCorrectionAndStarted); + + // For calculation assume that all exercises within an exerciseGroups have the same max points + double calculatedAverageScore = 0.0; + for (var exerciseGroup : exam.getExerciseGroups()) { + var exercise = exerciseGroup.getExercises().stream().findAny().orElseThrow(); + if (exercise.getIncludedInOverallScore().equals(IncludedInOverallScore.NOT_INCLUDED)) { + continue; + } + calculatedAverageScore += Math.round(exercise.getMaxPoints() * resultScore / 100.00 * 10) / 10.0; + } + + assertThat(examScores.averagePointsAchieved()).isEqualTo(calculatedAverageScore); + assertThat(examScores.title()).isEqualTo(exam.getTitle()); + assertThat(examScores.examId()).isEqualTo(exam.getId()); + + // Ensure that all exerciseGroups of the exam are present in the DTO + Set exerciseGroupIdsInDTO = examScores.exerciseGroups().stream().map(ExamScoresDTO.ExerciseGroup::id).collect(Collectors.toSet()); + Set exerciseGroupIdsInExam = exam.getExerciseGroups().stream().map(ExerciseGroup::getId).collect(Collectors.toSet()); + assertThat(exerciseGroupIdsInExam).isEqualTo(exerciseGroupIdsInDTO); + + // Compare exerciseGroups in DTO to exam exerciseGroups + // Tolerated absolute difference for floating-point number comparisons + double epsilon = 0000.1; + for (var exerciseGroupDTO : examScores.exerciseGroups()) { + // Find the original exerciseGroup of the exam using the id in ExerciseGroupId + ExerciseGroup originalExerciseGroup = exam.getExerciseGroups().stream().filter(exerciseGroup -> exerciseGroup.getId().equals(exerciseGroupDTO.id())).findFirst() + .orElseThrow(); + + // Assume that all exercises in a group have the same max score + Double groupMaxScoreFromExam = originalExerciseGroup.getExercises().stream().findAny().orElseThrow().getMaxPoints(); + assertThat(exerciseGroupDTO.maxPoints()).isEqualTo(originalExerciseGroup.getExercises().stream().findAny().orElseThrow().getMaxPoints()); + assertThat(groupMaxScoreFromExam).isEqualTo(exerciseGroupDTO.maxPoints(), withPrecision(epsilon)); + + // epsilon + // Compare exercise information + long noOfExerciseGroupParticipations = 0; + for (var originalExercise : originalExerciseGroup.getExercises()) { + // Find the corresponding ExerciseInfo object + var exerciseDTO = exerciseGroupDTO.containedExercises().stream().filter(exerciseInfo -> exerciseInfo.exerciseId().equals(originalExercise.getId())).findFirst() + .orElseThrow(); + // Check the exercise title + assertThat(originalExercise.getTitle()).isEqualTo(exerciseDTO.title()); + // Check the max points of the exercise + assertThat(originalExercise.getMaxPoints()).isEqualTo(exerciseDTO.maxPoints()); + // Check the number of exercise participants and update the group participant counter + var noOfExerciseParticipations = originalExercise.getStudentParticipations().size(); + noOfExerciseGroupParticipations += noOfExerciseParticipations; + assertThat(Long.valueOf(originalExercise.getStudentParticipations().size())).isEqualTo(exerciseDTO.numberOfParticipants()); + } + assertThat(noOfExerciseGroupParticipations).isEqualTo(exerciseGroupDTO.numberOfParticipants()); + } + + // Ensure that all registered students have a StudentResult + Set studentIdsWithStudentResults = examScores.studentResults().stream().map(ExamScoresDTO.StudentResult::userId).collect(Collectors.toSet()); + Set registeredUsers = exam.getRegisteredUsers(); + Set registeredUsersIds = registeredUsers.stream().map(User::getId).collect(Collectors.toSet()); + assertThat(studentIdsWithStudentResults).isEqualTo(registeredUsersIds); + + // Compare StudentResult with the generated results + for (var studentResult : examScores.studentResults()) { + // Find the original user using the id in StudentResult + User originalUser = userRepo.findByIdElseThrow(studentResult.userId()); + StudentExam studentExamOfUser = studentExams.stream().filter(studentExam -> studentExam.getUser().equals(originalUser)).findFirst().orElseThrow(); + + assertThat(studentResult.name()).isEqualTo(originalUser.getName()); + assertThat(studentResult.email()).isEqualTo(originalUser.getEmail()); + assertThat(studentResult.login()).isEqualTo(originalUser.getLogin()); + assertThat(studentResult.registrationNumber()).isEqualTo(originalUser.getRegistrationNumber()); + + // Calculate overall points achieved + + var calculatedOverallPoints = calculateOverallPoints(resultScore, studentExamOfUser); + + assertThat(studentResult.overallPointsAchieved()).isEqualTo(calculatedOverallPoints, withPrecision(epsilon)); + + double expectedPointsAchievedInFirstCorrection = withSecondCorrectionAndStarted ? calculateOverallPoints(correctionResultScore, studentExamOfUser) : 0.0; + assertThat(studentResult.overallPointsAchievedInFirstCorrection()).isEqualTo(expectedPointsAchievedInFirstCorrection, withPrecision(epsilon)); + + // Calculate overall score achieved + var calculatedOverallScore = calculatedOverallPoints / examScores.maxPoints() * 100; + assertThat(studentResult.overallScoreAchieved()).isEqualTo(calculatedOverallScore, withPrecision(epsilon)); + + assertThat(studentResult.overallGrade()).isNotNull(); + assertThat(studentResult.hasPassed()).isNotNull(); + assertThat(studentResult.mostSeverePlagiarismVerdict()).isNull(); + if (withCourseBonus) { + String studentLogin = studentResult.login(); + assertThat(studentResult.gradeWithBonus().bonusStrategy()).isEqualTo(BonusStrategy.GRADES_CONTINUOUS); + switch (studentLogin) { + case TEST_PREFIX + "student1" -> { + assertThat(studentResult.gradeWithBonus().mostSeverePlagiarismVerdict()).isNull(); + assertThat(studentResult.gradeWithBonus().studentPointsOfBonusSource()).isEqualTo(10.0); + assertThat(studentResult.gradeWithBonus().bonusGrade()).isEqualTo("0.0"); + assertThat(studentResult.gradeWithBonus().finalGrade()).isEqualTo("1.0"); + } + case TEST_PREFIX + "student2" -> { + assertThat(studentResult.gradeWithBonus().mostSeverePlagiarismVerdict()).isEqualTo(PlagiarismVerdict.POINT_DEDUCTION); + assertThat(studentResult.gradeWithBonus().studentPointsOfBonusSource()).isEqualTo(10.5); // 10.5 = 8 + 5 * 50% plagiarism point deduction. + assertThat(studentResult.gradeWithBonus().finalGrade()).isEqualTo("1.0"); + } + case TEST_PREFIX + "student3" -> { + assertThat(studentResult.gradeWithBonus().mostSeverePlagiarismVerdict()).isEqualTo(PlagiarismVerdict.PLAGIARISM); + assertThat(studentResult.gradeWithBonus().studentPointsOfBonusSource()).isZero(); + assertThat(studentResult.gradeWithBonus().bonusGrade()).isEqualTo(GradingScale.DEFAULT_PLAGIARISM_GRADE); + assertThat(studentResult.gradeWithBonus().finalGrade()).isEqualTo("1.0"); + } + default -> { + } + } + } + else { + assertThat(studentResult.gradeWithBonus()).isNull(); + } + + // Ensure that the exercise ids of the student exam are the same as the exercise ids in the students exercise results + Set exerciseIdsOfStudentResult = studentResult.exerciseGroupIdToExerciseResult().values().stream().map(ExamScoresDTO.ExerciseResult::exerciseId) + .collect(Collectors.toSet()); + Set exerciseIdsInStudentExam = studentExamOfUser.getExercises().stream().map(DomainObject::getId).collect(Collectors.toSet()); + assertThat(exerciseIdsOfStudentResult).isEqualTo(exerciseIdsInStudentExam); + for (Map.Entry entry : studentResult.exerciseGroupIdToExerciseResult().entrySet()) { + var exerciseResult = entry.getValue(); + + // Find the original exercise using the id in ExerciseResult + Exercise originalExercise = studentExamOfUser.getExercises().stream().filter(exercise -> exercise.getId().equals(exerciseResult.exerciseId())).findFirst() + .orElseThrow(); + + // Check that the key is associated with the exerciseGroup which actually contains the exercise in the exerciseResult + assertThat(originalExercise.getExerciseGroup().getId()).isEqualTo(entry.getKey()); + + assertThat(exerciseResult.title()).isEqualTo(originalExercise.getTitle()); + assertThat(exerciseResult.maxScore()).isEqualTo(originalExercise.getMaxPoints()); + assertThat(exerciseResult.achievedScore()).isEqualTo(resultScore); + if (originalExercise instanceof QuizExercise) { + assertThat(exerciseResult.hasNonEmptySubmission()).isTrue(); + } + else { + assertThat(exerciseResult.hasNonEmptySubmission()).isFalse(); + } + // TODO: create a test where hasNonEmptySubmission() is false for a quiz + assertThat(exerciseResult.achievedPoints()).isEqualTo(originalExercise.getMaxPoints() * resultScore / 100, withPrecision(epsilon)); + } + } + + // change back to instructor user + userUtilService.changeUser(TEST_PREFIX + "instructor1"); + + var expectedTotalExamAssessmentsFinishedByCorrectionRound = new Long[] { noGeneratedParticipations.longValue(), noGeneratedParticipations.longValue() }; + if (!withSecondCorrectionAndStarted) { + // The second correction has not started in this case. + expectedTotalExamAssessmentsFinishedByCorrectionRound[1] = 0L; + } + + // check if stats are set correctly for the instructor + examChecklistDTO = examService.getStatsForChecklist(exam, true); + assertThat(examChecklistDTO).isNotNull(); + var size = examScores.studentResults().size(); + assertThat(examChecklistDTO.getNumberOfGeneratedStudentExams()).isEqualTo(size); + assertThat(examChecklistDTO.getNumberOfExamsSubmitted()).isEqualTo(size); + assertThat(examChecklistDTO.getNumberOfExamsStarted()).isEqualTo(size); + assertThat(examChecklistDTO.getAllExamExercisesAllStudentsPrepared()).isTrue(); + assertThat(examChecklistDTO.getNumberOfTotalParticipationsForAssessment()).isEqualTo(size * 6L); + assertThat(examChecklistDTO.getNumberOfTestRuns()).isZero(); + assertThat(examChecklistDTO.getNumberOfTotalExamAssessmentsFinishedByCorrectionRound()).hasSize(2).containsExactly(expectedTotalExamAssessmentsFinishedByCorrectionRound); + + // change to a tutor + userUtilService.changeUser(TEST_PREFIX + "tutor1"); + + // check that a modified version is returned + // check if stats are set correctly for the instructor + examChecklistDTO = examService.getStatsForChecklist(exam, false); + assertThat(examChecklistDTO).isNotNull(); + assertThat(examChecklistDTO.getNumberOfGeneratedStudentExams()).isNull(); + assertThat(examChecklistDTO.getNumberOfExamsSubmitted()).isNull(); + assertThat(examChecklistDTO.getNumberOfExamsStarted()).isNull(); + assertThat(examChecklistDTO.getAllExamExercisesAllStudentsPrepared()).isFalse(); + assertThat(examChecklistDTO.getNumberOfTotalParticipationsForAssessment()).isEqualTo(size * 6L); + assertThat(examChecklistDTO.getNumberOfTestRuns()).isNull(); + assertThat(examChecklistDTO.getNumberOfTotalExamAssessmentsFinishedByCorrectionRound()).hasSize(2).containsExactly(expectedTotalExamAssessmentsFinishedByCorrectionRound); + + bambooRequestMockProvider.reset(); + + final ProgrammingExercise programmingExercise = (ProgrammingExercise) exam.getExerciseGroups().get(6).getExercises().iterator().next(); + + var usersOfExam = exam.getRegisteredUsers(); + mockDeleteProgrammingExercise(programmingExercise, usersOfExam); + + await().until(() -> participantScoreScheduleService.isIdle()); + + // change back to instructor user + userUtilService.changeUser(TEST_PREFIX + "instructor1"); + // Make sure delete also works if so many objects have been created before + waitForParticipantScores(); + request.delete("/api/courses/" + course.getId() + "/exams/" + exam.getId(), HttpStatus.OK); + assertThat(examRepository.findById(exam.getId())).isEmpty(); + } + + private void configureCourseAsBonusWithIndividualAndTeamResults(Course course, GradingScale bonusToGradingScale) { + ZonedDateTime pastTimestamp = ZonedDateTime.now().minusDays(5); + TextExercise textExercise = textExerciseUtilService.createIndividualTextExercise(course, pastTimestamp, pastTimestamp, pastTimestamp); + Long individualTextExerciseId = textExercise.getId(); + textExerciseUtilService.createIndividualTextExercise(course, pastTimestamp, pastTimestamp, pastTimestamp); + + Exercise teamExercise = textExerciseUtilService.createTeamTextExercise(course, pastTimestamp, pastTimestamp, pastTimestamp); + User tutor1 = userRepo.findOneByLogin(TEST_PREFIX + "tutor1").orElseThrow(); + Long teamTextExerciseId = teamExercise.getId(); + Long team1Id = teamUtilService.createTeam(Set.of(student1), tutor1, teamExercise, TEST_PREFIX + "team1").getId(); + User student2 = userRepo.findOneByLogin(TEST_PREFIX + "student2").orElseThrow(); + User student3 = userRepo.findOneByLogin(TEST_PREFIX + "student3").orElseThrow(); + User tutor2 = userRepo.findOneByLogin(TEST_PREFIX + "tutor2").orElseThrow(); + Long team2Id = teamUtilService.createTeam(Set.of(student2, student3), tutor2, teamExercise, TEST_PREFIX + "team2").getId(); + + participationUtilService.createParticipationSubmissionAndResult(individualTextExerciseId, student1, 10.0, 10.0, 50, true); + + Team team1 = teamRepository.findById(team1Id).orElseThrow(); + var result = participationUtilService.createParticipationSubmissionAndResult(teamTextExerciseId, team1, 10.0, 10.0, 40, true); + // Creating a second results for team1 to test handling multiple results. + participationUtilService.createSubmissionAndResult((StudentParticipation) result.getParticipation(), 50, true); + + var student2Result = participationUtilService.createParticipationSubmissionAndResult(individualTextExerciseId, student2, 10.0, 10.0, 50, true); + + var student3Result = participationUtilService.createParticipationSubmissionAndResult(individualTextExerciseId, student3, 10.0, 10.0, 30, true); + + Team team2 = teamRepository.findById(team2Id).orElseThrow(); + participationUtilService.createParticipationSubmissionAndResult(teamTextExerciseId, team2, 10.0, 10.0, 80, true); + + // Adding plagiarism cases + var bonusPlagiarismCase = new PlagiarismCase(); + bonusPlagiarismCase.setStudent(student3); + bonusPlagiarismCase.setExercise(student3Result.getParticipation().getExercise()); + bonusPlagiarismCase.setVerdict(PlagiarismVerdict.PLAGIARISM); + plagiarismCaseRepository.save(bonusPlagiarismCase); + + var bonusPlagiarismCase2 = new PlagiarismCase(); + bonusPlagiarismCase2.setStudent(student2); + bonusPlagiarismCase2.setExercise(student2Result.getParticipation().getExercise()); + bonusPlagiarismCase2.setVerdict(PlagiarismVerdict.POINT_DEDUCTION); + bonusPlagiarismCase2.setVerdictPointDeduction(50); + plagiarismCaseRepository.save(bonusPlagiarismCase2); + + BonusStrategy bonusStrategy = BonusStrategy.GRADES_CONTINUOUS; + bonusToGradingScale.setBonusStrategy(bonusStrategy); + gradingScaleRepository.save(bonusToGradingScale); + + GradingScale sourceGradingScale = gradingScaleUtilService.generateGradingScaleWithStickyStep(new double[] { 60, 40, 50 }, Optional.of(new String[] { "0", "0.3", "0.6" }), + true, 1); + sourceGradingScale.setGradeType(GradeType.BONUS); + sourceGradingScale.setCourse(course); + gradingScaleRepository.save(sourceGradingScale); + + var bonus = BonusFactory.generateBonus(bonusStrategy, -1.0, sourceGradingScale.getId(), bonusToGradingScale.getId()); + bonusRepository.save(bonus); + + course.setMaxPoints(100); + course.setPresentationScore(null); + courseRepo.save(course); + + } + + private void waitForParticipantScores() { + participantScoreScheduleService.executeScheduledTasks(); + await().until(() -> participantScoreScheduleService.isIdle()); + } + + private double calculateOverallPoints(Double correctionResultScore, StudentExam studentExamOfUser) { + return studentExamOfUser.getExercises().stream().filter(exercise -> !exercise.getIncludedInOverallScore().equals(IncludedInOverallScore.NOT_INCLUDED)) + .map(Exercise::getMaxPoints).reduce(0.0, (total, maxScore) -> (Math.round((total + maxScore * correctionResultScore / 100) * 10) / 10.0)); + } + + private Set getRegisteredStudentsForExam() { + var registeredStudents = new HashSet(); + for (int i = 1; i <= NUMBER_OF_STUDENTS; i++) { + registeredStudents.add(userUtilService.getUserByLogin(TEST_PREFIX + "student" + i)); + } + for (int i = 1; i <= NUMBER_OF_TUTORS; i++) { + registeredStudents.add(userUtilService.getUserByLogin(TEST_PREFIX + "tutor" + i)); + } + + return registeredStudents; + } +} diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamRegistrationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamRegistrationIntegrationTest.java new file mode 100644 index 000000000000..27293f20ffba --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamRegistrationIntegrationTest.java @@ -0,0 +1,396 @@ +package de.tum.in.www1.artemis.exam; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import java.util.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; + +import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.course.CourseUtilService; +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.exam.Exam; +import de.tum.in.www1.artemis.domain.exam.ExamUser; +import de.tum.in.www1.artemis.domain.metis.conversation.Channel; +import de.tum.in.www1.artemis.repository.ExamRepository; +import de.tum.in.www1.artemis.repository.ExamUserRepository; +import de.tum.in.www1.artemis.repository.UserRepository; +import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository; +import de.tum.in.www1.artemis.service.dto.StudentDTO; +import de.tum.in.www1.artemis.service.exam.ExamAccessService; +import de.tum.in.www1.artemis.service.exam.ExamRegistrationService; +import de.tum.in.www1.artemis.service.ldap.LdapUserDto; +import de.tum.in.www1.artemis.service.scheduled.ParticipantScoreScheduleService; +import de.tum.in.www1.artemis.service.user.PasswordService; +import de.tum.in.www1.artemis.user.UserFactory; +import de.tum.in.www1.artemis.user.UserUtilService; +import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; + +class ExamRegistrationIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { + + private static final String TEST_PREFIX = "examregistrationtest"; + + public static final String STUDENT_111 = TEST_PREFIX + "student111"; + + @Autowired + private UserRepository userRepo; + + @Autowired + private ExamRepository examRepository; + + @Autowired + private ExamUserRepository examUserRepository; + + @Autowired + private ExamRegistrationService examRegistrationService; + + @Autowired + private PasswordService passwordService; + + @Autowired + private ExamAccessService examAccessService; + + @Autowired + private ChannelRepository channelRepository; + + @Autowired + private UserUtilService userUtilService; + + @Autowired + private CourseUtilService courseUtilService; + + @Autowired + private ExamUtilService examUtilService; + + private Course course1; + + private Exam exam1; + + private Exam testExam1; + + private static final int NUMBER_OF_STUDENTS = 3; + + private static final int NUMBER_OF_TUTORS = 1; + + private User student1; + + @BeforeEach + void initTestCase() { + userUtilService.addUsers(TEST_PREFIX, NUMBER_OF_STUDENTS, NUMBER_OF_TUTORS, 0, 1); + // Add a student that is not in the course + userUtilService.createAndSaveUser(TEST_PREFIX + "student42", passwordService.hashPassword(UserFactory.USER_PASSWORD)); + + course1 = courseUtilService.addEmptyCourse(); + student1 = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); + + exam1 = examUtilService.addExam(course1); + examUtilService.addExamChannel(exam1, "exam1 channel"); + testExam1 = examUtilService.addTestExam(course1); + examUtilService.addStudentExamForTestExam(testExam1, student1); + + bitbucketRequestMockProvider.enableMockingOfRequests(); + + ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 200; + participantScoreScheduleService.activate(); + } + + @AfterEach + void tearDown() { + bitbucketRequestMockProvider.reset(); + + ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 500; + participantScoreScheduleService.shutdown(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testRegisterUserInExam_addedToCourseStudentsGroup() throws Exception { + User student42 = userUtilService.getUserByLogin(TEST_PREFIX + "student42"); + jiraRequestMockProvider.enableMockingOfRequests(); + jiraRequestMockProvider.mockAddUserToGroup(course1.getStudentGroupName(), false); + bitbucketRequestMockProvider.mockUpdateUserDetails(student42.getLogin(), student42.getEmail(), student42.getName()); + bitbucketRequestMockProvider.mockAddUserToGroups(); + + Set studentsInCourseBefore = userRepo.findAllInGroupWithAuthorities(course1.getStudentGroupName()); + request.postWithoutLocation("/api/courses/" + course1.getId() + "/exams/" + exam1.getId() + "/students/" + TEST_PREFIX + "student42", null, HttpStatus.OK, null); + Set studentsInCourseAfter = userRepo.findAllInGroupWithAuthorities(course1.getStudentGroupName()); + studentsInCourseBefore.add(student42); + assertThat(studentsInCourseBefore).containsExactlyInAnyOrderElementsOf(studentsInCourseAfter); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testAddStudentToExam_testExam() throws Exception { + request.postWithoutLocation("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/students/" + TEST_PREFIX + "student42", null, HttpStatus.BAD_REQUEST, + null); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testRemoveStudentToExam_testExam() throws Exception { + request.delete("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/students/" + TEST_PREFIX + "student42", HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testRegisterUsersInExam() throws Exception { + jiraRequestMockProvider.enableMockingOfRequests(); + + var savedExam = examUtilService.addExam(course1); + + List registrationNumbers = Arrays.asList("1111111", "1111112", "1111113"); + List students = userUtilService.setRegistrationNumberOfStudents(registrationNumbers, TEST_PREFIX); + + User student1 = students.get(0); + User student2 = students.get(1); + User student3 = students.get(2); + + var registrationNumber3WithTypo = "1111113" + "0"; + var registrationNumber4WithTypo = "1111115" + "1"; + var registrationNumber99 = "1111199"; + var registrationNumber111 = "1111100"; + var emptyRegistrationNumber = ""; + + // mock the ldap service + doReturn(Optional.empty()).when(ldapUserService).findByRegistrationNumber(registrationNumber3WithTypo); + doReturn(Optional.empty()).when(ldapUserService).findByRegistrationNumber(emptyRegistrationNumber); + doReturn(Optional.empty()).when(ldapUserService).findByRegistrationNumber(registrationNumber4WithTypo); + + var ldapUser111Dto = new LdapUserDto().registrationNumber(registrationNumber111).firstName(STUDENT_111).lastName(STUDENT_111).username(STUDENT_111) + .email(STUDENT_111 + "@tum.de"); + doReturn(Optional.of(ldapUser111Dto)).when(ldapUserService).findByRegistrationNumber(registrationNumber111); + + // first mocked call is expected to add student 99 to the course student group + jiraRequestMockProvider.mockAddUserToGroup(course1.getStudentGroupName(), false); + // second mocked call expected to create student 111 + jiraRequestMockProvider.mockCreateUserInExternalUserManagement(ldapUser111Dto.getUsername(), ldapUser111Dto.getFirstName() + " " + ldapUser111Dto.getLastName(), + ldapUser111Dto.getEmail()); + // the last mocked call is expected to add student 111 to the course student group + jiraRequestMockProvider.mockAddUserToGroup(course1.getStudentGroupName(), false); + + User student99 = userUtilService.createAndSaveUser("student99"); // not registered for the course + userUtilService.setRegistrationNumberOfUserAndSave("student99", registrationNumber99); + + bitbucketRequestMockProvider.mockUpdateUserDetails(student99.getLogin(), student99.getEmail(), student99.getName()); + bitbucketRequestMockProvider.mockAddUserToGroups(); + student99 = userRepo.findOneWithGroupsAndAuthoritiesByLogin("student99").orElseThrow(); + assertThat(student99.getGroups()).doesNotContain(course1.getStudentGroupName()); + + // Note: student111 is not yet a user of Artemis and should be retrieved from the LDAP + request.postWithoutLocation("/api/courses/" + course1.getId() + "/exams/" + savedExam.getId() + "/students/" + TEST_PREFIX + "student1", null, HttpStatus.OK, null); + request.postWithoutLocation("/api/courses/" + course1.getId() + "/exams/" + savedExam.getId() + "/students/nonExistingStudent", null, HttpStatus.NOT_FOUND, null); + + Exam storedExam = examRepository.findWithExamUsersById(savedExam.getId()).orElseThrow(); + ExamUser examUserStudent1 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student1.getId()).orElseThrow(); + assertThat(storedExam.getExamUsers()).containsExactly(examUserStudent1); + + request.delete("/api/courses/" + course1.getId() + "/exams/" + savedExam.getId() + "/students/" + TEST_PREFIX + "student1", HttpStatus.OK); + request.delete("/api/courses/" + course1.getId() + "/exams/" + savedExam.getId() + "/students/nonExistingStudent", HttpStatus.NOT_FOUND); + storedExam = examRepository.findWithExamUsersById(savedExam.getId()).orElseThrow(); + assertThat(storedExam.getExamUsers()).isEmpty(); + + var studentDto1 = UserFactory.generateStudentDTOWithRegistrationNumber(student1.getRegistrationNumber()); + var studentDto2 = UserFactory.generateStudentDTOWithRegistrationNumber(student2.getRegistrationNumber()); + var studentDto3 = new StudentDTO(student3.getLogin(), null, null, registrationNumber3WithTypo, null); // explicit typo, should be a registration failure later + var studentDto4 = UserFactory.generateStudentDTOWithRegistrationNumber(registrationNumber4WithTypo); // explicit typo, should fall back to login name later + var studentDto10 = UserFactory.generateStudentDTOWithRegistrationNumber(null); // completely empty + + var studentDto99 = new StudentDTO(student99.getLogin(), null, null, registrationNumber99, null); + var studentDto111 = new StudentDTO(null, null, null, registrationNumber111, null); + + // Add a student with login but empty registration number + var studentsToRegister = List.of(studentDto1, studentDto2, studentDto3, studentDto4, studentDto99, studentDto111, studentDto10); + + // now we register all these students for the exam. + List registrationFailures = request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + savedExam.getId() + "/students", + studentsToRegister, StudentDTO.class, HttpStatus.OK); + // all students get registered if they can be found in the LDAP + assertThat(registrationFailures).containsExactlyInAnyOrder(studentDto4, studentDto10); + + // TODO check audit events stored properly + + storedExam = examRepository.findWithExamUsersById(savedExam.getId()).orElseThrow(); + + // now a new user student101 should exist + var student111 = userUtilService.getUserByLogin(STUDENT_111); + + var examUser1 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student1.getId()).orElseThrow(); + var examUser2 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student2.getId()).orElseThrow(); + var examUser3 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student3.getId()).orElseThrow(); + var examUser99 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student99.getId()).orElseThrow(); + var examUser111 = examUserRepository.findByExamIdAndUserId(storedExam.getId(), student111.getId()).orElseThrow(); + + assertThat(storedExam.getExamUsers()).containsExactlyInAnyOrder(examUser1, examUser2, examUser3, examUser99, examUser111); + + for (var examUser : storedExam.getExamUsers()) { + // all registered users must have access to the course + var user = userRepo.findOneWithGroupsAndAuthoritiesByLogin(examUser.getUser().getLogin()).orElseThrow(); + assertThat(user.getGroups()).contains(course1.getStudentGroupName()); + } + + // Make sure delete also works if so many objects have been created before + request.delete("/api/courses/" + course1.getId() + "/exams/" + savedExam.getId(), HttpStatus.OK); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testRegisterLDAPUsersInExam() throws Exception { + jiraRequestMockProvider.enableMockingOfRequests(); + var savedExam = examUtilService.addExam(course1); + String student100 = TEST_PREFIX + "student100"; + String student200 = TEST_PREFIX + "student200"; + String student300 = TEST_PREFIX + "student300"; + + // setup mocks + var ldapUser1Dto = new LdapUserDto().firstName(student100).lastName(student100).username(student100).registrationNumber("100000").email(student100 + "@tum.de"); + doReturn(Optional.of(ldapUser1Dto)).when(ldapUserService).findByUsername(student100); + jiraRequestMockProvider.mockCreateUserInExternalUserManagement(ldapUser1Dto.getUsername(), ldapUser1Dto.getFirstName() + " " + ldapUser1Dto.getLastName(), null); + jiraRequestMockProvider.mockAddUserToGroup(course1.getStudentGroupName(), false); + + var ldapUser2Dto = new LdapUserDto().firstName(student200).lastName(student200).username(student200).registrationNumber("200000").email(student200 + "@tum.de"); + doReturn(Optional.of(ldapUser2Dto)).when(ldapUserService).findByEmail(student200 + "@tum.de"); + jiraRequestMockProvider.mockCreateUserInExternalUserManagement(ldapUser2Dto.getUsername(), ldapUser2Dto.getFirstName() + " " + ldapUser2Dto.getLastName(), null); + jiraRequestMockProvider.mockAddUserToGroup(course1.getStudentGroupName(), false); + + var ldapUser3Dto = new LdapUserDto().firstName(student300).lastName(student300).username(student300).registrationNumber("3000000").email(student300 + "@tum.de"); + doReturn(Optional.of(ldapUser3Dto)).when(ldapUserService).findByRegistrationNumber("3000000"); + jiraRequestMockProvider.mockCreateUserInExternalUserManagement(ldapUser3Dto.getUsername(), ldapUser3Dto.getFirstName() + " " + ldapUser3Dto.getLastName(), null); + jiraRequestMockProvider.mockAddUserToGroup(course1.getStudentGroupName(), false); + + // user with login + StudentDTO dto1 = new StudentDTO(student100, student100, student100, null, null); + // user with email + StudentDTO dto2 = new StudentDTO(null, student200, student200, null, student200 + "@tum.de"); + // user with registration number + StudentDTO dto3 = new StudentDTO(null, student300, student300, "3000000", null); + // user without anything + StudentDTO dto4 = new StudentDTO(null, null, null, null, null); + + List registrationFailures = request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + savedExam.getId() + "/students", + List.of(dto1, dto2, dto3, dto4), StudentDTO.class, HttpStatus.OK); + assertThat(registrationFailures).containsExactly(dto4); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testAddStudentsToExam_testExam() throws Exception { + userUtilService.setRegistrationNumberOfUserAndSave(TEST_PREFIX + "student1", "1111111"); + + StudentDTO studentDto1 = UserFactory.generateStudentDTOWithRegistrationNumber("1111111"); + List studentDTOS = List.of(studentDto1); + request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/students", studentDTOS, StudentDTO.class, HttpStatus.FORBIDDEN); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testRemoveAllStudentsFromExam_testExam() throws Exception { + request.delete("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/students", HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testDeleteStudentThatDoesNotExist() throws Exception { + Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, 1); + request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/students/nonExistingStudent", HttpStatus.NOT_FOUND); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testAddAllRegisteredUsersToExam() throws Exception { + Exam exam = examUtilService.addExam(course1); + Channel channel = examUtilService.addExamChannel(exam, "testchannel"); + int numberOfStudentsInCourse = userRepo.findAllInGroup(course1.getStudentGroupName()).size(); + + User student99 = userUtilService.createAndSaveUser(TEST_PREFIX + "student99"); // not registered for the course + student99.setGroups(Collections.singleton("tumuser")); + userUtilService.setRegistrationNumberOfUserAndSave(student99, "1234"); + assertThat(student99.getGroups()).contains(course1.getStudentGroupName()); + + var examUser99 = examUserRepository.findByExamIdAndUserId(exam.getId(), student99.getId()); + assertThat(examUser99).isEmpty(); + + request.postWithoutLocation("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/register-course-students", null, HttpStatus.OK, null); + + exam = examRepository.findWithExamUsersById(exam.getId()).orElseThrow(); + examUser99 = examUserRepository.findByExamIdAndUserId(exam.getId(), student99.getId()); + + // the course students + our custom student99 + assertThat(exam.getExamUsers()).hasSize(numberOfStudentsInCourse + 1); + assertThat(exam.getExamUsers()).contains(examUser99.orElseThrow()); + verify(examAccessService).checkCourseAndExamAccessForInstructorElseThrow(course1.getId(), exam.getId()); + + Channel channelFromDB = channelRepository.findChannelByExamId(exam.getId()); + assertThat(channelFromDB).isNotNull(); + assertThat(channelFromDB.getExam()).isEqualTo(exam); + assertThat(channelFromDB.getName()).isEqualTo(channel.getName()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testRegisterCourseStudents_testExam() throws Exception { + request.postWithoutLocation("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/register-course-students", null, HttpStatus.BAD_REQUEST, null); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testIsUserRegisteredForExam() { + var examUser = new ExamUser(); + examUser.setExam(exam1); + examUser.setUser(student1); + examUser = examUserRepository.save(examUser); + exam1.addExamUser(examUser); + final var exam = examRepository.save(exam1); + final var isUserRegistered = examRegistrationService.isUserRegisteredForExam(exam.getId(), student1.getId()); + final var isCurrentUserRegistered = examRegistrationService.isCurrentUserRegisteredForExam(exam.getId()); + assertThat(isUserRegistered).isTrue(); + assertThat(isCurrentUserRegistered).isFalse(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testRegisterInstructorToExam() throws Exception { + request.postWithoutLocation("/api/courses/" + course1.getId() + "/exams/" + exam1.getId() + "/students/" + TEST_PREFIX + "instructor1", null, HttpStatus.FORBIDDEN, null); + } + + // ExamRegistration Service - checkRegistrationOrRegisterStudentToTestExam + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testCheckRegistrationOrRegisterStudentToTestExam_noTestExam() { + assertThatThrownBy( + () -> examRegistrationService.checkRegistrationOrRegisterStudentToTestExam(course1, exam1.getId(), userUtilService.getUserByLogin(TEST_PREFIX + "student1"))) + .isInstanceOf(BadRequestAlertException.class); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student42", roles = "USER") + void testCheckRegistrationOrRegisterStudentToTestExam_studentNotPartOfCourse() { + assertThatThrownBy( + () -> examRegistrationService.checkRegistrationOrRegisterStudentToTestExam(course1, exam1.getId(), userUtilService.getUserByLogin(TEST_PREFIX + "student42"))) + .isInstanceOf(BadRequestAlertException.class); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testCheckRegistrationOrRegisterStudentToTestExam_successfulRegistration() { + Exam testExam = ExamFactory.generateTestExam(course1); + testExam = examRepository.save(testExam); + var examUser = new ExamUser(); + examUser.setExam(testExam); + examUser.setUser(student1); + examUser = examUserRepository.save(examUser); + testExam.addExamUser(examUser); + testExam = examRepository.save(testExam); + examRegistrationService.checkRegistrationOrRegisterStudentToTestExam(course1, testExam.getId(), student1); + Exam testExamReloaded = examRepository.findByIdWithExamUsersElseThrow(testExam.getId()); + assertThat(testExamReloaded.getExamUsers()).contains(examUser); + } +} diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamStartTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamStartTest.java new file mode 100644 index 000000000000..99e8eef5ecf0 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamStartTest.java @@ -0,0 +1,281 @@ +package de.tum.in.www1.artemis.exam; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.eclipse.jgit.api.errors.GitAPIException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithMockUser; + +import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.course.CourseUtilService; +import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.enumeration.DiagramType; +import de.tum.in.www1.artemis.domain.exam.*; +import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; +import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission; +import de.tum.in.www1.artemis.domain.participation.Participation; +import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; +import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; +import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseTestService; +import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.participation.ParticipationUtilService; +import de.tum.in.www1.artemis.repository.*; +import de.tum.in.www1.artemis.service.connectors.vcs.VersionControlRepositoryPermission; +import de.tum.in.www1.artemis.service.scheduled.ParticipantScoreScheduleService; +import de.tum.in.www1.artemis.user.UserUtilService; +import de.tum.in.www1.artemis.util.ExamPrepareExercisesTestUtil; + +// TODO IMPORTANT test more complex exam configurations (mixed exercise type, more variants and more registered students) +class ExamStartTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { + + private static final String TEST_PREFIX = "examstarttest"; + + @Autowired + private ExerciseRepository exerciseRepo; + + @Autowired + private ExamRepository examRepository; + + @Autowired + private ExerciseGroupRepository exerciseGroupRepository; + + @Autowired + private StudentExamRepository studentExamRepository; + + @Autowired + private ParticipationTestRepository participationTestRepository; + + @Autowired + private ProgrammingExerciseTestService programmingExerciseTestService; + + @Autowired + private UserUtilService userUtilService; + + @Autowired + private CourseUtilService courseUtilService; + + @Autowired + private ExamUtilService examUtilService; + + @Autowired + private ProgrammingExerciseUtilService programmingExerciseUtilService; + + @Autowired + private ParticipationUtilService participationUtilService; + + private Course course1; + + private Exam exam; + + private static final int NUMBER_OF_STUDENTS = 2; + + private Set registeredUsers; + + private final List createdStudentExams = new ArrayList<>(); + + @BeforeEach + void initTestCase() throws GitAPIException { + userUtilService.addUsers(TEST_PREFIX, NUMBER_OF_STUDENTS, 0, 0, 1); + + course1 = courseUtilService.addEmptyCourse(); + exam = examUtilService.addExamWithExerciseGroup(course1, true); + bitbucketRequestMockProvider.enableMockingOfRequests(); + + ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 200; + participantScoreScheduleService.activate(); + + doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); + + // registering users + User student1 = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); + User student2 = userUtilService.getUserByLogin(TEST_PREFIX + "student2"); + registeredUsers = Set.of(student1, student2); + exam.setExamUsers(Set.of(new ExamUser())); + // setting dates + exam.setStartDate(ZonedDateTime.now().plusHours(2)); + exam.setEndDate(ZonedDateTime.now().plusHours(3)); + exam.setVisibleDate(ZonedDateTime.now().plusHours(1)); + } + + @AfterEach + void tearDown() throws Exception { + bitbucketRequestMockProvider.reset(); + bambooRequestMockProvider.reset(); + if (programmingExerciseTestService.exerciseRepo != null) { + programmingExerciseTestService.tearDown(); + } + + // Cleanup of Bidirectional Relationships + for (StudentExam studentExam : createdStudentExams) { + exam.removeStudentExam(studentExam); + } + examRepository.save(exam); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testStartExercisesWithTextExercise() throws Exception { + // creating exercise + ExerciseGroup exerciseGroup = exam.getExerciseGroups().get(0); + + TextExercise textExercise = TextExerciseFactory.generateTextExerciseForExam(exerciseGroup); + exerciseGroup.addExercise(textExercise); + exerciseGroupRepository.save(exerciseGroup); + textExercise = exerciseRepo.save(textExercise); + + createStudentExams(textExercise); + + List studentParticipations = invokePrepareExerciseStart(); + + for (Participation participation : studentParticipations) { + assertThat(participation.getExercise()).isEqualTo(textExercise); + assertThat(participation.getExercise().getCourseViaExerciseGroupOrCourseMember()).isNotNull(); + assertThat(participation.getExercise().getExerciseGroup()).isEqualTo(exam.getExerciseGroups().get(0)); + assertThat(participation.getSubmissions()).hasSize(1); + var textSubmission = (TextSubmission) participation.getSubmissions().iterator().next(); + assertThat(textSubmission.getText()).isNull(); + } + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testStartExercisesWithModelingExercise() throws Exception { + // creating exercise + ModelingExercise modelingExercise = ModelingExerciseFactory.generateModelingExerciseForExam(DiagramType.ClassDiagram, exam.getExerciseGroups().get(0)); + exam.getExerciseGroups().get(0).addExercise(modelingExercise); + exerciseGroupRepository.save(exam.getExerciseGroups().get(0)); + modelingExercise = exerciseRepo.save(modelingExercise); + + createStudentExams(modelingExercise); + + List studentParticipations = invokePrepareExerciseStart(); + + for (Participation participation : studentParticipations) { + assertThat(participation.getExercise()).isEqualTo(modelingExercise); + assertThat(participation.getExercise().getCourseViaExerciseGroupOrCourseMember()).isNotNull(); + assertThat(participation.getExercise().getExerciseGroup()).isEqualTo(exam.getExerciseGroups().get(0)); + assertThat(participation.getSubmissions()).hasSize(1); + var modelingSubmission = (ModelingSubmission) participation.getSubmissions().iterator().next(); + assertThat(modelingSubmission.getModel()).isNull(); + assertThat(modelingSubmission.getExplanationText()).isNull(); + } + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testStartExerciseWithProgrammingExercise() throws Exception { + bitbucketRequestMockProvider.enableMockingOfRequests(true); + bambooRequestMockProvider.enableMockingOfRequests(true); + + ProgrammingExercise programmingExercise = createProgrammingExercise(); + + participationUtilService.mockCreationOfExerciseParticipation(programmingExercise, versionControlService, continuousIntegrationService); + + createStudentExams(programmingExercise); + + var studentParticipations = invokePrepareExerciseStart(); + + for (Participation participation : studentParticipations) { + assertThat(participation.getExercise()).isEqualTo(programmingExercise); + assertThat(participation.getExercise().getCourseViaExerciseGroupOrCourseMember()).isNotNull(); + assertThat(participation.getExercise().getExerciseGroup()).isEqualTo(exam.getExerciseGroups().get(0)); + // No initial submissions should be created for programming exercises + assertThat(participation.getSubmissions()).isEmpty(); + assertThat(((ProgrammingExerciseParticipation) participation).isLocked()).isTrue(); + verify(versionControlService, never()).configureRepository(eq(programmingExercise), (ProgrammingExerciseStudentParticipation) eq(participation), eq(true)); + } + } + + private static class ExamStartDateSource implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(Arguments.of(ZonedDateTime.now().minusHours(1)), // after exam start + Arguments.arguments(ZonedDateTime.now().plusMinutes(3)) // before exam start but after pe unlock date + ); + } + } + + @ParameterizedTest(name = "{displayName} [{index}]") + @ArgumentsSource(ExamStartDateSource.class) + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testStartExerciseWithProgrammingExercise_participationUnlocked(ZonedDateTime startDate) throws Exception { + exam.setVisibleDate(ZonedDateTime.now().minusHours(2)); + exam.setStartDate(startDate); + examRepository.save(exam); + + bitbucketRequestMockProvider.enableMockingOfRequests(true); + bambooRequestMockProvider.enableMockingOfRequests(true); + + ProgrammingExercise programmingExercise = createProgrammingExercise(); + + participationUtilService.mockCreationOfExerciseParticipation(programmingExercise, versionControlService, continuousIntegrationService); + + createStudentExams(programmingExercise); + + var studentParticipations = invokePrepareExerciseStart(); + + for (Participation participation : studentParticipations) { + assertThat(participation.getExercise()).isEqualTo(programmingExercise); + assertThat(participation.getExercise().getCourseViaExerciseGroupOrCourseMember()).isNotNull(); + assertThat(participation.getExercise().getExerciseGroup()).isEqualTo(exam.getExerciseGroups().get(0)); + // No initial submissions should be created for programming exercises + assertThat(participation.getSubmissions()).isEmpty(); + ProgrammingExerciseStudentParticipation studentParticipation = (ProgrammingExerciseStudentParticipation) participation; + // The participation should not get locked if it gets created after the exam already started + assertThat(studentParticipation.isLocked()).isFalse(); + verify(versionControlService).addMemberToRepository(studentParticipation.getVcsRepositoryUrl(), studentParticipation.getStudent().orElseThrow(), + VersionControlRepositoryPermission.REPO_WRITE); + } + } + + private void createStudentExams(Exercise exercise) { + // creating student exams + for (User user : registeredUsers) { + StudentExam studentExam = new StudentExam(); + studentExam.addExercise(exercise); + studentExam.setUser(user); + exam.addStudentExam(studentExam); + createdStudentExams.add(studentExamRepository.save(studentExam)); + } + + exam = examRepository.save(exam); + } + + private ProgrammingExercise createProgrammingExercise() { + ProgrammingExercise programmingExercise = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(exam.getExerciseGroups().get(0)); + programmingExercise = exerciseRepo.save(programmingExercise); + programmingExercise = programmingExerciseUtilService.addTemplateParticipationForProgrammingExercise(programmingExercise); + exam.getExerciseGroups().get(0).addExercise(programmingExercise); + exerciseGroupRepository.save(exam.getExerciseGroups().get(0)); + return programmingExercise; + } + + private List invokePrepareExerciseStart() throws Exception { + // invoke start exercises + int noGeneratedParticipations = ExamPrepareExercisesTestUtil.prepareExerciseStart(request, exam, course1); + verify(gitService, times(examUtilService.getNumberOfProgrammingExercises(exam.getId()))).combineAllCommitsOfRepositoryIntoOne(any()); + assertThat(noGeneratedParticipations).isEqualTo(exam.getStudentExams().size()); + return participationTestRepository.findByExercise_ExerciseGroup_Exam_Id(exam.getId()); + } + +} diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamUserIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamUserIntegrationTest.java index f32b3358d11d..9c93d719942a 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ExamUserIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamUserIntegrationTest.java @@ -7,10 +7,7 @@ import java.io.File; import java.io.FileInputStream; import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import javax.validation.constraints.NotNull; @@ -19,10 +16,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; +import org.springframework.http.*; import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; @@ -39,7 +33,9 @@ import de.tum.in.www1.artemis.domain.exam.ExamUser; import de.tum.in.www1.artemis.domain.exam.StudentExam; import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseTestService; -import de.tum.in.www1.artemis.repository.*; +import de.tum.in.www1.artemis.repository.ExamRepository; +import de.tum.in.www1.artemis.repository.StudentExamRepository; +import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.LocalRepository; import de.tum.in.www1.artemis.web.rest.dto.ExamUserAttendanceCheckDTO; diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamUtilService.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamUtilService.java index 10932a3ba61e..c40de1f9b855 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ExamUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamUtilService.java @@ -794,4 +794,23 @@ public StudentExam addExercisesWithParticipationsAndSubmissionsToStudentExam(Exa return studentExamRepository.save(studentExam); } + + /** + * gets the number of programming exercises in the exam + * + * @param examId id of the exam to be searched for programming exercises + * @return number of programming exercises in the exams + */ + public int getNumberOfProgrammingExercises(Long examId) { + Exam exam = examRepository.findWithExerciseGroupsAndExercisesByIdOrElseThrow(examId); + int count = 0; + for (var exerciseGroup : exam.getExerciseGroups()) { + for (var exercise : exerciseGroup.getExercises()) { + if (exercise instanceof ProgrammingExercise) { + count++; + } + } + } + return count; + } } diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExerciseGroupIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExerciseGroupIntegrationTest.java index 2e70b4d1a582..2b0e6eaee2bf 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ExerciseGroupIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExerciseGroupIntegrationTest.java @@ -4,9 +4,7 @@ import static org.mockito.Mockito.*; import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import java.util.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,6 +25,7 @@ import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.ExamRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.TextExerciseRepository; @@ -55,6 +54,9 @@ class ExerciseGroupIntegrationTest extends AbstractSpringIntegrationBambooBitbuc @Autowired private ExamUtilService examUtilService; + @Autowired + private TextExerciseUtilService textExerciseUtilService; + private Course course1; private Exam exam1; @@ -276,4 +278,53 @@ void importExerciseGroup_preCheckFailed() throws Exception { request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/import-exercise-group", List.of(programmingGroup), ExerciseGroup.class, HttpStatus.BAD_REQUEST); } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testUpdateOrderOfExerciseGroups() throws Exception { + Exam exam = ExamFactory.generateExam(course1); + ExerciseGroup exerciseGroup1 = ExamFactory.generateExerciseGroupWithTitle(true, exam, "first"); + ExerciseGroup exerciseGroup2 = ExamFactory.generateExerciseGroupWithTitle(true, exam, "second"); + ExerciseGroup exerciseGroup3 = ExamFactory.generateExerciseGroupWithTitle(true, exam, "third"); + examRepository.save(exam); + + TextExercise exercise1_1 = textExerciseUtilService.createTextExerciseForExam(exerciseGroup1); + TextExercise exercise1_2 = textExerciseUtilService.createTextExerciseForExam(exerciseGroup1); + TextExercise exercise2_1 = textExerciseUtilService.createTextExerciseForExam(exerciseGroup2); + TextExercise exercise3_1 = textExerciseUtilService.createTextExerciseForExam(exerciseGroup3); + TextExercise exercise3_2 = textExerciseUtilService.createTextExerciseForExam(exerciseGroup3); + TextExercise exercise3_3 = textExerciseUtilService.createTextExerciseForExam(exerciseGroup3); + + List orderedExerciseGroups = new ArrayList<>(List.of(exerciseGroup2, exerciseGroup3, exerciseGroup1)); + // Should save new order + request.put("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/exercise-groups-order", orderedExerciseGroups, HttpStatus.OK); + verify(examAccessService).checkCourseAndExamAccessForEditorElseThrow(course1.getId(), exam.getId()); + + List savedExerciseGroups = examRepository.findWithExerciseGroupsById(exam.getId()).orElseThrow().getExerciseGroups(); + assertThat(savedExerciseGroups.get(0).getTitle()).isEqualTo("second"); + assertThat(savedExerciseGroups.get(1).getTitle()).isEqualTo("third"); + assertThat(savedExerciseGroups.get(2).getTitle()).isEqualTo("first"); + + // Exercises should be preserved + Exam savedExam = examRepository.findWithExerciseGroupsAndExercisesById(exam.getId()).orElseThrow(); + ExerciseGroup savedExerciseGroup1 = savedExam.getExerciseGroups().get(2); + ExerciseGroup savedExerciseGroup2 = savedExam.getExerciseGroups().get(0); + ExerciseGroup savedExerciseGroup3 = savedExam.getExerciseGroups().get(1); + assertThat(savedExerciseGroup1.getExercises()).containsExactlyInAnyOrder(exercise1_1, exercise1_2); + assertThat(savedExerciseGroup2.getExercises()).containsExactlyInAnyOrder(exercise2_1); + assertThat(savedExerciseGroup3.getExercises()).containsExactlyInAnyOrder(exercise3_1, exercise3_2, exercise3_3); + + // Should fail with too many exercise groups + orderedExerciseGroups.add(exerciseGroup1); + request.put("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/exercise-groups-order", orderedExerciseGroups, HttpStatus.BAD_REQUEST); + + // Should fail with too few exercise groups + orderedExerciseGroups.remove(3); + orderedExerciseGroups.remove(2); + request.put("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/exercise-groups-order", orderedExerciseGroups, HttpStatus.BAD_REQUEST); + + // Should fail with different exercise group + orderedExerciseGroups = Arrays.asList(exerciseGroup2, exerciseGroup3, ExamFactory.generateExerciseGroup(true, exam)); + request.put("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/exercise-groups-order", orderedExerciseGroups, HttpStatus.BAD_REQUEST); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ProgrammingExamIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ProgrammingExamIntegrationTest.java new file mode 100644 index 000000000000..e3cfcb57bc23 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/exam/ProgrammingExamIntegrationTest.java @@ -0,0 +1,295 @@ +package de.tum.in.www1.artemis.exam; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.course.CourseUtilService; +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; +import de.tum.in.www1.artemis.domain.exam.Exam; +import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; +import de.tum.in.www1.artemis.domain.exam.StudentExam; +import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseTestService; +import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.participation.ParticipationUtilService; +import de.tum.in.www1.artemis.repository.*; +import de.tum.in.www1.artemis.service.scheduled.ParticipantScoreScheduleService; +import de.tum.in.www1.artemis.user.UserUtilService; +import de.tum.in.www1.artemis.util.ExamPrepareExercisesTestUtil; + +class ProgrammingExamIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { + + private static final String TEST_PREFIX = "programmingexamtest"; + + @Autowired + private ExerciseRepository exerciseRepo; + + @Autowired + private ExamRepository examRepository; + + @Autowired + private StudentExamRepository studentExamRepository; + + @Autowired + private ProgrammingExerciseRepository programmingExerciseRepository; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private ProgrammingExerciseTestService programmingExerciseTestService; + + @Autowired + private UserUtilService userUtilService; + + @Autowired + private CourseUtilService courseUtilService; + + @Autowired + private ExamUtilService examUtilService; + + @Autowired + private ProgrammingExerciseUtilService programmingExerciseUtilService; + + @Autowired + private ParticipationUtilService participationUtilService; + + private Course course1; + + private Exam exam1; + + private static final int NUMBER_OF_STUDENTS = 2; + + private static final int NUMBER_OF_TUTORS = 1; + + private User student1; + + @BeforeEach + void initTestCase() { + userUtilService.addUsers(TEST_PREFIX, NUMBER_OF_STUDENTS, NUMBER_OF_TUTORS, 0, 1); + + course1 = courseUtilService.addEmptyCourse(); + student1 = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); + exam1 = examUtilService.addExam(course1); + + bitbucketRequestMockProvider.enableMockingOfRequests(); + + ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 200; + participantScoreScheduleService.activate(); + } + + @AfterEach + void tearDown() throws Exception { + bitbucketRequestMockProvider.reset(); + bambooRequestMockProvider.reset(); + if (programmingExerciseTestService.exerciseRepo != null) { + programmingExerciseTestService.tearDown(); + } + + ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 500; + participantScoreScheduleService.shutdown(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testUpdateExam_rescheduleProgramming_visibleAndStartDateChanged() throws Exception { + // Add a programming exercise to the exam and change the dates in order to invoke a rescheduling + var programmingEx = programmingExerciseUtilService.addCourseExamExerciseGroupWithOneProgrammingExerciseAndTestCases(); + var examWithProgrammingEx = programmingEx.getExerciseGroup().getExam(); + + ZonedDateTime visibleDate = examWithProgrammingEx.getVisibleDate(); + ZonedDateTime startDate = examWithProgrammingEx.getStartDate(); + ZonedDateTime endDate = examWithProgrammingEx.getEndDate(); + examUtilService.setVisibleStartAndEndDateOfExam(examWithProgrammingEx, visibleDate.plusSeconds(1), startDate.plusSeconds(1), endDate); + + request.put("/api/courses/" + examWithProgrammingEx.getCourse().getId() + "/exams", examWithProgrammingEx, HttpStatus.OK); + verify(instanceMessageSendService).sendProgrammingExerciseSchedule(programmingEx.getId()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testUpdateExam_rescheduleProgramming_visibleDateChanged() throws Exception { + var programmingEx = programmingExerciseUtilService.addCourseExamExerciseGroupWithOneProgrammingExerciseAndTestCases(); + var examWithProgrammingEx = programmingEx.getExerciseGroup().getExam(); + examWithProgrammingEx.setVisibleDate(examWithProgrammingEx.getVisibleDate().plusSeconds(1)); + request.put("/api/courses/" + examWithProgrammingEx.getCourse().getId() + "/exams", examWithProgrammingEx, HttpStatus.OK); + verify(instanceMessageSendService).sendProgrammingExerciseSchedule(programmingEx.getId()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testUpdateExam_rescheduleProgramming_startDateChanged() throws Exception { + var programmingEx = programmingExerciseUtilService.addCourseExamExerciseGroupWithOneProgrammingExerciseAndTestCases(); + var examWithProgrammingEx = programmingEx.getExerciseGroup().getExam(); + + ZonedDateTime visibleDate = examWithProgrammingEx.getVisibleDate(); + ZonedDateTime startDate = examWithProgrammingEx.getStartDate(); + ZonedDateTime endDate = examWithProgrammingEx.getEndDate(); + examUtilService.setVisibleStartAndEndDateOfExam(examWithProgrammingEx, visibleDate, startDate.plusSeconds(1), endDate); + + request.put("/api/courses/" + examWithProgrammingEx.getCourse().getId() + "/exams", examWithProgrammingEx, HttpStatus.OK); + verify(instanceMessageSendService).sendProgrammingExerciseSchedule(programmingEx.getId()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void lockAllRepositories() throws Exception { + Exam exam = examUtilService.addExamWithExerciseGroup(course1, true); + + Exam examWithExerciseGroups = examRepository.findWithExerciseGroupsAndExercisesById(exam.getId()).orElseThrow(); + ExerciseGroup exerciseGroup1 = examWithExerciseGroups.getExerciseGroups().get(0); + + ProgrammingExercise programmingExercise = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(exerciseGroup1); + programmingExerciseRepository.save(programmingExercise); + + ProgrammingExercise programmingExercise2 = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(exerciseGroup1); + programmingExerciseRepository.save(programmingExercise2); + + Integer numOfLockedExercises = request.postWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams/lock-all-repositories", + Optional.empty(), Integer.class, HttpStatus.OK); + + assertThat(numOfLockedExercises).isEqualTo(2); + + verify(programmingExerciseScheduleService).lockAllStudentRepositories(programmingExercise); + verify(programmingExerciseScheduleService).lockAllStudentRepositories(programmingExercise2); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void lockAllRepositories_noInstructor() throws Exception { + request.postWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam1.getId() + "/student-exams/lock-all-repositories", Optional.empty(), Integer.class, + HttpStatus.FORBIDDEN); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void unlockAllRepositories_preAuthNoInstructor() throws Exception { + request.postWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam1.getId() + "/student-exams/unlock-all-repositories", Optional.empty(), Integer.class, + HttpStatus.FORBIDDEN); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void unlockAllRepositories() throws Exception { + bitbucketRequestMockProvider.enableMockingOfRequests(true); + assertThat(studentExamRepository.findStudentExam(new ProgrammingExercise(), null)).isEmpty(); + + Exam exam = examUtilService.addExamWithExerciseGroup(course1, true); + ExerciseGroup exerciseGroup1 = exam.getExerciseGroups().get(0); + + ProgrammingExercise programmingExercise = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(exerciseGroup1); + programmingExerciseRepository.save(programmingExercise); + + ProgrammingExercise programmingExercise2 = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(exerciseGroup1); + programmingExerciseRepository.save(programmingExercise2); + + User student2 = userUtilService.getUserByLogin(TEST_PREFIX + "student2"); + var studentExam1 = examUtilService.addStudentExamWithUser(exam, student1, 10); + studentExam1.setExercises(List.of(programmingExercise, programmingExercise2)); + var studentExam2 = examUtilService.addStudentExamWithUser(exam, student2, 0); + studentExam2.setExercises(List.of(programmingExercise, programmingExercise2)); + studentExamRepository.saveAll(Set.of(studentExam1, studentExam2)); + + var participationExSt1 = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, TEST_PREFIX + "student1"); + var participationExSt2 = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, TEST_PREFIX + "student2"); + + var participationEx2St1 = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise2, TEST_PREFIX + "student1"); + var participationEx2St2 = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise2, TEST_PREFIX + "student2"); + + assertThat(studentExamRepository.findStudentExam(programmingExercise, participationExSt1)).contains(studentExam1); + assertThat(studentExamRepository.findStudentExam(programmingExercise, participationExSt2)).contains(studentExam2); + assertThat(studentExamRepository.findStudentExam(programmingExercise2, participationEx2St1)).contains(studentExam1); + assertThat(studentExamRepository.findStudentExam(programmingExercise2, participationEx2St2)).contains(studentExam2); + + mockConfigureRepository(programmingExercise, TEST_PREFIX + "student1", Set.of(student1), true); + mockConfigureRepository(programmingExercise, TEST_PREFIX + "student2", Set.of(student2), true); + mockConfigureRepository(programmingExercise2, TEST_PREFIX + "student1", Set.of(student1), true); + mockConfigureRepository(programmingExercise2, TEST_PREFIX + "student2", Set.of(student2), true); + + Integer numOfUnlockedExercises = request.postWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams/unlock-all-repositories", + Optional.empty(), Integer.class, HttpStatus.OK); + + assertThat(numOfUnlockedExercises).isEqualTo(2); + + verify(programmingExerciseScheduleService).unlockAllStudentRepositories(programmingExercise); + verify(programmingExerciseScheduleService).unlockAllStudentRepositories(programmingExercise2); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGenerateStudentExamsTemplateCombine() throws Exception { + Exam examWithProgramming = examUtilService.addExerciseGroupsAndExercisesToExam(exam1, true); + doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); + + // invoke generate student exams + request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + examWithProgramming.getId() + "/generate-student-exams", Optional.empty(), + StudentExam.class, HttpStatus.OK); + + verify(gitService, never()).combineAllCommitsOfRepositoryIntoOne(any()); + + // invoke prepare exercise start + ExamPrepareExercisesTestUtil.prepareExerciseStart(request, exam1, course1); + + verify(gitService, times(examUtilService.getNumberOfProgrammingExercises(exam1.getId()))).combineAllCommitsOfRepositoryIntoOne(any()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testImportExamWithProgrammingExercise_preCheckFailed() throws Exception { + Exam exam = ExamFactory.generateExam(course1); + ExerciseGroup programmingGroup = ExamFactory.generateExerciseGroup(false, exam); + exam = examRepository.save(exam); + exam.setId(null); + ProgrammingExercise programming = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(programmingGroup, ProgrammingLanguage.JAVA); + programmingGroup.addExercise(programming); + exerciseRepo.save(programming); + + doReturn(true).when(versionControlService).checkIfProjectExists(any(), any()); + doReturn(null).when(continuousIntegrationService).checkIfProjectExists(any(), any()); + + request.getMvc().perform(post("/api/courses/" + course1.getId() + "/exam-import").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(exam))) + .andExpect(status().isBadRequest()) + .andExpect(result -> assertThat(result.getResolvedException()).hasMessage("Exam contains programming exercise(s) with invalid short name.")); + } + + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") + @CsvSource({ "A,A,B,C", "A,B,C,C", "A,A,B,B" }) + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testImportExamWithExercises_programmingExerciseSameShortNameOrTitle(String shortName1, String shortName2, String title1, String title2) throws Exception { + Exam exam = ExamFactory.generateExamWithExerciseGroup(course1, true); + ExerciseGroup exerciseGroup = exam.getExerciseGroups().get(0); + ProgrammingExercise exercise1 = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(exerciseGroup); + ProgrammingExercise exercise2 = ProgrammingExerciseFactory.generateProgrammingExerciseForExam(exerciseGroup); + + exercise1.setShortName(shortName1); + exercise2.setShortName(shortName2); + exercise1.setTitle(title1); + exercise2.setTitle(title2); + + request.postWithoutLocation("/api/courses/" + course1.getId() + "/exam-import", exam, HttpStatus.BAD_REQUEST, null); + } +} diff --git a/src/test/java/de/tum/in/www1/artemis/exam/TestExamIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/TestExamIntegrationTest.java new file mode 100644 index 000000000000..c23ac2a21094 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/exam/TestExamIntegrationTest.java @@ -0,0 +1,246 @@ +package de.tum.in.www1.artemis.exam; + +import static java.time.ZonedDateTime.now; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; + +import java.net.URI; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; + +import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.course.CourseUtilService; +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.exam.Exam; +import de.tum.in.www1.artemis.domain.exam.ExamUser; +import de.tum.in.www1.artemis.domain.exam.StudentExam; +import de.tum.in.www1.artemis.domain.metis.conversation.Channel; +import de.tum.in.www1.artemis.repository.ExamRepository; +import de.tum.in.www1.artemis.repository.ExamUserRepository; +import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository; +import de.tum.in.www1.artemis.service.exam.ExamAccessService; +import de.tum.in.www1.artemis.service.scheduled.ParticipantScoreScheduleService; +import de.tum.in.www1.artemis.service.user.PasswordService; +import de.tum.in.www1.artemis.user.UserFactory; +import de.tum.in.www1.artemis.user.UserUtilService; + +class TestExamIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { + + private static final String TEST_PREFIX = "testexamintegration"; + + @Autowired + private ExamRepository examRepository; + + @Autowired + private ExamUserRepository examUserRepository; + + @Autowired + private PasswordService passwordService; + + @Autowired + private ExamAccessService examAccessService; + + @Autowired + private UserUtilService userUtilService; + + @Autowired + private CourseUtilService courseUtilService; + + @Autowired + private ExamUtilService examUtilService; + + @Autowired + ChannelRepository channelRepository; + + private Course course1; + + private Course course2; + + private Exam testExam1; + + private static final int NUMBER_OF_STUDENTS = 1; + + private static final int NUMBER_OF_TUTORS = 1; + + private User student1; + + @BeforeEach + void initTestCase() { + userUtilService.addUsers(TEST_PREFIX, NUMBER_OF_STUDENTS, NUMBER_OF_TUTORS, 0, 1); + // Add a student that is not in the course + userUtilService.createAndSaveUser(TEST_PREFIX + "student42", passwordService.hashPassword(UserFactory.USER_PASSWORD)); + + course1 = courseUtilService.addEmptyCourse(); + course2 = courseUtilService.addEmptyCourse(); + + student1 = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); + testExam1 = examUtilService.addTestExam(course1); + examUtilService.addStudentExamForTestExam(testExam1, student1); + + bitbucketRequestMockProvider.enableMockingOfRequests(); + + ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 200; + participantScoreScheduleService.activate(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGenerateStudentExams_testExam() throws Exception { + request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/generate-student-exams", Optional.empty(), StudentExam.class, + HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGenerateMissingStudentExams_testExam() throws Exception { + request.postListWithResponseBody("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/generate-missing-student-exams", Optional.empty(), StudentExam.class, + HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testEvaluateQuizExercises_testExam() throws Exception { + request.post("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/student-exams/evaluate-quiz-exercises", Optional.empty(), HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testCreateTestExam_asInstructor() throws Exception { + // Test the creation of a test exam + Exam examA = ExamFactory.generateTestExam(course1); + URI examUri = request.post("/api/courses/" + course1.getId() + "/exams", examA, HttpStatus.CREATED); + Exam savedExam = request.get(String.valueOf(examUri), HttpStatus.OK, Exam.class); + + verify(examAccessService).checkCourseAccessForInstructorElseThrow(course1.getId()); + Channel channelFromDB = channelRepository.findChannelByExamId(savedExam.getId()); + assertThat(channelFromDB).isNotNull(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testCreateTestExam_asInstructor_withVisibleDateEqualsStartDate() throws Exception { + // Test the creation of a test exam, where visibleDate equals StartDate + Exam examB = ExamFactory.generateTestExam(course1); + examB.setVisibleDate(examB.getStartDate()); + request.post("/api/courses/" + course1.getId() + "/exams", examB, HttpStatus.CREATED); + + verify(examAccessService).checkCourseAccessForInstructorElseThrow(course1.getId()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testCreateTestExam_asInstructor_badRequestWithWorkingTimeGreaterThanWorkingWindow() throws Exception { + // Test for bad request, where workingTime is greater than difference between StartDate and EndDate + Exam examC = ExamFactory.generateTestExam(course1); + examC.setWorkingTime(5000); + request.post("/api/courses/" + course1.getId() + "/exams", examC, HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testCreateTestExam_asInstructor_badRequestWithWorkingTimeSetToZero() throws Exception { + // Test for bad request, if the working time is 0 + Exam examD = ExamFactory.generateTestExam(course1); + examD.setWorkingTime(0); + request.post("/api/courses/" + course1.getId() + "/exams", examD, HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testCreateTestExam_asInstructor_testExam_CorrectionRoundViolation() throws Exception { + Exam exam = ExamFactory.generateTestExam(course1); + exam.setNumberOfCorrectionRoundsInExam(1); + request.post("/api/courses/" + course1.getId() + "/exams", exam, HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testCreateTestExam_asInstructor_realExam_CorrectionRoundViolation() throws Exception { + Exam exam = ExamFactory.generateExam(course1); + exam.setNumberOfCorrectionRoundsInExam(0); + request.post("/api/courses/" + course1.getId() + "/exams", exam, HttpStatus.BAD_REQUEST); + + exam.setNumberOfCorrectionRoundsInExam(3); + request.post("/api/courses/" + course1.getId() + "/exams", exam, HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testUpdateTestExam_asInstructor_withExamModeChanged() throws Exception { + // The Exam-Mode should not be changeable with a PUT / update operation, a CONFLICT should be returned instead + // Case 1: test exam should be updated to real exam + Exam examA = ExamFactory.generateTestExam(course1); + Exam createdExamA = request.postWithResponseBody("/api/courses/" + course1.getId() + "/exams", examA, Exam.class, HttpStatus.CREATED); + createdExamA.setNumberOfCorrectionRoundsInExam(1); + createdExamA.setTestExam(false); + request.putWithResponseBody("/api/courses/" + course1.getId() + "/exams", createdExamA, Exam.class, HttpStatus.CONFLICT); + + // Case 2: real exam should be updated to test exam + Exam examB = ExamFactory.generateTestExam(course1); + examB.setNumberOfCorrectionRoundsInExam(1); + examB.setTestExam(false); + examB.setChannelName("examB"); + Exam createdExamB = request.postWithResponseBody("/api/courses/" + course1.getId() + "/exams", examB, Exam.class, HttpStatus.CREATED); + createdExamB.setTestExam(true); + createdExamB.setNumberOfCorrectionRoundsInExam(0); + request.putWithResponseBody("/api/courses/" + course1.getId() + "/exams", createdExamB, Exam.class, HttpStatus.CONFLICT); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testDeleteStudentForTestExam_badRequest() throws Exception { + // Create an exam with registered students + Exam exam = examUtilService.setupExamWithExerciseGroupsExercisesRegisteredStudents(TEST_PREFIX, course1, 1); + exam.setTestExam(true); + examRepository.save(exam); + + // Remove student1 from the exam + request.delete("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/students/" + TEST_PREFIX + "student1", HttpStatus.BAD_REQUEST); + } + + // ExamResource - getStudentExamForTestExamForStart + @Test + @WithMockUser(username = TEST_PREFIX + "student42", roles = "USER") + void testGetStudentExamForTestExamForStart_notRegisteredInCourse() throws Exception { + request.get("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/start", HttpStatus.FORBIDDEN, String.class); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testGetStudentExamForTestExamForStart_notVisible() throws Exception { + testExam1.setVisibleDate(now().plusMinutes(60)); + testExam1 = examRepository.save(testExam1); + + request.get("/api/courses/" + course1.getId() + "/exams/" + testExam1.getId() + "/start", HttpStatus.FORBIDDEN, StudentExam.class); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testGetStudentExamForTestExamForStart_ExamDoesNotBelongToCourse() throws Exception { + Exam testExam = examUtilService.addTestExam(course2); + + request.get("/api/courses/" + course1.getId() + "/exams/" + testExam.getId() + "/start", HttpStatus.CONFLICT, StudentExam.class); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testGetStudentExamForTestExamForStart_fetchExam_successful() throws Exception { + var testExam = examUtilService.addTestExam(course2); + testExam = examRepository.save(testExam); + var examUser = new ExamUser(); + examUser.setExam(testExam); + examUser.setUser(student1); + examUser = examUserRepository.save(examUser); + testExam.addExamUser(examUser); + examRepository.save(testExam); + var studentExam5 = examUtilService.addStudentExamForTestExam(testExam, student1); + StudentExam studentExamReceived = request.get("/api/courses/" + course2.getId() + "/exams/" + testExam.getId() + "/start", HttpStatus.OK, StudentExam.class); + assertThat(studentExamReceived).isEqualTo(studentExam5); + } +}