diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/DemoApplication.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/DemoApplication.kt index 0380d35a6..2bb40c607 100644 --- a/dgs-kotlin-co/src/main/kotlin/com/example/demo/DemoApplication.kt +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/DemoApplication.kt @@ -4,14 +4,13 @@ import com.example.demo.model.PostEntity import com.example.demo.repository.CommentRepository import com.example.demo.repository.PostRepository import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.runBlocking import org.slf4j.LoggerFactory -import org.springframework.boot.ApplicationArguments -import org.springframework.boot.ApplicationRunner import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.event.ApplicationReadyEvent import org.springframework.boot.runApplication import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.event.EventListener import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity import org.springframework.security.config.web.server.ServerHttpSecurity import org.springframework.security.web.server.SecurityWebFilterChain @@ -26,20 +25,24 @@ fun main(args: Array) { } @Component -class DataInitializer(val posts: PostRepository, val comments: CommentRepository) : ApplicationRunner { - private val log = LoggerFactory.getLogger(DataInitializer::class.java) - override fun run(args: ApplicationArguments?) { +class DataInitializer(val posts: PostRepository, val comments: CommentRepository) { + + companion object { + private val log = LoggerFactory.getLogger(DataInitializer::class.java) + } + + @EventListener(ApplicationReadyEvent::class) + suspend fun init() { val data = listOf( PostEntity(title = "Learn Spring", content = "content of Learn Spring"), PostEntity(title = "Learn Dgs framework", content = "content of Learn Dgs framework") ) - runBlocking { - comments.deleteAll() - posts.deleteAll() - val saved = posts.saveAll(data).toList() - saved.forEach { log.debug("saved: {}", it) } - } + comments.deleteAll() + posts.deleteAll() + + val saved = posts.saveAll(data).toList() + saved.forEach { log.debug("saved: {}", it) } } } @@ -51,7 +54,7 @@ class SecurityConfig { fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { return http .csrf { it.disable() } - .httpBasic{} + .httpBasic {} .securityMatcher(PathPatternParserServerWebExchangeMatcher("/graphql")) .build() } diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/ExceptionHandlers.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/ExceptionHandlers.kt index 0c0b42ea5..ec238b65b 100644 --- a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/ExceptionHandlers.kt +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/ExceptionHandlers.kt @@ -18,7 +18,7 @@ class ExceptionHandlers : DataFetcherExceptionHandler { when (val exception = handlerParameters.exception) { is PostNotFoundException, is AuthorNotFoundException -> { val graphqlError = TypedGraphQLError.newNotFoundBuilder() - .message(exception.message) + .message(exception.message?: "Not Found") .path(handlerParameters.path) .build(); CompletableFuture.completedFuture( diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/AuthorsDataFetcher.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/AuthorsDataFetcher.kt index ce3c0804b..2da4ea811 100644 --- a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/AuthorsDataFetcher.kt +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/AuthorsDataFetcher.kt @@ -19,7 +19,7 @@ class AuthorsDataFetcher( @DgsData(parentType = DgsConstants.AUTHOR.TYPE_NAME, field = DgsConstants.AUTHOR.Posts) suspend fun posts(dfe: DgsDataFetchingEnvironment): List { - val a: Author = dfe.getSource() + val a: Author = dfe.getSource()!! return postService.getPostsByAuthorId(a.id).toList() } } \ No newline at end of file diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/PostsDataFetcher.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/PostsDataFetcher.kt index 3ddfffae1..6a94efb5e 100644 --- a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/PostsDataFetcher.kt +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/PostsDataFetcher.kt @@ -23,13 +23,13 @@ class PostsDataFetcher(val postService: PostService) { fun author(dfe: DgsDataFetchingEnvironment): CompletableFuture { val dataLoader = dfe.getDataLoader("authorsLoader") val post = dfe.getSource() - return dataLoader.load(post.authorId) + return dataLoader!!.load(post!!.authorId) } @DgsData(parentType = DgsConstants.POST.TYPE_NAME, field = DgsConstants.POST.Comments) fun comments(dfe: DgsDataFetchingEnvironment): CompletableFuture> { val dataLoader = dfe.getDataLoader>(CommentsDataLoader::class.java) - val (id) = dfe.getSource() + val (id) = dfe.getSource()!! return dataLoader.load(id) } diff --git a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/scalars/LocalDateTimeScalar.kt b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/scalars/LocalDateTimeScalar.kt index 98adea02e..090bca208 100644 --- a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/scalars/LocalDateTimeScalar.kt +++ b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/scalars/LocalDateTimeScalar.kt @@ -1,43 +1,47 @@ package com.example.demo.gql.scalars import com.netflix.graphql.dgs.DgsScalar +import graphql.GraphQLContext +import graphql.execution.CoercedVariables import graphql.language.StringValue +import graphql.language.Value import graphql.schema.Coercing import graphql.schema.CoercingParseLiteralException import graphql.schema.CoercingParseValueException import graphql.schema.CoercingSerializeException import java.time.LocalDateTime import java.time.format.DateTimeFormatter - - -//@DgsComponent -//class DateTimeScalar { -// @DgsRuntimeWiring -// fun addScalar(builder: RuntimeWiring.Builder): RuntimeWiring.Builder { -// return builder.scalar(ExtendedScalars.DateTime) -// } -//} +import java.util.* @DgsScalar(name = "LocalDateTime") class LocalDateTimeScalar : Coercing { - @Throws(CoercingSerializeException::class) - override fun serialize(dataFetcherResult: Any): String? { + override fun serialize(dataFetcherResult: Any, graphQLContext: GraphQLContext, locale: Locale): String? { return when (dataFetcherResult) { is LocalDateTime -> dataFetcherResult.format(DateTimeFormatter.ISO_DATE_TIME) else -> throw CoercingSerializeException("Not a valid DateTime") } } - @Throws(CoercingParseValueException::class) - override fun parseValue(input: Any): LocalDateTime { + override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): LocalDateTime? { return LocalDateTime.parse(input.toString(), DateTimeFormatter.ISO_DATE_TIME) } - @Throws(CoercingParseLiteralException::class) - override fun parseLiteral(input: Any): LocalDateTime { + override fun parseLiteral( + input: Value<*>, + variables: CoercedVariables, + graphQLContext: GraphQLContext, + locale: Locale + ): LocalDateTime? { when (input) { is StringValue -> return LocalDateTime.parse(input.value, DateTimeFormatter.ISO_DATE_TIME) else -> throw CoercingParseLiteralException("Value is not a valid ISO date time") } } + + override fun valueToLiteral(input: Any, graphQLContext: GraphQLContext, locale: Locale): Value<*> { + return when (input) { + is String -> StringValue.newStringValue(input).build() + else -> throw CoercingParseValueException("Value is not a string") + } + } } diff --git a/dgs-subscription-ws/src/test/kotlin/com/example/demo/DemoApplicationTestsWithGraphQLClient.kt b/dgs-subscription-ws/src/test/kotlin/com/example/demo/DemoApplicationTestsWithGraphQLClient.kt index 2ddd1d8ad..aa04b51af 100644 --- a/dgs-subscription-ws/src/test/kotlin/com/example/demo/DemoApplicationTestsWithGraphQLClient.kt +++ b/dgs-subscription-ws/src/test/kotlin/com/example/demo/DemoApplicationTestsWithGraphQLClient.kt @@ -1,23 +1,21 @@ package com.example.demo import com.jayway.jsonpath.TypeRef -import com.netflix.graphql.dgs.DgsQueryExecutor +import com.netflix.graphql.dgs.client.WebClientGraphQLClient import com.netflix.graphql.dgs.client.WebSocketGraphQLClient import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient import reactor.test.StepVerifier +import java.time.Duration @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class DemoApplicationTestsWithGraphQLClient { - - @Autowired - lateinit var dgsQueryExecutor: DgsQueryExecutor - + lateinit var webClientGraphQLClient: WebClientGraphQLClient lateinit var socketGraphQLClient: WebSocketGraphQLClient @LocalServerPort @@ -25,52 +23,72 @@ class DemoApplicationTestsWithGraphQLClient { @BeforeEach fun setup() { - this.socketGraphQLClient = WebSocketGraphQLClient("ws://localhost:$port/subscriptions", ReactorNettyWebSocketClient()) + this.webClientGraphQLClient = WebClientGraphQLClient(WebClient.create("http://localhost:$port/graphql")) + this.socketGraphQLClient = + WebSocketGraphQLClient("ws://localhost:$port/subscriptions", ReactorNettyWebSocketClient()) } @Test fun testMessages() { - //Hooks.onOperatorDebug(); - val query = "subscription { messageSent { body } }" + val messageSentSubscriptionQuery = """ + subscription { + messageSent { + body + } + } + """.trimIndent() val variables = emptyMap() - val executionResult = socketGraphQLClient.reactiveExecuteQuery(query, variables) + val executionResult = socketGraphQLClient.reactiveExecuteQuery(messageSentSubscriptionQuery, variables) .map { it.extractValueAsObject( "data.messageSent", object : TypeRef>() {} )["body"] as String } + + val message1 = "text1" + val message2 = "text2" val verifier = StepVerifier.create(executionResult) - .consumeNextWith { assertThat(it).isEqualTo("text1 message") } - // .consumeNextWith { assertThat(it).isEqualTo("text2 message") } + .thenAwait(Duration.ofMillis(1000)) // see: https://github.com/Netflix/dgs-framework/issues/657 + .consumeNextWith { assertThat(it).isEqualTo(message1) } + .consumeNextWith { assertThat(it).isEqualTo(message2) } .thenCancel() .verifyLater() - val sendText1 = dgsQueryExecutor.executeAndExtractJsonPath( - "mutation sendMessage(\$msg: TextMessageInput!) { send(message:\$msg) { body}}", - "data.send.body", - mapOf("msg" to (mapOf("body" to "text1 message"))) - ) - assertThat(sendText1).contains("text1"); + val sendMessageQuery =""" + mutation sendMessage(${"$"}msg: TextMessageInput!) { + send(message:${"$"}msg) { + body + } + } + """.trimMargin() + webClientGraphQLClient.reactiveExecuteQuery(sendMessageQuery, mapOf("msg" to (mapOf("body" to message1)))) + .map { it.extractValueAsObject("data.send.body", String::class.java) } + .`as` { StepVerifier.create(it) } + .consumeNextWith { assertThat(it).isEqualTo(message1) } + .verifyComplete() -// val sendText2 = dgsQueryExecutor.executeAndExtractJsonPath( -// "mutation sendMessage(\$msg: TextMessageInput!) { send(message:\$msg) { body}}", -// "data.send.body", -// mapOf("msg" to (mapOf("body" to "text2 message"))) -// ) -// assertThat(sendText2).contains("text2"); + webClientGraphQLClient.reactiveExecuteQuery(sendMessageQuery, mapOf("msg" to (mapOf("body" to message2)))) + .map { it.extractValueAsObject("data.send.body", String::class.java) } + .`as` { StepVerifier.create(it) } + .consumeNextWith { assertThat(it).isEqualTo(message2) } + .verifyComplete() //verify it now. verifier.verify() - val msgs = dgsQueryExecutor.executeAndExtractJsonPath>( - " { messages { body }}", - "data.messages[*].body" - ) - assertThat(msgs).allMatch { s: String -> - s.contains( - "message" - ) - } + val allMessagesQuery = """ + { + messages{ + body + } + } + """.trimIndent(); + webClientGraphQLClient.reactiveExecuteQuery(allMessagesQuery) + .map { it.extractValueAsObject("data.messages[*].body", object : TypeRef>() {}) } + .`as` { StepVerifier.create(it) } + .consumeNextWith { assertThat(it).isEqualTo(message1) } + .consumeNextWith { assertThat(it).isEqualTo(message2) } + .verifyComplete() } } diff --git a/dgs-webflux/src/test/java/com/example/demo/MutationTests.java b/dgs-webflux/src/test/java/com/example/demo/MutationTests.java index 47ae6a263..9d39fd373 100644 --- a/dgs-webflux/src/test/java/com/example/demo/MutationTests.java +++ b/dgs-webflux/src/test/java/com/example/demo/MutationTests.java @@ -15,9 +15,9 @@ import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -45,10 +45,10 @@ static class MutationTestsConfig { @Autowired DgsReactiveQueryExecutor dgsQueryExecutor; - @MockBean + @MockitoBean PostService postService; - @MockBean + @MockitoBean AuthorService authorService; @Test diff --git a/dgs-webflux/src/test/java/com/example/demo/QueryTests.java b/dgs-webflux/src/test/java/com/example/demo/QueryTests.java index 993585e69..c31c75da1 100644 --- a/dgs-webflux/src/test/java/com/example/demo/QueryTests.java +++ b/dgs-webflux/src/test/java/com/example/demo/QueryTests.java @@ -18,6 +18,7 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -35,10 +36,10 @@ class QueryTests { @Autowired DgsReactiveQueryExecutor dgsQueryExecutor; - @MockBean + @MockitoBean PostService postService; - @MockBean + @MockitoBean AuthorService authorService; @Configuration diff --git a/dgs-webflux/src/test/java/com/example/demo/SubscriptionTests.java b/dgs-webflux/src/test/java/com/example/demo/SubscriptionTests.java index fb2e12d15..73396fdd7 100644 --- a/dgs-webflux/src/test/java/com/example/demo/SubscriptionTests.java +++ b/dgs-webflux/src/test/java/com/example/demo/SubscriptionTests.java @@ -25,10 +25,10 @@ import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import reactor.core.publisher.Mono; import java.util.Collections; @@ -65,19 +65,19 @@ static class SubscriptionTestsConfig { @Autowired ObjectMapper objectMapper; - @SpyBean + @MockitoSpyBean PostService postService; - @MockBean + @MockitoBean PostRepository postRepository; - @MockBean + @MockitoBean CommentRepository commentRepository; - @MockBean + @MockitoBean AuthorRepository authorRepository; - @MockBean + @MockitoBean AuthorService authorService; @SneakyThrows @@ -98,7 +98,15 @@ void createCommentAndSubscription() { // commentAdded producer var comments = new CopyOnWriteArrayList(); - @Language("GraphQL") var subscriptionQuery = "subscription onCommentAdded { commentAdded { id postId content } }"; + @Language("GraphQL") var subscriptionQuery = """ + subscription onCommentAdded { + commentAdded { + id + postId + content + } + } + """.stripIndent(); // var executionResult = dgsReactiveQueryExecutor.execute(subscriptionQuery, Collections.emptyMap()).block(); // var publisher = executionResult.>getData(); // publisher.subscribe(new Subscriber() { @@ -130,7 +138,7 @@ void createCommentAndSubscription() { // }); // var executionResultMono = dgsReactiveQueryExecutor.execute(subscriptionQuery, Collections.emptyMap()); - var publisher = executionResultMono.flatMapMany(result -> result.>getData()); + var publisher = executionResultMono.flatMapMany(ExecutionResult::>getData); publisher.subscribe(executionResult -> { log.debug("execution result in publisher: {}", executionResult); var commentAdded = objectMapper.convertValue( diff --git a/dgs-webflux/src/test/java/com/example/demo/SubscriptionTestsWithGraphQLClient.java b/dgs-webflux/src/test/java/com/example/demo/SubscriptionTestsWithGraphQLClient.java index e4c46c27e..cdeb51522 100644 --- a/dgs-webflux/src/test/java/com/example/demo/SubscriptionTestsWithGraphQLClient.java +++ b/dgs-webflux/src/test/java/com/example/demo/SubscriptionTestsWithGraphQLClient.java @@ -6,6 +6,7 @@ import com.netflix.graphql.dgs.client.WebSocketGraphQLClient; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @@ -14,9 +15,11 @@ import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; import reactor.test.StepVerifier; +import java.time.Duration; import java.util.Collections; import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicLong; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; @@ -45,7 +48,14 @@ public void setup() { @SneakyThrows @Test void createCommentAndSubscription() { - var createPostQuery = "mutation createPost($input: CreatePostInput!){ createPost(createPostInput:$input) {id, title} }"; + @Language("graphql") var createPostQuery = """ + mutation createPost($input: CreatePostInput!){ + createPost(createPostInput:$input) { + id + title + } + } + """.stripIndent(); var createPostVariables = Map.of( "input", Map.of( "title", "test title", @@ -54,79 +64,86 @@ void createCommentAndSubscription() { ); var countDownLatch = new CountDownLatch(1); - var postIdHolder = new PostIdHolder(); + var postIdHolder = new AtomicLong(); var createPostResult = this.client.reactiveExecuteQuery(createPostQuery, createPostVariables) .map(response -> response.extractValueAsObject("createPost", Post.class)) .map(Post::getId) //.doOnTerminate(countDownLatch::countDown) .subscribe(id -> { log.debug("post created, id: {}", id); - postIdHolder.setPostId(id); + postIdHolder.set(id); countDownLatch.countDown(); }); countDownLatch.await(5, SECONDS); log.debug("created post:{}", createPostResult); - Long postId = postIdHolder.getPostId(); + Long postId = postIdHolder.get(); log.debug("post id get from amotic long: {}", postId); assertThat(postId).isNotNull(); - String subscriptionQuery = "subscription onCommentAdded { commentAdded { id postId content } }"; - var executionResultMono = this.socketClient + @Language("graphql") var subscriptionQuery = """ + subscription onCommentAdded { + commentAdded { + id + postId + content + } + } + """.stripIndent(); + var executionResultPublisher = this.socketClient .reactiveExecuteQuery(subscriptionQuery, Collections.emptyMap()); - var publisher = executionResultMono + var commentAddedDataPublisher = executionResultPublisher .map(it -> it.extractValueAsObject("commentAdded", Comment.class)); - var verifier = StepVerifier.create(publisher) - .expectNextCount(1) + + // add two comments + String comment1 = "test comment"; + String comment2 = "test comment2"; + var verifier = StepVerifier.create(commentAddedDataPublisher) + .thenAwait(Duration.ofMillis(1000)) // see: https://github.com/Netflix/dgs-framework/issues/657 + .consumeNextWith(comment -> assertThat(comment.getContent()).isEqualTo(comment1)) + .consumeNextWith(comment -> assertThat(comment.getContent()).isEqualTo(comment2)) .thenCancel() .verifyLater(); // add comment - var addCommentQuery = "mutation addComment($input: CommentInput!) { addComment(commentInput:$input) { id postId content}}"; + @Language("graphql") var addCommentQuery = """ + mutation addComment($input: CommentInput!) { + addComment(commentInput:$input) { + id + postId + content + } + } + """.stripIndent(); + var addCommentVariables = Map.of( "input", Map.of( "postId", postId, - "content", "test comment" + "content", comment1 ) ); var addCommentVariables2 = Map.of( "input", Map.of( "postId", postId, - "content", "test comment2" + "content", comment2 ) ); this.client.reactiveExecuteQuery(addCommentQuery, addCommentVariables) .map(response -> response.extractValueAsObject("addComment", Comment.class)) .as(StepVerifier::create) - .consumeNextWith(comment -> assertThat(comment.getContent()).isEqualTo("test comment")) + .consumeNextWith(comment -> assertThat(comment.getContent()).isEqualTo(comment1)) .verifyComplete(); this.client.reactiveExecuteQuery(addCommentQuery, addCommentVariables2) .map(response -> response.extractValueAsObject("addComment", Comment.class)) .as(StepVerifier::create) - .consumeNextWith(comment -> assertThat(comment.getContent()).isEqualTo("test comment2")) + .consumeNextWith(comment -> assertThat(comment.getContent()).isEqualTo(comment2)) .verifyComplete(); // verify - await() - .atMost(5, SECONDS) - .untilAsserted( - () -> verifier.verify() - ); - + // await().atMost(5, SECONDS).untilAsserted(verifier::verify); + verifier.verify(); } } - -class PostIdHolder { - private Long postId; - - public Long getPostId() { - return postId; - } - - public void setPostId(Long postId) { - this.postId = postId; - } -} \ No newline at end of file