Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ out/
### VS Code ###
.vscode/

# Auto-generated API documentation
**/api.md
**/combined.graphql

node_modules/
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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.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'
Expand All @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RequestHexadPlayerTypeEvent> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,6 +70,8 @@ public PlayerHexadScore evaluate(UUID userId, PlayerAnswerInput input, String us

playerHexadScoreEntity.setUser(user);

sendUserHexadPlayerTypeSetEvent(userId, playerHexadScore);

return playerHexadScore;
}

Expand Down Expand Up @@ -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
*/
public void sendUserHexadPlayerTypeSetEvent(UUID userId, PlayerHexadScore playerHexadScore) {
HexadPlayerType primaryPlayerType = null;
Map<HexadPlayerType, Double> 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;
};
}


}
Original file line number Diff line number Diff line change
@@ -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<RequestHexadPlayerTypeEvent> 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<PlayerTypeScore> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -220,4 +220,87 @@ public void testPlayerHexadScoreExists() {
.thenReturn(user);
assertTrue(spyService.hasHexadScore(UUID.randomUUID()));
}

@Test
public void testSendUserHexadPlayerTypeSetEvent_CorrectlyMapsAndPublishes() {
UUID userId = UUID.randomUUID();
List<PlayerTypeScore> 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<PlayerTypeScore> 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<de.unistuttgart.iste.meitrex.common.event.UserHexadPlayerTypeSetEvent> 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<PlayerTypeScore> 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<de.unistuttgart.iste.meitrex.common.event.UserHexadPlayerTypeSetEvent> 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());
}
}
Loading