You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何独立测试Netflix DGS自定义响应式WebGraphQlInterceptor

问题分析与解决方案

核心问题1:Reactive Security Context 未正确设置

你在测试中使用了TestSecurityContextHolder,但拦截器依赖的是ReactiveSecurityContextHolder——这是两个完全独立的上下文容器:

  • TestSecurityContextHolder用于非反应式Spring Security场景,它的值不会自动同步到Reactor的Context中。
  • ReactiveSecurityContextHolder从当前Reactor流的Context中获取安全信息,必须通过contextWrite将SecurityContext绑定到Mono/Flux的上下文里。

核心问题2:Mock与变量不匹配

  1. 测试中mock的Bean是externalAuthorizationService,但你写的是authorizationService.isAuthorized(1, "ABC"),名字不匹配会导致mock失效。
  2. 拦截器中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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 19:04:54