Skip to content

Commit e835ca0

Browse files
dariuszkucsmyrick
authored andcommitted
spring-server: context web filter should only apply on graphql routes (#386)
* spring-server: context web filter should only apply on graphql routes * update to simple string compare instead of regex * update order of custom filter
1 parent 40649dc commit e835ca0

File tree

5 files changed

+247
-78
lines changed

5 files changed

+247
-78
lines changed

graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/GraphQLAutoConfiguration.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import org.springframework.web.server.WebFilter
4040
import java.util.Optional
4141

4242
/**
43-
* SpringBoot autoconfiguration that creates all beans required to start up reactive GraphQL web app.
43+
* SpringBoot auto-configuration that creates all beans required to start up reactive GraphQL web app.
4444
*/
4545
@Configuration
4646
@Import(
@@ -92,5 +92,8 @@ class GraphQLAutoConfiguration {
9292
fun graphQLContextFactory(): GraphQLContextFactory<*> = EmptyContextFactory
9393

9494
@Bean
95-
fun contextWebFilter(graphQLContextFactory: GraphQLContextFactory<*>): WebFilter = ContextWebFilter(graphQLContextFactory)
95+
fun contextWebFilter(
96+
config: GraphQLConfigurationProperties,
97+
graphQLContextFactory: GraphQLContextFactory<*>
98+
): WebFilter = ContextWebFilter(config, graphQLContextFactory)
9699
}

graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/execution/ContextWebFilter.kt

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.expediagroup.graphql.spring.execution
1818

19+
import com.expediagroup.graphql.spring.GraphQLConfigurationProperties
1920
import kotlinx.coroutines.reactor.mono
2021
import org.springframework.core.Ordered
2122
import org.springframework.web.server.ServerWebExchange
@@ -32,14 +33,24 @@ const val GRAPHQL_CONTEXT_FILTER_ODER = 0
3233
/**
3334
* Default web filter that populates GraphQL context in the reactor subscriber context.
3435
*/
35-
class ContextWebFilter(private val contextFactory: GraphQLContextFactory<Any>) : WebFilter, Ordered {
36+
class ContextWebFilter(config: GraphQLConfigurationProperties, private val contextFactory: GraphQLContextFactory<Any>) : WebFilter, Ordered {
37+
private val graphQLRoute = "/${config.endpoint}"
38+
private val subscriptionsRoute = "/${config.subscriptions.endpoint}"
3639

3740
@Suppress("ForbiddenVoid")
38-
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> = mono {
39-
contextFactory.generateContext(exchange.request, exchange.response)
40-
}.flatMap { graphQLContext ->
41-
chain.filter(exchange).subscriberContext { it.put(GRAPHQL_CONTEXT_KEY, graphQLContext) }
42-
}
41+
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> =
42+
if (isApplicable(exchange.request.uri.path)) {
43+
mono {
44+
contextFactory.generateContext(exchange.request, exchange.response)
45+
}.flatMap { graphQLContext ->
46+
chain.filter(exchange).subscriberContext { it.put(GRAPHQL_CONTEXT_KEY, graphQLContext) }
47+
}
48+
} else {
49+
chain.filter(exchange)
50+
}
4351

4452
override fun getOrder(): Int = GRAPHQL_CONTEXT_FILTER_ODER
53+
54+
internal fun isApplicable(path: String): Boolean =
55+
graphQLRoute.equals(path, ignoreCase = true) || subscriptionsRoute.equals(path, ignoreCase = true)
4556
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright 2019 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.spring.context
18+
19+
import com.expediagroup.graphql.spring.GraphQLConfigurationProperties
20+
import com.expediagroup.graphql.spring.execution.ContextWebFilter
21+
import com.expediagroup.graphql.spring.execution.GRAPHQL_CONTEXT_KEY
22+
import com.expediagroup.graphql.spring.execution.GraphQLContextFactory
23+
import graphql.GraphQLContext
24+
import io.mockk.coEvery
25+
import io.mockk.every
26+
import io.mockk.mockk
27+
import org.junit.jupiter.api.Test
28+
import org.springframework.web.server.ServerWebExchange
29+
import org.springframework.web.server.WebFilterChain
30+
import reactor.core.publisher.Mono
31+
import reactor.test.StepVerifier
32+
import reactor.util.context.Context
33+
import kotlin.test.assertEquals
34+
import kotlin.test.assertFalse
35+
import kotlin.test.assertNotNull
36+
import kotlin.test.assertTrue
37+
38+
class ContextWebFilterTest {
39+
40+
@Test
41+
@Suppress("ForbiddenVoid")
42+
fun `verify web filter populates context in the reactor subscriber context`() {
43+
var generatedContext: Context? = null
44+
val exchange: ServerWebExchange = mockk {
45+
every { request } returns mockk {
46+
every { uri.path } returns "/graphql"
47+
}
48+
every { response } returns mockk()
49+
}
50+
val chain: WebFilterChain = mockk {
51+
every { filter(any()) } returns Mono.subscriberContext().flatMap {
52+
generatedContext = it
53+
Mono.empty<Void>()
54+
}
55+
}
56+
57+
val simpleFactory: GraphQLContextFactory<Any> = mockk {
58+
coEvery { generateContext(any(), any()) } returns GraphQLContext.newContext().build()
59+
}
60+
61+
val contextFilter = ContextWebFilter(GraphQLConfigurationProperties(), simpleFactory)
62+
StepVerifier.create(contextFilter.filter(exchange, chain))
63+
.verifyComplete()
64+
65+
assertNotNull(generatedContext)
66+
val graphQLContext = generatedContext?.getOrDefault<GraphQLContext>(GRAPHQL_CONTEXT_KEY, null)
67+
assertNotNull(graphQLContext)
68+
}
69+
70+
@Test
71+
fun `verify web filter order`() {
72+
val contextFilter = ContextWebFilter(GraphQLConfigurationProperties(), mockk())
73+
assertEquals(expected = 0, actual = contextFilter.order)
74+
}
75+
76+
@Test
77+
@Suppress("ForbiddenVoid")
78+
fun `verify web filter does not generate context on non graphql routes`() {
79+
var reactorContext: Context? = null
80+
val exchange: ServerWebExchange = mockk {
81+
every { request } returns mockk {
82+
every { uri.path } returns "/whatever"
83+
}
84+
every { response } returns mockk()
85+
}
86+
val chain: WebFilterChain = mockk {
87+
every { filter(any()) } returns Mono.subscriberContext().flatMap {
88+
reactorContext = it
89+
Mono.empty<Void>()
90+
}
91+
}
92+
93+
val simpleFactory: GraphQLContextFactory<Any> = mockk {
94+
coEvery { generateContext(any(), any()) } returns GraphQLContext.newContext().build()
95+
}
96+
97+
val contextFilter = ContextWebFilter(GraphQLConfigurationProperties(), simpleFactory)
98+
StepVerifier.create(contextFilter.filter(exchange, chain))
99+
.verifyComplete()
100+
101+
assertNotNull(reactorContext)
102+
assertTrue(reactorContext?.isEmpty == true)
103+
}
104+
105+
@Test
106+
fun `verify context web filter is applicable on default graphql routes`() {
107+
val contextFilter = ContextWebFilter(GraphQLConfigurationProperties(), mockk())
108+
for (path in listOf("/graphql", "/subscriptions")) {
109+
assertTrue(contextFilter.isApplicable(path))
110+
}
111+
}
112+
113+
@Test
114+
fun `verify context web filter is applicable on non-default graphql routes`() {
115+
val graphQLRoute = "myGraphQL"
116+
val subscriptionRoute = "mySubscription"
117+
val props = GraphQLConfigurationProperties()
118+
props.endpoint = graphQLRoute
119+
props.subscriptions.endpoint = subscriptionRoute
120+
121+
val contextFilter = ContextWebFilter(props, mockk())
122+
for (path in listOf("/${graphQLRoute.toLowerCase()}", "/${subscriptionRoute.toLowerCase()}")) {
123+
assertTrue(contextFilter.isApplicable(path))
124+
}
125+
}
126+
127+
@Test
128+
fun `verify context web filter is not applicable on non graphql routes`() {
129+
val contextFilter = ContextWebFilter(GraphQLConfigurationProperties(), mockk())
130+
assertFalse(contextFilter.isApplicable("/whatever"))
131+
}
132+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2019 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.spring.context
18+
19+
import com.expediagroup.graphql.annotations.GraphQLContext
20+
import com.expediagroup.graphql.spring.execution.GRAPHQL_CONTEXT_FILTER_ODER
21+
import com.expediagroup.graphql.spring.execution.GraphQLContextFactory
22+
import com.expediagroup.graphql.spring.model.GraphQLRequest
23+
import com.expediagroup.graphql.spring.operations.Query
24+
import kotlinx.coroutines.ExperimentalCoroutinesApi
25+
import kotlinx.coroutines.reactor.ReactorContext
26+
import org.junit.jupiter.api.Test
27+
import org.springframework.beans.factory.annotation.Autowired
28+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
29+
import org.springframework.boot.test.context.SpringBootTest
30+
import org.springframework.context.annotation.Bean
31+
import org.springframework.context.annotation.Configuration
32+
import org.springframework.core.annotation.Order
33+
import org.springframework.http.MediaType
34+
import org.springframework.http.server.reactive.ServerHttpRequest
35+
import org.springframework.http.server.reactive.ServerHttpResponse
36+
import org.springframework.test.web.reactive.server.WebTestClient
37+
import org.springframework.web.server.WebFilter
38+
import kotlin.coroutines.coroutineContext
39+
40+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = ["graphql.packages=com.expediagroup.graphql.spring.context"])
41+
@EnableAutoConfiguration
42+
class GraphQLContextFactoryIT(@Autowired private val testClient: WebTestClient) {
43+
44+
@Test
45+
fun `verify context is generated and available to the GraphQL execution`() {
46+
testClient.post()
47+
.uri("/graphql")
48+
.header("X-First-Header", "JUNIT_FIRST")
49+
.header("X-Second-Header", "JUNIT_SECOND")
50+
.accept(MediaType.APPLICATION_JSON)
51+
.contentType(MediaType.APPLICATION_JSON)
52+
.bodyValue(GraphQLRequest("query { context { first second } }"))
53+
.exchange()
54+
.expectBody()
55+
.jsonPath("$.data.context").exists()
56+
.jsonPath("$.data.context.first").isEqualTo("JUNIT_FIRST")
57+
.jsonPath("$.data.context.second").isEqualTo("JUNIT_SECOND")
58+
.jsonPath("$.errors").doesNotExist()
59+
.jsonPath("$.extensions").doesNotExist()
60+
}
61+
62+
@Configuration
63+
class GraphQLContextFactoryConfiguration {
64+
65+
@Bean
66+
fun query(): Query = ContextualQuery()
67+
68+
@Bean
69+
@ExperimentalCoroutinesApi
70+
fun customContextFactory(): GraphQLContextFactory<CustomContext> = object : GraphQLContextFactory<CustomContext> {
71+
override suspend fun generateContext(request: ServerHttpRequest, response: ServerHttpResponse): CustomContext {
72+
val firstValue = coroutineContext[ReactorContext]?.context?.get<String>("firstFilterValue")
73+
return CustomContext(
74+
first = firstValue,
75+
second = request.headers.getFirst("X-Second-Header") ?: "DEFAULT_SECOND"
76+
)
77+
}
78+
}
79+
80+
@Bean
81+
@Order(GRAPHQL_CONTEXT_FILTER_ODER - 1)
82+
fun customWebFilter(): WebFilter = WebFilter { exchange, chain ->
83+
val headerValue = exchange.request.headers.getFirst("X-First-Header") ?: "DEFAULT_FIRST"
84+
chain.filter(exchange).subscriberContext { it.put("firstFilterValue", headerValue) }
85+
}
86+
}
87+
88+
class ContextualQuery : Query {
89+
fun context(@GraphQLContext ctx: CustomContext): CustomContext = ctx
90+
}
91+
92+
data class CustomContext(val first: String?, val second: String?)
93+
}

graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/ContextWebFilterTest.kt

Lines changed: 0 additions & 70 deletions
This file was deleted.

0 commit comments

Comments
 (0)