Skip to content

Commit

Permalink
Merge branch 'main' into integration-of-MElo
Browse files Browse the repository at this point in the history
  • Loading branch information
TessaKeller committed Jul 8, 2024
2 parents 585484e + 2023cde commit d976579
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 26 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-graphql'
implementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.graphql:spring-graphql-test'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package de.unistuttgart.iste.meitrex.common.testutil;

import org.junit.jupiter.api.extension.*;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.jdbc.JdbcTestUtils;

import javax.sql.DataSource;
import java.sql.*;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
Expand All @@ -26,27 +31,53 @@
public class ClearDatabase implements AfterEachCallback, BeforeAllCallback {

private DataSource dataSource;
private String[] tablesInOrderOfDeletion = null;
private List<String> tablesInOrderOfDeletion = null;

@Override
public void beforeAll(ExtensionContext context) {
context.getTestClass().ifPresent(testClass -> {
if (testClass.isAnnotationPresent(TablesToDelete.class)) {
this.tablesInOrderOfDeletion = testClass.getAnnotation(TablesToDelete.class).value();
this.tablesInOrderOfDeletion = Arrays.asList(testClass.getAnnotation(TablesToDelete.class).value());
}
});
this.dataSource = SpringExtension.getApplicationContext(context).getBean("dataSource", DataSource.class);
}

@Override
public void afterEach(ExtensionContext context) throws SQLException {
deleteTables();
}

private void deleteTables() throws SQLException {
JdbcTemplate template = new JdbcTemplate(this.dataSource);
JdbcTestUtils.deleteFromTables(template, getTablesToDelete());
List<String> notDeletedTables = getTablesToDelete();

while (!notDeletedTables.isEmpty()) {
List<String> tablesToDelete = new ArrayList<>(notDeletedTables);

RuntimeException lastException = new RuntimeException("Could not delete tables");

for (String table : notDeletedTables) {
try {
JdbcTestUtils.deleteFromTables(template, table);
tablesToDelete.remove(table);
} catch (RuntimeException e) {
lastException = e;
}
}

if (tablesToDelete.size() == notDeletedTables.size()) {
// no tables were deleted
throw lastException;
}

notDeletedTables = tablesToDelete;
}
}

private String[] getTablesToDelete() throws SQLException {
private List<String> getTablesToDelete() throws SQLException {
if (this.tablesInOrderOfDeletion == null) {
this.tablesInOrderOfDeletion = getAllDbTableNames(this.dataSource).toArray(new String[0]);
this.tablesInOrderOfDeletion = getAllDbTableNames(this.dataSource);
}
return this.tablesInOrderOfDeletion;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@

import de.unistuttgart.iste.meitrex.common.user_handling.LoggedInUser;
import lombok.SneakyThrows;
import org.junit.jupiter.api.extension.*;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.springframework.graphql.test.tester.GraphQlTester;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
import org.springframework.graphql.test.tester.WebGraphQlTester;
import org.springframework.graphql.test.tester.WebSocketGraphQlTester;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.client.MockMvcWebTestClient;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
import org.springframework.web.reactive.socket.client.WebSocketClient;

import java.lang.reflect.Field;
import java.util.Optional;
Expand All @@ -30,7 +37,7 @@
* public void test(GraphQlTester tester) {
* // ...
* </pre>
*
* <p>
* If the test class has a field annotated with {@link InjectCurrentUserHeader},
* the user header will be automatically added to the tester.
* The field must be of type {@link UUID} or {@link LoggedInUser}.
Expand All @@ -39,38 +46,73 @@
* private UUID userId;
* // ...
* </pre>
*
* This extension, by default, uses a {@link HttpGraphQlTester}, which can also be
* explicitly requested by using a parameter of type {@link HttpGraphQlTester}.
* To use a {@link WebSocketGraphQlTester}, the test method must have a parameter
* of type {@link WebSocketGraphQlTester}.
*/
public class GraphQlTesterParameterResolver implements ParameterResolver {

@Override
public boolean supportsParameter(final ParameterContext parameterContext,
final ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.getParameter().getType().equals(GraphQlTester.class)
|| parameterContext.getParameter().getType().equals(HttpGraphQlTester.class);
final var type = parameterContext.getParameter().getType();

return type.equals(GraphQlTester.class)
|| type.equals(HttpGraphQlTester.class)
|| type.equals(WebSocketGraphQlTester.class)
|| type.equals(WebGraphQlTester.class);
}

@Override
public Object resolveParameter(final ParameterContext parameterContext,
final ExtensionContext extensionContext) throws ParameterResolutionException {

if (parameterContext.getParameter().getType().equals(WebSocketGraphQlTester.class)) {
return createWebSocketGraphQlTester(extensionContext);
} else {
return createHttpGraphQlTester(extensionContext);
}
}

private HttpGraphQlTester createHttpGraphQlTester(final ExtensionContext extensionContext) {
final WebApplicationContext context = (WebApplicationContext) SpringExtension.getApplicationContext(extensionContext);

final WebTestClient webTestClient = MockMvcWebTestClient.bindToApplicationContext(context)
.configureClient()
.baseUrl("/graphql")
.baseUrl(getHttpGraphQlRoute())
.build();

HttpGraphQlTester tester = HttpGraphQlTester.create(webTestClient);

tester = injectCurrentUserHeaderIfNecessary(tester, extensionContext);
return (HttpGraphQlTester) injectCurrentUserHeaderIfNecessary(tester, extensionContext);
}

return tester;
private WebSocketGraphQlTester createWebSocketGraphQlTester(final ExtensionContext extensionContext) {
final String url = "ws://localhost:" + getPort() + getWebSocketGraphQlRoute();
WebSocketClient client = new ReactorNettyWebSocketClient();
WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client).build();

return (WebSocketGraphQlTester) injectCurrentUserHeaderIfNecessary(tester, extensionContext);
}

private String getPort() {
return System.getProperty("server.port");
}

private String getHttpGraphQlRoute() {
return System.getProperty("spring.graphql.path", "/graphql");
}

private String getWebSocketGraphQlRoute() {
return System.getProperty("spring.graphql.websocket.path", "/graphql-ws");
}

@SneakyThrows
@SuppressWarnings("java:S3011")
private HttpGraphQlTester injectCurrentUserHeaderIfNecessary(final HttpGraphQlTester tester,
final ExtensionContext extensionContext) {
private WebGraphQlTester injectCurrentUserHeaderIfNecessary(final WebGraphQlTester tester,
final ExtensionContext extensionContext) {

final Optional<Class<?>> testClass = extensionContext.getTestClass();

Expand All @@ -89,12 +131,14 @@ private HttpGraphQlTester injectCurrentUserHeaderIfNecessary(final HttpGraphQlTe
field.setAccessible(true); // make private fields accessible

if (UUID.class.equals(fieldType)) {
return HeaderUtils.addCurrentUserHeader(tester, (UUID) field.get(extensionContext.getTestInstance().orElseThrow()));
} else if (LoggedInUser.class.equals(fieldType)) {
return HeaderUtils.addCurrentUserHeader(tester, (LoggedInUser) field.get(extensionContext.getTestInstance().orElseThrow()));
} else {
throw new ParameterResolutionException("Field annotated with InjectCurrentUserHeader must be of type UUID or LoggedInUser");
UUID userId = (UUID) field.get(extensionContext.getTestInstance().orElseThrow());
return HeaderUtils.addCurrentUserHeader(tester, userId);
}
if (LoggedInUser.class.equals(fieldType)) {
LoggedInUser user = (LoggedInUser) field.get(extensionContext.getTestInstance().orElseThrow());
return HeaderUtils.addCurrentUserHeader(tester, user);
}
throw new ParameterResolutionException("Field annotated with InjectCurrentUserHeader must be of type UUID or LoggedInUser");
}

return tester;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
import org.springframework.graphql.test.tester.WebGraphQlTester;

import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

/**
* Utility class for adding the current user header to a {@link HttpGraphQlTester}.
Expand All @@ -21,7 +24,7 @@ public class HeaderUtils {
* @param user the user
* @return the tester
*/
public static HttpGraphQlTester addCurrentUserHeader(final HttpGraphQlTester tester, final LoggedInUser user) {
public static WebGraphQlTester addCurrentUserHeader(final WebGraphQlTester tester, final LoggedInUser user) {
return tester.mutate()
.header("CurrentUser", getJson(user))
.build();
Expand All @@ -36,7 +39,7 @@ public static HttpGraphQlTester addCurrentUserHeader(final HttpGraphQlTester tes
* @param userId the user id to use
* @return the tester
*/
public static HttpGraphQlTester addCurrentUserHeader(final HttpGraphQlTester tester, final UUID userId) {
public static WebGraphQlTester addCurrentUserHeader(final WebGraphQlTester tester, final UUID userId) {
final LoggedInUser user = LoggedInUser.builder()
.userName("test")
.id(userId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package de.unistuttgart.iste.meitrex.common.testutil;

import org.hamcrest.FeatureMatcher;
import org.hamcrest.Matcher;
import org.springframework.graphql.ResponseError;

import java.util.Collection;
import java.util.function.Function;

import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasEntry;

/**
* Collection of useful Hamcrest matchers.
*/
public class MeitrexMatchers {
private MeitrexMatchers() {
}

/**
* Matcher that checks if a collection of {@link ResponseError}s contains exactly
* one error that matches the given matcher.
*
* @param errorMatcher the matcher for the response error to check against
* @return the matcher
*/
public static Matcher<Iterable<? extends ResponseError>> containsError(Matcher<ResponseError> errorMatcher) {
return contains(errorMatcher);
}

/**
* Matcher for response errors that checks if the error is caused by a specific exception.
*
* @param exceptionClass the exception class
* @return the matcher
* @see #containsError(Matcher)
*/
public static Matcher<ResponseError> causedBy(Class<? extends Throwable> exceptionClass) {
return hasFeature("exception",
ResponseError::getExtensions,
hasEntry("exception", exceptionClass.getSimpleName()));
}

/**
* Matcher for response errors that checks if the error has a specific message.
*
* @param messageMatcher the message
* @return the matcher
*/
public static Matcher<ResponseError> withMessage(Matcher<String> messageMatcher) {
return hasFeature("message", ResponseError::getMessage, messageMatcher);
}

/**
* Matcher that accesses a function of the actual object and checks if the result matches the given matcher.
* This is useful, e.g., for checking if a certain field of an object has a specific value.
*
* @param accessor the function to access the feature
* @param matcher the matcher for the feature
* @param <T> type of the actual object
* @param <F> type of the feature
* @return the matcher
*/
public static <T, F> Matcher<T> hasFeature(Function<T, F> accessor, Matcher<F> matcher) {
return hasFeature("feature", accessor, matcher);
}

/**
* Like {@link #hasFeature(Function, Matcher)}, but with a custom feature name.
*/
public static <T, R> Matcher<T> hasFeature(String featureName, Function<T, R> accessor, Matcher<R> matcher) {
return new FeatureMatcher<>(matcher, "has " + featureName, featureName) {
@Override
protected R featureValueOf(T actual) {
return accessor.apply(actual);
}
};
}

/**
* Matcher that matches a collection of objects against a collection of matchers.
*
* @param collection the collection to match
* @param matcherFunction the function to create a matcher for each element
* @param <T> the type of the collection elements
* @param <E> the type of the matchers
* @return the array of matchers
*/
@SuppressWarnings("unchecked")
public static <T, E> Matcher<E>[] each(Collection<T> collection, Function<T, Matcher<E>> matcherFunction) {
return (Matcher<E>[]) collection.stream()
.map(matcherFunction)
.toArray(Matcher[]::new);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* This class is a singleton that starts a postgresql container for testing.
* It can be used in two ways:
* <p>
* 1. Use the {@link meitrexPostgresSqlContainer} as a JUnit 5 extension:
* 1. Use the {@link MeitrexPostgresSqlContainer} as a JUnit 5 extension:
* <pre>
* &#64;ExtendWith(meitrexPostgresSqlContainer.class)
* public class MyTest {
Expand All @@ -17,7 +17,7 @@
* </pre>
* This is the preferred way and is automatically done by the {@link GraphQlApiTest} annotation.
* <p>
* 2. Use the {@link meitrexPostgresSqlContainer} as a container:
* 2. Use the {@link MeitrexPostgresSqlContainer} as a container:
* <pre>
* &#64;Testcontainers
* public class MyTest {
Expand Down

0 comments on commit d976579

Please sign in to comment.