diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ResourceServiceImpl.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ResourceServiceImpl.java index ef5540f09917..20ab103dc58f 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ResourceServiceImpl.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ResourceServiceImpl.java @@ -208,6 +208,7 @@ public List findByParentId(final String id) { * * @param event event */ + @Transactional(rollbackFor = Exception.class) @EventListener(value = PluginCreatedEvent.class) public void onPluginCreated(final PluginCreatedEvent event) { Assert.isNull(resourceMapper.nameExisted(event.getPlugin().getName()), AdminConstants.RESOURCE_NAME_IS_EXIST); diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListenerTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListenerTest.java index 5d295ae16ba8..210d37dfc1e6 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListenerTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListenerTest.java @@ -21,6 +21,7 @@ import jakarta.servlet.AsyncEvent; import jakarta.servlet.http.HttpServletRequest; import org.apache.shenyu.admin.config.properties.HttpSyncProperties; +import org.apache.shenyu.admin.config.properties.InstanceRetentionProperties; import org.apache.shenyu.admin.listener.AbstractDataChangedListener; import org.apache.shenyu.admin.listener.ConfigDataCache; import org.apache.shenyu.admin.model.event.instance.InstanceInfoReportEvent; @@ -777,7 +778,7 @@ public void testAfterInitialize() throws Exception { @Test public void documentationReproduceFalseOfflineTimeoutBeforeFix() throws Exception { InstanceInfoService instanceInfoService = Mockito.mock(InstanceInfoService.class); - InstanceCheckService instanceCheckService = new InstanceCheckService(instanceInfoService); + InstanceCheckService instanceCheckService = new InstanceCheckService(instanceInfoService, new InstanceRetentionProperties()); Mockito.doAnswer(invocation -> { InstanceInfoReportEvent event = invocation.getArgument(0); @@ -829,7 +830,7 @@ public void documentationReproduceFalseOfflineTimeoutBeforeFix() throws Exceptio @Test public void testHeldLongPollingRefreshesLiveness() throws Exception { InstanceInfoService instanceInfoService = Mockito.mock(InstanceInfoService.class); - InstanceCheckService instanceCheckService = new InstanceCheckService(instanceInfoService); + InstanceCheckService instanceCheckService = new InstanceCheckService(instanceInfoService, new InstanceRetentionProperties()); Mockito.doAnswer(invocation -> { InstanceInfoReportEvent event = invocation.getArgument(0); diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/PluginResourceTransactionTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/PluginResourceTransactionTest.java new file mode 100644 index 000000000000..ba8ada1276d5 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/PluginResourceTransactionTest.java @@ -0,0 +1,79 @@ +/* + * 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.shenyu.admin.service; + +import jakarta.annotation.Resource; +import org.apache.shenyu.admin.AbstractSpringIntegrationTest; +import org.apache.shenyu.admin.mapper.ResourceMapper; +import org.apache.shenyu.admin.model.entity.PluginDO; +import org.apache.shenyu.admin.model.event.plugin.PluginCreatedEvent; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.jdbc.core.JdbcTemplate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.doAnswer; + +/** + * Exercise the actual event listener proxy and database rollback, without a test-managed transaction. + */ +public final class PluginResourceTransactionTest extends AbstractSpringIntegrationTest { + + private static final String PLUGIN_NAME = "review-rollback"; + + @Resource + private ApplicationEventPublisher publisher; + + @Resource + private JdbcTemplate jdbcTemplate; + + @SpyBean + private ResourceMapper resourceMapper; + + private String resourceId; + + @AfterEach + public void cleanup() { + jdbcTemplate.update("DELETE FROM permission WHERE resource_id IN (SELECT id FROM resource WHERE name = ?)", PLUGIN_NAME); + jdbcTemplate.update("DELETE FROM resource WHERE name = ?", PLUGIN_NAME); + } + + @Test + public void failedButtonInsertRollsBackResourceAndPermissionCreatedByEvents() { + doAnswer(invocation -> { + resourceId = jdbcTemplate.queryForObject("SELECT id FROM resource WHERE name = ?", String.class, PLUGIN_NAME); + assertEquals(1, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM permission WHERE resource_id = ?", Integer.class, resourceId)); + throw new DataIntegrityViolationException("forced button insert failure"); + }).when(resourceMapper).insertBatch(anyList()); + PluginDO plugin = new PluginDO(); + plugin.setId("review-plugin"); + plugin.setName(PLUGIN_NAME); + + assertThrows(DataIntegrityViolationException.class, () -> publisher.publishEvent(new PluginCreatedEvent(plugin, "test"))); + + assertNotNull(resourceId, "The resource and permission must exist before injecting the batch failure"); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM resource WHERE name = ?", Integer.class, PLUGIN_NAME)); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM permission WHERE resource_id = ?", Integer.class, resourceId)); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ResourceServiceTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ResourceServiceTest.java index db6e68d719c4..96339911b38f 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ResourceServiceTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ResourceServiceTest.java @@ -22,6 +22,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.reset; @@ -33,6 +34,7 @@ import org.apache.shenyu.admin.model.dto.CreateResourceDTO; import org.apache.shenyu.admin.model.dto.ResourceDTO; import org.apache.shenyu.admin.model.entity.ResourceDO; +import org.apache.shenyu.admin.model.event.plugin.PluginCreatedEvent; import org.apache.shenyu.admin.model.page.CommonPager; import org.apache.shenyu.admin.model.page.PageParameter; import org.apache.shenyu.admin.model.query.ResourceQuery; @@ -57,6 +59,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.springframework.transaction.annotation.Transactional; /** * test for {@linkplain ResourceService}. @@ -79,6 +82,16 @@ public class ResourceServiceTest { @Mock private DashboardProperties properties; + + @Test + public void testPluginCreationListenerIsTransactional() throws NoSuchMethodException { + Transactional transactional = ResourceServiceImpl.class + .getMethod("onPluginCreated", PluginCreatedEvent.class) + .getAnnotation(Transactional.class); + + assertNotNull(transactional); + assertEquals(Exception.class, transactional.rollbackFor()[0]); + } @Test public void testCreateResourceBatch() {