This is an automated email from the ASF dual-hosted git repository. jianbin pushed a commit to branch 2.x in repository https://gitbox.apache.org/repos/asf/incubator-seata.git
The following commit(s) were added to refs/heads/2.x by this push: new 208c2cd8a6 test:improve unit test coverage of seata-gRPC moudle (#7464) 208c2cd8a6 is described below commit 208c2cd8a6202d313af700257ac530b9c07c7983 Author: xiaoyu <93440108+yvce...@users.noreply.github.com> AuthorDate: Wed Jun 25 09:19:16 2025 +0800 test:improve unit test coverage of seata-gRPC moudle (#7464) --- changes/en-us/2.x.md | 1 + changes/zh-cn/2.x.md | 1 + .../interceptor/server/ServerListenerProxy.java | 22 +++- .../server/ServerTransactionInterceptor.java | 23 +++- .../client/ClientTransactionInterceptorTest.java | 143 +++++++++++++++++++++ .../server/ServerListenerProxyTest.java | 125 ++++++++++++++++++ .../server/ServerTransactionInterceptorTest.java | 94 ++++++++++++++ 7 files changed, 402 insertions(+), 7 deletions(-) diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md index e78723e74f..fcb0003de7 100644 --- a/changes/en-us/2.x.md +++ b/changes/en-us/2.x.md @@ -99,6 +99,7 @@ Add changes here for all PR submitted to the 2.x branch. - [[#7435](https://github.com/apache/incubator-seata/pull/7435)] Add common test config for dynamic server port assignment in tests - [[#7442](https://github.com/apache/incubator-seata/pull/7442)] add some UT for saga compatible - [[#7457](https://github.com/apache/incubator-seata/pull/7457)] improve unit test coverage of seata-rm moudle +- [[#7464](https://github.com/apache/incubator-seata/pull/7464)] improve unit test coverage of seata-gRPC moudle ### refactor: diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md index 18bb174686..e235ad53cf 100644 --- a/changes/zh-cn/2.x.md +++ b/changes/zh-cn/2.x.md @@ -100,6 +100,7 @@ - [[#7432](https://github.com/apache/incubator-seata/pull/7432)] 使用Maven Profile按条件引入Test模块 - [[#7442](https://github.com/apache/incubator-seata/pull/7442)] 增加 saga compatible 模块单测 - [[#7457](https://github.com/apache/incubator-seata/pull/7457)] 增加 rm 模块的单测 +- [[#7464](https://github.com/apache/incubator-seata/pull/7464)] 增加 gRPC 模块的单测 ### refactor: diff --git a/integration/grpc/src/main/java/org/apache/seata/integration/grpc/interceptor/server/ServerListenerProxy.java b/integration/grpc/src/main/java/org/apache/seata/integration/grpc/interceptor/server/ServerListenerProxy.java index 914afcfcb8..408f4b21a8 100644 --- a/integration/grpc/src/main/java/org/apache/seata/integration/grpc/interceptor/server/ServerListenerProxy.java +++ b/integration/grpc/src/main/java/org/apache/seata/integration/grpc/interceptor/server/ServerListenerProxy.java @@ -20,20 +20,23 @@ import io.grpc.ServerCall; import org.apache.seata.common.util.StringUtils; import org.apache.seata.core.context.RootContext; import org.apache.seata.core.model.BranchType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.Map; import java.util.Objects; public class ServerListenerProxy<ReqT> extends ServerCall.Listener<ReqT> { - private static final Logger LOGGER = LoggerFactory.getLogger(ServerListenerProxy.class); - private ServerCall.Listener<ReqT> target; private final String xid; private final Map<String, String> context; + /** + * Constructs a ServerListenerProxy. + * + * @param xid the global transaction id to bind + * @param context the context map containing metadata such as branch type + * @param target the original ServerCall.Listener to delegate calls to + */ public ServerListenerProxy(String xid, Map<String, String> context, ServerCall.Listener<ReqT> target) { super(); Objects.requireNonNull(target); @@ -42,11 +45,18 @@ public class ServerListenerProxy<ReqT> extends ServerCall.Listener<ReqT> { this.context = context; } + /** + * Delegates onMessage call to the target listener. + */ @Override public void onMessage(ReqT message) { target.onMessage(message); } + /** + * Cleans up previous transaction context and binds new XID and branch type (if applicable) + * before delegating onHalfClose call to the target listener. + */ @Override public void onHalfClose() { cleanContext(); @@ -75,6 +85,10 @@ public class ServerListenerProxy<ReqT> extends ServerCall.Listener<ReqT> { target.onReady(); } + /** + * Cleans up the transaction context from RootContext to avoid thread context pollution. + * Unbinds XID and branch type if previously set. + */ private void cleanContext() { RootContext.unbind(); BranchType previousBranchType = RootContext.getBranchType(); diff --git a/integration/grpc/src/main/java/org/apache/seata/integration/grpc/interceptor/server/ServerTransactionInterceptor.java b/integration/grpc/src/main/java/org/apache/seata/integration/grpc/interceptor/server/ServerTransactionInterceptor.java index be6a9797c6..39cea8fd7c 100644 --- a/integration/grpc/src/main/java/org/apache/seata/integration/grpc/interceptor/server/ServerTransactionInterceptor.java +++ b/integration/grpc/src/main/java/org/apache/seata/integration/grpc/interceptor/server/ServerTransactionInterceptor.java @@ -27,8 +27,23 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +/** + * ServerTransactionInterceptor intercepts incoming gRPC calls on the server side + * to extract global transaction context information (XID and branch type) from request metadata, + * and injects this context into a ServerListenerProxy to manage transaction context lifecycle. + */ public class ServerTransactionInterceptor implements ServerInterceptor { + /** + * Intercepts a gRPC call to extract transaction context and wrap the ServerCall.Listener. + * + * @param serverCall the gRPC ServerCall object + * @param metadata the request metadata (headers) + * @param serverCallHandler the next handler in the interceptor chain + * @param <ReqT> the request type + * @param <RespT> the response type + * @return a wrapped ServerCall.Listener that manages transaction context + */ @Override public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall( ServerCall<ReqT, RespT> serverCall, Metadata metadata, ServerCallHandler<ReqT, RespT> serverCallHandler) { @@ -41,9 +56,8 @@ public class ServerTransactionInterceptor implements ServerInterceptor { } /** - * get rpc xid - * @param metadata - * @return + * Extracts the global transaction ID (XID) from metadata headers, + * supporting both uppercase and lowercase keys. */ private String getRpcXid(Metadata metadata) { String rpcXid = metadata.get(GrpcHeaderKey.XID_HEADER_KEY); @@ -53,6 +67,9 @@ public class ServerTransactionInterceptor implements ServerInterceptor { return rpcXid; } + /** + * Extracts the branch transaction type name from metadata headers. + */ private String getBranchName(Metadata metadata) { return metadata.get(GrpcHeaderKey.BRANCH_HEADER_KEY); } diff --git a/integration/grpc/src/test/java/org/apache/seata/integration/grpc/interceptor/client/ClientTransactionInterceptorTest.java b/integration/grpc/src/test/java/org/apache/seata/integration/grpc/interceptor/client/ClientTransactionInterceptorTest.java new file mode 100644 index 0000000000..914a5ed733 --- /dev/null +++ b/integration/grpc/src/test/java/org/apache/seata/integration/grpc/interceptor/client/ClientTransactionInterceptorTest.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.integration.grpc.interceptor.client; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import org.apache.seata.core.context.RootContext; +import org.apache.seata.core.model.BranchType; +import org.apache.seata.integration.grpc.interceptor.GrpcHeaderKey; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.apache.seata.core.model.BranchType.AT; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ClientTransactionInterceptorTest { + + @Mock + private Channel channel; + + @Mock + private CallOptions callOptions; + + @Mock + private MethodDescriptor<String, String> method; + + @Mock + private ClientCall.Listener<String> listener; + + @Mock + private ClientCall<String, String> delegateCall; + + private ClientTransactionInterceptor interceptor; + + @BeforeEach + void setUp() { + interceptor = new ClientTransactionInterceptor(); + } + + @Test + void testInterceptCall_withXid_shouldInjectHeaders() { + // Ready + String xid = "123456"; + BranchType branchType = AT; + + // Bind transaction context + RootContext.bind(xid); + RootContext.bindBranchType(branchType); + + // Mock channel.newCall(...) to return our delegate call + Mockito.<ClientCall<String, String>>when(channel.newCall(any(), any())).thenReturn(delegateCall); + + // Metadata that will be passed into the call + Metadata requestHeaders = new Metadata(); + + // Create the interceptor and call interceptCall + ClientCall<String, String> interceptedCall = interceptor.interceptCall(method, callOptions, channel); + + // Act + interceptedCall.start(listener, requestHeaders); + + // Capture actual listener and metadata passed into delegateCall.start(...) + ArgumentCaptor<Metadata> headersCaptor = ArgumentCaptor.forClass(Metadata.class); + ArgumentCaptor<ClientCall.Listener<String>> listenerCaptor = ArgumentCaptor.forClass(ClientCall.Listener.class); + + verify(delegateCall).start(listenerCaptor.capture(), headersCaptor.capture()); + + Metadata actualHeaders = headersCaptor.getValue(); + + // Assert headers contain the expected XID and branch type + Assertions.assertEquals(xid, actualHeaders.get(GrpcHeaderKey.XID_HEADER_KEY)); + Assertions.assertEquals(branchType.name(), actualHeaders.get(GrpcHeaderKey.BRANCH_HEADER_KEY)); + + // Cleanup context + RootContext.unbind(); + RootContext.unbindBranchType(); + } + + @Test + void testInterceptCall_withoutXid_shouldNotInjectHeaders() { + // ready + Mockito.<ClientCall<String, String>>when(channel.newCall(any(), any())).thenReturn(delegateCall); + Metadata metadata = new Metadata(); + + // act + ClientCall<String, String> interceptedCall = interceptor.interceptCall(method, callOptions, channel); + interceptedCall.start(listener, metadata); + + // assert + Assertions.assertNull(metadata.get(GrpcHeaderKey.XID_HEADER_KEY)); + Assertions.assertNull(metadata.get(GrpcHeaderKey.BRANCH_HEADER_KEY)); + } + + @Test + void testOnHeaders_shouldDelegateToOriginalListener() { + // ready + Mockito.<ClientCall<String, String>>when(channel.newCall(any(), any())).thenReturn(delegateCall); + + Metadata requestHeaders = new Metadata(); + Metadata responseHeaders = new Metadata(); + responseHeaders.put(Metadata.Key.of("test-key", Metadata.ASCII_STRING_MARSHALLER), "test-value"); + + ArgumentCaptor<ClientCall.Listener<String>> listenerCaptor = ArgumentCaptor.forClass(ClientCall.Listener.class); + + ClientCall<String, String> interceptedCall = interceptor.interceptCall(method, callOptions, channel); + interceptedCall.start(listener, requestHeaders); + + // Verify and capture the listener argument passed to delegateCall.start() + verify(delegateCall).start(listenerCaptor.capture(), any()); + + // Act + ClientCall.Listener<String> interceptedListener = listenerCaptor.getValue(); + interceptedListener.onHeaders(responseHeaders); + + // Assert + verify(listener).onHeaders(responseHeaders); + } +} diff --git a/integration/grpc/src/test/java/org/apache/seata/integration/grpc/interceptor/server/ServerListenerProxyTest.java b/integration/grpc/src/test/java/org/apache/seata/integration/grpc/interceptor/server/ServerListenerProxyTest.java new file mode 100644 index 0000000000..9495733fc2 --- /dev/null +++ b/integration/grpc/src/test/java/org/apache/seata/integration/grpc/interceptor/server/ServerListenerProxyTest.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.integration.grpc.interceptor.server; + +import io.grpc.ServerCall; +import org.apache.seata.core.context.RootContext; +import org.apache.seata.core.model.BranchType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class ServerListenerProxyTest { + + private ServerCall.Listener<String> target; + + @BeforeEach + void setup() { + target = mock(ServerCall.Listener.class); + RootContext.unbind(); + RootContext.unbindBranchType(); + } + + @AfterEach + void cleanup() { + RootContext.unbind(); + RootContext.unbindBranchType(); + } + + @Test + void testOnMessage_shouldDelegateToTarget() { + ServerListenerProxy<String> proxy = new ServerListenerProxy<>(null, null, target); + String message = "test-message"; + + proxy.onMessage(message); + + verify(target, times(1)).onMessage(message); + } + + @Test + void testOnHalfClose_withNonEmptyXid_andTCCBranchType_shouldBindContext() { + String xid = "test-xid"; + Map<String, String> context = new HashMap<>(); + context.put(RootContext.KEY_BRANCH_TYPE, BranchType.TCC.name()); + + ServerListenerProxy<String> proxy = new ServerListenerProxy<>(xid, context, target); + + // Pre-bind some context to test cleanup + RootContext.bind("old-xid"); + RootContext.bindBranchType(BranchType.AT); + + proxy.onHalfClose(); + + // Verify RootContext binding updated + Assertions.assertEquals(xid, RootContext.getXID()); + Assertions.assertEquals(BranchType.TCC, RootContext.getBranchType()); + + verify(target, times(1)).onHalfClose(); + } + + @Test + void testOnHalfClose_withEmptyXid_shouldOnlyCleanContext_andCallTarget() { + ServerListenerProxy<String> proxy = new ServerListenerProxy<>(null, new HashMap<>(), target); + + // Pre-bind some context to test cleanup + RootContext.bind("old-xid"); + RootContext.bindBranchType(BranchType.TCC); + + proxy.onHalfClose(); + + // Context should be cleaned (unbind XID and branch type) + Assertions.assertNull(RootContext.getXID()); + Assertions.assertNull(RootContext.getBranchType()); + + verify(target, times(1)).onHalfClose(); + } + + @Test + void testOnCancel_shouldDelegate() { + ServerListenerProxy<String> proxy = new ServerListenerProxy<>(null, null, target); + + proxy.onCancel(); + + verify(target, times(1)).onCancel(); + } + + @Test + void testOnComplete_shouldDelegate() { + ServerListenerProxy<String> proxy = new ServerListenerProxy<>(null, null, target); + + proxy.onComplete(); + + verify(target, times(1)).onComplete(); + } + + @Test + void testOnReady_shouldDelegate() { + ServerListenerProxy<String> proxy = new ServerListenerProxy<>(null, null, target); + + proxy.onReady(); + + verify(target, times(1)).onReady(); + } +} diff --git a/integration/grpc/src/test/java/org/apache/seata/integration/grpc/interceptor/server/ServerTransactionInterceptorTest.java b/integration/grpc/src/test/java/org/apache/seata/integration/grpc/interceptor/server/ServerTransactionInterceptorTest.java new file mode 100644 index 0000000000..bfc07a07d2 --- /dev/null +++ b/integration/grpc/src/test/java/org/apache/seata/integration/grpc/interceptor/server/ServerTransactionInterceptorTest.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.integration.grpc.interceptor.server; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import org.apache.seata.core.context.RootContext; +import org.apache.seata.integration.grpc.interceptor.GrpcHeaderKey; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ServerTransactionInterceptorTest { + + private ServerTransactionInterceptor interceptor; + + @BeforeEach + void setUp() { + interceptor = new ServerTransactionInterceptor(); + RootContext.unbind(); + RootContext.unbindBranchType(); + } + + @Test + void testInterceptCall_shouldExtractXidAndBranchTypeAndWrapListener() { + // Ready + Metadata metadata = new Metadata(); + metadata.put(GrpcHeaderKey.XID_HEADER_KEY, "test-xid"); + metadata.put(GrpcHeaderKey.BRANCH_HEADER_KEY, "TCC"); + + // Mocks + ServerCall<String, String> serverCall = mock(ServerCall.class); + ServerCallHandler<String, String> serverCallHandler = mock(ServerCallHandler.class); + Listener<String> originalListener = mock(Listener.class); + + when(serverCallHandler.startCall(serverCall, metadata)).thenReturn(originalListener); + + // Call interceptor + ServerCall.Listener<String> listener = interceptor.interceptCall(serverCall, metadata, serverCallHandler); + + assertNotNull(listener); + assertInstanceOf(ServerListenerProxy.class, listener); + } + + @Test + void testGetRpcXid_shouldSupportUppercaseAndLowercaseKeys() throws Exception { + Metadata metadata = new Metadata(); + metadata.put(GrpcHeaderKey.XID_HEADER_KEY, "upper-xid"); + + Method getRpcXidMethod = interceptor.getClass().getDeclaredMethod("getRpcXid", Metadata.class); + getRpcXidMethod.setAccessible(true); + assertEquals("upper-xid", getRpcXidMethod.invoke(interceptor, metadata)); + + Metadata metadataLower = new Metadata(); + metadataLower.put(GrpcHeaderKey.XID_HEADER_KEY_LOWERCASE, "lower-xid"); + assertEquals("lower-xid", getRpcXidMethod.invoke(interceptor, metadataLower)); + } + + @Test + void testGetBranchName_shouldReturnCorrectValue() throws Exception { + Metadata metadata = new Metadata(); + metadata.put(GrpcHeaderKey.BRANCH_HEADER_KEY, "branch-type"); + + Method getBranchNameMethod = interceptor.getClass().getDeclaredMethod("getBranchName", Metadata.class); + getBranchNameMethod.setAccessible(true); + + String branchName = (String) getBranchNameMethod.invoke(interceptor, metadata); + + assertEquals("branch-type", branchName); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@seata.apache.org For additional commands, e-mail: notifications-h...@seata.apache.org