-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
393 additions
and
0 deletions.
There are no files selected for viewing
102 changes: 102 additions & 0 deletions
102
src/main/java/de/unistuttgart/iste/meitrex/course_service/client/CourseServiceClient.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
7 changes: 7 additions & 0 deletions
7
.../unistuttgart/iste/meitrex/course_service/exception/CourseServiceConnectionException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
223 changes: 223 additions & 0 deletions
223
...test/java/de/unistuttgart/iste/meitrex/course_service/client/CourseServiceClientTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
61 changes: 61 additions & 0 deletions
61
src/test/java/de/unistuttgart/iste/meitrex/course_service/client/WebTestClientTransport.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
|
||
} |