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

Reply via email to