This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 6981c44718 [ISSUE #7468] Detect duplicate configs within an import
archive. (#7556)
6981c44718 is described below
commit 6981c44718166fc5b74f28a0ebe2478f69f19410
Author: redyyang <[email protected]>
AuthorDate: Mon Oct 12 13:14:11 2026 +0800
[ISSUE #7468] Detect duplicate configs within an import archive. (#7556)
* fix(admin): detect duplicate configs within an import archive.
* test(admin): retain dict import rollback and deduplication coverage.
---------
Co-authored-by: redyyang <[email protected]>
---
.../admin/service/impl/AppAuthServiceImpl.java | 1 +
.../admin/service/impl/DiscoveryServiceImpl.java | 49 ++-
.../service/impl/DiscoveryUpstreamServiceImpl.java | 21 +-
.../admin/service/impl/MetaDataServiceImpl.java | 19 +-
.../service/impl/ProxySelectorServiceImpl.java | 11 +-
.../shenyu/admin/service/impl/RuleServiceImpl.java | 12 +-
.../admin/service/impl/SelectorServiceImpl.java | 20 +-
.../admin/service/impl/ShenyuDictServiceImpl.java | 19 +-
.../admin/service/ConfigsImportDuplicateTest.java | 451 +++++++++++++++++++++
.../admin/service/DictImportIntegrationTest.java | 33 +-
.../admin/service/ShenyuDictServiceTest.java | 1 +
11 files changed, 568 insertions(+), 69 deletions(-)
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/AppAuthServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/AppAuthServiceImpl.java
index a33a6d359f..21ea38efb1 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/AppAuthServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/AppAuthServiceImpl.java
@@ -380,6 +380,7 @@ public class AppAuthServiceImpl implements AppAuthService {
appAuthDO.setNamespaceId(namespace);
int inserted = appAuthMapper.insertSelective(appAuthDO);
if (inserted > 0) {
+ existAppKeySet.add(appKey);
successCount++;
// auth path
List<AuthPathDTO> authPathDTOList = appAuth.getAuthPathList();
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryServiceImpl.java
index 3d1a2e9b62..27f5ce4954 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryServiceImpl.java
@@ -62,6 +62,7 @@ import
org.springframework.transaction.support.TransactionSynchronizationManager
import org.springframework.transaction.support.TransactionTemplate;
import java.sql.Timestamp;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -487,43 +488,42 @@ public class DiscoveryServiceImpl implements
DiscoveryService {
return ConfigImportResult.success();
}
Map<String, String> discoveryHandlerIdMapping =
context.getDiscoveryHandlerIdMapping();
- Map<String, List<DiscoveryDO>> pluginDiscoveryMap = discoveryMapper
+ Map<String, Set<String>> pluginDiscoveryMap = discoveryMapper
.selectAllByNamespaceId(namespace)
.stream()
- .collect(Collectors.groupingBy(DiscoveryDO::getPluginName));
+ .collect(Collectors.groupingBy(DiscoveryDO::getPluginName,
+ Collectors.mapping(DiscoveryDO::getDiscoveryName,
Collectors.toSet())));
+ Map<String, Map<String, String>> importedHandlerIdMap =
Maps.newHashMap();
int successCount = 0;
StringBuilder errorMsgBuilder = new StringBuilder();
for (DiscoveryDTO discoveryDTO : discoveryList) {
String pluginName = discoveryDTO.getPluginName();
String discoveryName = discoveryDTO.getName();
- Set<String> existDiscoveryNameSet = pluginDiscoveryMap
- .getOrDefault(pluginName, Lists.newArrayList())
- .stream()
- .map(DiscoveryDO::getDiscoveryName)
- .collect(Collectors.toSet());
+ Set<String> existDiscoveryNameSet =
pluginDiscoveryMap.computeIfAbsent(pluginName, key -> new HashSet<>());
+ Map<String, String> importedHandlerIds =
importedHandlerIdMap.computeIfAbsent(pluginName, key -> Maps.newHashMap());
if (existDiscoveryNameSet.contains(discoveryName)) {
errorMsgBuilder
.append(discoveryName)
.append(",");
+ String importedHandlerId =
importedHandlerIds.get(discoveryName);
+ if (Objects.nonNull(importedHandlerId) &&
Objects.nonNull(discoveryDTO.getDiscoveryHandler())) {
+
discoveryHandlerIdMapping.put(discoveryDTO.getDiscoveryHandler().getId(),
importedHandlerId);
+ }
continue;
}
String discoveryId = UUIDUtils.getInstance().generateShortUuid();
discoveryDTO.setId(discoveryId);
discoveryDTO.setNamespaceId(namespace);
- create(discoveryDTO);
+ if (Objects.isNull(create(discoveryDTO))) {
+ continue;
+ }
+ existDiscoveryNameSet.add(discoveryName);
successCount++;
// import discovery handler data
- String discoveryHandlerId = null;
- if (Objects.nonNull(discoveryDTO.getDiscoveryHandler())) {
- DiscoveryHandlerDO discoveryHandlerDO = DiscoveryTransfer
- .INSTANCE
- .mapToDO(discoveryDTO.getDiscoveryHandler());
- discoveryHandlerDO.setDiscoveryId(discoveryId);
- discoveryHandlerId =
UUIDUtils.getInstance().generateShortUuid();
- discoveryHandlerIdMapping.put(discoveryHandlerDO.getId(),
discoveryHandlerId);
- discoveryHandlerDO.setId(discoveryHandlerId);
- discoveryHandlerMapper.insertSelective(discoveryHandlerDO);
+ String discoveryHandlerId = importDiscoveryHandler(discoveryDTO,
context);
+ if (Objects.nonNull(discoveryHandlerId)) {
+ importedHandlerIds.put(discoveryName, discoveryHandlerId);
}
// import discovery rel data
@@ -561,4 +561,17 @@ public class DiscoveryServiceImpl implements
DiscoveryService {
}
return ConfigImportResult.success(successCount);
}
+
+ private String importDiscoveryHandler(final DiscoveryDTO discoveryDTO,
final ConfigsImportContext context) {
+ if (Objects.isNull(discoveryDTO.getDiscoveryHandler())) {
+ return null;
+ }
+ DiscoveryHandlerDO discoveryHandlerDO =
DiscoveryTransfer.INSTANCE.mapToDO(discoveryDTO.getDiscoveryHandler());
+ discoveryHandlerDO.setDiscoveryId(discoveryDTO.getId());
+ String discoveryHandlerId =
UUIDUtils.getInstance().generateShortUuid();
+ context.getDiscoveryHandlerIdMapping().put(discoveryHandlerDO.getId(),
discoveryHandlerId);
+ discoveryHandlerDO.setId(discoveryHandlerId);
+ discoveryHandlerMapper.insertSelective(discoveryHandlerDO);
+ return discoveryHandlerId;
+ }
}
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
index 943ba7386d..e33a523ce9 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
@@ -55,6 +55,7 @@ import
org.springframework.transaction.support.TransactionSynchronizationManager
import org.springframework.util.StringUtils;
import java.util.Collections;
+import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -358,18 +359,16 @@ public class DiscoveryUpstreamServiceImpl implements
DiscoveryUpstreamService {
Map<String, String> discoveryHandlerIdMapping =
context.getDiscoveryHandlerIdMapping();
int successCount = 0;
StringBuilder errorMsgBuilder = new StringBuilder();
- Map<String, List<DiscoveryUpstreamDO>> discoveryHandlerUpstreamMap =
discoveryUpstreamMapper
+ Map<String, Set<String>> discoveryHandlerUpstreamMap =
discoveryUpstreamMapper
.selectByNamespaceId(namespace)
.stream()
-
.collect(Collectors.groupingBy(DiscoveryUpstreamDO::getDiscoveryHandlerId));
+
.collect(Collectors.groupingBy(DiscoveryUpstreamDO::getDiscoveryHandlerId,
+
Collectors.mapping(DiscoveryUpstreamDO::getUpstreamUrl, Collectors.toSet())));
for (DiscoveryUpstreamDTO discoveryUpstreamDTO :
discoveryUpstreamList) {
String discoveryHandlerId =
discoveryUpstreamDTO.getDiscoveryHandlerId();
String url = discoveryUpstreamDTO.getUrl();
- Set<String> existsUpstreamUrlSet = discoveryHandlerUpstreamMap
-
.getOrDefault(discoveryHandlerIdMapping.getOrDefault(discoveryHandlerId,
discoveryHandlerId), Lists.newArrayList())
- .stream()
- .map(DiscoveryUpstreamDO::getUpstreamUrl)
- .collect(Collectors.toSet());
+ String newDiscoveryHandlerId =
discoveryHandlerIdMapping.getOrDefault(discoveryHandlerId, discoveryHandlerId);
+ Set<String> existsUpstreamUrlSet =
discoveryHandlerUpstreamMap.computeIfAbsent(newDiscoveryHandlerId, key -> new
HashSet<>());
if (existsUpstreamUrlSet.contains(url)) {
errorMsgBuilder
.append(url)
@@ -378,10 +377,12 @@ public class DiscoveryUpstreamServiceImpl implements
DiscoveryUpstreamService {
}
discoveryUpstreamDTO.setNamespaceId(namespace);
discoveryUpstreamDTO.setId(null);
-
discoveryUpstreamDTO.setDiscoveryHandlerId(discoveryHandlerIdMapping.getOrDefault(discoveryUpstreamDTO.getDiscoveryHandlerId(),
discoveryUpstreamDTO.getDiscoveryHandlerId()));
+ discoveryUpstreamDTO.setDiscoveryHandlerId(newDiscoveryHandlerId);
DiscoveryUpstreamDO discoveryUpstreamDO =
DiscoveryUpstreamDO.buildDiscoveryUpstreamDO(discoveryUpstreamDTO);
- discoveryUpstreamMapper.insert(discoveryUpstreamDO);
- successCount++;
+ if (discoveryUpstreamMapper.insert(discoveryUpstreamDO) > 0) {
+ existsUpstreamUrlSet.add(url);
+ successCount++;
+ }
}
if (StringUtils.hasLength(errorMsgBuilder)) {
errorMsgBuilder.setLength(errorMsgBuilder.length() - 1);
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/MetaDataServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/MetaDataServiceImpl.java
index 4105874139..e118bc1c4d 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/MetaDataServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/MetaDataServiceImpl.java
@@ -104,7 +104,11 @@ public class MetaDataServiceImpl implements
MetaDataService {
@Override
@Transactional(rollbackFor = Exception.class)
public String createOrUpdate(final MetaDataDTO metaDataDTO) {
- return StringUtils.isBlank(metaDataDTO.getId()) ?
this.create(metaDataDTO) : this.update(metaDataDTO);
+ if (StringUtils.isBlank(metaDataDTO.getId())) {
+ create(metaDataDTO);
+ return ShenyuResultMessage.CREATE_SUCCESS;
+ }
+ return update(metaDataDTO);
}
@Override
@@ -276,8 +280,10 @@ public class MetaDataServiceImpl implements
MetaDataService {
continue;
}
metaDataDTO.setNamespaceId(namespace);
- create(metaDataDTO);
- successCount++;
+ if (create(metaDataDTO) > 0) {
+ existMetadataPathSet.add(metaDataPath);
+ successCount++;
+ }
}
if (StringUtils.isNotEmpty(errorMsgBuilder)) {
errorMsgBuilder.setLength(errorMsgBuilder.length() - 1);
@@ -286,7 +292,7 @@ public class MetaDataServiceImpl implements MetaDataService
{
return ConfigImportResult.success(successCount);
}
- private String create(final MetaDataDTO metaDataDTO) {
+ private int create(final MetaDataDTO metaDataDTO) {
Assert.isNull(metaDataMapper.pathExisted(metaDataDTO.getPath(),
metaDataDTO.getNamespaceId()), AdminConstants.DATA_PATH_IS_EXIST);
MetaDataDO metaDataDO =
MetaDataTransfer.INSTANCE.mapToEntity(metaDataDTO);
metaDataDO.setId(UUIDUtils.getInstance().generateShortUuid());
@@ -294,14 +300,15 @@ public class MetaDataServiceImpl implements
MetaDataService {
Timestamp currentTime = new Timestamp(System.currentTimeMillis());
metaDataDO.setDateCreated(currentTime);
metaDataDO.setDateUpdated(currentTime);
- if (metaDataMapper.insert(metaDataDO) > 0) {
+ final int inserted = metaDataMapper.insert(metaDataDO);
+ if (inserted > 0) {
publisher.onCreated(metaDataDO);
}
// publish MetaData's create event
eventPublisher.publishEvent(new
DataChangedEvent(ConfigGroupEnum.META_DATA, DataEventTypeEnum.CREATE,
Collections.singletonList(MetaDataTransfer.INSTANCE.mapToData(metaDataDO))));
- return ShenyuResultMessage.CREATE_SUCCESS;
+ return inserted;
}
private String update(final MetaDataDTO metaDataDTO) {
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ProxySelectorServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ProxySelectorServiceImpl.java
index 9e47973beb..9a8e88ebc4 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ProxySelectorServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ProxySelectorServiceImpl.java
@@ -581,19 +581,17 @@ public class ProxySelectorServiceImpl implements
ProxySelectorService {
return ConfigImportResult.success();
}
Map<String, String> proxySelectorIdMapping = new HashMap<>();
- Map<String, List<ProxySelectorDO>> pluginProxySelectorMap =
proxySelectorMapper
+ Map<String, Map<String, String>> pluginProxySelectorMap =
proxySelectorMapper
.selectByNamespaceId(namespace)
.stream()
-
.collect(Collectors.groupingBy(ProxySelectorDO::getPluginName));
+ .collect(Collectors.groupingBy(ProxySelectorDO::getPluginName,
+ Collectors.toMap(ProxySelectorDO::getName,
ProxySelectorDO::getId, (first, second) -> first)));
int successCount = 0;
StringBuilder errorMsgBuilder = new StringBuilder();
for (ProxySelectorData selectorData : proxySelectorList) {
String pluginName = selectorData.getPluginName();
String proxySelectorName = selectorData.getName();
- Map<String, String> existProxySelectorNameSet =
pluginProxySelectorMap
- .getOrDefault(pluginName, Lists.newArrayList())
- .stream()
- .collect(Collectors.toMap(ProxySelectorDO::getName,
ProxySelectorDO::getId));
+ Map<String, String> existProxySelectorNameSet =
pluginProxySelectorMap.computeIfAbsent(pluginName, key -> new HashMap<>());
if (existProxySelectorNameSet.containsKey(proxySelectorName)) {
errorMsgBuilder
@@ -608,6 +606,7 @@ public class ProxySelectorServiceImpl implements
ProxySelectorService {
proxySelectorDO.setId(newProxySelectorId);
proxySelectorDO.setNamespaceId(namespace);
if (proxySelectorMapper.insert(proxySelectorDO) > 0) {
+ existProxySelectorNameSet.put(proxySelectorName,
newProxySelectorId);
proxySelectorIdMapping.put(oldProxySelectorId,
newProxySelectorId);
successCount++;
}
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/RuleServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/RuleServiceImpl.java
index 5e835604bd..40e8125b92 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/RuleServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/RuleServiceImpl.java
@@ -347,10 +347,11 @@ public class RuleServiceImpl implements RuleService {
return ConfigImportResult.success();
}
Map<String, String> selectorIdMapping = context.getSelectorIdMapping();
- Map<String, List<RuleDO>> selectorRuleMap = ruleMapper
+ Map<String, Set<String>> selectorRuleMap = ruleMapper
.selectAllByNamespaceId(namespace)
.stream()
- .collect(Collectors.groupingBy(RuleDO::getSelectorId));
+ .collect(Collectors.groupingBy(RuleDO::getSelectorId,
+ Collectors.mapping(RuleDO::getRuleName,
Collectors.toSet())));
int successCount = 0;
StringBuilder errorMsgBuilder = new StringBuilder();
@@ -365,11 +366,7 @@ public class RuleServiceImpl implements RuleService {
.append(",");
continue;
}
- Set<String> existRuleNameSet = selectorRuleMap
- .getOrDefault(newSelectorId, Lists.newArrayList())
- .stream()
- .map(RuleDO::getRuleName)
- .collect(Collectors.toSet());
+ Set<String> existRuleNameSet =
selectorRuleMap.computeIfAbsent(newSelectorId, key -> new HashSet<>());
if (existRuleNameSet.contains(ruleName)) {
errorMsgBuilder
@@ -391,6 +388,7 @@ public class RuleServiceImpl implements RuleService {
});
addCondition(ruleDO, ruleDTO.getRuleConditions());
if (ruleCount > 0) {
+ existRuleNameSet.add(ruleName);
successCount++;
}
}
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SelectorServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SelectorServiceImpl.java
index 466cd4314f..5c08df0284 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SelectorServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SelectorServiceImpl.java
@@ -567,9 +567,10 @@ public class SelectorServiceImpl implements
SelectorService {
Map<String, String> selectorIdMapping = context.getSelectorIdMapping();
StringBuilder errorMsgBuilder = new StringBuilder();
int successCount = 0;
- Map<String, List<SelectorDO>> pluginSelectorMap =
selectorMapper.selectAllByNamespaceId(namespace).stream()
+ Map<String, Map<String, String>> pluginSelectorMap =
selectorMapper.selectAllByNamespaceId(namespace).stream()
.filter(Objects::nonNull)
- .collect(Collectors.groupingBy(SelectorDO::getPluginId));
+ .collect(Collectors.groupingBy(SelectorDO::getPluginId,
+ Collectors.toMap(SelectorDO::getSelectorName,
SelectorDO::getId, (first, second) -> first)));
Map<String, List<SelectorDTO>> importSelectorMap =
selectorList.stream()
.collect(Collectors.groupingBy(SelectorDTO::getPluginId));
@@ -580,11 +581,7 @@ public class SelectorServiceImpl implements
SelectorService {
List<SelectorDTO> selectorDTOList = selectorEntry.getValue();
if (CollectionUtils.isNotEmpty(selectorDTOList)) {
- Map<String, String> existSelectorSet = Optional
- .ofNullable(pluginSelectorMap.get(pluginId))
- .orElseGet(Lists::newArrayList)
- .stream()
- .collect(Collectors.toMap(SelectorDO::getSelectorName,
SelectorDO::getId));
+ Map<String, String> existSelectorSet =
pluginSelectorMap.computeIfAbsent(pluginId, key -> new HashMap<>());
for (SelectorDTO selectorDTO : selectorDTOList) {
// filter by selectorName
@@ -598,7 +595,7 @@ public class SelectorServiceImpl implements SelectorService
{
} else {
// gen new id
String selectorId =
UUIDUtils.getInstance().generateShortUuid();
- selectorIdMapping.put(selectorDTO.getId(), selectorId);
+ final String oldSelectorId = selectorDTO.getId();
selectorDTO.setId(selectorId);
selectorDTO.setNamespaceId(namespace);
selectorDTO.setPluginId(pluginId);
@@ -607,8 +604,11 @@ public class SelectorServiceImpl implements
SelectorService {
c.setSelectorId(selectorId);
c.setId(null);
});
- create(selectorDTO);
- successCount++;
+ if (create(selectorDTO) > 0) {
+ existSelectorSet.put(selectorName, selectorId);
+ selectorIdMapping.put(oldSelectorId, selectorId);
+ successCount++;
+ }
}
}
}
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ShenyuDictServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ShenyuDictServiceImpl.java
index d856deb9a6..8ff727aa0e 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ShenyuDictServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ShenyuDictServiceImpl.java
@@ -17,7 +17,6 @@
package org.apache.shenyu.admin.service.impl;
-import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shenyu.admin.aspect.annotation.Pageable;
@@ -35,6 +34,7 @@ import org.apache.shenyu.admin.utils.Assert;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -112,19 +112,16 @@ public class ShenyuDictServiceImpl implements
ShenyuDictService {
if (CollectionUtils.isEmpty(dictList)) {
return ConfigImportResult.success();
}
- Map<String, List<ShenyuDictVO>> dictTypeMap = listAllData()
+ Map<String, Set<String>> dictTypeMap = listAllData()
.stream()
- .collect(Collectors.groupingBy(ShenyuDictVO::getType));
+ .collect(Collectors.groupingBy(ShenyuDictVO::getType,
+ Collectors.mapping(ShenyuDictVO::getDictName,
Collectors.toSet())));
int successCount = 0;
StringBuilder errorMsgBuilder = new StringBuilder();
for (ShenyuDictDTO dictDTO : dictList) {
String type = dictDTO.getType();
String dictName = dictDTO.getDictName();
- Set<String> existDictNameSet = dictTypeMap
- .getOrDefault(type, Lists.newArrayList())
- .stream()
- .map(ShenyuDictVO::getDictName)
- .collect(Collectors.toSet());
+ Set<String> existDictNameSet = dictTypeMap.computeIfAbsent(type,
key -> new HashSet<>());
// check if dictName exists for this type
if (existDictNameSet.contains(dictName)) {
errorMsgBuilder
@@ -132,8 +129,10 @@ public class ShenyuDictServiceImpl implements
ShenyuDictService {
.append(",");
continue;
}
- create(dictDTO);
- successCount++;
+ if (create(dictDTO) > 0) {
+ existDictNameSet.add(dictName);
+ successCount++;
+ }
}
if (StringUtils.isNotEmpty(errorMsgBuilder)) {
errorMsgBuilder.setLength(errorMsgBuilder.length() - 1);
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ConfigsImportDuplicateTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ConfigsImportDuplicateTest.java
new file mode 100644
index 0000000000..7442bffcc9
--- /dev/null
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ConfigsImportDuplicateTest.java
@@ -0,0 +1,451 @@
+/*
+ * 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 org.apache.shenyu.admin.discovery.DiscoveryProcessor;
+import org.apache.shenyu.admin.discovery.DiscoveryProcessorHolder;
+import org.apache.shenyu.admin.mapper.AppAuthMapper;
+import org.apache.shenyu.admin.mapper.AuthParamMapper;
+import org.apache.shenyu.admin.mapper.AuthPathMapper;
+import org.apache.shenyu.admin.mapper.DiscoveryHandlerMapper;
+import org.apache.shenyu.admin.mapper.DiscoveryMapper;
+import org.apache.shenyu.admin.mapper.DiscoveryRelMapper;
+import org.apache.shenyu.admin.mapper.DiscoveryUpstreamMapper;
+import org.apache.shenyu.admin.mapper.MetaDataMapper;
+import org.apache.shenyu.admin.mapper.PluginMapper;
+import org.apache.shenyu.admin.mapper.ProxySelectorMapper;
+import org.apache.shenyu.admin.mapper.RuleConditionMapper;
+import org.apache.shenyu.admin.mapper.RuleMapper;
+import org.apache.shenyu.admin.mapper.SelectorConditionMapper;
+import org.apache.shenyu.admin.mapper.SelectorMapper;
+import org.apache.shenyu.admin.mapper.ShenyuDictMapper;
+import org.apache.shenyu.admin.model.dto.AppAuthDTO;
+import org.apache.shenyu.admin.model.dto.AuthPathDTO;
+import org.apache.shenyu.admin.model.dto.DiscoveryDTO;
+import org.apache.shenyu.admin.model.dto.DiscoveryHandlerDTO;
+import org.apache.shenyu.admin.model.dto.DiscoveryUpstreamDTO;
+import org.apache.shenyu.admin.model.dto.MetaDataDTO;
+import org.apache.shenyu.admin.model.dto.RuleDTO;
+import org.apache.shenyu.admin.model.dto.SelectorDTO;
+import org.apache.shenyu.admin.model.dto.ShenyuDictDTO;
+import org.apache.shenyu.admin.model.entity.AppAuthDO;
+import org.apache.shenyu.admin.model.entity.DiscoveryDO;
+import org.apache.shenyu.admin.model.entity.DiscoveryUpstreamDO;
+import org.apache.shenyu.admin.model.entity.MetaDataDO;
+import org.apache.shenyu.admin.model.entity.PluginDO;
+import org.apache.shenyu.admin.model.entity.ProxySelectorDO;
+import org.apache.shenyu.admin.model.entity.RuleDO;
+import org.apache.shenyu.admin.model.entity.SelectorDO;
+import org.apache.shenyu.admin.model.entity.ShenyuDictDO;
+import org.apache.shenyu.admin.model.result.ConfigImportResult;
+import org.apache.shenyu.admin.service.configs.ConfigsImportContext;
+import org.apache.shenyu.admin.service.impl.AppAuthServiceImpl;
+import org.apache.shenyu.admin.service.impl.DiscoveryServiceImpl;
+import org.apache.shenyu.admin.service.impl.DiscoveryUpstreamServiceImpl;
+import org.apache.shenyu.admin.service.impl.MetaDataServiceImpl;
+import org.apache.shenyu.admin.service.impl.ProxySelectorServiceImpl;
+import org.apache.shenyu.admin.service.impl.RuleServiceImpl;
+import org.apache.shenyu.admin.service.impl.SelectorServiceImpl;
+import org.apache.shenyu.admin.service.impl.ShenyuDictServiceImpl;
+import org.apache.shenyu.admin.service.publish.DictEventPublisher;
+import org.apache.shenyu.admin.service.publish.MetaDataEventPublisher;
+import org.apache.shenyu.admin.service.publish.RuleEventPublisher;
+import org.apache.shenyu.admin.service.publish.SelectorEventPublisher;
+import org.apache.shenyu.common.dto.ProxySelectorData;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.transaction.PlatformTransactionManager;
+
+import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Regression tests for duplicate business keys in namespace config imports.
+ */
+@ExtendWith(MockitoExtension.class)
+public final class ConfigsImportDuplicateTest {
+
+ private static final String NAMESPACE = "target-namespace";
+
+ @Mock
+ private SelectorMapper selectorMapper;
+
+ @Mock
+ private RuleMapper ruleMapper;
+
+ @Mock
+ private PluginMapper pluginMapper;
+
+ @Mock
+ private MetaDataMapper metaDataMapper;
+
+ @Mock
+ private AppAuthMapper appAuthMapper;
+
+ @Mock
+ private ShenyuDictMapper dictMapper;
+
+ @Mock
+ private ProxySelectorMapper proxySelectorMapper;
+
+ @Mock
+ private DiscoveryMapper discoveryMapper;
+
+ @Mock
+ private DiscoveryUpstreamMapper upstreamMapper;
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testSelectorsDeduplicateAfterPluginRemapping(final boolean
existing) {
+ final SelectorServiceImpl service = new
SelectorServiceImpl(selectorMapper, mock(SelectorConditionMapper.class),
pluginMapper,
+ mock(ApplicationEventPublisher.class), discoveryMapper,
mock(DiscoveryHandlerMapper.class), mock(DiscoveryRelMapper.class),
+ upstreamMapper, mock(DiscoveryProcessorHolder.class),
mock(SelectorEventPublisher.class));
+ final ConfigsImportContext context = new ConfigsImportContext();
+ context.getPluginTemplateIdMapping().put("source", "target");
+ context.getPluginTemplateIdMapping().put("alias", "target");
+ context.getPluginTemplateIdMapping().put("other", "other-target");
+
when(selectorMapper.selectAllByNamespaceId(NAMESPACE)).thenReturn(existing
+ ?
Arrays.asList(SelectorDO.builder().id("existing").pluginId("target").selectorName("same").build(),
+
SelectorDO.builder().id("legacy-duplicate").pluginId("target").selectorName("same").build())
: Collections.emptyList());
+ when(selectorMapper.insertSelective(any())).thenReturn(1);
+
when(pluginMapper.selectById(anyString())).thenReturn(PluginDO.builder().name("divide").build());
+
+ final ConfigImportResult result = service.importData(NAMESPACE,
Arrays.asList(selector("first", "source", "same"),
+ selector("duplicate", "alias", "same"),
selector("other-plugin", "other", "same"), selector("other-name", "source",
"different")), context);
+
+ assertResult(result, existing ? 2 : 3, "import fail selector: ",
existing ? "same,same" : "same");
+ assertNotNull(context.getSelectorIdMapping().get("first"));
+ assertEquals(context.getSelectorIdMapping().get("first"),
context.getSelectorIdMapping().get("duplicate"));
+ assertNotEquals(context.getSelectorIdMapping().get("first"),
context.getSelectorIdMapping().get("other-plugin"));
+ final ArgumentCaptor<SelectorDO> captor =
ArgumentCaptor.forClass(SelectorDO.class);
+ verify(selectorMapper,
times(result.getSuccessCount())).insertSelective(captor.capture());
+ captor.getAllValues().forEach(row -> assertEquals(NAMESPACE,
row.getNamespaceId()));
+ if (existing) {
+ assertEquals("existing",
context.getSelectorIdMapping().get("first"));
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testRulesDeduplicateAfterSelectorRemapping(final boolean
existing) {
+ final RuleServiceImpl service = new RuleServiceImpl(ruleMapper,
mock(RuleConditionMapper.class), selectorMapper, pluginMapper,
mock(RuleEventPublisher.class));
+ final ConfigsImportContext context = new ConfigsImportContext();
+ context.getSelectorIdMapping().put("source", "target");
+ context.getSelectorIdMapping().put("alias", "target");
+ context.getSelectorIdMapping().put("other", "other-target");
+ when(ruleMapper.selectAllByNamespaceId(NAMESPACE)).thenReturn(existing
+ ?
Collections.singletonList(RuleDO.builder().selectorId("target").ruleName("same").build())
: Collections.emptyList());
+ when(ruleMapper.insertSelective(any())).thenReturn(1);
+
when(selectorMapper.selectById(anyString())).thenReturn(SelectorDO.builder().pluginId("plugin").build());
+
when(pluginMapper.selectById("plugin")).thenReturn(PluginDO.builder().name("divide").build());
+
+ final ConfigImportResult result = service.importData(NAMESPACE,
Arrays.asList(rule("source", "same"), rule("alias", "same"),
+ rule("other", "same"), rule("source", "different")), context);
+
+ assertResult(result, existing ? 2 : 3, "import fail rule: ", existing
? "same,same" : "same");
+ final ArgumentCaptor<RuleDO> captor =
ArgumentCaptor.forClass(RuleDO.class);
+ verify(ruleMapper,
times(result.getSuccessCount())).insertSelective(captor.capture());
+ captor.getAllValues().forEach(row -> assertEquals(NAMESPACE,
row.getNamespaceId()));
+ assertEquals(existing ? 0 : 1, captor.getAllValues().stream()
+ .filter(row -> "target".equals(row.getSelectorId()) &&
"same".equals(row.getRuleName())).count());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testMetadataDeduplicatesPaths(final boolean existing) {
+ final MetaDataServiceImpl service = new
MetaDataServiceImpl(metaDataMapper, mock(ApplicationEventPublisher.class),
mock(MetaDataEventPublisher.class));
+
when(metaDataMapper.findAllByNamespaceId(NAMESPACE)).thenReturn(existing
+ ?
Collections.singletonList(MetaDataDO.builder().path("/same").build()) :
Collections.emptyList());
+ when(metaDataMapper.insert(any())).thenReturn(1);
+ when(metaDataMapper.pathExisted(anyString(),
anyString())).thenReturn(null);
+
+ final ConfigImportResult result = service.importData(NAMESPACE,
Arrays.asList(metadata("/same"), metadata("/same"), metadata("/different")));
+
+ assertResult(result, existing ? 1 : 2, "import fail meta: ", existing
? "/same,/same" : "/same");
+ final ArgumentCaptor<MetaDataDO> captor =
ArgumentCaptor.forClass(MetaDataDO.class);
+ verify(metaDataMapper,
times(result.getSuccessCount())).insert(captor.capture());
+ captor.getAllValues().forEach(row -> assertEquals(NAMESPACE,
row.getNamespaceId()));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testAuthDeduplicatesAppKeysAndChildRows(final boolean
existing) {
+ final AuthPathMapper pathMapper = mock(AuthPathMapper.class);
+ final AppAuthServiceImpl service = new
AppAuthServiceImpl(appAuthMapper, mock(ApplicationEventPublisher.class),
mock(AuthParamMapper.class), pathMapper);
+
when(appAuthMapper.selectAllByNamespaceId(NAMESPACE)).thenReturn(existing
+ ?
Collections.singletonList(AppAuthDO.builder().appKey("same").build()) :
Collections.emptyList());
+ when(appAuthMapper.insertSelective(any())).thenReturn(1);
+
+ final ConfigImportResult result = service.importData(NAMESPACE,
Arrays.asList(auth("same"), auth("same"), auth("different")));
+
+ assertResult(result, existing ? 1 : 2, "import fail appKey: ",
existing ? "same,same," : "same,");
+ final ArgumentCaptor<AppAuthDO> captor =
ArgumentCaptor.forClass(AppAuthDO.class);
+ verify(appAuthMapper,
times(result.getSuccessCount())).insertSelective(captor.capture());
+ verify(pathMapper, times(result.getSuccessCount())).batchSave(any());
+ captor.getAllValues().forEach(row -> assertEquals(NAMESPACE,
row.getNamespaceId()));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testDictsDeduplicateWithinType(final boolean existing) {
+ final ShenyuDictServiceImpl service = new
ShenyuDictServiceImpl(dictMapper, mock(DictEventPublisher.class));
+ final Timestamp now = new Timestamp(System.currentTimeMillis());
+ when(dictMapper.selectByQuery(any())).thenReturn(existing ?
Collections.singletonList(ShenyuDictDO.builder().id("existing")
+
.type("type").dictCode("code").dictName("same").dateCreated(now).dateUpdated(now).build())
: Collections.emptyList());
+ when(dictMapper.insertSelective(any())).thenReturn(1);
+
+ final ConfigImportResult result =
service.importData(Arrays.asList(dict("type", "same"), dict("type", "same"),
+ dict("other-type", "same"), dict("type", "different")));
+
+ assertResult(result, existing ? 2 : 3, "import fail dict: ", existing
? "same,same" : "same");
+ verify(dictMapper,
times(result.getSuccessCount())).insertSelective(any());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testProxySelectorsDeduplicateAndMapIds(final boolean existing)
{
+ final ProxySelectorServiceImpl service = new
ProxySelectorServiceImpl(proxySelectorMapper, discoveryMapper, upstreamMapper,
+ mock(DiscoveryHandlerMapper.class),
mock(DiscoveryRelMapper.class), selectorMapper,
mock(DiscoveryProcessorHolder.class), mock(ApplicationEventPublisher.class));
+ final ConfigsImportContext context = new ConfigsImportContext();
+
when(proxySelectorMapper.selectByNamespaceId(NAMESPACE)).thenReturn(existing
+ ?
Arrays.asList(ProxySelectorDO.builder().id("existing").pluginName("tcp").name("same").build(),
+
ProxySelectorDO.builder().id("legacy-duplicate").pluginName("tcp").name("same").build())
: Collections.emptyList());
+ when(proxySelectorMapper.insert(any())).thenReturn(1);
+
+ final ConfigImportResult result = service.importData(NAMESPACE,
Arrays.asList(proxy("first", "tcp", "same"), proxy("duplicate", "tcp", "same"),
+ proxy("other-plugin", "mqtt", "same"), proxy("other-name",
"tcp", "different")), context);
+
+ assertResult(result, existing ? 2 : 3, "import fail proxy selector: ",
existing ? "same,same" : "same");
+ assertNotNull(context.getProxySelectorIdMapping().get("first"));
+ assertEquals(context.getProxySelectorIdMapping().get("first"),
context.getProxySelectorIdMapping().get("duplicate"));
+ assertNotEquals(context.getProxySelectorIdMapping().get("first"),
context.getProxySelectorIdMapping().get("other-plugin"));
+ verify(proxySelectorMapper,
times(result.getSuccessCount())).insert(any());
+ if (existing) {
+ assertEquals("existing",
context.getProxySelectorIdMapping().get("first"));
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testDiscoveriesDeduplicateBeforeCreatingHandlers(final boolean
existing) {
+ final DiscoveryProcessorHolder holder =
mock(DiscoveryProcessorHolder.class);
+ final DiscoveryProcessor processor = mock(DiscoveryProcessor.class);
+ final DiscoveryHandlerMapper handlerMapper =
mock(DiscoveryHandlerMapper.class);
+ final DiscoveryServiceImpl service = new
DiscoveryServiceImpl(discoveryMapper, proxySelectorMapper,
mock(DiscoveryRelMapper.class),
+ handlerMapper, mock(SelectorService.class), selectorMapper,
holder, mock(PlatformTransactionManager.class));
+ when(holder.chooseProcessor("local")).thenReturn(processor);
+
when(discoveryMapper.selectAllByNamespaceId(NAMESPACE)).thenReturn(existing
+ ?
Collections.singletonList(DiscoveryDO.builder().pluginName("divide").discoveryName("same").build())
: Collections.emptyList());
+ when(discoveryMapper.insert(any())).thenReturn(1);
+
+ final ConfigImportResult result = service.importData(NAMESPACE,
Arrays.asList(discovery("divide", "same"), discovery("divide", "same"),
+ discovery("tcp", "same"), discovery("divide", "different")),
new ConfigsImportContext());
+
+ assertResult(result, existing ? 2 : 3, "import fail discovery: ",
existing ? "same,same" : "same");
+ verify(discoveryMapper, times(result.getSuccessCount())).insert(any());
+ verify(handlerMapper,
times(result.getSuccessCount())).insertSelective(any());
+ verify(processor,
times(result.getSuccessCount())).createDiscovery(any());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testUpstreamsDeduplicateAfterHandlerRemapping(final boolean
existing) {
+ final DiscoveryUpstreamServiceImpl service = new
DiscoveryUpstreamServiceImpl(upstreamMapper,
mock(DiscoveryHandlerMapper.class), proxySelectorMapper,
+ discoveryMapper, mock(DiscoveryRelMapper.class),
selectorMapper, pluginMapper, mock(DiscoveryProcessorHolder.class));
+ final ConfigsImportContext context = new ConfigsImportContext();
+ context.getDiscoveryHandlerIdMapping().put("source", "target");
+ context.getDiscoveryHandlerIdMapping().put("alias", "target");
+ context.getDiscoveryHandlerIdMapping().put("other", "other-target");
+ when(upstreamMapper.selectByNamespaceId(NAMESPACE)).thenReturn(existing
+ ?
Collections.singletonList(DiscoveryUpstreamDO.buildDiscoveryUpstreamDO(upstream("target",
"same"))) : Collections.emptyList());
+ when(upstreamMapper.insert(any())).thenReturn(1);
+
+ final ConfigImportResult result = service.importData(NAMESPACE,
Arrays.asList(upstream("source", "same"), upstream("alias", "same"),
+ upstream("other", "same"), upstream("source", "different")),
context);
+
+ assertResult(result, existing ? 2 : 3, "import fail discovery
upstream: ", existing ? "same,same" : "same");
+ final ArgumentCaptor<DiscoveryUpstreamDO> captor =
ArgumentCaptor.forClass(DiscoveryUpstreamDO.class);
+ verify(upstreamMapper,
times(result.getSuccessCount())).insert(captor.capture());
+ captor.getAllValues().forEach(row -> assertEquals(NAMESPACE,
row.getNamespaceId()));
+ assertEquals(existing ? 0 : 1, captor.getAllValues().stream()
+ .filter(row -> "target".equals(row.getDiscoveryHandlerId()) &&
"same".equals(row.getUpstreamUrl())).count());
+ }
+
+ private void assertResult(final ConfigImportResult result, final int
count, final String prefix, final String names) {
+ assertEquals(count, result.getSuccessCount());
+ assertEquals(prefix + names, result.getFailMessage());
+ }
+
+ @Test
+ public void testMetadataOnlyReservesPathsAfterSuccessfulInsert() {
+ final MetaDataServiceImpl service = new
MetaDataServiceImpl(metaDataMapper, mock(ApplicationEventPublisher.class),
mock(MetaDataEventPublisher.class));
+ when(metaDataMapper.insert(any())).thenReturn(0, 1);
+ when(metaDataMapper.pathExisted(anyString(),
anyString())).thenReturn(null);
+
+ final ConfigImportResult result = service.importData(NAMESPACE,
Arrays.asList(metadata("/same"), metadata("/same"), metadata("/same")));
+
+ assertResult(result, 1, "import fail meta: ", "/same");
+ verify(metaDataMapper, times(2)).insert(any());
+ }
+
+ @Test
+ public void
testDuplicateDiscoveryHandlersRemapUpstreamsToFirstInsertedHandler() {
+ final DiscoveryProcessorHolder holder =
mock(DiscoveryProcessorHolder.class);
+ final DiscoveryProcessor processor = mock(DiscoveryProcessor.class);
+ final DiscoveryHandlerMapper handlerMapper =
mock(DiscoveryHandlerMapper.class);
+ final DiscoveryRelMapper relMapper = mock(DiscoveryRelMapper.class);
+ final DiscoveryServiceImpl discoveries = new
DiscoveryServiceImpl(discoveryMapper, proxySelectorMapper, relMapper,
+ handlerMapper, mock(SelectorService.class), selectorMapper,
holder, mock(PlatformTransactionManager.class));
+ final DiscoveryUpstreamServiceImpl upstreams = new
DiscoveryUpstreamServiceImpl(upstreamMapper, handlerMapper, proxySelectorMapper,
+ discoveryMapper, relMapper, selectorMapper, pluginMapper,
holder);
+ final ConfigsImportContext context = new ConfigsImportContext();
+ final DiscoveryDTO first = discovery("divide", "same");
+ first.getDiscoveryHandler().setId("first-handler");
+ final DiscoveryDTO duplicate = discovery("divide", "same");
+ duplicate.getDiscoveryHandler().setId("duplicate-handler");
+ when(holder.chooseProcessor("local")).thenReturn(processor);
+ when(discoveryMapper.insert(any())).thenReturn(1);
+ when(handlerMapper.insertSelective(any())).thenReturn(1);
+ when(upstreamMapper.insert(any())).thenReturn(1);
+
+ assertResult(discoveries.importData(NAMESPACE, Arrays.asList(first,
duplicate), context), 1, "import fail discovery: ", "same");
+ final String targetHandler =
context.getDiscoveryHandlerIdMapping().get("first-handler");
+ assertNotNull(targetHandler);
+ assertEquals(targetHandler,
context.getDiscoveryHandlerIdMapping().get("duplicate-handler"));
+ assertResult(upstreams.importData(NAMESPACE,
Arrays.asList(upstream("first-handler", "same-url"),
+ upstream("duplicate-handler", "same-url")), context), 1,
"import fail discovery upstream: ", "same-url");
+ verify(handlerMapper).insertSelective(any());
+ final ArgumentCaptor<DiscoveryUpstreamDO> captor =
ArgumentCaptor.forClass(DiscoveryUpstreamDO.class);
+ verify(upstreamMapper).insert(captor.capture());
+ assertEquals(targetHandler, captor.getValue().getDiscoveryHandlerId());
+ }
+
+ @Test
+ public void testDictOnlyReservesNamesAfterSuccessfulInsert() {
+ final ShenyuDictServiceImpl service = new
ShenyuDictServiceImpl(dictMapper, mock(DictEventPublisher.class));
+ when(dictMapper.insertSelective(any())).thenReturn(0, 1);
+
+ final ConfigImportResult result =
service.importData(Arrays.asList(dict("type", "same"), dict("type", "same"),
dict("type", "same")));
+
+ assertResult(result, 1, "import fail dict: ", "same");
+ verify(dictMapper, times(2)).insertSelective(any());
+ }
+
+ @Test
+ public void testProxySelectorOnlyMapsIdsAfterSuccessfulInsert() {
+ final ProxySelectorServiceImpl service = new
ProxySelectorServiceImpl(proxySelectorMapper, discoveryMapper, upstreamMapper,
+ mock(DiscoveryHandlerMapper.class),
mock(DiscoveryRelMapper.class), selectorMapper,
mock(DiscoveryProcessorHolder.class), mock(ApplicationEventPublisher.class));
+ final ConfigsImportContext context = new ConfigsImportContext();
+ when(proxySelectorMapper.insert(any())).thenReturn(0, 1);
+
+ final ConfigImportResult result = service.importData(NAMESPACE,
Arrays.asList(proxy("failed", "tcp", "same"),
+ proxy("success", "tcp", "same"), proxy("duplicate", "tcp",
"same")), context);
+
+ assertResult(result, 1, "import fail proxy selector: ", "same");
+ assertEquals(2, context.getProxySelectorIdMapping().size());
+ assertNotNull(context.getProxySelectorIdMapping().get("success"));
+ assertEquals(context.getProxySelectorIdMapping().get("success"),
context.getProxySelectorIdMapping().get("duplicate"));
+ verify(proxySelectorMapper, times(2)).insert(any());
+ }
+
+ private SelectorDTO selector(final String id, final String pluginId, final
String name) {
+ final SelectorDTO dto = new SelectorDTO();
+ dto.setId(id);
+ dto.setPluginId(pluginId);
+ dto.setName(name);
+ dto.setType(0);
+ dto.setSelectorConditions(Collections.emptyList());
+ return dto;
+ }
+
+ private RuleDTO rule(final String selectorId, final String name) {
+ final RuleDTO dto = new RuleDTO();
+ dto.setSelectorId(selectorId);
+ dto.setName(name);
+ dto.setRuleConditions(Collections.emptyList());
+ return dto;
+ }
+
+ private MetaDataDTO metadata(final String path) {
+ final MetaDataDTO dto = new MetaDataDTO();
+ dto.setPath(path);
+ return dto;
+ }
+
+ private AppAuthDTO auth(final String key) {
+ final AppAuthDTO dto = new AppAuthDTO();
+ dto.setAppKey(key);
+ final AuthPathDTO path = new AuthPathDTO();
+ path.setPath("/" + key);
+ dto.setAuthPathList(Collections.singletonList(path));
+ return dto;
+ }
+
+ private ShenyuDictDTO dict(final String type, final String name) {
+ final ShenyuDictDTO dto = new ShenyuDictDTO();
+ dto.setType(type);
+ dto.setDictCode("code");
+ dto.setDictName(name);
+ return dto;
+ }
+
+ private ProxySelectorData proxy(final String id, final String plugin,
final String name) {
+ final ProxySelectorData data = new ProxySelectorData();
+ data.setId(id);
+ data.setPluginName(plugin);
+ data.setName(name);
+ return data;
+ }
+
+ private DiscoveryDTO discovery(final String plugin, final String name) {
+ final DiscoveryDTO dto = new DiscoveryDTO();
+ dto.setPluginName(plugin);
+ dto.setName(name);
+ dto.setType("local");
+ final DiscoveryHandlerDTO handler = new DiscoveryHandlerDTO();
+ handler.setId(plugin + "-" + name);
+ dto.setDiscoveryHandler(handler);
+ return dto;
+ }
+
+ private DiscoveryUpstreamDTO upstream(final String handler, final String
url) {
+ final DiscoveryUpstreamDTO dto = new DiscoveryUpstreamDTO();
+ dto.setDiscoveryHandlerId(handler);
+ dto.setUrl(url);
+ dto.setStatus(0);
+ dto.setWeight(50);
+ return dto;
+ }
+}
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DictImportIntegrationTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DictImportIntegrationTest.java
index 9fb64ce1ff..408f973e47 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DictImportIntegrationTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DictImportIntegrationTest.java
@@ -21,6 +21,7 @@ package org.apache.shenyu.admin.service;
import jakarta.annotation.Resource;
import org.apache.shenyu.admin.AbstractSpringIntegrationTest;
import org.apache.shenyu.admin.model.dto.ShenyuDictDTO;
+import org.apache.shenyu.admin.model.result.ConfigImportResult;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataIntegrityViolationException;
@@ -32,7 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
- * Verify dict imports roll back dictionary rows and synchronous audit events.
+ * Verify dict imports deduplicate business keys and retain transactional
audit events.
*/
public class DictImportIntegrationTest extends AbstractSpringIntegrationTest {
@@ -50,8 +51,12 @@ public class DictImportIntegrationTest extends
AbstractSpringIntegrationTest {
@Test
public void testFailedImportRollsBackRowsAndAuditLog() {
+ final ShenyuDictDTO first = dict("dict-import-first");
+ first.setId("dict-import-collision");
+ final ShenyuDictDTO second = dict("dict-import-second");
+ second.setId(first.getId());
assertThrows(DataIntegrityViolationException.class,
- () ->
shenyuDictService.importData(List.of(dict("dict-import-first"),
dict("dict-import-first"))));
+ () -> shenyuDictService.importData(List.of(first, second)));
assertEquals(0, dictCount());
assertEquals(0, auditCount());
}
@@ -63,6 +68,30 @@ public class DictImportIntegrationTest extends
AbstractSpringIntegrationTest {
assertEquals(2, auditCount());
}
+ @Test
+ public void
testDuplicateBusinessKeysReportFailureWithoutDuplicateRowsOrAuditLogs() {
+ final ConfigImportResult result =
shenyuDictService.importData(List.of(dict("dict-import-first"),
+ dict("dict-import-first"), dict("dict-import-second")));
+
+ assertEquals(2, result.getSuccessCount());
+ assertEquals("import fail dict: dict-import-first",
result.getFailMessage());
+ assertEquals(2, dictCount());
+ assertEquals(2, auditCount());
+ }
+
+ @Test
+ public void testExistingAndBatchDuplicatesShareTheSameDetection() {
+ assertEquals(1,
shenyuDictService.importData(List.of(dict("dict-import-first"))).getSuccessCount());
+
+ final ConfigImportResult result =
shenyuDictService.importData(List.of(dict("dict-import-first"),
+ dict("dict-import-second"), dict("dict-import-second")));
+
+ assertEquals(1, result.getSuccessCount());
+ assertEquals("import fail dict: dict-import-first,dict-import-second",
result.getFailMessage());
+ assertEquals(2, dictCount());
+ assertEquals(2, auditCount());
+ }
+
private int dictCount() {
return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM shenyu_dict
WHERE type = 'dict-import-test'", Integer.class);
}
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ShenyuDictServiceTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ShenyuDictServiceTest.java
index c99b04f112..705e3e70d4 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ShenyuDictServiceTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ShenyuDictServiceTest.java
@@ -164,6 +164,7 @@ public final class ShenyuDictServiceTest {
List<ShenyuDictDO> shenyuDictDOList =
Collections.singletonList(buildShenyuDictDO("haha"));
given(this.shenyuDictMapper.selectByQuery(any())).willReturn(shenyuDictDOList);
List<ShenyuDictDTO> shenyuDictDTOList =
Collections.singletonList(buildShenyuDictDTO(null, "lala"));
+ given(this.shenyuDictMapper.insertSelective(any())).willReturn(1);
ConfigImportResult configImportResult =
shenyuDictService.importData(shenyuDictDTOList);
assertNotNull(configImportResult);