I have a working WebGraphQLInterceptor that does some authorization checks based on a combination of authentication and GraphQL query parameters.
I want to write unit tests that test my CustomGraphQlInterceptor
in isolation, not as part of the larger servlet we’re building. But I cannot get the code to execute during a test.
Simplified code:
@Component
class CustomGraphQlInterceptor : WebGraphQlInterceptor {
private val logger = LoggerFactory.getLogger(javaClass)
// you can ignore IDE errors about being unable to Autowire this bean, it will
// successfully autowire at runtime.
@Suppress("SpringJavaInjectionPointsAutowiringInspection")
@Autowired
private lateinit var externalAuthorizationService: ExternalAuthorizationService
override fun intercept(
request: WebGraphQlRequest,
chain: WebGraphQlInterceptor.Chain,
): Mono<WebGraphQlResponse> {
logger.info("Validating request: $request")
logger.info("Context is ${ReactiveSecurityContextHolder.getContext()}")
return ReactiveSecurityContextHolder.getContext().flatMap { securityContext ->
val authentication = securityContext.authentication
if (authentication is JwtAuthenticationToken) {
val externalId = authentication.tokenAttributes["externalId"] as? String
val queryVariable = request.variables["queryVariable"]
val isAuthorized: Boolean = externalAuthorizationService.isAuthorized(externalId, queryVariable)
if (!isAuthorized) {
return@flatMap Mono.error(IllegalAccessException("not authorized"))
}
return@flatMap chain.next(request)
}
}
My test is like this:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
@ExtendWith(SpringExtension::class)
@SpringBootTest(
classes = [DgsAutoConfiguration::class, TestConfig::class],
webEnvironment = WebEnvironment.RANDOM_PORT,
properties = ["spring.main.web-application-type=reactive", "spring.profiles.active=test"],
)
@TestExecutionListeners(
ReactorContextTestExecutionListener::class,
mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS,
)
class SupplierGraphQlInterceptorTest {
@Autowired
private lateinit var customGraphQlInterceptor: CustomGraphQlInterceptor
@MockkBean
private lateinit var externalAuthorizationService: ExternalAuthorizationService
@BeforeEach
fun setupAuthorized() {
TestSecurityContextHolder.setAuthentication(
JwtAuthenticationToken(
Jwt(
"token",
Instant.now(),
Instant.MAX,
mapOf(
"alg" to "none",
),
mapOf(
"externalId" to "1",
),
),
),
)
}
@Test
fun testUserIsAuthorized() {
val request =
WebGraphQlRequest(
URI("http://localhost:8080/graphql"), // uri
HttpHeaders(CollectionUtils.toMultiValueMap(mapOf())), // headers
null, // cookies
null, // remote address
mapOf(), // attributes
mapOf( // body
"query" to "{someQuery{id name}}",
"operationName" to "POST",
"variables" to mapOf("queryVariable" to "ABC"),
),
"1", // id
null, // local
)
every {
authorizationService.isAuthorized(1, "ABC")
} returns true
val chain = mockk<WebGraphQlInterceptor.Chain>()
every { chain.next(any()) } returns Mono.just(mockk<WebGraphQlResponse>())
StepVerifier
.create(
supplierGraphQlInterceptor.intercept(request, chain),
).expectNextMatches { it is WebGraphQlResponse }
.verifyComplete()
}
}
This fails with
java.lang.AssertionError: expectation “expectNextMatches” failed (expected: onNext(); actual: onComplete())
I am new to DGS, Spring, and kotlin so I am sure I am doing at least several things wrong here.
I was able to solve this. Using ReactorContextTestExecutionListener
was a mistake, this can be done much cleaner:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
@ExtendWith(SpringExtension::class)
@SpringBootTest(
classes = [DgsAutoConfiguration::class, TestConfig::class],
webEnvironment = WebEnvironment.RANDOM_PORT,
properties = ["spring.main.web-application-type=reactive", "spring.profiles.active=test"],
)
class CustomGraphQlInterceptorTest {
@Autowired
private lateinit var customGraphQlInterceptor: CustomGraphQlInterceptor
@MockkBean
private lateinit var externalAuthorizationService: ExternalAuthorizationService
@Test
fun testUserIsAuthorized() {
val request =
WebGraphQlRequest(
URI("http://localhost:8080/graphql"), // uri
HttpHeaders(CollectionUtils.toMultiValueMap(mapOf())), // headers
null, // cookies
null, // remote address
mapOf(), // attributes
mapOf( // body
"query" to "{someQuery{id name}}",
"operationName" to "POST",
"variables" to mapOf("queryVariable" to "ABC"),
),
"1", // id
null, // local
)
every {
authorizationService.isAuthorized(1, "ABC")
} returns true
val jwt =
JwtAuthenticationToken(
Jwt(
"token",
Instant.now(),
Instant.MAX,
mapOf(
"alg" to "none",
),
mapOf(
"externalId" to 1,
),
),
)
val securityContext: SecurityContext = mockk()
every { securityContext.authentication } returns jwt
val chain = mockk<WebGraphQlInterceptor.Chain>()
every { chain.next(any()) } returns Mono.just(mockk<WebGraphQlResponse>())
StepVerifier
.create(
supplierGraphQlInterceptor.intercept(request, chain).contextWrite(
ReactiveSecurityContextHolder
.withSecurityContext(Mono.just(securityContext)),
),
).expectNextMatches { it is WebGraphQlResponse }
.verifyComplete()
}
}