From bd3a56b01f945b464dd40feddb740433c9edaf5f Mon Sep 17 00:00:00 2001 From: mp-dg <57503145+MP-DG@users.noreply.github.com> Date: Thu, 27 Nov 2025 00:53:50 +0100 Subject: [PATCH 1/4] sends UserHexadPlayerTypeSet event when the PlayerType is evaluated --- build.gradle | 2 +- .../service/PlayerHexadScoreService.java | 51 ++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 2f6fbd2..8119b0a 100755 --- a/build.gradle +++ b/build.gradle @@ -119,7 +119,7 @@ repositories { dependencies { - implementation 'de.unistuttgart.iste.meitrex:meitrex-common:1.4.11' + implementation 'de.unistuttgart.iste.meitrex:meitrex-common:1.4.12pre2' implementation 'de.unistuttgart.iste.meitrex:content_service:1.6.0' implementation 'de.unistuttgart.iste.meitrex:course_service:1.1.0rc2' implementation 'de.unistuttgart.iste.meitrex:user_service:1.0.0rc1' diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/PlayerHexadScoreService.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/PlayerHexadScoreService.java index 249084d..780295c 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/PlayerHexadScoreService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/PlayerHexadScoreService.java @@ -3,12 +3,13 @@ import java.util.*; import de.unistuttgart.iste.meitrex.common.dapr.TopicPublisher; +import de.unistuttgart.iste.meitrex.common.event.HexadPlayerType; import de.unistuttgart.iste.meitrex.common.event.ServerSource; +import de.unistuttgart.iste.meitrex.common.event.UserHexadPlayerTypeSetEvent; import de.unistuttgart.iste.meitrex.gamification_service.persistence.entity.PlayerHexadScoreQuestionEntity; import de.unistuttgart.iste.meitrex.gamification_service.persistence.entity.UserEntity; import de.unistuttgart.iste.meitrex.gamification_service.persistence.repository.IPlayerHexadScoreQuestionRepository; import de.unistuttgart.iste.meitrex.gamification_service.service.internal.IUserCreator; -import io.dapr.client.DaprClient; import org.springframework.stereotype.Service; import de.unistuttgart.iste.meitrex.gamification_service.persistence.entity.PlayerHexadScoreEntity; @@ -69,6 +70,8 @@ public PlayerHexadScore evaluate(UUID userId, PlayerAnswerInput input, String us playerHexadScoreEntity.setUser(user); + sendUserHexadPlayerTypeSetEvent(userId, playerHexadScore); + return playerHexadScore; } @@ -173,5 +176,51 @@ public Boolean hasHexadScore(UUID userId) { return user.getPlayerHexadScore() != null; } + /** + * Publishes an event with the userId, PlayerHexadScore of the user and his primary player type + * @param userId the ID of the user + * @param playerHexadScore the updated player hexad score + */ + private void sendUserHexadPlayerTypeSetEvent(UUID userId, PlayerHexadScore playerHexadScore) { + HexadPlayerType primaryPlayerType = null; + Map scoresMap = new HashMap<>(); + + double maxScore = Double.NEGATIVE_INFINITY; + + for (PlayerTypeScore score : playerHexadScore.getScores()) { + HexadPlayerType hexadType = mapPlayerTypeToHexadPlayerType(score.getType()); + scoresMap.put(hexadType, score.getValue()); + + if (score.getValue() > maxScore) { + maxScore = score.getValue(); + primaryPlayerType = hexadType; + } + } + + UserHexadPlayerTypeSetEvent event = UserHexadPlayerTypeSetEvent.builder() + .userId(userId) + .primaryPlayerType(primaryPlayerType) + .playerTypePercentages(scoresMap) + .build(); + + topicPublisher.notifyUserHexadPlayerTypeSet(event); + } + + /** + * Maps the GraphQL PlayerType enum to the common HexadPlayerType enum + * @param playerType the GraphQL player type + * @return the corresponding HexadPlayerType + */ + private HexadPlayerType mapPlayerTypeToHexadPlayerType(PlayerType playerType) { + return switch (playerType) { + case ACHIEVER -> HexadPlayerType.ACHIEVER; + case PLAYER -> HexadPlayerType.PLAYER; + case SOCIALISER -> HexadPlayerType.SOCIALISER; + case FREE_SPIRIT -> HexadPlayerType.FREE_SPIRIT; + case PHILANTHROPIST -> HexadPlayerType.PHILANTHROPIST; + case DISRUPTOR -> HexadPlayerType.DISRUPTOR; + }; + } + } From 6cd764f74a4e4560d07de1882984fd2fd1656d90 Mon Sep 17 00:00:00 2001 From: mp-dg <57503145+MP-DG@users.noreply.github.com> Date: Thu, 27 Nov 2025 02:57:11 +0100 Subject: [PATCH 2/4] listens to RequestHexadPlayerTypeEvent. Once recieved, sends UserHexadPlayerTypeSetEvent for the given user --- build.gradle | 2 +- .../RequestHexadPlayerTypeEventListener.java | 49 +++++++++++++++++++ .../service/IPlayerHexadScoreService.java | 7 +++ .../service/PlayerHexadScoreService.java | 2 +- 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/main/java/de/unistuttgart/iste/meitrex/gamification_service/dapr/RequestHexadPlayerTypeEventListener.java diff --git a/build.gradle b/build.gradle index 8119b0a..27437e5 100755 --- a/build.gradle +++ b/build.gradle @@ -119,7 +119,7 @@ repositories { dependencies { - implementation 'de.unistuttgart.iste.meitrex:meitrex-common:1.4.12pre2' + implementation 'de.unistuttgart.iste.meitrex:meitrex-common:1.4.12pre4' implementation 'de.unistuttgart.iste.meitrex:content_service:1.6.0' implementation 'de.unistuttgart.iste.meitrex:course_service:1.1.0rc2' implementation 'de.unistuttgart.iste.meitrex:user_service:1.0.0rc1' diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/dapr/RequestHexadPlayerTypeEventListener.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/dapr/RequestHexadPlayerTypeEventListener.java new file mode 100644 index 0000000..a5d7afc --- /dev/null +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/dapr/RequestHexadPlayerTypeEventListener.java @@ -0,0 +1,49 @@ +package de.unistuttgart.iste.meitrex.gamification_service.dapr; + +import de.unistuttgart.iste.meitrex.common.dapr.TopicPublisher; +import de.unistuttgart.iste.meitrex.common.event.RequestHexadPlayerTypeEvent; +import de.unistuttgart.iste.meitrex.gamification_service.service.IPlayerHexadScoreService; +import io.dapr.Topic; +import io.dapr.client.domain.CloudEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * Listener for RequestHexadPlayerTypeEvent. + * When a request is received, it fetches the user's hexad player type + * and publishes a UserHexadPlayerTypeSetEvent in response. + */ +@Slf4j +@RestController +@RequiredArgsConstructor +public class RequestHexadPlayerTypeEventListener { + + private final IPlayerHexadScoreService playerHexadScoreService; + private final TopicPublisher topicPublisher; + + @Topic(name = "request-hexad-player-type", pubsubName = "meitrex") + @PostMapping(path = "/request-hexad-player-type-pubsub") + public void onRequestHexadPlayerTypeEvent(@RequestBody CloudEvent cloudEvent) { + RequestHexadPlayerTypeEvent event = cloudEvent.getData(); + + log.info("Received RequestHexadPlayerTypeEvent for user: {}", event.getUserId()); + + try { + // Fetch the player hexad score for the user + var playerHexadScore = playerHexadScoreService.getById(event.getUserId()); + + if (playerHexadScore != null) { + log.info("Publishing UserHexadPlayerTypeSetEvent for user: {}", event.getUserId()); + playerHexadScoreService.sendUserHexadPlayerTypeSetEvent(event.getUserId(), playerHexadScore); + } else { + log.warn("No hexad player type found for user: {}", event.getUserId()); + } + } catch (Exception e) { + log.error("Error handling RequestHexadPlayerTypeEvent for user {}: {}", + event.getUserId(), e.getMessage(), e); + } + } +} diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/IPlayerHexadScoreService.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/IPlayerHexadScoreService.java index ef5f7f8..7f379c2 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/IPlayerHexadScoreService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/IPlayerHexadScoreService.java @@ -51,4 +51,11 @@ public interface IPlayerHexadScoreService { * @return true if a hexad score exists otherwise false */ Boolean hasHexadScore(UUID userId); + + /** + * Publishes an UserHexadPlayerTypeSetEvent + * @param userId the ID of the user + * @param playerHexadScore the player hexad score of the user + */ + void sendUserHexadPlayerTypeSetEvent(UUID userId, PlayerHexadScore playerHexadScore); } diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/PlayerHexadScoreService.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/PlayerHexadScoreService.java index 780295c..4faa7a8 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/PlayerHexadScoreService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/PlayerHexadScoreService.java @@ -181,7 +181,7 @@ public Boolean hasHexadScore(UUID userId) { * @param userId the ID of the user * @param playerHexadScore the updated player hexad score */ - private void sendUserHexadPlayerTypeSetEvent(UUID userId, PlayerHexadScore playerHexadScore) { + public void sendUserHexadPlayerTypeSetEvent(UUID userId, PlayerHexadScore playerHexadScore) { HexadPlayerType primaryPlayerType = null; Map scoresMap = new HashMap<>(); From 2eae4e80792944c21f66451bbfe52fac6087e89d Mon Sep 17 00:00:00 2001 From: mp-dg <57503145+MP-DG@users.noreply.github.com> Date: Mon, 15 Dec 2025 21:16:39 +0100 Subject: [PATCH 3/4] updated meitrex-common to 1.4.12 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 27437e5..1ce5c11 100755 --- a/build.gradle +++ b/build.gradle @@ -119,7 +119,7 @@ repositories { dependencies { - implementation 'de.unistuttgart.iste.meitrex:meitrex-common:1.4.12pre4' + implementation 'de.unistuttgart.iste.meitrex:meitrex-common:1.4.12' implementation 'de.unistuttgart.iste.meitrex:content_service:1.6.0' implementation 'de.unistuttgart.iste.meitrex:course_service:1.1.0rc2' implementation 'de.unistuttgart.iste.meitrex:user_service:1.0.0rc1' @@ -139,7 +139,7 @@ dependencies { runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' annotationProcessor 'org.projectlombok:lombok' - testImplementation 'de.unistuttgart.iste.meitrex:meitrex-common-test:1.4.11' + testImplementation 'de.unistuttgart.iste.meitrex:meitrex-common-test:1.4.12' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework:spring-webflux' testImplementation 'org.springframework.graphql:spring-graphql-test' From 4360f0e0da717a59c617aca56d8f845322c22d25 Mon Sep 17 00:00:00 2001 From: mp-dg <57503145+MP-DG@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:08:26 +0100 Subject: [PATCH 4/4] added tests for RequestHexadPlayerTypeEvent --- .gitignore | 4 + ...questHexadPlayerTypeEventListenerTest.java | 127 ++++++++++++++++++ .../service/PlayerHexadScoreServiceTest.java | 83 ++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 src/test/java/de/unistuttgart/iste/meitrex/gamification_service/dapr/RequestHexadPlayerTypeEventListenerTest.java diff --git a/.gitignore b/.gitignore index 891e458..0a638ed 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,8 @@ out/ ### VS Code ### .vscode/ +# Auto-generated API documentation +**/api.md +**/combined.graphql + node_modules/ diff --git a/src/test/java/de/unistuttgart/iste/meitrex/gamification_service/dapr/RequestHexadPlayerTypeEventListenerTest.java b/src/test/java/de/unistuttgart/iste/meitrex/gamification_service/dapr/RequestHexadPlayerTypeEventListenerTest.java new file mode 100644 index 0000000..9baddd4 --- /dev/null +++ b/src/test/java/de/unistuttgart/iste/meitrex/gamification_service/dapr/RequestHexadPlayerTypeEventListenerTest.java @@ -0,0 +1,127 @@ +package de.unistuttgart.iste.meitrex.gamification_service.dapr; + +import de.unistuttgart.iste.meitrex.common.dapr.TopicPublisher; +import de.unistuttgart.iste.meitrex.common.event.RequestHexadPlayerTypeEvent; +import de.unistuttgart.iste.meitrex.gamification_service.service.IPlayerHexadScoreService; +import de.unistuttgart.iste.meitrex.generated.dto.PlayerHexadScore; +import de.unistuttgart.iste.meitrex.generated.dto.PlayerType; +import de.unistuttgart.iste.meitrex.generated.dto.PlayerTypeScore; +import io.dapr.client.domain.CloudEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static org.mockito.Mockito.*; + +/** + * Unit tests for RequestHexadPlayerTypeEventListener. + */ +@ExtendWith(MockitoExtension.class) +class RequestHexadPlayerTypeEventListenerTest { + + @Mock + private IPlayerHexadScoreService playerHexadScoreService; + + @Mock + private TopicPublisher topicPublisher; + + @InjectMocks + private RequestHexadPlayerTypeEventListener eventListener; + + private UUID userId; + private CloudEvent cloudEvent; + + @BeforeEach + void setUp() { + userId = UUID.randomUUID(); + RequestHexadPlayerTypeEvent event = RequestHexadPlayerTypeEvent.builder() + .userId(userId) + .build(); + cloudEvent = mock(CloudEvent.class); + when(cloudEvent.getData()).thenReturn(event); + } + + /** + * Test handling of RequestHexadPlayerTypeEvent with existing player type. + */ + @Test + void testOnRequestHexadPlayerTypeEvent_WithExistingPlayerType() { + PlayerHexadScore playerHexadScore = createMockPlayerHexadScore(); + when(playerHexadScoreService.getById(userId)).thenReturn(playerHexadScore); + + eventListener.onRequestHexadPlayerTypeEvent(cloudEvent); + + verify(playerHexadScoreService, times(1)).getById(userId); + verify(playerHexadScoreService, times(1)) + .sendUserHexadPlayerTypeSetEvent(userId, playerHexadScore); + } + + /** + * Test handling of RequestHexadPlayerTypeEvent with null player type. + */ + @Test + void testOnRequestHexadPlayerTypeEvent_WithNullPlayerType() { + when(playerHexadScoreService.getById(userId)).thenReturn(null); + + eventListener.onRequestHexadPlayerTypeEvent(cloudEvent); + + verify(playerHexadScoreService, times(1)).getById(userId); + verify(playerHexadScoreService, never()) + .sendUserHexadPlayerTypeSetEvent(any(), any()); + } + + /** + * Test handling of RequestHexadPlayerTypeEvent when an exception occurs. + */ + @Test + void testOnRequestHexadPlayerTypeEvent_WithException() { + when(playerHexadScoreService.getById(userId)) + .thenThrow(new RuntimeException("Database error")); + + eventListener.onRequestHexadPlayerTypeEvent(cloudEvent); + + verify(playerHexadScoreService, times(1)).getById(userId); + verify(playerHexadScoreService, never()) + .sendUserHexadPlayerTypeSetEvent(any(), any()); + } + + /** + * Test handling of RequestHexadPlayerTypeEvent when sending event throws an exception. + */ + @Test + void testOnRequestHexadPlayerTypeEvent_SendEventThrowsException() { + PlayerHexadScore playerHexadScore = createMockPlayerHexadScore(); + when(playerHexadScoreService.getById(userId)).thenReturn(playerHexadScore); + doThrow(new RuntimeException("Event publishing error")) + .when(playerHexadScoreService) + .sendUserHexadPlayerTypeSetEvent(userId, playerHexadScore); + + eventListener.onRequestHexadPlayerTypeEvent(cloudEvent); + + verify(playerHexadScoreService, times(1)).getById(userId); + verify(playerHexadScoreService, times(1)) + .sendUserHexadPlayerTypeSetEvent(userId, playerHexadScore); + } + + /** + * Creates a mock PlayerHexadScore for testing. Values are arbitrary and not significant. + */ + private PlayerHexadScore createMockPlayerHexadScore() { + List scores = Arrays.asList( + new PlayerTypeScore(PlayerType.ACHIEVER, 0.85), + new PlayerTypeScore(PlayerType.PLAYER, 0.70), + new PlayerTypeScore(PlayerType.SOCIALISER, 0.65), + new PlayerTypeScore(PlayerType.FREE_SPIRIT, 0.60), + new PlayerTypeScore(PlayerType.PHILANTHROPIST, 0.55), + new PlayerTypeScore(PlayerType.DISRUPTOR, 0.50) + ); + return new PlayerHexadScore(false, scores); + } +} diff --git a/src/test/java/de/unistuttgart/iste/meitrex/gamification_service/service/PlayerHexadScoreServiceTest.java b/src/test/java/de/unistuttgart/iste/meitrex/gamification_service/service/PlayerHexadScoreServiceTest.java index 280343f..b9b1b06 100644 --- a/src/test/java/de/unistuttgart/iste/meitrex/gamification_service/service/PlayerHexadScoreServiceTest.java +++ b/src/test/java/de/unistuttgart/iste/meitrex/gamification_service/service/PlayerHexadScoreServiceTest.java @@ -220,4 +220,87 @@ public void testPlayerHexadScoreExists() { .thenReturn(user); assertTrue(spyService.hasHexadScore(UUID.randomUUID())); } + + @Test + public void testSendUserHexadPlayerTypeSetEvent_CorrectlyMapsAndPublishes() { + UUID userId = UUID.randomUUID(); + List scores = Arrays.asList( + new PlayerTypeScore(PlayerType.ACHIEVER, 0.85), + new PlayerTypeScore(PlayerType.PLAYER, 0.70), + new PlayerTypeScore(PlayerType.SOCIALISER, 0.65), + new PlayerTypeScore(PlayerType.FREE_SPIRIT, 0.60), + new PlayerTypeScore(PlayerType.PHILANTHROPIST, 0.55), + new PlayerTypeScore(PlayerType.DISRUPTOR, 0.50) + ); + PlayerHexadScore playerHexadScore = new PlayerHexadScore(false, scores); + + playerHexadScoreService.sendUserHexadPlayerTypeSetEvent(userId, playerHexadScore); + + verify(mockTopicPublisher, times(1)).notifyUserHexadPlayerTypeSet(any()); + } + + @Test + public void testSendUserHexadPlayerTypeSetEvent_IdentifiesPhilanthropistAsPrimary() { + UUID userId = UUID.randomUUID(); + List scores = Arrays.asList( + new PlayerTypeScore(PlayerType.ACHIEVER, 0.60), + new PlayerTypeScore(PlayerType.PLAYER, 0.55), + new PlayerTypeScore(PlayerType.SOCIALISER, 0.50), + new PlayerTypeScore(PlayerType.FREE_SPIRIT, 0.45), + new PlayerTypeScore(PlayerType.PHILANTHROPIST, 0.90), // Highest + new PlayerTypeScore(PlayerType.DISRUPTOR, 0.40) + ); + PlayerHexadScore playerHexadScore = new PlayerHexadScore(false, scores); + + playerHexadScoreService.sendUserHexadPlayerTypeSetEvent(userId, playerHexadScore); + + ArgumentCaptor eventCaptor = + ArgumentCaptor.forClass(de.unistuttgart.iste.meitrex.common.event.UserHexadPlayerTypeSetEvent.class); + verify(mockTopicPublisher, times(1)).notifyUserHexadPlayerTypeSet(eventCaptor.capture()); + + de.unistuttgart.iste.meitrex.common.event.UserHexadPlayerTypeSetEvent capturedEvent = eventCaptor.getValue(); + assertEquals(userId, capturedEvent.getUserId()); + assertEquals(de.unistuttgart.iste.meitrex.common.event.HexadPlayerType.PHILANTHROPIST, capturedEvent.getPrimaryPlayerType()); + assertEquals(0.90, capturedEvent.getPlayerTypePercentages().get(de.unistuttgart.iste.meitrex.common.event.HexadPlayerType.PHILANTHROPIST)); + } + + @Test + public void testSendUserHexadPlayerTypeSetEvent_IdentifiesDisruptorAsPrimary() { + UUID userId = UUID.randomUUID(); + List scores = Arrays.asList( + new PlayerTypeScore(PlayerType.ACHIEVER, 0.45), + new PlayerTypeScore(PlayerType.PLAYER, 0.50), + new PlayerTypeScore(PlayerType.SOCIALISER, 0.55), + new PlayerTypeScore(PlayerType.FREE_SPIRIT, 0.40), + new PlayerTypeScore(PlayerType.PHILANTHROPIST, 0.60), + new PlayerTypeScore(PlayerType.DISRUPTOR, 0.95) // Highest + ); + PlayerHexadScore playerHexadScore = new PlayerHexadScore(false, scores); + + playerHexadScoreService.sendUserHexadPlayerTypeSetEvent(userId, playerHexadScore); + + ArgumentCaptor eventCaptor = + ArgumentCaptor.forClass(de.unistuttgart.iste.meitrex.common.event.UserHexadPlayerTypeSetEvent.class); + verify(mockTopicPublisher, times(1)).notifyUserHexadPlayerTypeSet(eventCaptor.capture()); + + de.unistuttgart.iste.meitrex.common.event.UserHexadPlayerTypeSetEvent capturedEvent = eventCaptor.getValue(); + assertEquals(userId, capturedEvent.getUserId()); + assertEquals(de.unistuttgart.iste.meitrex.common.event.HexadPlayerType.DISRUPTOR, capturedEvent.getPrimaryPlayerType()); + assertEquals(0.95, capturedEvent.getPlayerTypePercentages().get(de.unistuttgart.iste.meitrex.common.event.HexadPlayerType.DISRUPTOR)); + } + + @Test + public void testEvaluate_PublishesEventAfterCalculation() { + String username = "Test user"; + UUID userId = UUID.randomUUID(); + PlayerHexadScoreService spyService = spy(playerHexadScoreService); + + when(input.getQuestions()).thenReturn(Collections.emptyList()); + when(userCreator.fetchOrCreate(userId)).thenReturn(new UserEntity()); + + spyService.evaluate(userId, input, username); + + verify(spyService, times(1)).sendUserHexadPlayerTypeSetEvent(eq(userId), any()); + verify(mockTopicPublisher, times(1)).notifyUserHexadPlayerTypeSet(any()); + } }