如何独立测试Netflix DGS自定义响应式WebGraphQlInterceptor
问题分析与解决方案
核心问题1:Reactive Security Context 未正确设置
你在测试中使用了TestSecurityContextHolder,但拦截器依赖的是ReactiveSecurityContextHolder——这是两个完全独立的上下文容器:
TestSecurityContextHolder用于非反应式Spring Security场景,它的值不会自动同步到Reactor的Context中。ReactiveSecurityContextHolder从当前Reactor流的Context中获取安全信息,必须通过contextWrite将SecurityContext绑定到Mono/Flux的上下文里。
核心问题2:Mock与变量不匹配
- 测试中mock的Bean是
externalAuthorizationService,但你写的是authorizationService.isAuthorized(1, "ABC"),名字不匹配会导致mock失效。 - 拦截器中
externalId是String类型(从Jwt的attributes中取的是String),但测试中传的是1(Int),类型不匹配会导致mock调用不命中。
核心问题3:拦截器代码逻辑不完整
当authentication不是JwtAuthenticationToken类型时,你的拦截器没有返回任何Mono,会导致流直接完成(onComplete),没有输出。
修正后的测试代码
@ExtendWith(SpringExtension::class) @SpringBootTest( classes = [DgsAutoConfiguration::class, TestConfig::class], webEnvironment = WebEnvironment.NONE, // 不需要启动web服务器,节省资源 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() { // 1. 构造JWT认证信息 val jwt = Jwt( "token", Instant.now(), Instant.MAX, mapOf("alg" to "none"), mapOf("externalId" to "1") ) val authentication = JwtAuthenticationToken(jwt) val securityContext = SecurityContextHolder.createEmptyContext().apply { this.authentication = authentication } // 2. 构造GraphQL请求 val request = DefaultWebGraphQlRequest( URI("http://localhost:8080/graphql"), HttpHeaders.EMPTY, null, null, emptyMap(), // 直接传入query、variables等参数,无需手动构造body map "{someQuery{id name}}", "someQuery", mapOf("queryVariable" to "ABC"), "1" ) // 3. Mock授权服务 every { externalAuthorizationService.isAuthorized("1", "ABC") } returns true // 4. Mock拦截器链 val chain = mockk<WebGraphQlInterceptor.Chain>() val mockResponse = mockk<WebGraphQlResponse>() every { chain.next(any()) } returns Mono.just(mockResponse) // 5. 绑定SecurityContext到Reactor流,并验证结果 StepVerifier.create( customGraphQlInterceptor.intercept(request, chain) .contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext))) ) .expectNextMatches { it === mockResponse } .verifyComplete() } }
修正后的拦截器代码
补全非JWT认证的分支逻辑,避免流无输出:
@Component class CustomGraphQlInterceptor : WebGraphQlInterceptor { private val logger = LoggerFactory.getLogger(javaClass) @Autowired private lateinit var externalAuthorizationService: ExternalAuthorizationService override fun intercept( request: WebGraphQlRequest, chain: WebGraphQlInterceptor.Chain, ): Mono<WebGraphQlResponse> { logger.info("Validating request: $request") 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"] // 处理externalId或queryVariable为空的情况 if (externalId.isNullOrBlank() || queryVariable == null) { return@flatMap Mono.error(IllegalAccessException("Missing required authorization parameters")) } val isAuthorized = externalAuthorizationService.isAuthorized(externalId, queryVariable) if (!isAuthorized) { return@flatMap Mono.error(IllegalAccessException("not authorized")) } chain.next(request) } else { // 非JWT认证的情况,可根据业务需求返回错误或放行 Mono.error(IllegalAccessException("Unsupported authentication type")) } } } }
额外优化建议
- 测试中使用
WebEnvironment.NONE,不需要启动完整的web服务器,加快测试速度。 - 用
DefaultWebGraphQlRequest替代手动构造WebGraphQlRequest,它提供了更清晰的构造参数,避免手动拼接body map的错误。 - 对
externalId和queryVariable增加空值校验,避免空指针异常。
内容的提问来源于stack exchange,提问作者Max
相关产品推荐
相关产品推荐

