diff --git a/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomApiMappings.kt b/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomApiMappings.kt index cb58fe5e..be4e2c35 100644 --- a/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomApiMappings.kt +++ b/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomApiMappings.kt @@ -6,6 +6,8 @@ import edu.ntnu.idi.idatt.backend.classroom.application.JoinClassroomResult import edu.ntnu.idi.idatt.backend.classroom.application.OwnedClassroomSummary import edu.ntnu.idi.idatt.backend.classroom.application.UpdateClassroomCommand import edu.ntnu.idi.idatt.backend.classroom.domain.Classroom +import edu.ntnu.idi.idatt.backend.classroom.domain.ClassroomPupilStatus +import edu.ntnu.idi.idatt.backend.game.gameProgress.domain.PupilGameProgress import edu.ntnu.idi.idatt.backend.iam.domain.User /** @@ -74,6 +76,10 @@ fun Classroom.toResponse(): ClassroomResponse = title = title, description = description, joinCode = joinCode ?: "", + teacherEmails = teacherMemberships.mapNotNull { it.teacher?.email }, + pupilCount = pupilMemberships.count { it.status == ClassroomPupilStatus.ACTIVE }, + pendingPupilCount = pupilMemberships.count { it.status == ClassroomPupilStatus.PENDING }, + teacherCount = teacherMemberships.size, status = status.name, ownerTeacherId = ownerTeacherId, teachers = teacherResponses(), @@ -87,13 +93,16 @@ fun Classroom.toResponse(): ClassroomResponse = */ fun Classroom.toDetailResponse( pupilDisplayNames: Map = emptyMap(), - pupilProgressByUserId: Map = emptyMap(), + pupilProgressByUserId: Map = emptyMap(), ): ClassroomDetailResponse = ClassroomDetailResponse( - id = id, + classroomId = id, title = title, description = description, joinCode = joinCode ?: "", + pupilCount = pupilMemberships.count { it.status == ClassroomPupilStatus.ACTIVE }, + pendingPupilCount = pupilMemberships.count { it.status == ClassroomPupilStatus.PENDING }, + teacherCount = teacherMemberships.size, status = status.name, ownerTeacherId = ownerTeacherId, teachers = teacherResponses(), @@ -106,6 +115,7 @@ fun Classroom.toDetailResponse( ClassroomPupilResponse( userId = pupilId, displayName = pupilDisplayNames[pupilId] ?: pupil.username ?: "Unknown", + pupilStatus = membership.status, currentLevel = progress?.currentLevel ?: 1, completedStopsCount = progress?.completedStopsCount ?: 0, ) diff --git a/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomDetailResponse.kt b/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomDetailResponse.kt index 8c0fc01e..de384f06 100644 --- a/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomDetailResponse.kt +++ b/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomDetailResponse.kt @@ -3,7 +3,7 @@ package edu.ntnu.idi.idatt.backend.classroom.api /** * DTO for response to retrieving classroom details. * - * @param id the id of the classroom. + * @param classroomId the id of the classroom. * @param title the title of the classroom. * @param description the description of the classroom. * @param joinCode the code for joining the classroom. @@ -13,10 +13,13 @@ package edu.ntnu.idi.idatt.backend.classroom.api * @param pupils a list of the pupils that are members of the classroom. */ data class ClassroomDetailResponse( - val id: Long, + val classroomId: Long, val title: String, val description: String?, val joinCode: String, + val pupilCount: Int, + val pendingPupilCount: Int, + val teacherCount: Int, val status: String, val ownerTeacherId: Long, val teachers: List, diff --git a/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomPupilResponse.kt b/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomPupilResponse.kt index b2cde7b2..9c821ab1 100644 --- a/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomPupilResponse.kt +++ b/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomPupilResponse.kt @@ -1,14 +1,18 @@ package edu.ntnu.idi.idatt.backend.classroom.api +import edu.ntnu.idi.idatt.backend.classroom.domain.ClassroomPupilStatus + /** * DTO for response to retrieving pupil information for a classroom. * * @param userId the id of a pupil in the classroom. * @param displayName the name of a pupil in the classroom. + * @param pupilStatus the pupils pending/approved status. */ data class ClassroomPupilResponse( val userId: Long, val displayName: String, + val pupilStatus: ClassroomPupilStatus, val currentLevel: Int, val completedStopsCount: Int, ) diff --git a/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomResponse.kt b/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomResponse.kt index ab163e5a..fa367a5f 100644 --- a/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomResponse.kt +++ b/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/api/ClassroomResponse.kt @@ -7,6 +7,7 @@ package edu.ntnu.idi.idatt.backend.classroom.api * @param title the title of a classroom. * @param description the optional description of a classroom. * @param joinCode the code to join the classroom. + * @param teacherEmails a list of ids of teahcers belonging to the classroom by email. * @param status whether the classroom is active or inactive. * @param ownerTeacherId the id of the teacher who owns the classroom. * @param teachers a list of teachers belonging to the classroom. @@ -17,6 +18,10 @@ data class ClassroomResponse( val title: String, val description: String?, val joinCode: String, + val teacherEmails: List, + val pupilCount: Int, + val pendingPupilCount: Int, + val teacherCount: Int, val status: String, val ownerTeacherId: Long, val teachers: List, diff --git a/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/application/ClassroomService.kt b/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/application/ClassroomService.kt index 72a080eb..f3c7ef08 100644 --- a/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/application/ClassroomService.kt +++ b/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/application/ClassroomService.kt @@ -1,6 +1,7 @@ package edu.ntnu.idi.idatt.backend.classroom.application import edu.ntnu.idi.idatt.backend.classroom.domain.Classroom +import edu.ntnu.idi.idatt.backend.classroom.domain.ClassroomPupilMembership import edu.ntnu.idi.idatt.backend.classroom.domain.ClassroomPupilStatus import edu.ntnu.idi.idatt.backend.classroom.domain.ClassroomStatus import edu.ntnu.idi.idatt.backend.classroom.infrastructure.ClassroomRepository @@ -124,7 +125,7 @@ class ClassroomService( * * @param command command with classroom code and authenticated user subject * @throws ResponseStatusException when classroom or user does not exist, user is not a pupil, - * user is already a member, or classroom is inactive + * user is already a member of the classroom, user is already a member of a classroom, or classroom is inactive */ @Transactional fun joinClassroom(command: JoinClassroomCommand): JoinClassroomResult { @@ -144,12 +145,18 @@ class ClassroomService( throw ResponseStatusException(HttpStatus.CONFLICT, "User already a member") } + val hasMembership = classroomRepository.findAllByPupilMembershipsPupilId(userId).flatMap { it.pupilMemberships }.any { it.pupil?. id == userId } + if (hasMembership) { + throw ResponseStatusException(HttpStatus.CONFLICT, "User is already a member in a classroom") + } + classroom.addPupilMembership(user) try { userProfileService.updateCurrentClassroom(userId, classroom) } catch (_: NoSuchElementException) { throw ResponseStatusException(HttpStatus.NOT_FOUND, "Profile not found") } + classroomRepository.save(classroom) pupilGameProgressService.createPupilGameProgressEntry( pupilId = userId, @@ -192,6 +199,8 @@ class ClassroomService( throw ResponseStatusException(HttpStatus.NOT_FOUND, "Pupil not found in classroom") } + userProfileService.updateCurrentClassroom(pupilUserId, classroom) + classroomRepository.save(classroom) } @@ -216,6 +225,8 @@ class ClassroomService( classroomRepository.findById(classroomId).orElse(null) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Classroom not found") + userProfileService.updateCurrentClassroom(pupilUserId, null) + try { classroom.removePupil(pupilUserId) } catch (e: IllegalArgumentException) { @@ -352,6 +363,28 @@ class ClassroomService( classroomRepository.deleteById(classroomId) } + /** + * Finds a pupil membership based on the pupil's id. + * + * @param userId persisted pupil user id + * @return pupil membership, or `null`when no membership exists + * @throws IllegalStateException if pupil has more than one classroom membership + */ + @Transactional(readOnly = true) + fun findPupilMembership(userId: Long): ClassroomPupilMembership? { + val memberships = + classroomRepository + .findAllByPupilMembershipsPupilId(userId) + .flatMap { it.pupilMemberships } + .filter { it.pupil?.id == userId } + + if (memberships.size > 1) { + throw IllegalStateException("Invariant violated: Pupil has multiple classroom memberships") + } + + return memberships.firstOrNull() + } + /** * Normalizes a classroom join code by trimming whitespace and converting to uppercase. * diff --git a/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/infrastructure/ClassroomRepository.kt b/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/infrastructure/ClassroomRepository.kt index d8b08fdf..4e44ad14 100644 --- a/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/infrastructure/ClassroomRepository.kt +++ b/src/main/kotlin/edu/ntnu/idi/idatt/backend/classroom/infrastructure/ClassroomRepository.kt @@ -68,4 +68,12 @@ interface ClassroomRepository : JpaRepository { nativeQuery = true, ) fun findAllWithAverageTotalScore(): List + + /** + * Returns classroom memberships for a pupil based on the pupils id. + * + * @param userId the id of the pupil being checked. + * @return a list of classrooms the pupil is a member of. + */ + fun findAllByPupilMembershipsPupilId(userId: Long): List } diff --git a/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/api/CurrentUserProfileApiMappings.kt b/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/api/CurrentUserProfileApiMappings.kt index df14ce97..331b6c05 100644 --- a/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/api/CurrentUserProfileApiMappings.kt +++ b/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/api/CurrentUserProfileApiMappings.kt @@ -1,7 +1,7 @@ package edu.ntnu.idi.idatt.backend.users.profile.api -import edu.ntnu.idi.idatt.backend.users.profile.application.CurrentUserProfile -import edu.ntnu.idi.idatt.backend.users.profile.application.PupilProfileDetails +import edu.ntnu.idi.idatt.backend.users.profile.application.CurrentUserProfileService.CurrentUserProfile +import edu.ntnu.idi.idatt.backend.users.profile.application.CurrentUserProfileService.PupilProfileDetails import edu.ntnu.idi.idatt.backend.users.profile.application.UpdateCurrentUserDisplayNameCommand /** @@ -20,6 +20,7 @@ fun CurrentUserProfile.toCurrentUserProfileResponse(): CurrentUserProfileRespons PupilProfileResponse( displayName = it.displayName, currentClassroomId = it.currentClassroomId, + membershipStatus = it.membershipStatus, totalXp = it.totalXp, currentLevel = it.currentLevel, hasCompletedOnboarding = it.hasCompletedOnboarding, @@ -43,6 +44,7 @@ fun PupilProfileDetails.toPupilProfileResponse(): PupilProfileResponse = PupilProfileResponse( displayName = displayName, currentClassroomId = currentClassroomId, + membershipStatus = membershipStatus, totalXp = totalXp, currentLevel = currentLevel, hasCompletedOnboarding = hasCompletedOnboarding, diff --git a/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/api/CurrentUserProfileResponse.kt b/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/api/CurrentUserProfileResponse.kt index 2edda0b7..dc2fadc2 100644 --- a/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/api/CurrentUserProfileResponse.kt +++ b/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/api/CurrentUserProfileResponse.kt @@ -32,6 +32,7 @@ data class CurrentUserProfileResponse( data class PupilProfileResponse( val displayName: String, val currentClassroomId: Long?, + val membershipStatus: String? = null, val totalXp: Int, val currentLevel: Int, val hasCompletedOnboarding: Boolean, diff --git a/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/application/CurrentUserProfileService.kt b/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/application/CurrentUserProfileService.kt index 8cb7c049..4dd39fce 100644 --- a/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/application/CurrentUserProfileService.kt +++ b/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/application/CurrentUserProfileService.kt @@ -1,5 +1,6 @@ package edu.ntnu.idi.idatt.backend.users.profile.application +import edu.ntnu.idi.idatt.backend.classroom.application.ClassroomService import edu.ntnu.idi.idatt.backend.game.gameProgress.application.PupilGameProgressService import edu.ntnu.idi.idatt.backend.game.gameProgress.domain.PupilGameProgress import edu.ntnu.idi.idatt.backend.iam.application.CurrentUserResolver @@ -16,12 +17,14 @@ import org.springframework.web.server.ResponseStatusException * @property currentUserResolver resolver to authenticate users * @property userProfileService service used to load profile data * @property pupilGameProgressService service used to load classroom-scoped game progress + * @property classroomService service used to load classroom data */ @Service class CurrentUserProfileService( private val currentUserResolver: CurrentUserResolver, private val userProfileService: UserProfileService, private val pupilGameProgressService: PupilGameProgressService, + private val classroomService: ClassroomService, ) { /** * Returns the current authenticated user's profile. @@ -40,10 +43,12 @@ class CurrentUserProfileService( .findPupilProfile(userId) ?.let { val progress = getCurrentClassroomProgress(userId, it.currentClassroom?.id) + val membership = classroomService.findPupilMembership(userId) PupilProfileDetails( displayName = it.displayName, currentClassroomId = it.currentClassroom?.id, + membershipStatus = membership?.status?.name, totalXp = progress?.totalXp ?: 0, currentLevel = progress?.currentLevel ?: 1, hasCompletedOnboarding = it.hasCompletedOnboarding, @@ -98,9 +103,15 @@ class CurrentUserProfileService( pupilProfile.displayName = command.displayName.trim() val savedProfile = userProfileService.savePupilProfile(pupilProfile) + val membership = + savedProfile.currentClassroom + ?.pupilMemberships + ?.firstOrNull { it.pupil?.id == userId } + return PupilProfileDetails( displayName = savedProfile.displayName, currentClassroomId = savedProfile.currentClassroom?.id, + membershipStatus = membership?.status?.name, totalXp = progress?.totalXp ?: 0, currentLevel = progress?.currentLevel ?: 1, hasCompletedOnboarding = savedProfile.hasCompletedOnboarding, @@ -119,6 +130,7 @@ class CurrentUserProfileService( val userId = requireNotNull(user.id) { "Authenticated user is missing an id" } val pupilProfile = findPupilProfile(userId) val progress = getCurrentClassroomProgress(userId, pupilProfile.currentClassroom?.id) + val membership = classroomService.findPupilMembership(userId) pupilProfile.hasCompletedOnboarding = true val savedProfile = userProfileService.savePupilProfile(pupilProfile) @@ -127,6 +139,7 @@ class CurrentUserProfileService( currentClassroomId = savedProfile.currentClassroom?.id, totalXp = progress?.totalXp ?: 0, currentLevel = progress?.currentLevel ?: 1, + membershipStatus = membership?.status?.name, hasCompletedOnboarding = savedProfile.hasCompletedOnboarding, ) } @@ -152,50 +165,51 @@ class CurrentUserProfileService( userId: Long, classroomId: Long?, ): PupilGameProgress? = classroomId?.let { pupilGameProgressService.getPupilProgress(userId, it) } -} -/** - * Internal representation of the authenticated user's profile. - * - * @property userId owning user id - * @property role persisted user role - * @property email teacher/admin email, or `null` for pupils - * @property username pupil username, or `null` for teachers/admins - * @property pupilProfile pupil-specific profile data, or `null` when not applicable - * @property teacherProfile teacher-specific profile data, or `null` when not applicable - */ -data class CurrentUserProfile( - val userId: Long, - val role: String, - val email: String?, - val username: String?, - val pupilProfile: PupilProfileDetails?, - val teacherProfile: TeacherProfileDetails?, -) + /** + * Internal representation of the authenticated user's profile. + * + * @property userId owning user id + * @property role persisted user role + * @property email teacher/admin email, or `null` for pupils + * @property username pupil username, or `null` for teachers/admins + * @property pupilProfile pupil-specific profile data, or `null` when not applicable + * @property teacherProfile teacher-specific profile data, or `null` when not applicable + */ + data class CurrentUserProfile( + val userId: Long, + val role: String, + val email: String?, + val username: String?, + val pupilProfile: PupilProfileDetails?, + val teacherProfile: TeacherProfileDetails?, + ) -/** - * Internal pupil profile projection. - * - * @property displayName pupil-facing display name - * @property currentClassroomId active classroom id, or `null` when the pupil has none selected - * @property totalXp classroom-scoped accumulated XP for the active classroom - * @property currentLevel classroom-scoped level derived from the active classroom XP total - */ -data class PupilProfileDetails( - val displayName: String, - val currentClassroomId: Long?, - val totalXp: Int, - val currentLevel: Int, - val hasCompletedOnboarding: Boolean, -) + /** + * Internal pupil profile projection. + * + * @property displayName pupil-facing display name + * @property currentClassroomId active classroom id, or `null` when the pupil has none selected + * @property totalXp classroom-scoped accumulated XP for the active classroom + * @property currentLevel classroom-scoped level derived from the active classroom XP total + */ + data class PupilProfileDetails( + val displayName: String, + val currentClassroomId: Long?, + val totalXp: Int, + val currentLevel: Int, + val membershipStatus: String?, + val hasCompletedOnboarding: Boolean, + ) -/** - * Internal teacher profile projection reserved for future support. - * - * @property teacherName teacher full name shown in the product - * @property schoolName school name associated with the teacher - */ -data class TeacherProfileDetails( - val teacherName: String, - val schoolName: String, -) + /** + * Internal teacher profile projection reserved for future support. + * + * @property teacherName teacher full name shown in the product + * @property schoolName school name associated with the teacher + */ + data class TeacherProfileDetails( + val teacherName: String, + val schoolName: String, + ) +} diff --git a/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/application/UserProfileService.kt b/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/application/UserProfileService.kt index ce60ed59..c33635f7 100644 --- a/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/application/UserProfileService.kt +++ b/src/main/kotlin/edu/ntnu/idi/idatt/backend/users/profile/application/UserProfileService.kt @@ -99,7 +99,7 @@ class UserProfileService( */ fun updateCurrentClassroom( pupilUserId: Long, - classroom: Classroom, + classroom: Classroom?, ): PupilProfile { val profile = pupilProfileRepository diff --git a/src/test/kotlin/edu/ntnu/idi/idatt/backend/classroom/ClassroomIntegrationTests.kt b/src/test/kotlin/edu/ntnu/idi/idatt/backend/classroom/ClassroomIntegrationTests.kt index cc0c611a..50e17ad4 100644 --- a/src/test/kotlin/edu/ntnu/idi/idatt/backend/classroom/ClassroomIntegrationTests.kt +++ b/src/test/kotlin/edu/ntnu/idi/idatt/backend/classroom/ClassroomIntegrationTests.kt @@ -286,41 +286,30 @@ class ClassroomIntegrationTests { } @Test - fun `should get classroom by id`() { - val classroom = createClassroom("DETAIL1") - val pupil = userRepository.save(activePupil(username = "detailuser")) - pupilProfileRepository.save(PupilProfile(user = pupil, displayName = "detail", currentClassroom = classroom)) - pupilGameProgressRepository.save( - PupilGameProgress( - id = PupilGameProgressId(pupilUserId = requireNotNull(pupil.id), classroomId = requireNotNull(classroom.id)), - pupil = pupil, - classroom = classroom, - currentLevel = 4, - completedStopsCount = 2, - ), - ) - val token = pupilAccessToken(username = "detailuser", password = "secret123") - - mockMvc - .post("/api/v1/classrooms/join") { - with(csrf()) - contentType = MediaType.APPLICATION_JSON - header(HttpHeaders.AUTHORIZATION, "Bearer $token") - content = """{"classroomCode":"DETAIL1"}""" - }.andExpect { - status { isOk() } - } + fun `teacher should get owned classroom by id`() { + val teacher = userRepository.save(activeTeacher(email = "teacher-detail@example.com")) + val token = teacherPortalAccessToken("teacher-detail@example.com", "secret123") + + val classroom = + classroomRepository.save( + Classroom( + ownerTeacherId = requireNotNull(teacher.id), + title = "Class DETAIL1", + description = "Integration test classroom", + joinCode = "DETAIL1", + status = ClassroomStatus.ACTIVE, + createdAt = Instant.now(), + updatedAt = Instant.now(), + ), + ) mockMvc - .get("/api/v1/classrooms/${classroom.id}") { + .get("/api/v1/classrooms/owned/${classroom.id}") { header(HttpHeaders.AUTHORIZATION, "Bearer $token") }.andExpect { status { isOk() } jsonPath("$.id") { value(classroom.id.toInt()) } jsonPath("$.title") { value("Class DETAIL1") } - jsonPath("$.pupils[0].displayName") { value("detail") } - jsonPath("$.pupils[0].currentLevel") { value(4) } - jsonPath("$.pupils[0].completedStopsCount") { value(2) } } } diff --git a/src/test/kotlin/edu/ntnu/idi/idatt/backend/classroom/ClassroomServiceTest.kt b/src/test/kotlin/edu/ntnu/idi/idatt/backend/classroom/ClassroomServiceTest.kt index ae9bb8c2..ead787d7 100644 --- a/src/test/kotlin/edu/ntnu/idi/idatt/backend/classroom/ClassroomServiceTest.kt +++ b/src/test/kotlin/edu/ntnu/idi/idatt/backend/classroom/ClassroomServiceTest.kt @@ -120,8 +120,6 @@ class ClassroomServiceTest { assertEquals("PENDING", result.membershipStatus) verify(classroomRepository).save(classroom) - verify(pupilProfileRepository).save(pupilProfile) - assertEquals(classroom, pupilProfile.currentClassroom) verify(pupilGameProgressService).createPupilGameProgressEntry(3L, 1L) verify(classroomRepository).findByJoinCode(classroomCode) } @@ -258,17 +256,16 @@ class ClassroomServiceTest { val pupilProfile = PupilProfile(userId = 3, user = user, displayName = "test", currentClassroom = null) + `when`(pupilProfileRepository.findById(3)).thenReturn(Optional.of(pupilProfile)) + `when`(classroomRepository.findByJoinCode("ABC123")).thenReturn(classroom) `when`(currentUserResolver.requirePupil("3")).thenReturn(user) - `when`(pupilProfileRepository.findById(3)).thenReturn(Optional.of(pupilProfile)) classroomService.joinClassroom( JoinClassroomCommand(classroomCode = " abc123 ", authenticatedSubject = "3"), ) verify(classroomRepository).findByJoinCode("ABC123") - verify(pupilProfileRepository).save(pupilProfile) - assertEquals(classroom, pupilProfile.currentClassroom) } @Test @@ -276,12 +273,15 @@ class ClassroomServiceTest { val classroom = Classroom(id = 1, ownerTeacherId = 2, title = "class", description = "description", joinCode = "ABC123", status = ClassroomStatus.ACTIVE) val pupil = User(id = 3, email = null, username = "pupil", passwordHash = "password", role = UserRole.PUPIL, status = UserStatus.ACTIVE) val teacher = User(id = 2, email = "test@test.com", username = "teacher", passwordHash = "password", role = UserRole.TEACHER, status = UserStatus.ACTIVE) + val pupilProfile = PupilProfile(userId = 3, user = pupil, displayName = "test", currentClassroom = null) classroom.addTeacher(teacher) classroom.addPupilMembership(pupil) `when`(classroomRepository.findById(1)).thenReturn(Optional.of(classroom)) + `when`(pupilProfileRepository.findById(3)).thenReturn(Optional.of(pupilProfile)) + `when`(currentUserResolver.requireTeacherOrAdmin("teacher")).thenReturn(teacher) classroomService.approvePupil(1, 3, "teacher") @@ -312,12 +312,14 @@ class ClassroomServiceTest { val classroom = Classroom(id = 1, ownerTeacherId = 2, title = "class", description = "description", joinCode = "ABC123", status = ClassroomStatus.ACTIVE) val pupil = User(id = 3, email = null, username = "pupil", passwordHash = "password", role = UserRole.PUPIL, status = UserStatus.ACTIVE) val teacher = User(id = 2, email = "test@test.com", username = "teacher", passwordHash = "password", role = UserRole.TEACHER, status = UserStatus.ACTIVE) + val pupilProfile = PupilProfile(userId = 3, user = pupil, displayName = "test", currentClassroom = null) classroom.addTeacher(teacher) classroom.addPupilMembership(pupil) `when`(classroomRepository.findById(1)).thenReturn(Optional.of(classroom)) `when`(currentUserResolver.requireTeacherOrAdmin("teacher")).thenReturn(teacher) + `when`(pupilProfileRepository.findById(3)).thenReturn(Optional.of(pupilProfile)) classroomService.removePupil(1, 3, "teacher") verify(classroomRepository).findById(1) diff --git a/src/test/kotlin/edu/ntnu/idi/idatt/backend/notebook/NotebookEntryControllerIntegrationTests.kt b/src/test/kotlin/edu/ntnu/idi/idatt/backend/notebook/NotebookEntryControllerIntegrationTests.kt index e6472da4..cdc25c25 100644 --- a/src/test/kotlin/edu/ntnu/idi/idatt/backend/notebook/NotebookEntryControllerIntegrationTests.kt +++ b/src/test/kotlin/edu/ntnu/idi/idatt/backend/notebook/NotebookEntryControllerIntegrationTests.kt @@ -91,8 +91,8 @@ class NotebookEntryControllerIntegrationTests { ) val pupil = userRepository.save(activePupil("ada")) val otherPupil = userRepository.save(activePupil("grace")) - pupilProfileRepository.save(pupilProfile(pupil, "Ada", classroom)) - pupilProfileRepository.save(pupilProfile(otherPupil, "Grace", classroom)) + pupilProfileRepository.save(PupilProfile(user = pupil, displayName = "Ada", currentClassroom = classroom)) + pupilProfileRepository.save(PupilProfile(user = otherPupil, displayName = "Grace", currentClassroom = classroom)) notebookEntryRepository.save( NotebookEntry( pupil = pupil, @@ -230,7 +230,7 @@ class NotebookEntryControllerIntegrationTests { MapStop(slug = "intro", title = "Intro", displayOrder = 1), ) val pupil = userRepository.save(activePupil("ada")) - pupilProfileRepository.save(pupilProfile(pupil, "Ada", classroom)) + pupilProfileRepository.save(PupilProfile(user = pupil, displayName = "Ada", currentClassroom = classroom)) val accessToken = pupilAccessToken("ada", "secret123") val body = @@ -277,7 +277,7 @@ class NotebookEntryControllerIntegrationTests { MapStop(slug = "intro", title = "Intro", displayOrder = 1), ) val pupil = userRepository.save(activePupil("ada")) - pupilProfileRepository.save(pupilProfile(pupil, "Ada", classroom)) + pupilProfileRepository.save(PupilProfile(user = pupil, displayName = "Ada", currentClassroom = classroom)) val entry = notebookEntryRepository.save( NotebookEntry( @@ -363,8 +363,8 @@ class NotebookEntryControllerIntegrationTests { ) val pupil = userRepository.save(activePupil("ada")) val otherPupil = userRepository.save(activePupil("grace")) - pupilProfileRepository.save(pupilProfile(pupil, "Ada", classroom)) - pupilProfileRepository.save(pupilProfile(otherPupil, "Grace", classroom)) + pupilProfileRepository.save(PupilProfile(user = pupil, displayName = "Ada", currentClassroom = classroom)) + pupilProfileRepository.save(PupilProfile(user = otherPupil, displayName = "Grace", currentClassroom = classroom)) val entry = notebookEntryRepository.save( NotebookEntry( diff --git a/src/test/kotlin/edu/ntnu/idi/idatt/backend/users/profile/CurrentUserProfileServiceTests.kt b/src/test/kotlin/edu/ntnu/idi/idatt/backend/users/profile/CurrentUserProfileServiceTests.kt index 30345bd1..a315e800 100644 --- a/src/test/kotlin/edu/ntnu/idi/idatt/backend/users/profile/CurrentUserProfileServiceTests.kt +++ b/src/test/kotlin/edu/ntnu/idi/idatt/backend/users/profile/CurrentUserProfileServiceTests.kt @@ -1,5 +1,6 @@ package edu.ntnu.idi.idatt.backend.users.profile +import edu.ntnu.idi.idatt.backend.classroom.application.ClassroomService import edu.ntnu.idi.idatt.backend.classroom.domain.Classroom import edu.ntnu.idi.idatt.backend.game.gameProgress.application.PupilGameProgressService import edu.ntnu.idi.idatt.backend.game.gameProgress.domain.PupilGameProgress @@ -17,6 +18,7 @@ import edu.ntnu.idi.idatt.backend.users.profile.infrastructure.TeacherProfileRep import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.mockito.Mockito import org.springframework.http.HttpStatus import org.springframework.web.server.ResponseStatusException @@ -29,9 +31,17 @@ class CurrentUserProfileServiceTests { val pupilProfileRepository = Mockito.mock(PupilProfileRepository::class.java) val pupilGameProgressService = Mockito.mock(PupilGameProgressService::class.java) val teacherProfileRepository = Mockito.mock(TeacherProfileRepository::class.java) + val classroomService = Mockito.mock(ClassroomService::class.java) val classroom = Classroom(id = 101L, ownerTeacherId = 7L, title = "Class A") val service = - currentUserProfileService(currentUserResolver, pupilProfileRepository, pupilGameProgressService, teacherProfileRepository) + currentUserProfileService( + currentUserResolver, + pupilProfileRepository, + pupilGameProgressService, + teacherProfileRepository, + classroomService, + ) + val user = pupilUser(42L) val pupilProfile = PupilProfile( @@ -68,8 +78,15 @@ class CurrentUserProfileServiceTests { val pupilProfileRepository = Mockito.mock(PupilProfileRepository::class.java) val pupilGameProgressService = Mockito.mock(PupilGameProgressService::class.java) val teacherProfileRepository = Mockito.mock(TeacherProfileRepository::class.java) + val classroomService = Mockito.mock(ClassroomService::class.java) val service = - currentUserProfileService(currentUserResolver, pupilProfileRepository, pupilGameProgressService, teacherProfileRepository) + currentUserProfileService( + currentUserResolver, + pupilProfileRepository, + pupilGameProgressService, + teacherProfileRepository, + classroomService, + ) val user = pupilUser(42L) val pupilProfile = PupilProfile( @@ -96,9 +113,16 @@ class CurrentUserProfileServiceTests { val pupilProfileRepository = Mockito.mock(PupilProfileRepository::class.java) val pupilGameProgressService = Mockito.mock(PupilGameProgressService::class.java) val teacherProfileRepository = Mockito.mock(TeacherProfileRepository::class.java) + val classroomService = Mockito.mock(ClassroomService::class.java) val classroom = Classroom(id = 101L, ownerTeacherId = 7L, title = "Class A") val service = - currentUserProfileService(currentUserResolver, pupilProfileRepository, pupilGameProgressService, teacherProfileRepository) + currentUserProfileService( + currentUserResolver, + pupilProfileRepository, + pupilGameProgressService, + teacherProfileRepository, + classroomService, + ) val user = pupilUser(42L) val pupilProfile = PupilProfile( @@ -125,8 +149,15 @@ class CurrentUserProfileServiceTests { val pupilProfileRepository = Mockito.mock(PupilProfileRepository::class.java) val pupilGameProgressService = Mockito.mock(PupilGameProgressService::class.java) val teacherProfileRepository = Mockito.mock(TeacherProfileRepository::class.java) + val classroomService = Mockito.mock(ClassroomService::class.java) val service = - currentUserProfileService(currentUserResolver, pupilProfileRepository, pupilGameProgressService, teacherProfileRepository) + currentUserProfileService( + currentUserResolver, + pupilProfileRepository, + pupilGameProgressService, + teacherProfileRepository, + classroomService, + ) val user = teacherUser(7L) val teacherProfile = TeacherProfile( @@ -157,14 +188,21 @@ class CurrentUserProfileServiceTests { val pupilProfileRepository = Mockito.mock(PupilProfileRepository::class.java) val pupilGameProgressService = Mockito.mock(PupilGameProgressService::class.java) val teacherProfileRepository = Mockito.mock(TeacherProfileRepository::class.java) + val classroomService = Mockito.mock(ClassroomService::class.java) val service = - currentUserProfileService(currentUserResolver, pupilProfileRepository, pupilGameProgressService, teacherProfileRepository) + currentUserProfileService( + currentUserResolver, + pupilProfileRepository, + pupilGameProgressService, + teacherProfileRepository, + classroomService, + ) val exception = ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid authenticated user subject") Mockito.`when`(currentUserResolver.requireUser("not-a-number")).thenThrow(exception) val thrown = - org.junit.jupiter.api.assertThrows { + assertThrows { service.getCurrentUserProfile("not-a-number") } @@ -177,14 +215,21 @@ class CurrentUserProfileServiceTests { val pupilProfileRepository = Mockito.mock(PupilProfileRepository::class.java) val pupilGameProgressService = Mockito.mock(PupilGameProgressService::class.java) val teacherProfileRepository = Mockito.mock(TeacherProfileRepository::class.java) + val classroomService = Mockito.mock(ClassroomService::class.java) val service = - currentUserProfileService(currentUserResolver, pupilProfileRepository, pupilGameProgressService, teacherProfileRepository) + currentUserProfileService( + currentUserResolver, + pupilProfileRepository, + pupilGameProgressService, + teacherProfileRepository, + classroomService, + ) val exception = ResponseStatusException(HttpStatus.UNAUTHORIZED, "Authenticated user was not found") Mockito.`when`(currentUserResolver.requireUser("42")).thenThrow(exception) val thrown = - org.junit.jupiter.api.assertThrows { + assertThrows { service.getCurrentUserProfile("42") } @@ -197,15 +242,22 @@ class CurrentUserProfileServiceTests { val pupilProfileRepository = Mockito.mock(PupilProfileRepository::class.java) val pupilGameProgressService = Mockito.mock(PupilGameProgressService::class.java) val teacherProfileRepository = Mockito.mock(TeacherProfileRepository::class.java) + val classroomService = Mockito.mock(ClassroomService::class.java) val service = - currentUserProfileService(currentUserResolver, pupilProfileRepository, pupilGameProgressService, teacherProfileRepository) + currentUserProfileService( + currentUserResolver, + pupilProfileRepository, + pupilGameProgressService, + teacherProfileRepository, + classroomService, + ) val user = pupilUser(42L) Mockito.`when`(currentUserResolver.requireUser("42")).thenReturn(user) Mockito.`when`(pupilProfileRepository.findById(42L)).thenReturn(Optional.empty()) val exception = - org.junit.jupiter.api.assertThrows { + assertThrows { service.getCurrentUserProfile("42") } @@ -218,15 +270,22 @@ class CurrentUserProfileServiceTests { val pupilProfileRepository = Mockito.mock(PupilProfileRepository::class.java) val pupilGameProgressService = Mockito.mock(PupilGameProgressService::class.java) val teacherProfileRepository = Mockito.mock(TeacherProfileRepository::class.java) + val classroomService = Mockito.mock(ClassroomService::class.java) val service = - currentUserProfileService(currentUserResolver, pupilProfileRepository, pupilGameProgressService, teacherProfileRepository) + currentUserProfileService( + currentUserResolver, + pupilProfileRepository, + pupilGameProgressService, + teacherProfileRepository, + classroomService, + ) val user = teacherUser(7L) Mockito.`when`(currentUserResolver.requireUser("7")).thenReturn(user) Mockito.`when`(teacherProfileRepository.findById(7L)).thenReturn(Optional.empty()) val exception = - org.junit.jupiter.api.assertThrows { + assertThrows { service.getCurrentUserProfile("7") } @@ -239,9 +298,16 @@ class CurrentUserProfileServiceTests { val pupilProfileRepository = Mockito.mock(PupilProfileRepository::class.java) val pupilGameProgressService = Mockito.mock(PupilGameProgressService::class.java) val teacherProfileRepository = Mockito.mock(TeacherProfileRepository::class.java) + val classroomService = Mockito.mock(ClassroomService::class.java) val classroom = Classroom(id = 101L, ownerTeacherId = 7L, title = "Class A") val service = - currentUserProfileService(currentUserResolver, pupilProfileRepository, pupilGameProgressService, teacherProfileRepository) + currentUserProfileService( + currentUserResolver, + pupilProfileRepository, + pupilGameProgressService, + teacherProfileRepository, + classroomService, + ) val user = pupilUser(42L) val pupilProfile = PupilProfile( @@ -281,9 +347,16 @@ class CurrentUserProfileServiceTests { val pupilProfileRepository = Mockito.mock(PupilProfileRepository::class.java) val pupilGameProgressService = Mockito.mock(PupilGameProgressService::class.java) val teacherProfileRepository = Mockito.mock(TeacherProfileRepository::class.java) + val classroomService = Mockito.mock(ClassroomService::class.java) val classroom = Classroom(id = 101L, ownerTeacherId = 7L, title = "Class A") val service = - currentUserProfileService(currentUserResolver, pupilProfileRepository, pupilGameProgressService, teacherProfileRepository) + currentUserProfileService( + currentUserResolver, + pupilProfileRepository, + pupilGameProgressService, + teacherProfileRepository, + classroomService, + ) val user = pupilUser(42L) val pupilProfile = PupilProfile( @@ -319,14 +392,21 @@ class CurrentUserProfileServiceTests { val pupilProfileRepository = Mockito.mock(PupilProfileRepository::class.java) val pupilGameProgressService = Mockito.mock(PupilGameProgressService::class.java) val teacherProfileRepository = Mockito.mock(TeacherProfileRepository::class.java) + val classroomService = Mockito.mock(ClassroomService::class.java) val service = - currentUserProfileService(currentUserResolver, pupilProfileRepository, pupilGameProgressService, teacherProfileRepository) + currentUserProfileService( + currentUserResolver, + pupilProfileRepository, + pupilGameProgressService, + teacherProfileRepository, + classroomService, + ) val exception = ResponseStatusException(HttpStatus.FORBIDDEN, "Only pupils are allowed") Mockito.`when`(currentUserResolver.requirePupil("7")).thenThrow(exception) val thrown = - org.junit.jupiter.api.assertThrows { + assertThrows { service.updateCurrentUserDisplayName( "7", UpdateCurrentUserDisplayNameCommand(displayName = "Teacher"), @@ -342,15 +422,22 @@ class CurrentUserProfileServiceTests { val pupilProfileRepository = Mockito.mock(PupilProfileRepository::class.java) val pupilGameProgressService = Mockito.mock(PupilGameProgressService::class.java) val teacherProfileRepository = Mockito.mock(TeacherProfileRepository::class.java) + val classroomService = Mockito.mock(ClassroomService::class.java) val service = - currentUserProfileService(currentUserResolver, pupilProfileRepository, pupilGameProgressService, teacherProfileRepository) + currentUserProfileService( + currentUserResolver, + pupilProfileRepository, + pupilGameProgressService, + teacherProfileRepository, + classroomService, + ) val user = pupilUser(42L) Mockito.`when`(currentUserResolver.requirePupil("42")).thenReturn(user) Mockito.`when`(pupilProfileRepository.findById(42L)).thenReturn(Optional.empty()) val thrown = - org.junit.jupiter.api.assertThrows { + assertThrows { service.updateCurrentUserDisplayName( "42", UpdateCurrentUserDisplayNameCommand(displayName = "Ada"), @@ -383,10 +470,12 @@ class CurrentUserProfileServiceTests { pupilProfileRepository: PupilProfileRepository, pupilGameProgressService: PupilGameProgressService, teacherProfileRepository: TeacherProfileRepository, + classroomService: ClassroomService, ): CurrentUserProfileService = CurrentUserProfileService( currentUserResolver, UserProfileService(pupilProfileRepository, teacherProfileRepository), pupilGameProgressService, + classroomService, ) }