Skip to content

Commit

Permalink
add CourseServiceClient
Browse files Browse the repository at this point in the history
  • Loading branch information
julianlxs committed Dec 30, 2024
1 parent 05d9a76 commit d03e11f
Show file tree
Hide file tree
Showing 4 changed files with 393 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package de.unistuttgart.iste.meitrex.course_service.client;

import de.unistuttgart.iste.meitrex.course_service.exception.CourseServiceConnectionException;
import de.unistuttgart.iste.meitrex.generated.dto.CourseMembership;
import org.springframework.graphql.client.ClientGraphQlResponse;
import org.springframework.graphql.client.FieldAccessException;
import org.springframework.graphql.client.GraphQlClient;
import org.springframework.orm.jpa.JpaObjectRetrievalFailureException;
import reactor.core.publisher.SynchronousSink;

import java.util.List;
import java.util.UUID;

/*
Client allowing to query course memberships.
*/
public class CourseServiceClient {

private static final long RETRY_COUNT = 3;
private final GraphQlClient graphQlClient;

public CourseServiceClient(GraphQlClient graphQlClient) {
this.graphQlClient = graphQlClient;
}

public List<CourseMembership> queryMembershipsInCourse(final UUID courseId) throws CourseServiceConnectionException {
if (courseId == null) {
throw new CourseServiceConnectionException("Error fetching courseMemberships from CourseService: Course ID cannot be null");
}

final String query =
"""
query($courseId: UUID!) {
coursesByIds(ids: [$courseId]) {
memberships {
userId
courseId
role
}
}
}
""";
String queryName = "coursesByIds[0].memberships";

List<CourseMembership> courseMembershipList = null;

try {
courseMembershipList = graphQlClient.document(query)
.variable("courseId", courseId)
.execute()
.handle((ClientGraphQlResponse result, SynchronousSink<List<CourseMembership>> sink)
-> handleGraphQlResponse(result, sink, queryName))
.retry(RETRY_COUNT)
.block();
} catch (RuntimeException e) {
if (e.getCause() instanceof JpaObjectRetrievalFailureException && e.getMessage().contains("Entities(s) with id(s) %s not found".formatted(courseId))) {
throw new CourseServiceConnectionException(e.getMessage());
} else {
unwrapCourseServiceConnectionException(e);
}
}

if (courseMembershipList == null) {
throw new CourseServiceConnectionException("Error fetching courseMemberships from CourseService");
}

return courseMembershipList;
}

private void handleGraphQlResponse(final ClientGraphQlResponse result, final SynchronousSink<List<CourseMembership>> sink, final String queryName) {
if (!result.isValid()) {
sink.error(new CourseServiceConnectionException(result.getErrors().toString()));
return;
}

List<CourseMembership> retrievedCourseMemberships;
try {
retrievedCourseMemberships = result.field(queryName).toEntityList(CourseMembership.class);
} catch (FieldAccessException e) {
sink.error(new CourseServiceConnectionException(e.toString()));
return;
}

// retrievedCourseMemberships == null is always false, therefore no check
if (retrievedCourseMemberships.isEmpty()) {
sink.error(new CourseServiceConnectionException("Error fetching courseMemberships from CourseService: CourseMembership List is empty."));
return;
}

sink.next(retrievedCourseMemberships);
}

private static void unwrapCourseServiceConnectionException(final RuntimeException e) throws CourseServiceConnectionException {
// block wraps exceptions in a RuntimeException, so we need to unwrap them
if (e.getCause() instanceof final CourseServiceConnectionException courseServiceConnectionException) {
throw courseServiceConnectionException;
}
// if the exception is not a ContentServiceConnectionException, we don't know how to handle it
throw e;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.unistuttgart.iste.meitrex.course_service.exception;

public class CourseServiceConnectionException extends Exception {
public CourseServiceConnectionException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package de.unistuttgart.iste.meitrex.course_service.client;

import de.unistuttgart.iste.meitrex.common.testutil.GraphQlApiTest;
import de.unistuttgart.iste.meitrex.common.user_handling.LoggedInUser;
import de.unistuttgart.iste.meitrex.course_service.exception.CourseServiceConnectionException;
import de.unistuttgart.iste.meitrex.course_service.persistence.entity.CourseEntity;
import de.unistuttgart.iste.meitrex.course_service.persistence.entity.CourseMembershipEntity;
import de.unistuttgart.iste.meitrex.course_service.persistence.repository.CourseMembershipRepository;
import de.unistuttgart.iste.meitrex.course_service.persistence.repository.CourseRepository;
import de.unistuttgart.iste.meitrex.generated.dto.CourseMembership;
import de.unistuttgart.iste.meitrex.generated.dto.UserRoleInCourse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.graphql.client.GraphQlClient;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.client.MockMvcWebTestClient;
import org.springframework.web.context.WebApplicationContext;

import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import static de.unistuttgart.iste.meitrex.common.testutil.TestUsers.userWithMembershipInCourseWithId;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

@GraphQlApiTest
public class CourseServiceClientTest {

private CourseEntity course;

private GraphQlClient graphQlClient;

@Autowired
private WebApplicationContext applicationContext;

@Autowired
private CourseRepository courseRepository;

@Autowired
private CourseMembershipRepository courseMembershipRepository;

@BeforeEach
void setUp() {
this.course = courseRepository.save(createTestCourse());
LoggedInUser loggedInUser = userWithMembershipInCourseWithId(course.getId(), LoggedInUser.UserRoleInCourse.ADMINISTRATOR);

WebTestClient webTestClient = MockMvcWebTestClient.bindToApplicationContext(applicationContext)
.configureClient().baseUrl("/graphql").defaultHeaders(httpHeaders -> httpHeaders.add("CurrentUser", getJson(loggedInUser))).build();

graphQlClient = GraphQlClient.builder(new WebTestClientTransport(webTestClient)).build();
}

@Test
void testQueryMembershipsInCourse() throws CourseServiceConnectionException {
final CourseServiceClient courseServiceClient = new CourseServiceClient(graphQlClient);

final UUID userId1 = UUID.randomUUID();
final UUID userId2 = UUID.randomUUID();
final UUID userId3 = UUID.randomUUID();

courseMembershipRepository.save(CourseMembershipEntity.builder()
.userId(userId1)
.courseId(course.getId())
.role(UserRoleInCourse.ADMINISTRATOR)
.build());
courseMembershipRepository.save(CourseMembershipEntity.builder()
.userId(userId2)
.courseId(course.getId())
.role(UserRoleInCourse.STUDENT)
.build());
courseMembershipRepository.save(CourseMembershipEntity.builder()
.userId(userId3)
.courseId(course.getId())
.role(UserRoleInCourse.STUDENT)
.build());

List<CourseMembership> queriedMemberships = courseServiceClient.queryMembershipsInCourse(course.getId());

assertThat(queriedMemberships, hasSize(3));

System.out.println(queriedMemberships);

CourseMembership membership1 = queriedMemberships.get(0);
CourseMembership membership2 = queriedMemberships.get(1);
CourseMembership membership3 = queriedMemberships.get(2);

assertThat(membership1.getCourseId(), is(course.getId()));
assertThat(membership1.getRole(), is(UserRoleInCourse.ADMINISTRATOR));
assertThat(membership1.getUserId(), is(userId1));

assertThat(membership2.getCourseId(), is(course.getId()));
assertThat(membership2.getRole(), is(UserRoleInCourse.STUDENT));
assertThat(membership2.getUserId(), is(userId2));

assertThat(membership3.getCourseId(), is(course.getId()));
assertThat(membership3.getRole(), is(UserRoleInCourse.STUDENT));
assertThat(membership3.getUserId(), is(userId3));

List<CourseMembership> queriedStudents = queriedMemberships.stream()
.filter(membership -> membership.getRole() == UserRoleInCourse.STUDENT)
.toList();

assertThat(queriedStudents, hasSize(2));

List<UUID> studentIds = queriedStudents.stream().map(CourseMembership::getUserId).toList();

assertThat(studentIds, hasSize(2));

System.out.println(studentIds);

}


@Test
void testQueryMembershipsInCourseNoMembers() {
final CourseServiceClient courseServiceClient = new CourseServiceClient(graphQlClient);

try {
courseServiceClient.queryMembershipsInCourse(course.getId());
assertThat(true, is(false));
} catch (CourseServiceConnectionException e) {
assertThat(e.getMessage(), is("Error fetching courseMemberships from CourseService: CourseMembership List is empty."));
}

}

@Test
void testQueryMembershipsInCourseWrongCourseId() {
final CourseServiceClient courseServiceClient = new CourseServiceClient(graphQlClient);
final UUID wrongCourseId = UUID.randomUUID();
try {
courseServiceClient.queryMembershipsInCourse(wrongCourseId);
assertThat(true, is(false));
} catch (CourseServiceConnectionException e) {
assertThat(e.getMessage(), containsString("Entities(s) with id(s) %s not found".formatted(wrongCourseId)));
}
}


@Test
void testQueryMembershipsInCourseNullCourseId() {
final CourseServiceClient courseServiceClient = new CourseServiceClient(graphQlClient);
try {
courseServiceClient.queryMembershipsInCourse(null);
assertThat(true, is(false));
} catch (CourseServiceConnectionException e) {
assertThat(e.getMessage(), is("Error fetching courseMemberships from CourseService: Course ID cannot be null"));
}
}

private static CourseEntity createTestCourse() {
return CourseEntity.builder()
.startDate(OffsetDateTime.parse("2021-01-01T00:00:00+00:00"))
.endDate(OffsetDateTime.parse("2021-01-01T00:00:00+00:00"))
.title("Test Course")
.description("Test Description")
.chapters(new ArrayList<>())
.build();
}


/*
Copied from de.unistuttgart.iste.meitrex.common.testutil.HeaderUtils
*/
private static String getJson(final LoggedInUser user) {

final StringBuilder courseMemberships = new StringBuilder().append("[");
final StringBuilder realmRoles = new StringBuilder().append("[");

for (int i = 0; i < user.getCourseMemberships().size(); i++) {
final LoggedInUser.CourseMembership courseMembership = user.getCourseMemberships().get(i);

courseMemberships.append("{")
.append("\"courseId\": \"").append(courseMembership.getCourseId()).append("\",")
.append("\"role\": \"").append(courseMembership.getRole()).append("\",")
.append("\"published\": ").append(courseMembership.isPublished()).append(",")
.append("\"startDate\": \"").append(courseMembership.getStartDate()).append("\",")
.append("\"endDate\": \"").append(courseMembership.getEndDate()).append("\"")
.append("}");

if (i < user.getCourseMemberships().size() - 1) {
courseMemberships.append(",");
}
}

courseMemberships.append("]");

List<String> roleStrings = LoggedInUser.RealmRole.getRoleStringsFromEnum(user.getRealmRoles()).stream().toList();

for (int j = 0; j < roleStrings.size(); j++) {

realmRoles.append("\"")
.append(roleStrings.get(j))
.append("\"");

if (j < roleStrings.size() - 1) {
realmRoles.append(",");
}
}

realmRoles.append("]");

return """
{
"id": "%s",
"userName": "%s",
"firstName": "%s",
"lastName": "%s",
"courseMemberships": %s,
"realmRoles": %s
}
"""
.formatted(user.getId(),
user.getUserName(),
user.getFirstName(),
user.getLastName(),
courseMemberships.toString(),
realmRoles.toString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package de.unistuttgart.iste.meitrex.course_service.client;


// COPY PASTED from org.springframework.graphql.test.tester.WebTestClientTransport
// because it is not public
// author: Rossen Stoyanchev

import org.springframework.core.ParameterizedTypeReference;
import org.springframework.graphql.GraphQlRequest;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.client.GraphQlTransport;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.Collections;
import java.util.Map;

public class WebTestClientTransport implements GraphQlTransport {

private static final ParameterizedTypeReference<Map<String, Object>> MAP_TYPE =
new ParameterizedTypeReference<Map<String, Object>>() {
};


private final WebTestClient webTestClient;


WebTestClientTransport(WebTestClient webTestClient) {
Assert.notNull(webTestClient, "WebTestClient is required");
this.webTestClient = webTestClient;
}


@Override
public Mono<GraphQlResponse> execute(GraphQlRequest request) {

Map<String, Object> responseMap = this.webTestClient.post()
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.bodyValue(request.toMap())
.exchange()
.expectStatus().isOk()
.expectHeader().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)
.expectBody(MAP_TYPE)
.returnResult()
.getResponseBody();

responseMap = (responseMap != null ? responseMap : Collections.emptyMap());
GraphQlResponse response = GraphQlTransport.createResponse(responseMap);
return Mono.just(response);
}

@Override
public Flux<GraphQlResponse> executeSubscription(GraphQlRequest request) {
throw new UnsupportedOperationException("Subscriptions not supported over HTTP");
}

}

0 comments on commit d03e11f

Please sign in to comment.