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 <[email protected]>
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: [email protected]
For additional commands, e-mail: [email protected]